From 37981ac860093b62ec249bff86c92fb6ff6a7391 Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Thu, 21 Nov 2024 16:46:50 +0100 Subject: [PATCH 01/87] feat(infra): decrease windows output size in self-contained deployment #9302 (#9303) --- .github/workflows/admin-sample.cd.yml | 16 ++- .github/workflows/blazorui.demo.cd.yml | 6 +- .github/workflows/todo-sample.cd.yml | 26 ++-- .../Services/ExceptionHandlerBase.cs | 3 +- .../Bit.BlazorUI.Demo.Client.Windows/App.xaml | 4 - .../App.xaml.cs | 37 ------ .../AssemblyInfo.cs | 10 -- .../Bit.BlazorUI.Demo.Client.Windows.csproj | 37 +++++- .../MainWindow.xaml | 18 --- .../MainWindow.xaml.cs | 42 ------- .../Program.cs | 117 +++++++++++++++-- .../Services/WindowsDeviceCoordinator.cs | 8 +- .../.azure-devops/workflows/cd.yml | 16 +-- .../Bit.Boilerplate/.github/workflows/cd.yml | 13 +- .../.template.config/template.json | 2 +- .../IClientCoreServiceCollectionExtensions.cs | 11 +- .../Extensions/IConfigurationExtensions.cs | 4 +- .../Boilerplate.Client.Maui.csproj | 6 + .../Program.Services.cs | 7 +- .../Boilerplate.Client.Windows/App.xaml | 14 --- .../Boilerplate.Client.Windows/App.xaml.cs | 84 ------------- .../AssemblyInfo.cs | 10 -- .../Boilerplate.Client.Windows.csproj | 39 +++++- .../MainWindow.xaml | 22 ---- .../MainWindow.xaml.cs | 69 ---------- .../Program.Services.cs | 19 ++- .../Boilerplate.Client.Windows/Program.cs | 119 ++++++++++++++++-- .../Resources/SplashScreen.png | Bin 11498 -> 0 bytes .../Services/WindowsDeviceCoordinator.cs | 9 +- .../Services/WindowsLocalHttpServer.cs | 2 +- .../Services/WindowsStorageService.cs | 8 +- .../Bit.Boilerplate/src/Directory.Build.props | 2 + .../src/Directory.Packages.props | 2 +- .../src/Directory.Packages8.props | 2 +- .../Controllers/AppControllerBase.cs | 2 +- .../IdentityController.SocialSignIn.cs | 2 +- .../Extensions/HttpRequestExtensions.cs | 4 +- .../Program.Middlewares.cs | 4 +- .../Program.Services.cs | 19 ++- .../Program.Middlewares.cs | 14 +-- .../Program.Services.cs | 7 +- .../src/Shared/Boilerplate.Shared.csproj | 1 - .../ISharedServiceCollectionExtensions.cs | 7 +- 43 files changed, 421 insertions(+), 423 deletions(-) delete mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/App.xaml delete mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/App.xaml.cs delete mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/AssemblyInfo.cs delete mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/MainWindow.xaml delete mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/MainWindow.xaml.cs delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/App.xaml delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/App.xaml.cs delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/AssemblyInfo.cs delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml.cs delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Resources/SplashScreen.png diff --git a/.github/workflows/admin-sample.cd.yml b/.github/workflows/admin-sample.cd.yml index 4f26fb8605..280c2e7518 100644 --- a/.github/workflows/admin-sample.cd.yml +++ b/.github/workflows/admin-sample.cd.yml @@ -144,7 +144,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 --sample Admin --windows --appInsights --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --offlineDb --framework net9.0 + cd ..\..\..\ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --windows --appInsights --appCenter --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --offlineDb --framework net9.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 @@ -156,15 +156,19 @@ jobs: WindowsUpdate.FilesUrl: https://windows-adminpanel.bitplatform.dev ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} + - name: Set app center secret + run: (Get-Content AdminPanel\src\Client\AdminPanel.Client.Windows\Program.cs) -Replace 'appCenterSecret = null;', 'appCenterSecret = "a9ed2257-fb82-496a-ba10-78c2d9ef33a6";' | Out-File -Encoding utf8 AdminPanel\src\Client\AdminPanel.Client.Windows\Program.cs + shell: pwsh + - name: Generate CSS/JS files run: dotnet build AdminPanel\src\Client\AdminPanel.Client.Core\AdminPanel.Client.Core.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - name: Publish run: | cd AdminPanel\src\Client\AdminPanel.Client.Windows\ - dotnet publish AdminPanel.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:CompressionEnabled=false + dotnet publish AdminPanel.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --self-contained dotnet tool restore - dotnet vpk pack -u AdminPanel.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e AdminPanel.Client.Windows.exe -r win-x86 --framework net9.0-x86-desktop,webview2 --icon .\wwwroot\favicon.ico --packTitle 'AdminPanel' + dotnet vpk pack -u AdminPanel.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e AdminPanel.Client.Windows.exe -r win-x86 --framework webview2 --icon .\wwwroot\favicon.ico --packTitle 'AdminPanel' - name: Upload artifact uses: actions/upload-artifact@v4 @@ -235,8 +239,8 @@ jobs: dotnet build AdminPanel/src/Client/AdminPanel.Client.Core/AdminPanel.Client.Core.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release dotnet build AdminPanel/src/Client/AdminPanel.Client.Maui/AdminPanel.Client.Maui.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - - name: Build aab - run: dotnet publish AdminPanel/src/Client/AdminPanel.Client.Maui/AdminPanel.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="AdminPanel.keystore" -p:AndroidSigningKeyAlias=bitplatform -p:AndroidSigningKeyPass="${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}" -p:AndroidSigningStorePass="${{ secrets.ANDROID_RELEASE_SIGNING_PASSWORD }}" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="AdminPanel" -p:ApplicationId="com.bitplatform.AdminPanel.Template" -p:CompressionEnabled=false -f net9.0-android + - name: Publish aab + run: dotnet publish AdminPanel/src/Client/AdminPanel.Client.Maui/AdminPanel.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="AdminPanel.keystore" -p:AndroidSigningKeyAlias=bitplatform -p:AndroidSigningKeyPass="${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}" -p:AndroidSigningStorePass="${{ secrets.ANDROID_RELEASE_SIGNING_PASSWORD }}" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="AdminPanel" -p:ApplicationId="com.bitplatform.AdminPanel.Template" -f net9.0-android - name: Upload artifact uses: actions/upload-artifact@v4 @@ -309,7 +313,7 @@ jobs: dotnet build AdminPanel/src/Client/AdminPanel.Client.Maui/AdminPanel.Client.Maui.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - name: Build ipa - run: dotnet publish AdminPanel/src/Client/AdminPanel.Client.Maui/AdminPanel.Client.Maui.csproj -p:RuntimeIdentifier=ios-arm64 -c Release -p:ArchiveOnBuild=true -p:CodesignKey="iPhone Distribution" -p:CodesignProvision="AdminPanel" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="AdminPanel" -p:ApplicationId="com.bitplatform.AdminPanel.Template" -p:CompressionEnabled=false -f net9.0-ios + run: dotnet publish AdminPanel/src/Client/AdminPanel.Client.Maui/AdminPanel.Client.Maui.csproj -p:RuntimeIdentifier=ios-arm64 -c Release -p:ArchiveOnBuild=true -p:CodesignKey="iPhone Distribution" -p:CodesignProvision="AdminPanel" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="AdminPanel" -p:ApplicationId="com.bitplatform.AdminPanel.Template" -f net9.0-ios - name: Upload artifact uses: actions/upload-artifact@v4 diff --git a/.github/workflows/blazorui.demo.cd.yml b/.github/workflows/blazorui.demo.cd.yml index 4babd7c6d2..1739777dc5 100644 --- a/.github/workflows/blazorui.demo.cd.yml +++ b/.github/workflows/blazorui.demo.cd.yml @@ -120,9 +120,9 @@ jobs: - name: Publish run: | cd src\BlazorUI\Demo\Client\Bit.BlazorUI.Demo.Client.Windows\ - dotnet publish Bit.BlazorUI.Demo.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:CompressionEnabled=false + dotnet publish Bit.BlazorUI.Demo.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:CompressionEnabled=false --self-contained dotnet tool restore - dotnet vpk pack -u Bit.BlazorUI.Demo.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e Bit.BlazorUI.Demo.Client.Windows.exe -r win-x86 --framework net9.0-x86-desktop,webview2 --icon .\wwwroot\favicon.ico --packTitle 'Bit Blazor UI' + dotnet vpk pack -u Bit.BlazorUI.Demo.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e Bit.BlazorUI.Demo.Client.Windows.exe -r win-x86 --framework webview2 --icon .\wwwroot\favicon.ico --packTitle 'Bit Blazor UI' - name: Upload artifact uses: actions/upload-artifact@v4 @@ -171,7 +171,7 @@ jobs: - name: Generate CSS/JS files run: dotnet build src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -c Release - - name: Build aab + - name: Publish aab run: dotnet publish src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="BitBlazorUIDemo.keystore" -p:AndroidSigningKeyAlias=bitplatform -p:AndroidSigningKeyPass="${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}" -p:AndroidSigningStorePass="${{ secrets.ANDROID_RELEASE_SIGNING_PASSWORD }}" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:CompressionEnabled=false -f net9.0-android - name: Upload artifact diff --git a/.github/workflows/todo-sample.cd.yml b/.github/workflows/todo-sample.cd.yml index 5b7cc8156a..5548de0bdf 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 Todo --appInsights --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net8.0 + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample Todo --appInsights --appCenter --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net8.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 @@ -156,26 +156,20 @@ jobs: GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} WindowsUpdate.FilesUrl: https://windows-todo.bitplatform.dev ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - - - name: Delete App Splash Screen - run: rm TodoSample/src/Client/TodoSample.Client.Windows/Resources/SplashScreen.png - - - name: Extract App Splash Screen from env - uses: timheuer/base64-to-file@v1.2 - with: - fileDir: './TodoSample/src/Client/TodoSample.Client.Windows/Resources/' - fileName: 'SplashScreen.png' - encodedString: ${{ vars.TODO_WPF_SPLASH_SCREEN }} + - name: Set app center secret + run: (Get-Content TodoSample\src\Client\TodoSample.Client.Windows\Program.cs) -Replace 'appCenterSecret = null;', 'appCenterSecret = "39f576f2-7c16-4990-af3f-7b70509d41e2";' | Out-File -Encoding utf8 TodoSample\src\Client\TodoSample.Client.Windows\Program.cs + shell: pwsh + - name: Generate CSS/JS files run: dotnet build TodoSample\src\Client\TodoSample.Client.Core\TodoSample.Client.Core.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - name: Publish run: | cd TodoSample\src\Client\TodoSample.Client.Windows\ - dotnet publish TodoSample.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:CompressionEnabled=false + dotnet publish TodoSample.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" dotnet tool restore - dotnet vpk pack -u TodoSample.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e TodoSample.Client.Windows.exe -r win-x86 --framework net8.0-x86-desktop,webview2 --icon .\wwwroot\favicon.ico --packTitle TodoSample + dotnet vpk pack -u TodoSample.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e TodoSample.Client.Windows.exe -r win-x86 --framework webview2 --icon .\wwwroot\favicon.ico --packTitle TodoSample - name: Upload artifact uses: actions/upload-artifact@v4 @@ -270,8 +264,8 @@ jobs: dotnet build TodoSample/src/Client/TodoSample.Client.Core/TodoSample.Client.Core.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release dotnet build TodoSample/src/Client/TodoSample.Client.Maui/TodoSample.Client.Maui.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - - name: Build aab - run: dotnet publish TodoSample/src/Client/TodoSample.Client.Maui/TodoSample.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="TodoSample.keystore" -p:AndroidSigningKeyAlias=bitplatform -p:AndroidSigningKeyPass="${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}" -p:AndroidSigningStorePass="${{ secrets.ANDROID_RELEASE_SIGNING_PASSWORD }}" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="TodoSample" -p:ApplicationId="com.bitplatform.Todo.Template" -p:CompressionEnabled=false -f net8.0-android + - name: Publish aab + run: dotnet publish TodoSample/src/Client/TodoSample.Client.Maui/TodoSample.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="TodoSample.keystore" -p:AndroidSigningKeyAlias=bitplatform -p:AndroidSigningKeyPass="${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}" -p:AndroidSigningStorePass="${{ secrets.ANDROID_RELEASE_SIGNING_PASSWORD }}" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="TodoSample" -p:ApplicationId="com.bitplatform.Todo.Template" -f net8.0-android - name: Upload artifact uses: actions/upload-artifact@v4 @@ -368,7 +362,7 @@ jobs: dotnet build TodoSample/src/Client/TodoSample.Client.Maui/TodoSample.Client.Maui.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - name: Build ipa - run: dotnet publish TodoSample/src/Client/TodoSample.Client.Maui/TodoSample.Client.Maui.csproj -p:RuntimeIdentifier=ios-arm64 -c Release -p:ArchiveOnBuild=true -p:CodesignKey="iPhone Distribution" -p:CodesignProvision="TodoTemplate" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="Todo" -p:ApplicationId="com.bitplatform.Todo.Template" -p:CompressionEnabled=false -f net8.0-ios + run: dotnet publish TodoSample/src/Client/TodoSample.Client.Maui/TodoSample.Client.Maui.csproj -p:RuntimeIdentifier=ios-arm64 -c Release -p:ArchiveOnBuild=true -p:CodesignKey="iPhone Distribution" -p:CodesignProvision="TodoTemplate" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="Todo" -p:ApplicationId="com.bitplatform.Todo.Template" -f net8.0-ios - name: Upload artifact uses: actions/upload-artifact@v4 diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Services/ExceptionHandlerBase.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Services/ExceptionHandlerBase.cs index 02b051f15b..0754a79c08 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Services/ExceptionHandlerBase.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Services/ExceptionHandlerBase.cs @@ -15,10 +15,11 @@ public virtual void Handle(Exception exception, IDictionary? pa if (isDebug) { - _ = Console.Out.WriteLineAsync(exceptionMessage); Debugger.Break(); } + _ = Console.Out.WriteLineAsync(exception.ToString()); + _ = MessageBoxService.Show(exceptionMessage, "Error"); } } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/App.xaml b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/App.xaml deleted file mode 100644 index 169b22551d..0000000000 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/App.xaml +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/App.xaml.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/App.xaml.cs deleted file mode 100644 index 81e3a6968d..0000000000 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/App.xaml.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.IO.IsolatedStorage; -using System.IO; -using System.Windows; -using System.Text.Json; -using System.Collections; - -namespace Bit.BlazorUI.Demo.Client.Windows; - -public partial class App -{ - const string WindowsStorageFilename = "windows.storage.json"; - - private void App_Startup(object sender, StartupEventArgs e) - { - // Restore application-scope property from isolated storage - using IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForDomain(); - try - { - using IsolatedStorageFileStream stream = new IsolatedStorageFileStream(WindowsStorageFilename, FileMode.Open, storage); - foreach (DictionaryEntry item in JsonSerializer.Deserialize(stream)!) - { - Properties.Add(item.Key, item.Value); - } - } - catch (IsolatedStorageException exp) when (exp.InnerException is FileNotFoundException) { } - } - - private void App_Exit(object sender, ExitEventArgs e) - { - // Persist application-scope property to isolated storage - IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForDomain(); - using IsolatedStorageFileStream stream = new IsolatedStorageFileStream(WindowsStorageFilename, FileMode.Create, storage); - using StreamWriter writer = new StreamWriter(stream); - writer.Write(JsonSerializer.Serialize(Properties)); - } -} - diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/AssemblyInfo.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/AssemblyInfo.cs deleted file mode 100644 index b0ec827578..0000000000 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Windows; - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj index de6d94d797..c2f43c648a 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj @@ -1,22 +1,34 @@ - + WinExe net9.0-windows enable enable - true true true Bit.BlazorUI.Demo.Client.Windows Bit.BlazorUI.Demo.Client.Windows.Program wwwroot\favicon.ico + $(NoWarn);WFO5001 + + + + true + partial + true + <_SuppressWinFormsTrimError>true + true + false + - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -25,15 +37,15 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + - PreserveNewest - @@ -41,4 +53,17 @@ + + + + + + + + + + + + + diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/MainWindow.xaml b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/MainWindow.xaml deleted file mode 100644 index 5f403e521c..0000000000 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/MainWindow.xaml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/MainWindow.xaml.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/MainWindow.xaml.cs deleted file mode 100644 index 48d2167f6e..0000000000 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/MainWindow.xaml.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Net.Http; - -namespace Bit.BlazorUI.Demo.Client.Windows; - -public partial class MainWindow -{ - public MainWindow() - { - AppRenderMode.IsBlazorHybrid = true; - var services = new ServiceCollection(); - ConfigurationBuilder configurationBuilder = new(); - configurationBuilder.AddClientConfigurations(); - var configuration = configurationBuilder.Build(); - services.AddTransient(sp => configuration); - Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.Absolute, out var apiServerAddress); - services.AddTransient(sp => - { - HttpClient httpClient = new() - { - BaseAddress = apiServerAddress - }; - return httpClient; - }); - services.AddWpfBlazorWebView(); - if (BuildConfiguration.IsDebug()) - { - services.AddBlazorWebViewDeveloperTools(); - } - services.AddWindowsServices(); - InitializeComponent(); - BlazorWebView.Services = services.BuildServiceProvider(); - BlazorWebView.Loaded += async delegate - { - await BlazorWebView.WebView.EnsureCoreWebView2Async(); - - BlazorWebView.WebView.NavigationCompleted += async delegate - { - await BlazorWebView.WebView.ExecuteScriptAsync("Blazor.start()"); - }; - }; - } -} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Program.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Program.cs index f93efbc8c4..e1a6303d98 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Program.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Program.cs @@ -1,24 +1,54 @@ -using Bit.BlazorUI.Demo.Client.Windows.Configuration; -using Velopack; +using Velopack; +using Bit.BlazorUI.Demo.Client.Core; +using Bit.BlazorUI.Demo.Client.Windows.Configuration; +using Microsoft.Web.WebView2.Core; +using Microsoft.AspNetCore.Components.WebView.WindowsForms; namespace Bit.BlazorUI.Demo.Client.Windows; -public class Program +public partial class Program { [STAThread] public static void Main(string[] args) { + Application.ThreadException += (_, e) => LogException(e.Exception); + AppDomain.CurrentDomain.UnhandledException += (_, e) => LogException(e.ExceptionObject); + + ApplicationConfiguration.Initialize(); + + Application.SetColorMode(SystemColorMode.System); + + AppRenderMode.IsBlazorHybrid = true; + var services = new ServiceCollection(); + ConfigurationBuilder configurationBuilder = new(); + configurationBuilder.AddClientConfigurations(); + var configuration = configurationBuilder.Build(); + services.AddTransient(sp => configuration); + Uri.TryCreate(configuration.GetApiServerAddress(), UriKind.Absolute, out var apiServerAddress); + services.AddTransient(sp => + { + HttpClient httpClient = new() + { + BaseAddress = apiServerAddress + }; + return httpClient; + }); + services.AddWindowsFormsBlazorWebView(); + if (BuildConfiguration.IsDebug()) + { + services.AddBlazorWebViewDeveloperTools(); + } + services.AddWindowsServices(); + Services = services.BuildServiceProvider(); + // https://github.com/velopack/velopack VelopackApp.Build().Run(); - var application = new App(); - application.InitializeComponent(); - Task.Run(async () => + _ = Task.Run(async () => { try { - var services = await App.Current.Dispatcher.InvokeAsync(() => ((MainWindow)App.Current.MainWindow).BlazorWebView.Services); - var windowsUpdateSettings = services.GetRequiredService().GetSection("WindowsUpdateSettings")?.Get(); - if (windowsUpdateSettings?.FilesUrl is null) + var windowsUpdateSettings = Services.GetRequiredService().GetSection("WindowsUpdateSettings")?.Get(); + if (string.IsNullOrEmpty(windowsUpdateSettings?.FilesUrl)) { return; } @@ -33,8 +63,73 @@ public static void Main(string[] args) } } } - catch { } + catch (Exception exp) + { + Services.GetRequiredService().Handle(exp); + } }); - application.Run(); + + var form = new Form() + { + Text = "bit BlazorUI", + WindowState = FormWindowState.Maximized, + BackColor = ColorTranslator.FromHtml("#0D2960"), + Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath) + }; + + var blazorWebView = new BlazorWebView + { + Dock = DockStyle.Fill, + Services = Services, + HostPage = @"wwwroot\index.html", + BackColor = ColorTranslator.FromHtml("#0D2960") + }; + + blazorWebView.WebView.DefaultBackgroundColor = ColorTranslator.FromHtml("#0D2960"); + + blazorWebView.RootComponents.Add(new RootComponent("#app-container", typeof(Routes), null)); + + blazorWebView.BlazorWebViewInitialized += delegate + { + blazorWebView.WebView.CoreWebView2.PermissionRequested += async (sender, args) => + { + args.Handled = true; + args.State = CoreWebView2PermissionState.Allow; + }; + var settings = blazorWebView.WebView.CoreWebView2.Settings; +#if DEBUG + + settings.IsZoomControlEnabled = false; + settings.AreBrowserAcceleratorKeysEnabled = false; +#endif + bool hasBlazorStarted = false; + blazorWebView.WebView.NavigationCompleted += async delegate + { + if (hasBlazorStarted) + return; + hasBlazorStarted = true; + await blazorWebView.WebView.ExecuteScriptAsync("Blazor.start()"); + }; + }; + + form.Controls.Add(blazorWebView); + + Application.Run(form); } + + private static void LogException(object? error) + { + var errorMessage = error?.ToString() ?? "Unknown error"; + if (Services is not null && error is Exception exp) + { + Services.GetRequiredService().Handle(exp); + } + else + { + Clipboard.SetText(errorMessage); + MessageBox.Show(errorMessage, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + public static IServiceProvider? Services { get; private set; } } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Services/WindowsDeviceCoordinator.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Services/WindowsDeviceCoordinator.cs index bdde280072..91ad81f9be 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Services/WindowsDeviceCoordinator.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Services/WindowsDeviceCoordinator.cs @@ -2,4 +2,10 @@ namespace Bit.BlazorUI.Demo.Client.Windows.Services; -public class WindowsDeviceCoordinator : IBitDeviceCoordinator { } +public partial class WindowsDeviceCoordinator : IBitDeviceCoordinator +{ + public async Task ApplyTheme(bool isDark) + { + Application.SetColorMode(isDark ? SystemColorMode.Dark : SystemColorMode.Classic); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.azure-devops/workflows/cd.yml b/src/Templates/Boilerplate/Bit.Boilerplate/.azure-devops/workflows/cd.yml index bbec2368fd..3aaa1af716 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.azure-devops/workflows/cd.yml +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.azure-devops/workflows/cd.yml @@ -1,4 +1,4 @@ -trigger: +trigger: - main # https://bitplatform.dev/templates/devops @@ -175,9 +175,9 @@ jobs: targetType: 'inline' script: | cd src\Client\Boilerplate.Client.Windows\ - dotnet publish Boilerplate.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" + dotnet publish Boilerplate.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --self-contained dotnet tool restore - dotnet vpk pack -u Boilerplate.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e Boilerplate.Client.Windows.exe -r win-x86 --framework net9.0-x86-desktop,webview2 --icon .\wwwroot\favicon.ico --packTitle 'Boilerplate' + dotnet vpk pack -u Boilerplate.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e Boilerplate.Client.Windows.exe -r win-x86 --framework webview2 --icon .\wwwroot\favicon.ico --packTitle 'Boilerplate' - task: PublishPipelineArtifact@1 displayName: Upload artifact @@ -241,16 +241,10 @@ jobs: displayName: Generate CSS/JS files - task: Bash@3 - displayName: 'Build aab' + displayName: 'Publish aab' inputs: targetType: 'inline' - script: 'dotnet build src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="Boilerplate.keystore" -p:AndroidSigningKeyAlias=Boilerplate -p:AndroidSigningKeyPass="$(ANDROID_RELEASE_KEYSTORE_PASSWORD)" -p:AndroidSigningStorePass="$(ANDROID_RELEASE_SIGNING_PASSWORD)" -f net9.0-android' - - - task: Bash@3 - displayName: 'Build apk' - inputs: - targetType: 'inline' - script: 'dotnet build src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj -c Release -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="Boilerplate.keystore" -p:AndroidSigningKeyAlias=Boilerplate -p:AndroidSigningKeyPass="$(ANDROID_RELEASE_KEYSTORE_PASSWORD)" -p:AndroidSigningStorePass="$(ANDROID_RELEASE_SIGNING_PASSWORD)" -f net9.0-android' + script: 'dotnet publish src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="Boilerplate.keystore" -p:AndroidSigningKeyAlias=Boilerplate -p:AndroidSigningKeyPass="$(ANDROID_RELEASE_KEYSTORE_PASSWORD)" -p:AndroidSigningStorePass="$(ANDROID_RELEASE_SIGNING_PASSWORD)" -f net9.0-android' - script: | mkdir drop diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.github/workflows/cd.yml b/src/Templates/Boilerplate/Bit.Boilerplate/.github/workflows/cd.yml index c1be40362c..9b16fa6966 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.github/workflows/cd.yml +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.github/workflows/cd.yml @@ -1,4 +1,4 @@ -name: Boilerplate CD +name: Boilerplate CD # https://bitplatform.dev/templates/dev-ops @@ -158,9 +158,9 @@ jobs: - name: Publish run: | cd src\Client\Boilerplate.Client.Windows\ - dotnet publish Boilerplate.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" + dotnet publish Boilerplate.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --self-contained dotnet tool restore - dotnet vpk pack -u Boilerplate.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e Boilerplate.Client.Windows.exe -r win-x86 --framework net9.0-x86-desktop,webview2 --icon .\wwwroot\favicon.ico --packTitle 'Boilerplate' + dotnet vpk pack -u Boilerplate.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e Boilerplate.Client.Windows.exe -r win-x86 --framework webview2 --icon .\wwwroot\favicon.ico --packTitle 'Boilerplate' - name: Upload artifact uses: actions/upload-artifact@v4 @@ -212,11 +212,8 @@ jobs: dotnet build src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj -t:BeforeBuildTasks --no-restore -c Release dotnet build src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj -t:BeforeBuildTasks --no-restore -c Release - - name: Build aab - run: dotnet build src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="Boilerplate.keystore" -p:AndroidSigningKeyAlias=Boilerplate -p:AndroidSigningKeyPass="${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}" -p:AndroidSigningStorePass="${{ secrets.ANDROID_RELEASE_SIGNING_PASSWORD }}" -f net9.0-android - - - name: Build apk - run: dotnet build src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj -c Release -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="Boilerplate.keystore" -p:AndroidSigningKeyAlias=Boilerplate -p:AndroidSigningKeyPass="${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}" -p:AndroidSigningStorePass="${{ secrets.ANDROID_RELEASE_SIGNING_PASSWORD }}" -f net9.0-android + - name: Publish aab + run: dotnet publish src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="Boilerplate.keystore" -p:AndroidSigningKeyAlias=Boilerplate -p:AndroidSigningKeyPass="${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}" -p:AndroidSigningStorePass="${{ secrets.ANDROID_RELEASE_SIGNING_PASSWORD }}" -f net9.0-android - name: Upload artifact uses: actions/upload-artifact@v4 diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json index c3a0e0bdaf..7aba4c2ef0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json @@ -209,7 +209,7 @@ "type": "parameter", "datatype": "bool", "defaultValue": "true", - "description": "WPF-based Windows project supporting Windows 7 SP1 and above." + "description": ".NET desktop app project supporting Windows 7 SP1 and above." }, "appInsights": { "displayName": "Add Azure application insights to project?", 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 08187d693f..310a29d334 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 @@ -43,7 +43,12 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle services.AddSessioned(); services.AddSessioned(sp => (AuthenticationManager)sp.GetRequiredService()); - services.AddSingleton(sp => configuration.Get()!); + services.AddSingleton(sp => + { + ClientCoreSettings settings = new(); + configuration.Bind(settings); + return settings; + }); services.AddOptions() .Bind(configuration) @@ -111,7 +116,9 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle services.Add(ServiceDescriptor.Describe(typeof(IApplicationInsights), typeof(AppInsightsJsSdkService), AppPlatform.IsBrowser ? ServiceLifetime.Singleton : ServiceLifetime.Scoped)); services.AddBlazorApplicationInsights(x => { - x.ConnectionString = configuration.Get()!.ApplicationInsights?.ConnectionString; + ClientCoreSettings settings = new(); + configuration.Bind(settings); + x.ConnectionString = settings.ApplicationInsights?.ConnectionString; }); //#endif diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IConfigurationExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IConfigurationExtensions.cs index 1d4a1ad45c..b4ec8c4355 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IConfigurationExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IConfigurationExtensions.cs @@ -7,7 +7,9 @@ public static partial class IConfigurationExtensions { public static string GetServerAddress(this IConfiguration configuration) { - var serverAddress = configuration.Get()!.ServerAddress; + ClientCoreSettings settings = new(); + configuration.Bind(settings); + var serverAddress = settings.ServerAddress; if (AppEnvironment.IsDev() && serverAddress.Contains("localhost", StringComparison.InvariantCultureIgnoreCase) && diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj index 52b4b5434f..b130e2742a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj @@ -32,6 +32,12 @@ + + false + + + true android-arm64 diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Program.Services.cs index 613ad4eb03..e2fb6b9f4e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Program.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Program.Services.cs @@ -42,7 +42,12 @@ public static void AddClientWebProjectServices(this IServiceCollection services, services.AddScoped(); //#endif - services.AddSingleton(sp => configuration.Get()!); + services.AddSingleton(sp => + { + ClientWebSettings settings = new(); + configuration.Bind(settings); + return settings; + }); services.AddOptions() .Bind(configuration) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/App.xaml b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/App.xaml deleted file mode 100644 index 0718cfd462..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/App.xaml +++ /dev/null @@ -1,14 +0,0 @@ - - - - #0D2960 - - - diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/App.xaml.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/App.xaml.cs deleted file mode 100644 index acc1f9a615..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/App.xaml.cs +++ /dev/null @@ -1,84 +0,0 @@ -//+:cnd:noEmit -using System.IO; -using System.Windows; -using Microsoft.Win32; -using System.Collections; -using System.Windows.Media; -using System.Windows.Threading; -using System.IO.IsolatedStorage; -using Boilerplate.Client.Core.Styles; - -namespace Boilerplate.Client.Windows; - -public partial class App -{ - public App() - { - InitializeComponent(); - - var splash = new SplashScreen(typeof(App).Assembly, @"Resources\SplashScreen.png"); - splash.Show(autoClose: true, topMost: true); - - ConfigureAppTheme(); - } - - private void ConfigureAppTheme() - { - //#if (framework == 'net9.0') - ThemeMode = ThemeMode.System; - Resources.MergedDictionaries.Add(new ResourceDictionary - { - Source = new Uri("pack://application:,,,/PresentationFramework.Fluent;component/Themes/Fluent.xaml", UriKind.Absolute) - }); - //#endif - - using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"); - var value = key?.GetValue("AppsUseLightTheme"); - var isDark = value is int i && i == 0; - - Resources["PrimaryBgColor"] = new BrushConverter().ConvertFrom(isDark ? ThemeColors.PrimaryDarkBgColor : ThemeColors.PrimaryLightBgColor); - } - - const string WindowsStorageFilename = "windows.storage.json"; - - private void App_Startup(object sender, StartupEventArgs e) - { - // Restore application-scope property from isolated storage - using IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForDomain(); - try - { - using IsolatedStorageFileStream stream = new IsolatedStorageFileStream(WindowsStorageFilename, FileMode.Open, storage); - foreach (DictionaryEntry item in JsonSerializer.Deserialize(stream)!) - { - Properties.Add(item.Key, item.Value); - } - } - catch (IsolatedStorageException exp) when (exp.InnerException is FileNotFoundException) { } - } - - private void App_Exit(object sender, ExitEventArgs e) - { - // Persist application-scope property to isolated storage - IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForDomain(); - using IsolatedStorageFileStream stream = new IsolatedStorageFileStream(WindowsStorageFilename, FileMode.Create, storage); - using StreamWriter writer = new StreamWriter(stream); - writer.Write(JsonSerializer.Serialize(Properties)); - } - - private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) - { - try - { - ((MainWindow)MainWindow).AppWebView.Services.GetRequiredService().Handle(e.Exception); - } - catch - { - var errorMessage = e.Exception.ToString(); - Clipboard.SetText(errorMessage); - System.Windows.MessageBox.Show(errorMessage, "Error", MessageBoxButton.OK, MessageBoxImage.Error); - } - - e.Handled = true; - } -} - diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/AssemblyInfo.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/AssemblyInfo.cs deleted file mode 100644 index b0ec827578..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Windows; - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Boilerplate.Client.Windows.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Boilerplate.Client.Windows.csproj index d137dc0ad5..81dc2b9b60 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Boilerplate.Client.Windows.csproj +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Boilerplate.Client.Windows.csproj @@ -5,7 +5,7 @@ net9.0-windows enable enable - true + true true Boilerplate.Client.Windows Boilerplate.Client.Windows.Program @@ -14,7 +14,18 @@ BeforeBuildTasks; $(ResolveStaticWebAssetsInputsDependsOn) - $(NoWarn);WPF0001 + $(NoWarn);WFO5001 + + + + true + partial + true + <_SuppressWinFormsTrimError>true + true + false + @@ -30,12 +41,15 @@ - + + + + PreserveNewest @@ -50,9 +64,9 @@ - - - + + + @@ -63,4 +77,17 @@ + + + + + + + + + + + + + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml deleted file mode 100644 index f5dd3dfe3f..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml.cs deleted file mode 100644 index 4a05bf130d..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/MainWindow.xaml.cs +++ /dev/null @@ -1,69 +0,0 @@ -//+:cnd:noEmit -using Microsoft.Web.WebView2.Core; -using Microsoft.AspNetCore.Components.WebView; -using System.Drawing; -using System.Diagnostics; - -namespace Boilerplate.Client.Windows; - -public partial class MainWindow -{ - public MainWindow() - { - var services = new ServiceCollection(); - ConfigurationBuilder configurationBuilder = new(); - configurationBuilder.AddClientConfigurations(clientEntryAssemblyName: "Boilerplate.Client.Windows"); - var configuration = configurationBuilder.Build(); - services.AddClientWindowsProjectServices(configuration); - InitializeComponent(); - //#if (appInsights == true) - AppWebView.RootComponents.Insert(0, new() - { - ComponentType = typeof(BlazorApplicationInsights.ApplicationInsightsInit), - Selector = "head::after" - }); - //#endif - AppWebView.Services = services.BuildServiceProvider(); - if (CultureInfoManager.MultilingualEnabled) - { - AppWebView.Services.GetRequiredService().SetCurrentCulture( - App.Current.Properties["Culture"]?.ToString() ?? // 1- User settings - CultureInfo.CurrentUICulture.Name); // 2- OS Settings - } - AppWebView.Services.GetRequiredService().Subscribe(ClientPubSubMessages.CULTURE_CHANGED, async culture => - { - string executablePath = Environment.ProcessPath!; - Process.Start(executablePath); - App.Current.Shutdown(); - }); - AppWebView.Loaded += async delegate - { - AppWebView.WebView.DefaultBackgroundColor = ColorTranslator.FromHtml(App.Current.Resources["PrimaryBgColor"].ToString()!); - await AppWebView.WebView.EnsureCoreWebView2Async(); - AppWebView.WebView.CoreWebView2.PermissionRequested += async (sender, args) => - { - args.Handled = true; - args.State = CoreWebView2PermissionState.Allow; - }; - var settings = AppWebView.WebView.CoreWebView2.Settings; - if (AppEnvironment.IsDev() is false) - { - settings.IsZoomControlEnabled = false; - settings.AreBrowserAcceleratorKeysEnabled = false; - } - bool hasBlazorStarted = false; - AppWebView.WebView.NavigationCompleted += async delegate - { - if (hasBlazorStarted) - return; - hasBlazorStarted = true; - await AppWebView.WebView.ExecuteScriptAsync("Blazor.start()"); - }; - }; - } - - void BlazorWebViewInitializing(object sender, BlazorWebViewInitializingEventArgs e) - { - e.EnvironmentOptions = new() { AdditionalBrowserArguments = "--unsafely-treat-insecure-origin-as-secure=https://0.0.0.0 --enable-notifications" }; - } -} 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 b55365d779..a019a1ed72 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 @@ -27,13 +27,18 @@ public static void AddClientWindowsProjectServices(this IServiceCollection servi services.AddSingleton(sp => configuration); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(sp => configuration.Get()!); + services.AddSingleton(sp => + { + ClientWindowsSettings settings = new(); + configuration.Bind(settings); + return settings; + }); services.AddSingleton(ITelemetryContext.Current!); //#if (notification == true) services.AddSingleton(); //#endif - services.AddWpfBlazorWebView(); + services.AddWindowsFormsBlazorWebView(); services.AddBlazorWebViewDeveloperTools(); services.AddLogging(loggingBuilder => @@ -43,11 +48,19 @@ public static void AddClientWindowsProjectServices(this IServiceCollection servi loggingBuilder.AddEventSourceLogger(); loggingBuilder.AddEventLog(); + //#if (appCenter == true) + if (Microsoft.AppCenter.AppCenter.Configured) + { + loggingBuilder.AddAppCenter(options => options.IncludeScopes = true); + } + //#endif //#if (appInsights == true) loggingBuilder.AddApplicationInsights(config => { config.TelemetryInitializers.Add(new WindowsAppInsightsTelemetryInitializer()); - var connectionString = configuration.Get()!.ApplicationInsights?.ConnectionString; + ClientWindowsSettings settings = new(); + configuration.Bind(settings); + var connectionString = settings.ApplicationInsights?.ConnectionString; if (string.IsNullOrEmpty(connectionString) is false) { config.ConnectionString = connectionString; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs index 9e30c106a8..5e977beca0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs @@ -1,5 +1,9 @@ -using Velopack; +//+:cnd:noEmit +using Velopack; +using Microsoft.Web.WebView2.Core; +using Boilerplate.Client.Core.Components; using Boilerplate.Client.Windows.Services; +using Microsoft.AspNetCore.Components.WebView.WindowsForms; namespace Boilerplate.Client.Windows; @@ -8,18 +12,51 @@ public partial class Program [STAThread] public static void Main(string[] args) { + //#if (appCenter == true) + string? appCenterSecret = null; + if (appCenterSecret is not null) + { + Microsoft.AppCenter.AppCenter.Start(appCenterSecret, typeof(Microsoft.AppCenter.Crashes.Crashes), typeof(Microsoft.AppCenter.Analytics.Analytics)); + } + //#endif + + Application.ThreadException += (_, e) => LogException(e.Exception); + AppDomain.CurrentDomain.UnhandledException += (_, e) => LogException(e.ExceptionObject); + + ApplicationConfiguration.Initialize(); + AppPlatform.IsBlazorHybrid = true; ITelemetryContext.Current = new WindowsTelemetryContext(); + //#if (framework == 'net9.0') + Application.SetColorMode(SystemColorMode.System); + //#endif + + var services = new ServiceCollection(); + ConfigurationBuilder configurationBuilder = new(); + configurationBuilder.AddClientConfigurations(clientEntryAssemblyName: "Boilerplate.Client.Windows"); + var configuration = configurationBuilder.Build(); + services.AddClientWindowsProjectServices(configuration); + Services = services.BuildServiceProvider(); + + if (CultureInfoManager.MultilingualEnabled) + { + Services.GetRequiredService().SetCurrentCulture( + Application.UserAppDataRegistry.GetValue("Culture") as string ?? // 1- User settings + CultureInfo.CurrentUICulture.Name); // 2- OS Settings + } + Services.GetRequiredService().Subscribe(ClientPubSubMessages.CULTURE_CHANGED, async culture => + { + Application.Restart(); + }); + // https://github.com/velopack/velopack VelopackApp.Build().Run(); - var application = new App(); - Task.Run(async () => + _ = Task.Run(async () => { - var services = await App.Current.Dispatcher.InvokeAsync(() => ((MainWindow)App.Current.MainWindow).AppWebView.Services); try { - var windowsUpdateSettings = services.GetRequiredService().WindowsUpdate; + var windowsUpdateSettings = Services.GetRequiredService().WindowsUpdate; if (string.IsNullOrEmpty(windowsUpdateSettings?.FilesUrl)) { return; @@ -37,9 +74,77 @@ public static void Main(string[] args) } catch (Exception exp) { - services.GetRequiredService().Handle(exp); + Services.GetRequiredService().Handle(exp); } }); - application.Run(); + + var form = new Form() + { + Text = "Boilerplate", + WindowState = FormWindowState.Maximized, + BackColor = ColorTranslator.FromHtml("#0D2960"), + Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath) + }; + + Environment.SetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "--unsafely-treat-insecure-origin-as-secure=https://0.0.0.1 --enable-notifications"); + + var blazorWebView = new BlazorWebView + { + Dock = DockStyle.Fill, + Services = Services, + HostPage = @"wwwroot\index.html", + BackColor = ColorTranslator.FromHtml("#0D2960") + }; + + blazorWebView.WebView.DefaultBackgroundColor = ColorTranslator.FromHtml("#0D2960"); + + //#if (appInsights == true) + blazorWebView.RootComponents.Add(new RootComponent("head::after", typeof(BlazorApplicationInsights.ApplicationInsightsInit), null)); + //#endif + + blazorWebView.RootComponents.Add(new RootComponent("#app-container", typeof(Routes), null)); + + blazorWebView.BlazorWebViewInitialized += delegate + { + blazorWebView.WebView.CoreWebView2.PermissionRequested += async (sender, args) => + { + args.Handled = true; + args.State = CoreWebView2PermissionState.Allow; + }; + var settings = blazorWebView.WebView.CoreWebView2.Settings; + if (AppEnvironment.IsDev() is false) + { + settings.IsZoomControlEnabled = false; + settings.AreBrowserAcceleratorKeysEnabled = false; + } + bool hasBlazorStarted = false; + blazorWebView.WebView.NavigationCompleted += async delegate + { + if (hasBlazorStarted) + return; + hasBlazorStarted = true; + await blazorWebView.WebView.ExecuteScriptAsync("Blazor.start()"); + }; + }; + + form.Controls.Add(blazorWebView); + + Application.Run(form); + } + + private static void LogException(object? error) + { + var errorMessage = error?.ToString() ?? "Unknown error"; + if (Services is not null && error is Exception exp) + { + Services.GetRequiredService().Handle(exp); + } + else + { + Clipboard.SetText(errorMessage); + System.Windows.Forms.MessageBox.Show(errorMessage, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } } + + public static IServiceProvider? Services { get; private set; } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Resources/SplashScreen.png b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Resources/SplashScreen.png deleted file mode 100644 index 3caa371185ce8217fc612c29734b2c95e2bc746c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11498 zcmch-XH*km*ETvJigXkN=^)ZXiXgoT3euzrNEZR=y_W!rfS`cVr6W>90O=j17YV%+ zqVyg*2_)nj&-r!ES?BxlKJWXjMMzdMv+q6o-uv3uwP&JrUZ_%#GLixSK%w^hsU83j zf*%QioB!T|7?oiFzz(QAee%+Oeh(33_R?n=f5?&iuJI$GbgrjvdF1k#Kl9i_Ed_ zTF4>-^0kUE5`lRlbLy-9OTT2Qjfu6@tM~KWzV_+SQS4=(g;c;1E!Pcle$J$ow>qS( z4kz}*pGGP^#gOiuO5m`M8MeG)H2CM|;d-te2fvhGkF4&=ZEx|+yhsy<4uv@D8%;*v;(B{c)Xc##?w`xuX+pV9XQL(6b{sJm1po6klA&q<}FV|+?LNCd1t zc=Sl6;Ajh3GcZ6x%MPAgo^bokg`A6$(&Otlsev!hFva!|Z77pO3L()=pg-9pfBU?~ z8e8*!AKP+YQ|8U;O;YOrLV}q&-hcKdppB)oBy7U>%F460WJCI8q7^@55k9GBQj@G9P*BgaI9|4-7Ef+VSa( zOmr*^bTVR^`Mzg6k11j=(&rPWreej!cpg9*V^tE%hB!D~;ERnD`cILsT*Yhd&?ZB^ zTyAwlyfazJNSEd0lHvdZvL&aEw$jkJw2O)hHgsP_ADV3QbpI>+B3jwsg+(asT~rZd zBnM@ZfYDLl{`{Hzn`qF*@9wmQhMTF*JrX__)_sm~Nrp)f1Fj0ES@f2!F%^RD2U+Dg zS+hKPHQC6wF}QK&chg0EyK#XYX7)0os;ZPHXUX@%o4D`hO@OfDI)SiNCmL@e?DPx! zjf11<-NN^a8#vINU4&mTY_IIn5^X2vZCcT8Xzuj}*CljU>#dQ1M1U(6#nyUU3JnY> zotd1hI=dQvdmKPX!%G`;YtTcK_c~SLS8*9$-rUd$ix&SaL{>oSRl53KGrH(_zjdS6 za`ESV4*9_mZP8rQ$GzH23Ub@+sW|(iRT35bA`9epHkX2*Yo}YNSCnY`7D_={TwFF3 zzaePs>g}7hN?qb1B^G zzmn2LIK^yx2NH{Xv0QjIwmVG*Kbhqtf2 zF2irOz4$(cj$;(t?u;N~enTc-Q7KeF%LD(kW#kNF);^G|tTycBar_dJO%)5ZxIG2-tjM(& z$)gq(C#<#hR;6VUmL(@wdv1oXw#&#{oGRq=N)(Z2S6Z*kC!WS$Cje6aQm{oR{oK8{ zE|tM{S5!q*@{?#y&D6=xON$GGJ2GTMfLZUSdpf+dq|X+o^exaiJ}ZQM>xHflZ{_*Ilwl3QEPhg%5dqb3(`u`AMjS@@+J`=tV4*!WryzPef+2vURjoD zSI-oi_2TMie^GnL9#233%q%m6-HBB>1v6o6%<5WeZu&gU4r|ELH}cu6%dKgJ_K|Y% zDEbu!MQwTcK^`_s8p(r^YmScEWi1e8i#CLnd!SRGFZngDX zAx(5N|1Xo)I=H4=Xpp6+XYk;nf2~Qu1ZHMB+g`keo;LY#k{)!CNJ%-I73qH76_0hG zj{F2-)LO_vw1!I1jhlev5G0%6uGV_Y(FF~U31bF1&x@K&87J8pE9oHk!f_#4>cyTe9*$d?4dn zR@jy|w}h(6lty@N*C^%j3i9EvNagOMx1l^juY{qn^KZ-FE>lz%qP>{t0KL5C1XIgGr`8s9DXf||rlX&0qYL%`2CrUVTjDj{H z?Q_-$-l1<$M!e}@*KKUp-rw;Ue-%8{JJK$>ee`XlU7U}PFGUWdber+J=;7=V zBB`=_hqm z!||bcGk&X+HS@34uTPXe^&6s>C8hBYOvvawYGZjdaGL65AMG+ zi1hREX?8yiao1W*=sD(&(q@)${fwq8{5hth^RjNED;%=@u5{wM&5x0|9M*zqw8{&H z4lAu#ZZ9n@eXYG{ur+jk7op@L1dggPTbFj^iU2dd>1-yx#rrBaHN$y*_UAl(_R#ni z;g6B+T9=@!Ba1eT=?5J0+vUwE&z^mdL}!(k@0&N`vF&~e+xu;!J0+n#uo>GQBMjJO zvAgqiEmh55zJ8LOzq!`Hkn}!EO(TPanfA|oK(?gSD|`n1@cN7Mx!=bd%?kKsbiC;L zXGX45pOk^WG)Uy>&l7{Dx6@PFSAVlR!Yq23S)@F>PaMAL7ivnoSE3i(_T;P7K#cC= znbwTWlD*#p9V-rz3@v=IF#-b#iPXUJ43^b#K++F>G8C092f4^>c5zURD#^>kL>=3y zXU11l9Az{mQp67Hz5PCMebqUnV<9LhvHf3(=;o`WFqc%e(oAf~9_`3v<}3iwAJd-R z@E2w!%=e3zJKQeOFG6@BNIwnAV;7!m9XIat@ZH`x=zt7Cf|DTWHW;m~JPvVIYx~L4 zDL)~v)Sc)Psc$nve|XseVdlMyKZzA>rM??tix-3Cg^5M?;WpF1U*Uh|U&>*+qgU*P zTbkHt1TCCbo_!GWL+0v27mGd^E&67e!l-TnvumB_Kzh2#lK+ab1aj#Uz0jKMWnK3- z|9}7iv$T2CfU@%C9Gin@S}e&1x3(Yj_H?nqz`!B1F%e+X5#dJY7GP^sA!o4o>!-C? zD1yJ1gO^Jx4>?h4FnFaUIj?Pfn)m_DE77tTcej7Ph#mM7VmGx?YZ)A%Dd=?B?sld1 z#_*Zuw_+yLNJzbrBrPFG{oD;`M)@oDQ^pMaywi}0i^++RltG^hUy!~7I@}MZ#X_%XAM4=+|)`cN~ zKT>C?L-i(J+e1rSJnO3jmJL7!RK?y(QVhfG+C`~@y(Q@C!Io}i{dw`nk9ncr5qGb8HRhJ_>=efIMq9#EwaW68MA1rLd#+qs3Pf=4(Ayd$S zVU~Z$mx@{s4S6+w&T0L6YF zKRMf~$_EBL=)yrWlb=OkK5H^g%1DYi#T8V6Lx%3EyT-wvb)*5i**E${nBj*#PXP|S`Y@d{`mEzPyH?(EojdAlp?Uvt+Qmh zdYmwfsv#T*lT~vd+!jt3G;82Jzisc5e1b{;X?+tEuU~lhv=mc?AUgzHuf;VkJRu_- z=>&v8HtNV$`(_v!ylxlfz@rCa75mLMABr3mIf#5aVvzjeKH{eljDBDb?ferw^sUMlY>a?A1Abx1g)2S z{l9*iya%JP(Q(gR)L`f3Rn!AAw{q#;0bvitcDN{(n933%fM%NTi^0{sJt9zH_a}D0 zqyBEK1YRQ*pF;^s@L8HqpFHp>Zna%G|8Z4o}6%d&;zz=0CQTi9jWCIF4 zZtWM@f=CxjVFk6p)wMOGPAI6D-~>8+e8Ah`7U@^nSx-Qw`~AC8!SBBfScQ)_sDUn% zh#DCHq)3}hv{J7A>!)NtG?(TBd)fC$NELzCACprIpJ#S;MjVj=Fhg8E8%}YMIYwi! zL>64rtw`JNrN++icU>r*P&!Wqtj#tcay%%h1}M%w3IoD+ahH~s8kt>t3iuNBHO|yM z3UX#I+&G*1x2^(>)OPd{uNX0iDJUJQl5Q#c+R|ixx&hlD)T>7S)&5(0>jxE~Y!Zwz zB~sv0L3i%>fI3Spb6~vZn5REaM#uZ5ec*+2v?ykEB2?D})VIpm<3u4YxK;N@=(H`& z8!ym`U&`Qu|HWd;-*<{^livjVKgJ(XY2>JZOwVw!X*5gXnBMPUY#e2>WTG||)^vGi z0^9?M@i+E*i5#!7~wh#*SSG!GG+K3`usF_GTDA}^*h>JNuh#rt2aKq;eiLgJox7tHL-PapQ zb!7ONAs@0GU8~@?H8dd9ie;U;2J)80W*PbM9aD3$`i_oA_~YNoPBV+EGQ8o}!RdlP zi{RR+5nGnjL~_o9%Uzv;ujA_5AjkA?0L$}ggbdYGwQaUm>pjr&PXMiFh}@`&+T5L+ zu=E$)Nj*n?pK9>rGe^+ZT?I(oxz;uJJmxi;S4z+0&+9{MfSO>YU06G^R3@dY$$CH) z^9UHUh2y!e(Yoj(k1e<#23! zyHnV@0w+hHiqgrJC=34L2z_|u7RL(nJ<3)rILl&W3h~)p{qbYA0a`?E&ygwZvmUL` z_QDpp8D>>fS-cCYKZmur0DXQ;GAbF_@}_rl8llkUvy8&R!heGUpG?tgIXh{;>COBj zUCc%Cl>CEaw=eL}?Mc4Gu5e-)5o_pCt*G7SNun^|ZroCcon?1YG~WP8c;&-_@gT?D z)smly2%81`$v8YFMtU2_&7~i$Je@4W!=N%!i**+|zrMC#?TMbrD+1(B2n0PVqM^3S zo>4x-%wVdL?^3{54n&BtH};_?ex~5WfRU0Z=xDV>HKaa5yA2=$+KzoN5>&9zC3}&e zv$u7%Ip+BO9C-HL>v%91+KddQzSLe};wa;X!`><{2(bkh_Kd>Yz#V@Dg}Gc9Ojp~D z1il{WkL+E)WbGHPaX%vl*@rH26#GYcBwHQ=PZTfRbf1Gz*#y~C5`a={>#6=wdaX8_ zoZ$+=g6HA*nnHU@^WUEee!jjx?Le?^by9%1BDzM}aqzaG)b?bQAt*0!&@wwDvd1n2 zJOJXcHNIBAv~&)BXKFgq?|~yD)j*lxYytvUns2iMqilWcSL^WR9LbmNPgQoxX6mp0 z4$QD!c0s{O#2Nb^@?OICE^m)9eL4Ov@XW3AMToMFPOeOaBZ$$>;H+%p->l5z?ml0B zNj2&`Ak|+Qtn*gMU^H}j`6$Cpz5TG)yWpHx!mO(W+qJNSu7ng{3f0z}950A$-1pNH zxjTO$Jur|6OBJ##3=9Vpg_@d$T29Z}7UhUjkZE<^1HlF33tEO{VB%!?;dKqB{!sEy zjLwc|=Rvp{7pA_BlU8F|N`=_$1Rp-gf1{kI^I0b@;|X8Kx4XQ8CFq4EWp3*YkpE3O zBJ)fV!TDb6C8b;~X1IM1SJsLv%Sal*93wzPiHqA`nBi}3YLajXsn(x&*Gv@(m2sTx z2YK6upDkQ;K?~w0NeGq@>owK*snvbV$?PGBS11yiK1-kEnOp(f>VW9 zO&;6ywKhshF&J(Z_c+;<4{p5rwy`92XkY&YQzkE(s%%N7(|OA7F)v<;po~L(u#kcek&vGF3qEW@2RPi3-4)o z$hivcsK&iaPI}qzS~+g&*0{gHPcMazh>2<1wOewqpKSJRG=W`A=iqF{I$vhQ?=Uf* zy0z7gM0~wQ2vn}G1^tU!p9zP`razZHXnr8iv1uB=r-{kxNH?2*SI z4Kti_wuhEEOQMYjHY}M7Jl_Ky)T6})@@-4<^6G2__2Yd(s~=HF#a-caC#MYFd#?{b z^&0*QtVgV!9WZCbd-K0I#s^#7|Nohws0OS5Kb%YdKW{j&1OM^uh})lk++_YlPVx$J z%siw}zb|Qqm=%A?pGeJ=NljCx!|S`jCYs(nD>F$NpIYtubqy8p%pf4@0Ur(FpBQCb z*ijv}x^dXve$V|ccFGa;VfOW*LX@M1{_QS_r3`MYoTz|nPgf{I0{Lih(k+gwv#S}K zeOAgMCB9mvj|W#C0jIv9%btR;QLjO6psk2((seG4?@G-|4Hrp&Eaz!tTi6#p(#gQ5 z+k6~PdFA-@!^J0|L1&Q~-=vhC&+lFQJA|f^V`$J z)o$Tfb+zOEFT`FdrQ)1oO*LjYxZYoXeaA?@F-8_q1}G3&q|nVAM}g*98=*&}(1)QC z-SOH zCr3`lp*LSgD1B{4nzIt78g2^g$trol44CRw3VvI(yBBA_YTyW>m|OM~tzpP;L5MTV z$I|9H!CocQcsh(%B7zx+72jSw9zv37Y$xI;m}wapbIB$HZK{*R{dOV5tZTiHv{(ap%r<(Omdy>Flowt24Tsh0+~1pOr6>y0NZ4Xjh&BCH({RSGAp zy~RNw7V9z$V~J2@+!eC`zkfo&IHV9H&h?4uYY$6r#7v*}6{C4>U74PxInvPoM+o(q zu9s8Pi<(YxnIWETBA)IWP)IAM5+N;Vgw9P%gU2$w!9mwu4bnif5nL267fOv(lRE=U^~N9#@Y8#|MO^4(#aPjhoE|^joqV+ zy9G^s=E%4pz~vw%_+fr_asDsX)w6||Qwf_@p^*~)%#7WT_-lBoNni>#m&FjwyM_=p zAcE_ZG~#>i54g>KT_$4!O;;uf5Ubw!Ms2x@KmGIjJET1^Txtbt7qLQnC?86w+c)cy z#|3vT%7%k{-p%{?{ibQ}xcA%L?`ljyhSD^oYJFX5d5yHa-P1vD|H|5Mj|a&TGN8L0 zd3s{m^7fQ$nQ~nz>ahPySd`)LCvu8P6LlZu1ZQh;6`RXdD3sMH@diY6>JCg0t&X3u>8fhy z;Z)Jh4f!@I>Pb@rJ?p=6T3!E&$WW1g5d6$&X`3@GAaeipPUH>@e)ZmVYhoJlNc)v5 zyy=;EDpZ%x=%Jd)&+hAp+hDa;%?Q!v;M(yOqhY(JWG-iUo*b}w*Rj@ChsK6mBX9rO zRakCnLcfM*73=cNARbk0lsqwex*&nD@j0uXr)NTG?bP0*@WpAz!j0?u-?Gqj`mD4% ze5(t5eqLj%TsZulX3(;jL#ega_$JBEUraNFQkG0E<<&ay&b+bs>QK-lO5dLcGN8fL zgsil$jIFlTg)0e`oQ#|wfVzsPB!#c!Ds}Opz zulIUNHZ`@y*28cnpq1{XmSTdSb5E^!#ldA_h>vwf-mtxc9~`Tj8NPtOqmj^k{ODV= z-x}L6KeoKUg1)$QV_j+>+6p3li)=3&bjsA1mINmT_KF!;Iu9$X-;>|`l^*^muB&rd zL@ys@EZ=wJ#F#9?^+6aXME`1dS8GPs6>cIPyOPUsw%gdSs4 zkLyeY`b#?mWF&Uzj|GPNz#Rf%oHD)iLmhi!r&f(?Y>JMRPdaf>C zPwE-B*Lh^>k$}0g*n_q7jEz@=4@3pZ!6Ign{-Y8}^l{_~=q{7R?|Rx?L7mIl)~%HB zArl{$t*$`g;hk>|9?Rds7D_EqK~g_P^5}`ub2~Q~_N1^W`R!B1YMaWuVf1uVPejjY z=YxX#krl7f?`Mw;7N{AX{!)t+U)jgzU*|eqcA$9^&Ssh+=t@I3b5^$7ig%#*FLq&XqBgfa|%vVwaYoXOQDmF5{R>d7as3S?*$LV@mtWREtpE( zyo7railjG}W!HlD8$YiWXK%=RAgFV25#L@V8>m#7noUHzMSn==7F1MrQk`CJODVlE%2|lSA5CC&~78v;&mY4Rt;2@wL8k-TB)`hB`Qk#Nej99iJHUs3mblu-7N@GUpN-4}ld5lb zpTj6s7QM$o3KC=y{z||oaOQU|prxc)xfXw*-fGYspk>`W74oyuHHA|rSa0)m^^F&D z53cY)UfKBcgqm@#+m1TJ=)4ir*L|m#MG)UpRJrRy9GH?fw&>H7C>*t+e`vb+-E_M& zo6Fh9RV=-kv|RQiur;AS0C6^NLV=i_*~uRF#0{b7=AaseBi0du0pl81VCn`51Z>f} z6NT+*XdHek*oZ9}CAGn9#P2cJ8@B3up0icgcW3v7TZ>Dz5i$<+jQL|i|oTtf&8$3uy?fS$gVjsk8PB^Q|3wp<|50? zJq?B?@rUOAC?^rW1*e4D_{Brp50ZbGlOl(2*OiXP-AS3<6Lz%U+tgn=Us0^YBA!$b z^_96Q?-|_nSbENI5+ppZ%MgZE`aSx##&6EmfpGe|BWO5?^}*qzC(93(b8E}B1rj2~ z^M{R%hkxWYz?brNN&{DNf0-;A`{t5p6~r+q)4X}_^Nukuys_74uE>zjgV>9|Hg@lf zRR1nGw|WGpy`&By{cdNnV=A)?J4OSwiK(}G;etj64t=Fe%s!HWyyY~+$-~#C89JeW zz7n?9v0%7q5;eqf7+lIUznhFrQoAyQ+3hQ^$7T|ClwOAVC_{(ktPCtg6Y_(mc@E$G z?TiWS^HQou$yJy)W$N)GW~uYJ*b(%eHb(Hq{cgK_sVEAS%T;VDem>wI)r;Q%o`KLGJk3c zl?%W0Sh<()ValmYD87^-KI&G}Um(}lb&XxFQGseX@Qjdob6ceBqv|yuPe0k}{TRQ| z59MYc-Lg#%Oe|?``YB+CyGJFz`ls{hE{0oGOPR@M;M%o+VL-aFtgA#{>)2kdLB>ap zQW@fes*#eC=o}240;_O(vj}QI@Y8pi_LXI|9Qj&lz=?4_E7N@~;eL>tX9HGz#_{A9 z;plh*xjR_3NSeCZZHj^$=wS+?3)u=cE6v`qBC z4pf!o(XWN9=IROu_p`&aFNOmk>c98r6k#!d-)0HUx3PO{L1gs^eHlKL$>>KK5N1948K5{N`u_lOFZe)U=<_#@H zLj-=W6%XE@e^StJhgTG0a#nXO#lIgBsu}$5%U8xMY9=!sJt6P$92ri{2GKj`(UIPU zmTn?i&P0)F^Mhx+%dw2mkZ3?=GF{Gyr^wUoRo4^H|a z>TWyDQT5KhX^rMre^j=-F9ktc$F1lCnhYGn7bihjdTjor{YG?BZ-T8!>5#N}q10I* zswlC$uTWUf+rp|KZ>pGV@;*-_38Xs+rV;78l5BMDb&ngvG#~3|iON-kt*JG)CR8{$ zi`FnA>Q?1AfHFaRmIMCh;)|GTirD_=c{(k%MUwK|uYx^it~hvx7HTDlH`+Vy2n>8A%LnYy@5n{M#ERm z)GhY=^2yV~%9`*p_!hYR{_Ua&l>%BnuG=>bsu@2Y#K^dgY@^n=)rU^!+?c(;%;E07 zt3~omfXV=NBd)&Qa9=+RRNi16@`glOp}sF*Gz#9m-QD%nndJ8J!v|iYPnwjP6$1-O z>NqlHZ1MC6-t{5{+e?2w1YNZaB?cvBx^%OE?(QG{tTRe;k2+HdV*AH_u%!kWPO(7a z`NvT0wEd;fm&ISX_3P0}in31|6tS|6(=Relq-ll_#o-yMeRwCIUvEGZoVnxsvb|!F zj+o5Tot?o%CZ#}&21)vHXz`Fyy78)&F7*pRJq-C3Od}D_vWij2Qd;QQy0(bs2Ov+aVjwSgZuY?}Lw|kW&#qOj`nnF@Kh65am zmf*G}T+Xx*<~7psA+wzw^F48E>-*g|$lSgR7L$uTuzgw_>IEHxl$MX)bHfxJX#II- zVGn9nRlE9bC2(fpm|UMI>d}>OULzCstGguh`QYN8w*oJ#_?7b;HR7Oqo`TUX`He#u zi80yS)r#*+K*zyTaL)7podY$lJQD`~Py+w?v!eemMC({zuRA{=0DZc|37{&E{Ek*HH{;4^Vsd;%T|k>-YZ)kt6ar diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsDeviceCoordinator.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsDeviceCoordinator.cs index 86f542899e..54debd7d64 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsDeviceCoordinator.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsDeviceCoordinator.cs @@ -1,12 +1,15 @@ //+:cnd:noEmit +using Boilerplate.Client.Core.Styles; + namespace Boilerplate.Client.Windows.Services; public partial class WindowsDeviceCoordinator : IBitDeviceCoordinator { - //#if (framework == 'net9.0') public async Task ApplyTheme(bool isDark) { - App.Current.ThemeMode = isDark ? System.Windows.ThemeMode.Dark : System.Windows.ThemeMode.Light; + //#if (framework == 'net9.0') + Application.SetColorMode(isDark ? SystemColorMode.Dark : SystemColorMode.Classic); + Application.OpenForms[0]!.FormCaptionBackColor = ColorTranslator.FromHtml(isDark ? ThemeColors.PrimaryDarkBgColor : ThemeColors.PrimaryLightBgColor); + //#endif } - //#endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs index c10bb849e8..3072c5c562 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs @@ -32,7 +32,7 @@ public int Start(CancellationToken cancellationToken) ctx.Redirect(url); - await App.Current.Dispatcher.InvokeAsync(() => App.Current.MainWindow.Activate()); + Application.OpenForms[0]!.Activate(); await Routes.OpenUniversalLink(ctx.Request.Url.PathAndQuery, replace: true); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsStorageService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsStorageService.cs index dedc59870a..89f82a275e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsStorageService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsStorageService.cs @@ -9,17 +9,17 @@ public partial class WindowsStorageService : IStorageService if (tempStorage.TryGetValue(key, out string? value)) return value; - return App.Current.Properties[key]?.ToString(); + return Application.UserAppDataRegistry.GetValue(key) as string; } public async ValueTask IsPersistent(string key) { - return App.Current.Properties.Contains(key); + return string.IsNullOrEmpty(await GetItem(key)) is false; } public async ValueTask RemoveItem(string key) { - App.Current.Properties.Remove(key); + Application.UserAppDataRegistry.DeleteValue(key); tempStorage.Remove(key); } @@ -27,7 +27,7 @@ public async ValueTask SetItem(string key, string? value, bool persistent = true { if (persistent) { - App.Current.Properties[key] = value; + Application.UserAppDataRegistry.SetValue(key, value ?? string.Empty); } else { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props index bf2210e1c7..fcd8c5eb32 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props @@ -8,6 +8,7 @@ true $(NoWarn);CS1998;CS1591 $(WarningsAsErrors);CS0114 + true @@ -57,6 +58,7 @@ + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props index 919457c075..1075a9f1c5 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props @@ -19,7 +19,7 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props index 57bbca2cae..d50dc52623 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props @@ -19,7 +19,7 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/AppControllerBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/AppControllerBase.cs index e42b3631f0..0f1711e787 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/AppControllerBase.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/AppControllerBase.cs @@ -4,7 +4,7 @@ public partial class AppControllerBase : ControllerBase { [AutoInject] protected ServerApiSettings AppSettings = default!; - [AutoInject] protected IConfiguration Configuration = default!; + [AutoInject] protected ServerApiSettings Settings = default!; [AutoInject] protected AppDbContext DbContext = default!; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.SocialSignIn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.SocialSignIn.cs index 956c4b48bf..a86aca127c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.SocialSignIn.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.SocialSignIn.cs @@ -95,7 +95,7 @@ public async Task SocialSignInCallback(string? returnUrl = null, i } if (localHttpPort is not null) return Redirect(new Uri(new Uri($"http://localhost:{localHttpPort}"), url).ToString()); - var webClientUrl = Configuration.Get()!.WebClientUrl; + var webClientUrl = Settings.WebClientUrl; if (string.IsNullOrEmpty(webClientUrl) is false) return Redirect(new Uri(new Uri(webClientUrl), url).ToString()); return LocalRedirect($"~{url}"); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Extensions/HttpRequestExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Extensions/HttpRequestExtensions.cs index 3512ff5db3..306f709ca9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Extensions/HttpRequestExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Extensions/HttpRequestExtensions.cs @@ -20,9 +20,9 @@ internal static Uri GetBaseUrl(this HttpRequest req) internal static Uri GetWebClientUrl(this HttpRequest req) { - var configuration = req.HttpContext.RequestServices.GetRequiredService(); + var settings = req.HttpContext.RequestServices.GetRequiredService(); - var webClientUrl = configuration.Get()!.WebClientUrl; + var webClientUrl = settings.WebClientUrl; if (string.IsNullOrEmpty(webClientUrl) is false) return new Uri(webClientUrl); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Middlewares.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Middlewares.cs index 6b437d9a04..cd0f59b765 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Middlewares.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Middlewares.cs @@ -14,7 +14,9 @@ private static void ConfigureMiddlewares(this WebApplication app) var configuration = app.Configuration; var env = app.Environment; - var forwardedHeadersOptions = configuration.Get()!.ForwardedHeaders; + ServerApiSettings settings = new(); + configuration.Bind(settings); + var forwardedHeadersOptions = settings.ForwardedHeaders; if (forwardedHeadersOptions is not null && (app.Environment.IsDevelopment() || forwardedHeadersOptions.AllowedHosts.Any())) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs index 23991fc5fa..5512537acc 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs @@ -34,7 +34,9 @@ public static void AddServerApiProjectServices(this WebApplicationBuilder builde var env = builder.Environment; var services = builder.Services; var configuration = builder.Configuration; - var appSettings = configuration.Get()!; + + ServerApiSettings appSettings = new(); + configuration.Bind(appSettings); services.AddScoped(); services.AddScoped(); @@ -118,7 +120,10 @@ public static void AddServerApiProjectServices(this WebApplicationBuilder builde policy.SetPreflightMaxAge(TimeSpan.FromDays(1)); // https://stackoverflow.com/a/74184331 } - var webClientUrl = configuration.Get()!.WebClientUrl; + ServerApiSettings settings = new(); + configuration.Bind(settings); + + var webClientUrl = settings.WebClientUrl; policy.SetIsOriginAllowed(origin => AllowedOriginsRegex().IsMatch(origin) || @@ -214,7 +219,12 @@ void AddDbContext(DbContextOptionsBuilder options) .ValidateDataAnnotations() .ValidateOnStart(); - services.AddSingleton(sp => configuration.Get()!); + services.AddSingleton(sp => + { + ServerApiSettings settings = new(); + configuration.Bind(settings); + return settings; + }); services.AddEndpointsApiExplorer(); @@ -272,7 +282,8 @@ private static void AddIdentity(WebApplicationBuilder builder) var services = builder.Services; var configuration = builder.Configuration; var env = builder.Environment; - var appSettings = configuration.Get()!; + ServerApiSettings appSettings = new(); + configuration.Bind(appSettings); var identityOptions = appSettings.Identity; var certificatePath = Path.Combine(AppContext.BaseDirectory, "DataProtectionCertificate.pfx"); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs index 46409dcf14..79041e59da 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs @@ -22,13 +22,15 @@ public static void ConfigureMiddlewares(this WebApplication app) var configuration = app.Configuration; var env = app.Environment; - var forwarededHeadersOptions = configuration.Get()!.ForwardedHeaders; + ServerWebSettings settings = new(); + configuration.Bind(settings); + var forwardedHeadersOptions = settings.ForwardedHeaders; - if (forwarededHeadersOptions is not null - && (app.Environment.IsDevelopment() || forwarededHeadersOptions.AllowedHosts.Any())) + if (forwardedHeadersOptions is not null + && (app.Environment.IsDevelopment() || forwardedHeadersOptions.AllowedHosts.Any())) { // If the list is empty then all hosts are allowed. Failing to restrict this these values may allow an attacker to spoof links generated for reset password etc. - app.UseForwardedHeaders(forwarededHeadersOptions); + app.UseForwardedHeaders(forwardedHeadersOptions); } if (CultureInfoManager.MultilingualEnabled) @@ -140,9 +142,7 @@ public static void ConfigureMiddlewares(this WebApplication app) .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(AssemblyLoadContext.Default.Assemblies.Where(asm => asm.GetName().Name?.Contains("Boilerplate.Client") is true).ToArray()); - var webAppRenderMode = configuration.Get()!; - - if (webAppRenderMode.WebAppRender.PrerenderEnabled is false) + if (settings.WebAppRender.PrerenderEnabled is false) { blazorApp.AllowAnonymous(); // Server may not check authorization for pages when there's no pre rendering, let the client handle it. } 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 f156819583..6eda668616 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 @@ -26,7 +26,12 @@ public static void AddServerWebProjectServices(this WebApplicationBuilder builde services.AddClientWebProjectServices(configuration); - services.AddSingleton(sp => configuration.Get()!); + services.AddSingleton(sp => + { + ServerWebSettings settings = new(); + configuration.Bind(settings); + return settings; + }); //#if (api == "Integrated") builder.AddServerApiProjectServices(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Boilerplate.Shared.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Boilerplate.Shared.csproj index a38fb10801..cb3169506b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Boilerplate.Shared.csproj +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Boilerplate.Shared.csproj @@ -10,7 +10,6 @@ - diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ISharedServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ISharedServiceCollectionExtensions.cs index f9ec763658..81404dda86 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ISharedServiceCollectionExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ISharedServiceCollectionExtensions.cs @@ -13,7 +13,12 @@ public static IServiceCollection AddSharedProjectServices(this IServiceCollectio services.AddScoped(); services.AddScoped(); - services.AddSingleton(sp => configuration.Get()!); + services.AddSingleton(sp => + { + SharedSettings settings = new(); + configuration.Bind(settings); + return settings; + }); services.AddSingleton(sp => { JsonSerializerOptions options = new JsonSerializerOptions(AppJsonContext.Default.Options); From ece36acdbb9fac48621ffb9e9a91e2d3994e98ef Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Thu, 21 Nov 2024 19:20:45 +0330 Subject: [PATCH 02/87] feat(blazorui): add BitNavBar component #8784 (#9270) --- .../Components/Navs/{Nav => }/BitNavMode.cs | 7 +- .../Components/Navs/NavBar/BitNavBar.razor | 51 ++ .../Components/Navs/NavBar/BitNavBar.razor.cs | 527 ++++++++++++++++++ .../Components/Navs/NavBar/BitNavBar.scss | 130 +++++ .../Navs/NavBar/BitNavBarClassStyles.cs | 34 ++ .../Components/Navs/NavBar/BitNavBarItem.cs | 64 +++ .../Navs/NavBar/BitNavBarNameSelectors.cs | 64 +++ .../Components/Navs/NavBar/BitNavBarOption.cs | 97 ++++ .../Navs/NavBar/_BitNavBarChild.razor | 27 + .../Navs/NavBar/_BitNavBarChild.razor.cs | 21 + .../Styles/Fluent/typography.fluent.scss | 2 +- .../Bit.BlazorUI/Styles/components.scss | 1 + .../Navs/NavBar/BitNavBarDemo.razor | 28 + .../Navs/NavBar/BitNavBarDemo.razor.cs | 482 ++++++++++++++++ .../Navs/NavBar/BitNavBarDemo.razor.scss | 43 ++ .../Pages/Components/Navs/NavBar/MenuItem.cs | 8 + .../Navs/NavBar/_BitNavBarCustomDemo.razor | 54 ++ .../Navs/NavBar/_BitNavBarCustomDemo.razor.cs | 139 +++++ .../Navs/NavBar/_BitNavBarItemDemo.razor | 40 ++ .../Navs/NavBar/_BitNavBarItemDemo.razor.cs | 94 ++++ .../Navs/NavBar/_BitNavBarOptionDemo.razor | 69 +++ .../Navs/NavBar/_BitNavBarOptionDemo.razor.cs | 57 ++ .../Pages/Home/ComponentsSection.razor | 3 + .../Shared/NavMenu.razor.cs | 1 + .../compilerconfig.json | 6 + 25 files changed, 2046 insertions(+), 3 deletions(-) rename src/BlazorUI/Bit.BlazorUI/Components/Navs/{Nav => }/BitNavMode.cs (58%) create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor.cs create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.scss create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarClassStyles.cs create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarItem.cs create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarNameSelectors.cs create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarOption.cs create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/_BitNavBarChild.razor create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/_BitNavBarChild.razor.cs create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor.cs create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor.scss create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/MenuItem.cs create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor.cs create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.cs create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.cs diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Nav/BitNavMode.cs b/src/BlazorUI/Bit.BlazorUI/Components/Navs/BitNavMode.cs similarity index 58% rename from src/BlazorUI/Bit.BlazorUI/Components/Navs/Nav/BitNavMode.cs rename to src/BlazorUI/Bit.BlazorUI/Components/Navs/BitNavMode.cs index feab22a410..552b8bc4bf 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Nav/BitNavMode.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/BitNavMode.cs @@ -1,14 +1,17 @@ namespace Bit.BlazorUI; +/// +/// Defines the mode in which navigation is handled by the nav component. +/// public enum BitNavMode { /// - /// the value of selected key will change using NavigationManager and the current url inside the component + /// the value of selected key will change using NavigationManager and the current url inside the component. /// Automatic, /// - /// selected key changes will be sent back to the parent component and the component won't change its value + /// selected key changes will be sent back to the parent component and the component won't change its value. /// Manual } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor new file mode 100644 index 0000000000..1d98fbc641 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor @@ -0,0 +1,51 @@ +@namespace Bit.BlazorUI +@inherits BitComponentBase +@typeparam TItem + + \ No newline at end of file diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor.cs new file mode 100644 index 0000000000..8a3c1260ed --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor.cs @@ -0,0 +1,527 @@ +using Microsoft.AspNetCore.Components.Routing; + +namespace Bit.BlazorUI; + +public partial class BitNavBar : BitComponentBase, IDisposable where TItem : class +{ + private bool _disposed; + internal TItem? _currentItem; + internal List _items = []; + private IEnumerable? _oldItems; + + + + [Inject] private NavigationManager _navigationManager { get; set; } = default!; + + + + /// + /// Items to render as children. + /// + [Parameter] + [CallOnSet(nameof(OnSetParameters))] + public RenderFragment? ChildContent { get; set; } + + /// + /// Custom CSS classes for different parts of the navbar. + /// + [Parameter] public BitNavBarClassStyles? Classes { get; set; } + + /// + /// The general color of the navbar. + /// + [Parameter, ResetClassBuilder] + public BitColor? Color { get; set; } + + /// + /// The initially selected item in manual mode. + /// + [Parameter] public TItem? DefaultSelectedItem { get; set; } + + /// + /// Only renders the icon of each navbar item. + /// + [Parameter, ResetClassBuilder] + public bool IconOnly { get; set; } + + /// + /// A collection of items to display in the navbar. + /// + [Parameter] + [CallOnSet(nameof(OnSetParameters))] + public IList Items { get; set; } = []; + + /// + /// Used to customize how content inside the item is rendered. + /// + [Parameter] public RenderFragment? ItemTemplate { get; set; } + + /// + /// Determines how the navigation will be handled. + /// + [Parameter] + [CallOnSet(nameof(OnSetMode))] + public BitNavMode Mode { get; set; } + + /// + /// Names and selectors of the custom input type properties. + /// + [Parameter] public BitNavNameSelectors? NameSelectors { get; set; } + + /// + /// Callback invoked when an item is clicked. + /// + [Parameter] public EventCallback OnItemClick { get; set; } + + /// + /// Callback invoked when an item is selected. + /// + [Parameter] public EventCallback OnSelectItem { get; set; } + + /// + /// Alias of ChildContent. + /// + [Parameter] + [CallOnSet(nameof(OnSetParameters))] + public RenderFragment? Options { get; set; } + + /// + /// Enables recalling the select events when the same item is selected. + /// + [Parameter] public bool Reselectable { get; set; } + + /// + /// Selected item to show in the navbar. + /// + [Parameter, TwoWayBound] + public TItem? SelectedItem { get; set; } + + /// + /// Custom CSS styles for different parts of the navbar. + /// + [Parameter] public BitNavBarClassStyles? Styles { get; set; } + + + + internal void RegisterOption(BitNavBarOption option) + { + _items.Add((option as TItem)!); + StateHasChanged(); + } + + internal void UnregisterOption(BitNavBarOption option) + { + _items.Remove((option as TItem)!); + StateHasChanged(); + } + + + + protected override string RootElementClass => "bit-nbr"; + + protected override void RegisterCssClasses() + { + ClassBuilder.Register(() => Classes?.Root); + + ClassBuilder.Register(() => IconOnly ? "bit-nbr-ion" : string.Empty); + + ClassBuilder.Register(() => Color switch + { + BitColor.Primary => "bit-nbr-pri", + BitColor.Secondary => "bit-nbr-sec", + BitColor.Tertiary => "bit-nbr-ter", + BitColor.Info => "bit-nbr-inf", + BitColor.Success => "bit-nbr-suc", + BitColor.Warning => "bit-nbr-wrn", + BitColor.SevereWarning => "bit-nbr-swr", + BitColor.Error => "bit-nbr-err", + BitColor.PrimaryBackground => "bit-nbr-pbg", + BitColor.SecondaryBackground => "bit-nbr-sbg", + BitColor.TertiaryBackground => "bit-nbr-tbg", + BitColor.PrimaryForeground => "bit-nbr-pfg", + BitColor.SecondaryForeground => "bit-nbr-sfg", + BitColor.TertiaryForeground => "bit-nbr-tfg", + BitColor.PrimaryBorder => "bit-nbr-pbr", + BitColor.SecondaryBorder => "bit-nbr-sbr", + BitColor.TertiaryBorder => "bit-nbr-tbr", + _ => "bit-nbr-pri", + }); + } + + protected override void RegisterCssStyles() + { + StyleBuilder.Register(() => Styles?.Root); + } + + protected override async Task OnInitializedAsync() + { + if (ChildContent is null && Options is null && Items.Any()) + { + _items = [.. Items]; + _oldItems = Items; + } + + if (Mode != BitNavMode.Automatic && SelectedItemHasBeenSet is false && DefaultSelectedItem is not null) + { + await AssignSelectedItem(DefaultSelectedItem); + } + + await base.OnInitializedAsync(); + } + + + + private void OnLocationChanged(object? sender, LocationChangedEventArgs args) + { + SetSelectedItemByCurrentUrl(); + + StateHasChanged(); + } + + private void SetSelectedItemByCurrentUrl() + { + if (Mode is not BitNavMode.Automatic) return; + + var currentUrl = _navigationManager.Uri.Replace(_navigationManager.BaseUri, "/", StringComparison.Ordinal); + var currentItem = _items.FirstOrDefault(item => string.Equals(GetUrl(item), currentUrl, StringComparison.OrdinalIgnoreCase) || + (GetAdditionalUrls(item)?.Any(u => string.Equals(u, currentUrl, StringComparison.OrdinalIgnoreCase)) ?? false)); + + if (currentItem is not null) + { + _ = AssignSelectedItem(currentItem); + } + } + + private void OnSetParameters() + { + if (ChildContent is null && Options is null && Items != _oldItems) + { + _items = Items?.ToList() ?? []; + _oldItems = Items; + } + } + + private void OnSetMode() + { + if (Mode is BitNavMode.Automatic) + { + SetSelectedItemByCurrentUrl(); + _navigationManager.LocationChanged += OnLocationChanged; + } + else + { + _navigationManager.LocationChanged -= OnLocationChanged; + } + } + + private async Task SetSelectedItem(TItem item) + { + if (await AssignSelectedItem(item) is false) return; + + if (item != SelectedItem || Reselectable) + { + await OnSelectItem.InvokeAsync(item); + } + + StateHasChanged(); + } + + private string GetItemKey(TItem item) + { + return GetKey(item) ?? Guid.NewGuid().ToString(); + } + + private async Task HandleOnClick(TItem item) + { + if (GetIsEnabled(item) is false) return; + + if (Mode == BitNavMode.Manual) + { + await SetSelectedItem(item); + } + + if (SelectedItem != item || Reselectable) + { + await OnItemClick.InvokeAsync(item); + } + } + + private string GetItemCssStyle(TItem item) + { + var itm = Styles?.Item; + var style = GetStyle(item); + var selected = SelectedItem == item ? Styles?.SelectedItem : string.Empty; + return $"{itm} {style} {selected}".Trim(); + } + + private string GetItemCssClass(TItem item, bool isEnabled) + { + var itm = Classes?.Item; + var @class = GetClass(item); + var selected = SelectedItem == item ? $"bit-nbr-sel {Classes?.SelectedItem}" : string.Empty; + var disabled = isEnabled ? string.Empty : "bit-nbr-dis"; + + return $"bit-nbr-itm {itm} {@class} {selected} {disabled}".Trim(); + } + + + + private string? GetClass(TItem item) + { + if (item is BitNavBarItem navItem) + { + return navItem.Class; + } + + if (item is BitNavBarOption navOption) + { + return navOption.Class; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.Class.Selector is not null) + { + return NameSelectors.Class.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.Class.Name); + } + + private string? GetIconName(TItem item) + { + if (item is BitNavBarItem navItem) + { + return navItem.IconName; + } + + if (item is BitNavBarOption navOption) + { + return navOption.IconName; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.IconName.Selector is not null) + { + return NameSelectors.IconName.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.IconName.Name); + } + + private bool GetIsEnabled(TItem item) + { + if (item is BitNavBarItem navItem) + { + return navItem.IsEnabled; + } + + if (item is BitNavBarOption navOption) + { + return navOption.IsEnabled; + } + + if (NameSelectors is null) return true; + + if (NameSelectors.IsEnabled.Selector is not null) + { + return NameSelectors.IsEnabled.Selector!(item) ?? true; + } + + return item.GetValueFromProperty(NameSelectors.IsEnabled.Name, true); + } + + private string? GetKey(TItem item) + { + if (item is BitNavBarItem navItem) + { + return navItem.Key; + } + + if (item is BitNavBarOption navOption) + { + return navOption.Key; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.Key.Selector is not null) + { + return NameSelectors.Key.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.Key.Name); + } + + private string? GetStyle(TItem item) + { + if (item is BitNavBarItem navItem) + { + return navItem.Style; + } + + if (item is BitNavBarOption navOption) + { + return navOption.Style; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.Style.Selector is not null) + { + return NameSelectors.Style.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.Style.Name); + } + + private string? GetTarget(TItem item) + { + if (item is BitNavBarItem navItem) + { + return navItem.Target; + } + + if (item is BitNavBarOption navOption) + { + return navOption.Target; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.Target.Selector is not null) + { + return NameSelectors.Target.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.Target.Name); + } + + private RenderFragment? GetTemplate(TItem item) + { + if (item is BitNavBarItem navItem) + { + return navItem.Template as RenderFragment; + } + + if (item is BitNavBarOption navOption) + { + return navOption.Template as RenderFragment; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.Template.Selector is not null) + { + return NameSelectors.Template.Selector!(item); + } + + return item.GetValueFromProperty?>(NameSelectors.Template.Name); + } + + private string? GetText(TItem item) + { + if (item is BitNavBarItem navItem) + { + return navItem.Text; + } + + if (item is BitNavBarOption navOption) + { + return navOption.Text; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.Text.Selector is not null) + { + return NameSelectors.Text.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.Text.Name); + } + + private string? GetTitle(TItem item) + { + if (item is BitNavBarItem navItem) + { + return navItem.Title; + } + + if (item is BitNavBarOption navOption) + { + return navOption.Title; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.Title.Selector is not null) + { + return NameSelectors.Title.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.Title.Name); + } + + private string? GetUrl(TItem item) + { + if (item is BitNavBarItem navItem) + { + return navItem.Url; + } + + if (item is BitNavBarOption navOption) + { + return navOption.Url; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.Url.Selector is not null) + { + return NameSelectors.Url.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.Url.Name); + } + + private IEnumerable? GetAdditionalUrls(TItem item) + { + if (item is BitNavBarItem navItem) + { + return navItem.AdditionalUrls; + } + + if (item is BitNavBarOption navOption) + { + return navOption.AdditionalUrls; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.AdditionalUrls.Selector is not null) + { + return NameSelectors.AdditionalUrls.Selector!(item); + } + + return item.GetValueFromProperty?>(NameSelectors.AdditionalUrls.Name); + } + + + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing is false || _disposed) return; + + _navigationManager.LocationChanged -= OnLocationChanged; + + _disposed = true; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.scss b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.scss new file mode 100644 index 0000000000..a202ef207a --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.scss @@ -0,0 +1,130 @@ +@import "../../../Styles/functions.scss"; + +.bit-nbr { + font-weight: $tg-font-weight; + font-family: $tg-font-family; + + &.bit-dis { + cursor: none; + pointer-events: none; + + .bit-nbr-itm { + color: $clr-fg-dis; + } + } +} + +.bit-nbr-cnt { + display: flex; + justify-content: space-around; +} + +.bit-nbr-itm { + display: flex; + cursor: pointer; + gap: spacing(0.5); + color: $clr-fg-pri; + padding: spacing(1); + align-items: center; + text-decoration: none; + flex-direction: column; + background: transparent; + + @media (hover:hover) { + &:hover { + color: var(--bit-nbr-clr); + } + } +} + +.bit-nbr-ico { + font-size: 1rem; +} + +.bit-nbr-txt { + font-size: 0.85rem; +} + +.bit-nbr-ion { + .bit-nbr-txt { + display: none; + } +} + +.bit-nbr-sel { + color: var(--bit-nbr-clr); +} + +.bit-nbr-dis { + color: $clr-fg-dis; + pointer-events: none; +} + + +.bit-nbr-pri { + --bit-nbr-clr: #{$clr-pri}; +} + +.bit-nbr-sec { + --bit-nbr-clr: #{$clr-sec}; +} + +.bit-nbr-ter { + --bit-nbr-clr: #{$clr-ter}; +} + +.bit-nbr-inf { + --bit-nbr-clr: #{$clr-inf}; +} + +.bit-nbr-suc { + --bit-nbr-clr: #{$clr-suc}; +} + +.bit-nbr-wrn { + --bit-nbr-clr: #{$clr-wrn}; +} + +.bit-nbr-swr { + --bit-nbr-clr: #{$clr-swr}; +} + +.bit-nbr-err { + --bit-nbr-clr: #{$clr-err}; +} + +.bit-nbr-pbg { + --bit-nbr-clr: #{$clr-bg-pri}; +} + +.bit-nbr-sbg { + --bit-nbr-clr: #{$clr-bg-sec}; +} + +.bit-nbr-tbg { + --bit-nbr-clr: #{$clr-bg-ter}; +} + +.bit-nbr-pfg { + --bit-nbr-clr: #{$clr-fg-pri}; +} + +.bit-nbr-sfg { + --bit-nbr-clr: #{$clr-fg-sec}; +} + +.bit-nbr-tfg { + --bit-nbr-clr: #{$clr-fg-ter}; +} + +.bit-nbr-pbr { + --bit-nbr-clr: #{$clr-brd-pri}; +} + +.bit-nbr-sbr { + --bit-nbr-clr: #{$clr-brd-sec}; +} + +.bit-nbr-tbr { + --bit-nbr-clr: #{$clr-brd-ter}; +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarClassStyles.cs b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarClassStyles.cs new file mode 100644 index 0000000000..5e522b507a --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarClassStyles.cs @@ -0,0 +1,34 @@ +namespace Bit.BlazorUI; + +public class BitNavBarClassStyles +{ + /// + /// Custom CSS classes/styles for the root element of the BitNavBar. + /// + public string? Root { get; set; } + + /// + /// Custom CSS classes/styles for the container of the items of the BitNavBar. + /// + public string? Container { get; set; } + + /// + /// Custom CSS classes/styles for the item of the BitNavBar. + /// + public string? Item { get; set; } + + /// + /// Custom CSS classes/styles for the item icon of the BitNavBar. + /// + public string? ItemIcon { get; set; } + + /// + /// Custom CSS classes/styles for the item text of the BitNavBar. + /// + public string? ItemText { get; set; } + + /// + /// Custom CSS classes/styles for the selected item of the BitNavBar. + /// + public string? SelectedItem { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarItem.cs b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarItem.cs new file mode 100644 index 0000000000..9794c9aa7c --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarItem.cs @@ -0,0 +1,64 @@ +namespace Bit.BlazorUI; + +public class BitNavBarItem +{ + /// + /// Custom CSS class for the navbar item. + /// + public string? Class { get; set; } + + /// + /// The custom data for the navbar item to provide additional state. + /// + public object? Data { get; set; } + + /// + /// Name of an icon to render next to the navbar item. + /// + public string? IconName { get; set; } + + /// + /// Whether or not the navbar item is enabled. + /// + public bool IsEnabled { get; set; } = true; + + /// + /// A unique value to use as a key or id of the navbar item. + /// + public string? Key { get; set; } + + /// + /// Custom CSS style for the navbar item. + /// + public string? Style { get; set; } + + /// + /// Link target, specifies how to open the navbar item's link. + /// + public string? Target { get; set; } + + /// + /// The custom template for the navbar item to render. + /// + public RenderFragment? Template { get; set; } + + /// + /// Text to render for the navbar item. + /// + public string? Text { get; set; } + + /// + /// Text for the tooltip of the navbar item. + /// + public string? Title { get; set; } + + /// + /// The navbar item's link URL. + /// + public string? Url { get; set; } + + /// + /// Alternative URLs to be considered when auto mode tries to detect the selected navbar item by the current URL. + /// + public IEnumerable? AdditionalUrls { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarNameSelectors.cs b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarNameSelectors.cs new file mode 100644 index 0000000000..3f245993aa --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarNameSelectors.cs @@ -0,0 +1,64 @@ +namespace Bit.BlazorUI; + +public class BitNavBarNameSelectors +{ + /// + /// The Class field name and selector of the custom input class. + /// + public BitNameSelectorPair Class { get; set; } = new(nameof(BitNavBarItem.Class)); + + /// + /// The Data field name and selector of the custom input class. + /// + public BitNameSelectorPair Data { get; set; } = new(nameof(BitNavBarItem.Data)); + + /// + /// The IconName field name and selector of the custom input class. + /// + public BitNameSelectorPair IconName { get; set; } = new(nameof(BitNavBarItem.IconName)); + + /// + /// The IsEnabled field name and selector of the custom input class. + /// + public BitNameSelectorPair IsEnabled { get; set; } = new(nameof(BitNavBarItem.IsEnabled)); + + /// + /// The Key field name and selector of the custom input class. + /// + public BitNameSelectorPair Key { get; set; } = new(nameof(BitNavBarItem.Key)); + + /// + /// The Style field name and selector of the custom input class. + /// + public BitNameSelectorPair Style { get; set; } = new(nameof(BitNavBarItem.Style)); + + /// + /// The Target field name and selector of the custom input class. + /// + public BitNameSelectorPair Target { get; set; } = new(nameof(BitNavBarItem.Target)); + + /// + /// The Template field name and selector of the custom input class. + /// + public BitNameSelectorPair?> Template { get; set; } = new(nameof(BitNavBarItem.Template)); + + /// + /// The Text field name and selector of the custom input class. + /// + public BitNameSelectorPair Text { get; set; } = new(nameof(BitNavBarItem.Text)); + + /// + /// The Title field name and selector of the custom input class. + /// + public BitNameSelectorPair Title { get; set; } = new(nameof(BitNavBarItem.Title)); + + /// + /// The Url field name and selector of the custom input class. + /// + public BitNameSelectorPair Url { get; set; } = new(nameof(BitNavBarItem.Url)); + + /// + /// The AdditionalUrls field name and selector of the custom input class. + /// + public BitNameSelectorPair?> AdditionalUrls { get; set; } = new(nameof(BitNavBarItem.AdditionalUrls)); +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarOption.cs b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarOption.cs new file mode 100644 index 0000000000..f4d6c167d9 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBarOption.cs @@ -0,0 +1,97 @@ +namespace Bit.BlazorUI; + +public partial class BitNavBarOption : ComponentBase, IDisposable +{ + private bool _disposed; + + + + [CascadingParameter] protected BitNavBar NavBar { get; set; } = default!; + + + + /// + /// Custom CSS class for the navbar option. + /// + [Parameter] public string? Class { get; set; } + + /// + /// The custom data for the navbar option to provide additional state. + /// + [Parameter] public object? Data { get; set; } + + /// + /// Name of an icon to render next to the navbar option. + /// + [Parameter] public string? IconName { get; set; } + + /// + /// Whether or not the navbar option is enabled. + /// + [Parameter] public bool IsEnabled { get; set; } = true; + + /// + /// A unique value to use as a key or id of the navbar option. + /// + [Parameter] public string? Key { get; set; } + + /// + /// Custom CSS style for the navbar option. + /// + [Parameter] public string? Style { get; set; } + + /// + /// Link target, specifies how to open the navbar option's link. + /// + [Parameter] public string? Target { get; set; } + + /// + /// The custom template for the navbar option to render. + /// + [Parameter] public RenderFragment? Template { get; set; } + + /// + /// Text to render for the navbar option. + /// + [Parameter] public string? Text { get; set; } + + /// + /// Text for the tooltip of the navbar option. + /// + [Parameter] public string? Title { get; set; } + + /// + /// The navbar option's link URL. + /// + [Parameter] public string? Url { get; set; } + + /// + /// Alternative URLs to be considered when auto mode tries to detect the selected navbar option by the current URL. + /// + [Parameter] public IEnumerable? AdditionalUrls { get; set; } + + + + protected override async Task OnInitializedAsync() + { + NavBar?.RegisterOption(this); + + await base.OnInitializedAsync(); + } + + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing is false || _disposed) return; + + NavBar?.UnregisterOption(this); + + _disposed = true; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/_BitNavBarChild.razor b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/_BitNavBarChild.razor new file mode 100644 index 0000000000..a5d4957c9c --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/_BitNavBarChild.razor @@ -0,0 +1,27 @@ +@namespace Bit.BlazorUI + +@if (Href.HasValue()) +{ + + @ChildContent + +} +else +{ + +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/_BitNavBarChild.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/_BitNavBarChild.razor.cs new file mode 100644 index 0000000000..497d2529aa --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/_BitNavBarChild.razor.cs @@ -0,0 +1,21 @@ +namespace Bit.BlazorUI; + +public partial class _BitNavBarChild +{ + [Parameter] public RenderFragment? ChildContent { get; set; } + + [Parameter] public string? Class { get; set; } + + [Parameter] public bool Disabled { get; set; } + + [Parameter] public string? Href { get; set; } + + [Parameter] public EventCallback OnClick { get; set; } + + [Parameter] public string? Style { get; set; } + + [Parameter] public string? Target { get; set; } + + [Parameter] public string? Title { get; set; } + +} diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/typography.fluent.scss b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/typography.fluent.scss index 77545a7b81..8b89d5d496 100644 --- a/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/typography.fluent.scss +++ b/src/BlazorUI/Bit.BlazorUI/Styles/Fluent/typography.fluent.scss @@ -8,7 +8,7 @@ :root[bit-theme="fluent-dark"] { // main --bit-tpg-font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; - --bit-tpg-font-weight: 600; + --bit-tpg-font-weight: 500; --bit-tpg-line-height: 1.75rem; --bit-tpg-gutter-size: 0.35em; // h1 diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/components.scss b/src/BlazorUI/Bit.BlazorUI/Styles/components.scss index 868d0dcef4..ecaa2a9e4b 100644 --- a/src/BlazorUI/Bit.BlazorUI/Styles/components.scss +++ b/src/BlazorUI/Bit.BlazorUI/Styles/components.scss @@ -38,6 +38,7 @@ @import "../Components/Navs/Breadcrumb/BitBreadcrumb.scss"; @import "../Components/Navs/DropMenu/BitDropMenu.scss"; @import "../Components/Navs/Nav/BitNav.scss"; +@import "../Components/Navs/NavBar/BitNavBar.scss"; @import "../Components/Navs/Pagination/BitPagination.scss"; @import "../Components/Navs/Pivot/BitPivot.scss"; diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor new file mode 100644 index 0000000000..f45420adca --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor @@ -0,0 +1,28 @@ +@page "/components/navbar" + + + +
+ + + + <_BitNavBarItemDemo /> + + + + <_BitNavBarCustomDemo /> + + + + <_BitNavBarOptionDemo /> + + + +
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor.cs new file mode 100644 index 0000000000..152201f4fd --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor.cs @@ -0,0 +1,482 @@ +namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Navs.NavBar; + +public partial class BitNavBarDemo +{ + private readonly List componentParameters = + [ + new() + { + Name = "ChildContent", + Type = "RenderFragment?", + DefaultValue = "null", + Description = "Items to render as children.", + }, + new() + { + Name = "Classes", + Type = "BitNavClassStyles?", + DefaultValue = "null", + Description = "Custom CSS classes for different parts of the BitNav component.", + LinkType = LinkType.Link, + Href = "#class-styles", + }, + new() + { + Name = "Color", + Type = "BitColor?", + DefaultValue = "null", + Description = "The general color of the nav.", + }, + new() + { + Name = "DefaultSelectedItem", + Type = "TItem?", + DefaultValue = "null", + Description = "The initially selected item in manual mode." + }, + new() + { + Name = "HeaderTemplate", + Type = "RenderFragment?", + DefaultValue = "null", + Description = "Used to customize how content inside the group header is rendered." + }, + new() + { + Name = "IconOnly", + Type = "bool", + DefaultValue = "false", + Description = "Only renders the icon of each nav item." + }, + new() + { + Name = "Items", + Type = "IList", + DefaultValue = "new List()", + Description = "A collection of item to display in the navigation bar.", + LinkType = LinkType.Link, + Href="#navbar-item", + }, + new() + { + Name = "ItemTemplate", + Type = "RenderFragment?", + DefaultValue = "null", + Description = "Used to customize how content inside the item is rendered." + }, + new() + { + Name = "Mode", + Type = "BitNavMode", + DefaultValue = "BitNavMode.Automatic", + Description = "Determines how the navigation will be handled.", + LinkType = LinkType.Link, + Href = "#nav-mode-enum", + }, + new() + { + Name = "NameSelectors", + Type = "BitNavNameSelectors?", + DefaultValue = "null", + Description = "Names and selectors of the custom input type properties.", + LinkType = LinkType.Link, + Href = "#name-selectors", + }, + new() + { + Name = "OnItemClick", + Type = "EventCallback", + Description = "Callback invoked when an item is clicked." + }, + new() + { + Name = "OnSelectItem", + Type = "EventCallback", + Description = "Callback invoked when an item is selected." + }, + new() + { + Name = "Options", + Type = "RenderFragment?", + DefaultValue = "null", + Description = "Alias of ChildContent.", + }, + new() + { + Name = "Reselectable", + Type = "bool", + DefaultValue = "false", + Description = "Enables recalling the select events when the same item is selected." + }, + new() + { + Name = "SelectedItem", + Type = "TItem?", + DefaultValue = "null", + Description = "Selected item to show in the BitNavBar." + }, + new() + { + Name = "Styles", + Type = "BitNavClassStyles?", + DefaultValue = "null", + Description = "Custom CSS styles for different parts of the BitNav component.", + LinkType = LinkType.Link, + Href = "#class-styles", + } + ]; + + private readonly List componentSubClasses = + [ + new() + { + Id = "navbar-item", + Title = "BitNavBarItem", + Parameters = + [ + new() + { + Name = "Class", + Type = "string?", + DefaultValue = "null", + Description = "Custom CSS class for the navbar item.", + }, + new() + { + Name = "Data", + Type = "object?", + DefaultValue = "null", + Description = "The custom data for the navbar item to provide additional state.", + }, + new() + { + Name = "IconName", + Type = "string?", + DefaultValue = "null", + Description = "Name of an icon to render next to the navbar item.", + }, + new() + { + Name = "IsEnabled", + Type = "bool", + DefaultValue = "true", + Description = "Whether or not the navbar item is enabled.", + }, + new() + { + Name = "Key", + Type = "string?", + DefaultValue = "null", + Description = "A unique value to use as a key or id of the navbar item.", + }, + new() + { + Name = "Style", + Type = "string?", + DefaultValue = "null", + Description = "Custom CSS style for the navbar item.", + }, + new() + { + Name = "Target", + Type = "string?", + DefaultValue = "null", + Description = "Link target, specifies how to open the navbar item's link.", + }, + new() + { + Name = "Template", + Type = "RenderFragment?", + DefaultValue = "null", + Description = "The custom template for the navbar item to render.", + }, + new() + { + Name = "Text", + Type = "string", + DefaultValue = "string.Empty", + Description = "Text to render for the navbar item.", + }, + new() + { + Name = "Title", + Type = "string?", + DefaultValue = "null", + Description = "Text for the tooltip of the navbar item.", + }, + new() + { + Name = "Url", + Type = "string?", + DefaultValue = "null", + Description = "The navbar item's link URL.", + }, + new() + { + Name = "AdditionalUrls", + Type = "IEnumerable?", + DefaultValue = "null", + Description = "Alternative URLs to be considered when auto mode tries to detect the selected navbar item by the current URL.", + } + ] + }, + new() + { + Id = "navbar-option", + Title = "BitNavBarOption", + Parameters = + [ + new() + { + Name = "Class", + Type = "string?", + DefaultValue = "null", + Description = "Custom CSS class for the navbar option.", + }, + new() + { + Name = "Data", + Type = "object?", + DefaultValue = "null", + Description = "The custom data for the navbar option to provide additional state.", + }, + new() + { + Name = "IconName", + Type = "string?", + DefaultValue = "null", + Description = "Name of an icon to render next to the navbar option.", + }, + new() + { + Name = "IsEnabled", + Type = "bool", + DefaultValue = "true", + Description = "Whether or not the navbar option is enabled.", + }, + new() + { + Name = "Key", + Type = "string?", + DefaultValue = "null", + Description = "A unique value to use as a key or id of the navbar option.", + }, + new() + { + Name = "Style", + Type = "string?", + DefaultValue = "null", + Description = "Custom CSS style for the navbar option.", + }, + new() + { + Name = "Target", + Type = "string?", + DefaultValue = "null", + Description = "Link target, specifies how to open the navbar option's link.", + }, + new() + { + Name = "Template", + Type = "RenderFragment?", + DefaultValue = "null", + Description = "The custom template for the navbar option to render.", + }, + new() + { + Name = "Text", + Type = "string", + DefaultValue = "string.Empty", + Description = "Text to render for the navbar option.", + }, + new() + { + Name = "Title", + Type = "string?", + DefaultValue = "null", + Description = "Text for the tooltip of the navbar option.", + }, + new() + { + Name = "Url", + Type = "string?", + DefaultValue = "null", + Description = "The navbar option's link URL.", + }, + new() + { + Name = "AdditionalUrls", + Type = "IEnumerable?", + DefaultValue = "null", + Description = "Alternative URLs to be considered when auto mode tries to detect the selected navbar option by the current URL.", + } + ] + }, + new() + { + Id = "name-selectors", + Title = "BitNavBarNameSelectors", + Parameters = + [ + new() + { + Name = "Class", + Type = "BitNameSelectorPair", + DefaultValue = "new(nameof(BitNavBarItem.Class))", + Description = "The Class field name and selector of the custom input class." + }, + new() + { + Name = "Data", + Type = "BitNameSelectorPair", + DefaultValue = "new(nameof(BitNavBarItem.Data))", + Description = "The Data field name and selector of the custom input class." + }, + new() + { + Name = "IconName", + Type = "BitNameSelectorPair", + DefaultValue = "new(nameof(BitNavBarItem.IconName))", + Description = "The IconName field name and selector of the custom input class." + }, + new() + { + Name = "IsEnabled", + Type = "BitNameSelectorPair", + DefaultValue = "new(nameof(BitNavBarItem.IsEnabled))", + Description = "The IsEnabled field name and selector of the custom input class." + }, + new() + { + Name = "Key", + Type = "BitNameSelectorPair", + DefaultValue = "new(nameof(BitNavBarItem.Key))", + Description = "The Key field name and selector of the custom input class." + }, + new() + { + Name = "Style", + Type = "BitNameSelectorPair", + DefaultValue = "new(nameof(BitNavBarItem.Style))", + Description = "The Style field name and selector of the custom input class." + }, + new() + { + Name = "Target", + Type = "BitNameSelectorPair", + DefaultValue = "new(nameof(BitNavBarItem.Target))", + Description = "The Target field name and selector of the custom input class." + }, + new() + { + Name = "Template", + Type = "BitNameSelectorPair?>", + DefaultValue = "new(nameof(BitNavBarItem.Template))", + Description = "The Template field name and selector of the custom input class." + }, + new() + { + Name = "Text", + Type = "BitNameSelectorPair", + DefaultValue = "new(nameof(BitNavBarItem.Text))", + Description = "The Text field name and selector of the custom input class." + }, + new() + { + Name = "Title", + Type = "BitNameSelectorPair", + DefaultValue = "new(nameof(BitNavBarItem.Title))", + Description = "The Title field name and selector of the custom input class." + }, + new() + { + Name = "Url", + Type = "BitNameSelectorPair", + DefaultValue = "new(nameof(BitNavBarItem.Url))", + Description = "The Url field name and selector of the custom input class." + }, + new() + { + Name = "AdditionalUrls", + Type = "BitNameSelectorPair?>", + DefaultValue = "new(nameof(BitNavBarItem.AdditionalUrls))", + Description = "The AdditionalUrls field name and selector of the custom input class." + }, + ] + }, + new() + { + Id = "class-styles", + Title = "BitNavBarClassStyles", + Parameters = + [ + new() + { + Name = "Root", + Type = "string?", + DefaultValue = "null", + Description = "Custom CSS classes/styles for the root element of the BitNavBar." + }, + new() + { + Name = "Container", + Type = "string?", + DefaultValue = "null", + Description = "Custom CSS classes/styles for the container of the items of the BitNavBar." + }, + new() + { + Name = "Item", + Type = "string?", + DefaultValue = "null", + Description = "Custom CSS classes/styles for the item of the BitNavBar." + }, + new() + { + Name = "ItemIcon", + Type = "string?", + DefaultValue = "null", + Description = "Custom CSS classes/styles for the item icon of the BitNavBar." + }, + new() + { + Name = "ItemText", + Type = "string?", + DefaultValue = "null", + Description = "Custom CSS classes/styles for the item text of the BitNavBar." + }, + new() + { + Name = "SelectedItem", + Type = "string?", + DefaultValue = "null", + Description = "Custom CSS classes/styles for the selected item of the BitNavBar." + }, + ] + } + ]; + + private readonly List componentSubEnums = + [ + new() + { + Id = "nav-mode-enum", + Name = "BitNavMode", + Items = + [ + new() + { + Name = "Automatic", + Description = "The value of selected key will change using NavigationManager and the current url inside the component.", + Value = "0", + }, + new() + { + Name = "Manual", + Description = "Selected key changes will be sent back to the parent component and the component won't change its value.", + Value = "1", + } + ] + }, + ]; +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor.scss b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor.scss new file mode 100644 index 0000000000..838ccf1540 --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor.scss @@ -0,0 +1,43 @@ +@import '../../../../Styles/abstracts/_media-queries.scss'; + +::deep { + .example-box { + gap: 100px; + display: flex; + flex-direction: row; + align-items: flex-start; + + .bit-nav { + width: 350px; + } + + @include sm { + flex-direction: column; + align-items: flex-start; + gap: 0; + + .bit-nav { + width: 250px; + } + } + } + + a { + text-decoration: none; + } + + .nav-custom-header { + color: green; + font-size: 17px; + font-weight: 600; + } + + .nav-custom-item { + gap: 4px; + display: flex; + color: #ff7800; + font-weight: 600; + align-items: center; + flex-flow: row nowrap; + } +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/MenuItem.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/MenuItem.cs new file mode 100644 index 0000000000..7d1e6c3454 --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/MenuItem.cs @@ -0,0 +1,8 @@ +namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Navs.NavBar; + +public class MenuItem +{ + public string? Title { get; set; } + public string? Icon { get; set; } + public bool Disabled { get; set; } +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor new file mode 100644 index 0000000000..f6658a83e4 --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor @@ -0,0 +1,54 @@ + + + + + + + + +
Disabled:

+ +



+
Disabled item:

+ +
+
+ + + + + + + + + + + + + + + + + + @custom.Title + + + + + \ No newline at end of file diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor.cs new file mode 100644 index 0000000000..14f3159efe --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor.cs @@ -0,0 +1,139 @@ +namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Navs.NavBar; + +public partial class _BitNavBarCustomDemo +{ + private static readonly List basicNavBarCustoms = + [ + new() { Title = "Home", Icon = BitIconName.Home }, + new() { Title = "Products", Icon = BitIconName.ProductVariant }, + new() { Title = "Academy", Icon = BitIconName.LearningTools }, + new() { Title = "Profile", Icon = BitIconName.Contact }, + ]; + + private static readonly List basicNavBarCustomsDisabled = + [ + new() { Title = "Home", Icon = BitIconName.Home }, + new() { Title = "Products", Icon = BitIconName.ProductVariant }, + new() { Title = "Academy", Icon = BitIconName.LearningTools, Disabled = true }, + new() { Title = "Profile", Icon = BitIconName.Contact }, + ]; + + + + private readonly string example1RazorCode = @" + item.Title }, + IconName = { Selector = item => item.Icon } })"" />"; + private readonly string example1CsharpCode = @" +public class MenuItem +{ + public string? Title { get; set; } + public string? Icon { get; set; } +} + +private static readonly List basicNavBarCustoms = +[ + new() { Title = ""Home"", Icon = BitIconName.Home }, + new() { Title = ""Products"", Icon = BitIconName.ProductVariant }, + new() { Title = ""Academy"", Icon = BitIconName.LearningTools }, + new() { Title = ""Profile"", Icon = BitIconName.Contact }, +];"; + + private readonly string example2RazorCode = @" + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + + item.Title }, + IconName = { Selector = item => item.Icon }, + IsEnabled = { Selector = item => item.Disabled is false } })"" />"; + private readonly string example2CsharpCode = @" +public class MenuItem +{ + public string? Title { get; set; } + public string? Icon { get; set; } + public bool Disabled { get; set; } +} + +private static readonly List basicNavBarCustoms = +[ + new() { Title = ""Home"", Icon = BitIconName.Home }, + new() { Title = ""Products"", Icon = BitIconName.ProductVariant }, + new() { Title = ""Academy"", Icon = BitIconName.LearningTools }, + new() { Title = ""Profile"", Icon = BitIconName.Contact }, +]; + +private static readonly List basicNavBarCustomsDisabled = +[ + new() { Title = ""Home"", Icon = BitIconName.Home }, + new() { Title = ""Products"", Icon = BitIconName.ProductVariant }, + new() { Title = ""Academy"", Icon = BitIconName.LearningTools, Disabled = true }, + new() { Title = ""Profile"", Icon = BitIconName.Contact }, +];"; + + private readonly string example3RazorCode = @" + item.Title }, + IconName = { Selector = item => item.Icon } })"" />"; + private readonly string example3CsharpCode = @" +public class MenuItem +{ + public string? Title { get; set; } + public string? Icon { get; set; } +} + +private static readonly List basicNavBarCustoms = +[ + new() { Title = ""Home"", Icon = BitIconName.Home }, + new() { Title = ""Products"", Icon = BitIconName.ProductVariant }, + new() { Title = ""Academy"", Icon = BitIconName.LearningTools }, + new() { Title = ""Profile"", Icon = BitIconName.Contact }, +];"; + + private readonly string example4RazorCode = @" + item.Title }, + IconName = { Selector = item => item.Icon } })"" />"; + private readonly string example4CsharpCode = @" +public class MenuItem +{ + public string? Title { get; set; } + public string? Icon { get; set; } +} + +private static readonly List basicNavBarCustoms = +[ + new() { Title = ""Home"", Icon = BitIconName.Home }, + new() { Title = ""Products"", Icon = BitIconName.ProductVariant }, + new() { Title = ""Academy"", Icon = BitIconName.LearningTools }, + new() { Title = ""Profile"", Icon = BitIconName.Contact }, +];"; + + private readonly string example5RazorCode = @" + item.Title }, + IconName = { Selector = item => item.Icon } })""> + + @custom.Title + + +"; + private readonly string example5CsharpCode = @" +public class MenuItem +{ + public string? Title { get; set; } + public string? Icon { get; set; } +} + +private static readonly List basicNavBarCustoms = +[ + new() { Title = ""Home"", Icon = BitIconName.Home }, + new() { Title = ""Products"", Icon = BitIconName.ProductVariant }, + new() { Title = ""Academy"", Icon = BitIconName.LearningTools }, + new() { Title = ""Profile"", Icon = BitIconName.Contact }, +];"; +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor new file mode 100644 index 0000000000..82e9886e1a --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor @@ -0,0 +1,40 @@ + + + + + + + + +
Disabled:

+ +



+
Disabled item:

+ +
+
+ + + + + + + + + + + + + + + + + + @item.Text + + + + + \ No newline at end of file diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.cs new file mode 100644 index 0000000000..7099cf4470 --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.cs @@ -0,0 +1,94 @@ +namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Navs.NavBar; + +public partial class _BitNavBarItemDemo +{ + private static readonly List basicNavBarItems = + [ + new() { Text = "Home", IconName = BitIconName.Home }, + new() { Text = "Products", IconName = BitIconName.ProductVariant }, + new() { Text = "Academy", IconName = BitIconName.LearningTools }, + new() { Text = "Profile", IconName = BitIconName.Contact }, + ]; + + private static readonly List basicNavBarItemsDisabled = + [ + new() { Text = "Home", IconName = BitIconName.Home }, + new() { Text = "Products", IconName = BitIconName.ProductVariant }, + new() { Text = "Academy", IconName = BitIconName.LearningTools, IsEnabled = false }, + new() { Text = "Profile", IconName = BitIconName.Contact }, + ]; + + + + private readonly string example1RazorCode = @" +"; + private readonly string example1CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example2RazorCode = @" + + +"; + private readonly string example2CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +]; + +private static readonly List basicNavBarItemsDisabled = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools, IsEnabled = false }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example3RazorCode = @" +"; + private readonly string example3CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example4RazorCode = @" +"; + private readonly string example4CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example5RazorCode = @" + + + @item.Text + + +"; + private readonly string example5CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor new file mode 100644 index 0000000000..593f015eca --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor @@ -0,0 +1,69 @@ + + + + + + + + + + + + + +
Disabled:

+ + + + + + +



+
Disabled item:

+ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @option.Text + + + + + \ No newline at end of file diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.cs new file mode 100644 index 0000000000..2c19f01295 --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.cs @@ -0,0 +1,57 @@ +namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Navs.NavBar; + +public partial class _BitNavBarOptionDemo +{ + private readonly string example1RazorCode = @" + + + + + +"; + + private readonly string example2RazorCode = @" + + + + + + + + + + + + +"; + + private readonly string example3RazorCode = @" + + + + + +"; + + private readonly string example4RazorCode = @" + + + + + +"; + + private readonly string example5RazorCode = @" + + + + + + + + + @option.Text + + +"; +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor index d01878f361..3b0daddf91 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor @@ -134,6 +134,9 @@ Nav (TreeList) featured + + NavBar + Pagination diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs index 0a20b9d210..73a93a565f 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs @@ -87,6 +87,7 @@ public partial class NavMenu : IDisposable new() { Text = "Breadcrumb", Url = "/components/breadcrumb" }, new() { Text = "DropMenu", Url = "/components/dropmenu" }, new() { Text = "Nav", Url = "/components/nav", Description = "Tree" }, + new() { Text = "NavBar", Url = "/components/navbar", Description = "NavMenu, TabPanel" }, new() { Text = "Pagination", Url = "/components/pagination" }, new() { Text = "Pivot", Url = "/components/pivot", Description = "Tab" }, ] diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json index c8a38edf57..f83a0d1b06 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json @@ -239,6 +239,12 @@ "minify": { "enabled": false }, "options": { "sourceMap": false } }, + { + "outputFile": "Pages/Components/Navs/NavBar/BitNavBarDemo.razor.css", + "inputFile": "Pages/Components/Navs/NavBar/BitNavBarDemo.razor.scss", + "minify": { "enabled": false }, + "options": { "sourceMap": false } + }, { "outputFile": "Pages/Components/Navs/Pagination/BitPaginationDemo.razor.css", "inputFile": "Pages/Components/Navs/Pagination/BitPaginationDemo.razor.scss", From d1726b09867308a45af81a01fb59c3c7f846f934 Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Sat, 23 Nov 2024 07:29:39 +0100 Subject: [PATCH 03/87] feat(templates): Refactor Boilerplate User session relation with Signalr and push notification channels #9304 (#9305) --- .../.template.config/template.json | 23 ++-- .../Components/AppComponentBase.cs | 16 ++- .../Components/ClientAppCoordinator.cs | 52 +++++--- .../Authorized/Settings/SessionsSection.razor | 8 +- .../Settings/SessionsSection.razor.cs | 14 +-- .../Components/Parameters.cs | 2 +- .../IClientCoreServiceCollectionExtensions.cs | 2 + .../Extensions/IJSRuntimeExtensions.cs | 4 +- .../Boilerplate.Client.Core/Scripts/app.ts | 4 +- .../Services/AuthenticationManager.cs | 17 ++- .../Contracts/IPushNotificationService.cs | 4 +- .../ExceptionDelegatingHandler.cs | 3 +- .../Services/PushNotificationServiceBase.cs | 16 +-- .../Services/SignalRInfinitiesRetryPolicy.cs | 4 +- .../AndroidPushNotificationService.cs | 8 +- ...ushNotificationFirebaseMessagingService.cs | 2 +- .../Platforms/MacCatalyst/AppDelegate.cs | 2 +- .../MacCatalystPushNotificationService.cs | 8 +- .../WindowsPushNotificationService.cs | 2 +- .../Platforms/iOS/AppDelegate.cs | 2 +- .../Services/iOSPushNotificationService.cs | 8 +- .../Services/WebPushNotificationService.cs | 4 +- .../WindowsPushNotificationService.cs | 2 +- .../src/Directory.Packages.props | 12 +- .../src/Directory.Packages8.props | 12 +- .../Categories/CategoryController.cs | 3 +- .../IdentityController.ResetPassword.cs | 3 +- .../Identity/IdentityController.cs | 35 +++--- .../Controllers/Identity/UserController.cs | 60 ++++----- .../Controllers/Products/ProductController.cs | 3 +- .../PushNotificationController.cs | 10 +- .../Data/AppDbContext.cs | 8 +- .../Identity/UserConfiguration.cs | 5 - ...shNotificationSubscriptionConfiguration.cs | 31 +++++ ...241122104915_InitialMigration.Designer.cs} | 114 +++++++++++++----- ....cs => 20241122104915_InitialMigration.cs} | 101 ++++++++++------ .../Migrations/AppDbContextModelSnapshot.cs | 112 ++++++++++++----- .../Mappers/PushNotificationMapper.cs | 2 +- .../Models/Identity/User.cs | 5 +- .../Models/Identity/UserSession.cs | 24 +++- ...ion.cs => PushNotificationSubscription.cs} | 15 ++- .../Program.Services.cs | 6 + .../ServerApiSettings.cs | 2 - .../Services/PushNotificationService.cs | 64 +++++----- .../Boilerplate.Server.Api/Signalr/AppHub.cs | 1 + .../Signalr/AppHubConnectionHandler.cs | 38 ++++++ .../Boilerplate.Server.Api/appsettings.json | 1 - .../Boilerplate.Server.Web/appsettings.json | 1 - .../IPushNotificationController.cs | 4 +- .../src/Shared/Dtos/AppJsonContext.cs | 2 +- .../Shared/Dtos/Identity/UserSessionDto.cs | 7 +- ....cs => PushNotificationSubscriptionDto.cs} | 4 +- .../Extensions/ClaimsPrincipalExtensions.cs | 6 +- .../src/Shared/Resources/AppStrings.fa.resx | 3 - .../src/Shared/Resources/AppStrings.nl.resx | 3 - .../src/Shared/Resources/AppStrings.resx | 3 - .../Shared/Services/SharedPubSubMessages.cs | 4 +- .../src/Tests/PageTests/IdentityPagesTests.cs | 4 +- .../src/Tests/TestsInitializer.cs | 2 +- 59 files changed, 603 insertions(+), 314 deletions(-) create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/PushNotification/PushNotificationSubscriptionConfiguration.cs rename src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/{20241107182721_InitialMigration.Designer.cs => 20241122104915_InitialMigration.Designer.cs} (89%) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/{20241107182721_InitialMigration.cs => 20241122104915_InitialMigration.cs} (91%) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/PushNotification/{DeviceInstallation.cs => PushNotificationSubscription.cs} (63%) create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHubConnectionHandler.cs rename src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/PushNotification/{DeviceInstallationDto.cs => PushNotificationSubscriptionDto.cs} (85%) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json index 7aba4c2ef0..811adff765 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json @@ -429,6 +429,12 @@ "src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Offline/**" ] }, + { + "condition": "(offlineDb != true || framework == \"net8.0\")", + "exclude": [ + "src/Client/Boilerplate.Client.Core/Data/CompiledModel/**" + ] + }, { "condition": "(signalR != true)", "exclude": [ @@ -453,12 +459,13 @@ "src/Client/Boilerplate.Client.Maui/Platforms/iOS/Services/iOSPushNotificationService.cs", "src/Client/Boilerplate.Client.Web/Services/WebPushNotificationService.cs", "src/Client/Boilerplate.Client.Windows/Services/WindowsPushNotificationService.cs", - "src/Server/Boilerplate.Server.Api/Models/PushNotification/**", + "src/Server/Boilerplate.Server.Api/Models/PushNotification/**", "src/Server/Boilerplate.Server.Api/Controllers/PushNotification/**", "src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs", "src/Shared/Controllers/PushNotification/**", "src/Shared/Dtos/PushNotification/**", - "src/Server/Boilerplate.Server.Api/Mappers/PushNotificationMapper.cs" + "src/Server/Boilerplate.Server.Api/Mappers/PushNotificationMapper.cs", + "src/Server/Boilerplate.Server.Api/Data/Configurations/PushNotification/**" ] }, { @@ -516,7 +523,7 @@ "src/Tests/Extensions/PlaywrightNetworkExtensions.cs", "src/Tests/Extensions/PlaywrightCacheExtensions.cs", "src/Tests/Extensions/PlaywrightHydrationExtensions.cs", - "src/Tests/Extensions/BrowserContextExtensions.cs", + "src/Tests/Extensions/BrowserContextExtensions.cs" ] }, { @@ -538,11 +545,11 @@ } }, { - "condition": "(framework == \"net9.0\")", - "exclude": [ - "global8.json", - "src/Directory.Packages8.props" - ] + "condition": "(framework == \"net9.0\")", + "exclude": [ + "global8.json", + "src/Directory.Packages8.props" + ] } ] } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs index edbcb2df8d..a37859f207 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs @@ -50,7 +50,7 @@ public partial class AppComponentBase : ComponentBase, IAsyncDisposable [AutoInject] protected AbsoluteServerAddressProvider AbsoluteServerAddress { get; set; } = default!; - private readonly CancellationTokenSource cts = new(); + private CancellationTokenSource cts = new(); protected CancellationToken CurrentCancellationToken => cts.Token; protected bool InPrerenderSession => AppPlatform.IsBlazorHybrid is false && JSRuntime.IsInitialized() is false; @@ -214,6 +214,16 @@ public virtual Func WrapHandled(Func func, }; } + /// + /// Cancells running codes inside current component. + /// + protected void Abort() + { + cts.Cancel(); + cts.Dispose(); + cts = new(); + } + public async ValueTask DisposeAsync() { await DisposeAsync(true); @@ -226,8 +236,8 @@ protected virtual async ValueTask DisposeAsync(bool disposing) if (disposing) { await PrerenderStateService.DisposeAsync(); - cts?.Cancel(); - cts?.Dispose(); + cts.Cancel(); + cts.Dispose(); } } 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 ff7dc2ef67..f1f0f76398 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 @@ -42,21 +42,11 @@ protected override async Task OnInitAsync() { if (AppPlatform.IsBlazorHybrid) { - if (CultureInfoManager.MultilingualEnabled) - { - cultureInfoManager.SetCurrentCulture(new Uri(NavigationManager.Uri).GetCulture() ?? // 1- Culture query string OR Route data request culture - await storageService.GetItem("Culture") ?? // 2- User settings - CultureInfo.CurrentUICulture.Name); // 3- OS settings - } - - await SetupBodyClasses(); + await ConfigureUISetup(); } if (InPrerenderSession is false) { - NavigationManager.LocationChanged += NavigationManager_LocationChanged; - AuthenticationManager.AuthenticationStateChanged += AuthenticationStateChanged; - TelemetryContext.UserAgent = await navigator.GetUserAgent(); TelemetryContext.TimeZone = await jsRuntime.GetTimeZone(); TelemetryContext.Culture = CultureInfo.CurrentCulture.Name; @@ -78,7 +68,9 @@ await storageService.GetItem("Culture") ?? // 2- User settings }); //#endif - AuthenticationStateChanged(AuthenticationManager.GetAuthenticationStateAsync()); + NavigationManager.LocationChanged += NavigationManager_LocationChanged; + AuthenticationManager.AuthenticationStateChanged += AuthenticationStateChanged; + await PropagateUserId(firstRun: true, AuthenticationManager.GetAuthenticationStateAsync()); } await base.OnInitAsync(); @@ -91,15 +83,19 @@ private void NavigationManager_LocationChanged(object? sender, LocationChangedEv } private SemaphoreSlim semaphore = new(1, 1); - /// /// This code manages the association of a user with sensitive services, such as SignalR, push notifications, App Insights, and others, /// ensuring the user is correctly set or cleared as needed. /// - private async void AuthenticationStateChanged(Task task) + public async Task PropagateUserId(bool firstRun, Task task) { try { + if (firstRun is false) + { + Abort(); + } + await semaphore.WaitAsync(CurrentCancellationToken); // About Semaphore: The following code may take significant time to execute. // During this period, the authentication state could change. For instance, the app might start with the user authenticated, but they could sign out while this method is running. @@ -111,6 +107,16 @@ private async void AuthenticationStateChanged(Task task) TelemetryContext.UserId = isAuthenticated ? user.GetUserId() : null; TelemetryContext.UserSessionId = isAuthenticated ? user.GetSessionId() : null; + // Typically, we use the logger directly without utilizing logger.BeginScope. + // While many loggers provide specific methods to set userId and other context-related information, + // we use this method to propagate the user ID and other telemetry contexts via Microsoft.Extensions.Logging's Scope feature. + // PropagateUserId method is invoked both during app startup and when the authentication state changes. + // Additionally, this is a convenient place to manage user-specific contexts for services like: + // - App Insights: Set or clear the user ID for tracking purposes. + // - Push Notifications: Update subscriptions to ensure user-specific notifications are routed to the correct devices. + // - SignalR: Map connection IDs to a user's group of connections for message targeting. + // By leveraging this method during authentication state changes, we streamline the propagation of user-specific contexts across these systems. + //#if (appInsights == true) if (isAuthenticated) { @@ -125,11 +131,11 @@ private async void AuthenticationStateChanged(Task task) var data = TelemetryContext.ToDictionary(); using var scope = authLogger.BeginScope(data); { - authLogger.LogInformation("Authentication state changed."); + authLogger.LogInformation($"Propagating {(firstRun ? "initial" : "changed")} authentication state."); } //#if (notification == true) - await pushNotificationService.RegisterDevice(CurrentCancellationToken); + await pushNotificationService.RegisterSubscription(CurrentCancellationToken); //#endif //#if (signalR == true) @@ -146,6 +152,11 @@ private async void AuthenticationStateChanged(Task task) } } + private void AuthenticationStateChanged(Task task) + { + _ = PropagateUserId(firstRun: false, task); + } + //#if (signalR == true) private async Task ConnectSignalR() { @@ -247,8 +258,15 @@ private async Task HubConnectionStateChange(Exception? exception) //#endif - private async Task SetupBodyClasses() + private async Task ConfigureUISetup() { + if (CultureInfoManager.MultilingualEnabled) + { + cultureInfoManager.SetCurrentCulture(new Uri(NavigationManager.Uri).GetCulture() ?? // 1- Culture query string OR Route data request culture + await storageService.GetItem("Culture") ?? // 2- User settings + CultureInfo.CurrentUICulture.Name); // 3- OS settings + } + var cssClasses = new List { }; if (AppPlatform.IsWindows) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor index f17ffc34c5..8f5abe8273 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor @@ -6,14 +6,14 @@ { @Localizer[nameof(AppStrings.CurrentSession)] - + ImageUrl="@($"/_content/Boilerplate.Client.Core/images/os/{GetImageUrl(currentSession.DeviceInfo)}")" /> } @@ -27,13 +27,13 @@ + ImageUrl="@($"/_content/Boilerplate.Client.Core/images/os/{GetImageUrl(session.DeviceInfo)}")" /> s.SessionUniqueId != currentSessionId).ToArray(); - currentSession = userSessions.SingleOrDefault(s => s.SessionUniqueId == currentSessionId); + otherSessions = userSessions.Where(s => s.Id != currentSessionId).ToArray(); + currentSession = userSessions.Single(s => s.Id == currentSessionId); } } private async Task RevokeSession(UserSessionDto session) { - if (isWaiting || session.SessionUniqueId == currentSessionId) return; + if (isWaiting || session.Id == currentSessionId) return; isWaiting = true; try { - await userController.RevokeSession(session.SessionUniqueId, CurrentCancellationToken); + await userController.RevokeSession(session.Id, CurrentCancellationToken); SnackBarService.Success(Localizer[nameof(AppStrings.RemoveSessionSuccessMessage)]); await LoadSessions(); @@ -60,11 +60,11 @@ private async Task RevokeSession(UserSessionDto session) } } - private static string GetImageUrl(string? device) + private static string GetImageUrl(string? deviceInfo) { - if (string.IsNullOrEmpty(device)) return "unknown.png"; + if (string.IsNullOrEmpty(deviceInfo)) return "unknown.png"; - var d = device.ToLowerInvariant(); + var d = deviceInfo.ToLowerInvariant(); if (d.Contains("win") /*Windows, WinUI, Win32*/) return "windows.png"; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Parameters.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Parameters.cs index 253f240cfc..9aabbca204 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Parameters.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Parameters.cs @@ -18,7 +18,7 @@ public class Parameters /// /// Indicates the connection status, with default behavior tied to the SignalR connection status. - /// also allows this value to be updated based on server responses: + /// allows this value to be updated based on server responses: /// - When the first response is received from the server, this value becomes true (Online). /// - When a server connection exception occurs, it becomes false (Offline). /// By default, this value is null (Unknown). 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 310a29d334..b25436cc32 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 @@ -102,10 +102,12 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle optionsBuilder .UseSqlite($"Data Source={dbPath}"); + //#if (framework == 'net9.0') if (AppEnvironment.IsProd()) { optionsBuilder.UseModel(OfflineDbContextModel.Instance); } + //#endif optionsBuilder.EnableSensitiveDataLogging(AppEnvironment.IsDev()) .EnableDetailedErrors(AppEnvironment.IsDev()); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeExtensions.cs index 57df98820e..f8a6baf7db 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IJSRuntimeExtensions.cs @@ -36,9 +36,9 @@ public static ValueTask GoogleRecaptchaReset(this IJSRuntime jsRuntime) //#endif //#if (notification == true) - public static async ValueTask GetDeviceInstallation(this IJSRuntime jsRuntime, string vapidPublicKey) + public static async ValueTask GetPushNotificationSubscription(this IJSRuntime jsRuntime, string vapidPublicKey) { - return await jsRuntime.InvokeAsync("App.getDeviceInstallation", vapidPublicKey); + return await jsRuntime.InvokeAsync("App.getPushNotificationSubscription", vapidPublicKey); } //#endif 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 106bade8b2..e8e2053ed3 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 @@ -25,7 +25,7 @@ class App { } //#if (notification == true) - public static async getDeviceInstallation(vapidPublicKey: string) { + public static async getPushNotificationSubscription(vapidPublicKey: string) { const registration = await navigator.serviceWorker.ready; if (!registration) return null; @@ -42,7 +42,7 @@ class App { const pushChannel = subscription.toJSON(); const p256dh = pushChannel.keys!['p256dh']; const auth = pushChannel.keys!['auth']; - return { installationId: `${p256dh}-${auth}`, platform: 'browser', p256dh: p256dh, auth: auth, endpoint: pushChannel.endpoint }; + return { deviceId: `${p256dh}-${auth}`, platform: 'browser', p256dh: p256dh, auth: auth, endpoint: pushChannel.endpoint }; }; //#endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs index 85483850fa..43b7ce1f12 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs @@ -3,7 +3,7 @@ namespace Boilerplate.Client.Core.Services; -public partial class AuthenticationManager : AuthenticationStateProvider +public partial class AuthenticationManager : AuthenticationStateProvider, IAsyncDisposable { [AutoInject] private Cookie cookie = default!; [AutoInject] private IJSRuntime jsRuntime = default!; @@ -16,6 +16,16 @@ public partial class AuthenticationManager : AuthenticationStateProvider [AutoInject] private IIdentityController identityController = default!; [AutoInject] private ILogger authLogger = default!; + private Action? unsubscribe; + [AutoInject] + private PubSubService pubSubService + { + set + { + unsubscribe = value.Subscribe(SharedPubSubMessages.SESSION_REVOKED, _ => SignOut(default)); + } + } + /// /// Sign in and return whether the user requires two-factor authentication. /// @@ -158,4 +168,9 @@ private async Task ClearTokens() } NotifyAuthenticationStateChanged(Task.FromResult(await GetAuthenticationStateAsync())); } + + public async ValueTask DisposeAsync() + { + unsubscribe?.Invoke(); + } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPushNotificationService.cs index ffba49b061..6e1093a766 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPushNotificationService.cs @@ -6,6 +6,6 @@ public interface IPushNotificationService { string Token { get; set; } Task IsPushNotificationSupported(CancellationToken cancellationToken); - Task GetDeviceInstallation(CancellationToken cancellationToken); - Task RegisterDevice(CancellationToken cancellationToken); + Task GetSubscription(CancellationToken cancellationToken); + Task RegisterSubscription(CancellationToken cancellationToken); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs index 6d5313e969..41e4457e55 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs @@ -1,5 +1,4 @@ //+:cnd:noEmit -using System.Diagnostics; using System.Net; namespace Boilerplate.Client.Core.Services.HttpMessageHandlers; @@ -69,6 +68,7 @@ response.IsSuccessStatusCode is false && { throw new ServerConnectionException(localizer[nameof(AppStrings.ServerConnectionException)], exp); } + //#if (signalR != true) finally { if (isInternalRequest) @@ -76,5 +76,6 @@ response.IsSuccessStatusCode is false && pubSubService.Publish(ClientPubSubMessages.IS_ONLINE_CHANGED, serverCommunicationSuccess); } } + //#endif } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PushNotificationServiceBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PushNotificationServiceBase.cs index 5265598f92..3487d24d78 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PushNotificationServiceBase.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PushNotificationServiceBase.cs @@ -10,9 +10,9 @@ public abstract partial class PushNotificationServiceBase : IPushNotificationSer public virtual string Token { get; set; } public virtual Task IsPushNotificationSupported(CancellationToken cancellationToken) => Task.FromResult(false); - public abstract Task GetDeviceInstallation(CancellationToken cancellationToken); + public abstract Task GetSubscription(CancellationToken cancellationToken); - public async Task RegisterDevice(CancellationToken cancellationToken) + public async Task RegisterSubscription(CancellationToken cancellationToken) { if (await IsPushNotificationSupported(cancellationToken) is false) { @@ -20,19 +20,19 @@ public async Task RegisterDevice(CancellationToken cancellationToken) return; } - var deviceInstallation = await GetDeviceInstallation(cancellationToken); + var subscription = await GetSubscription(cancellationToken); - if (deviceInstallation is null) + if (subscription is null) { - Logger.LogWarning("Could not retrieve device installation"); // Browser's incognito mode etc. + Logger.LogWarning("Could not retrieve push notification subscription"); // Browser's incognito mode etc. return; } - await pushNotificationController.RegisterDevice(deviceInstallation, cancellationToken); + await pushNotificationController.RegisterSubscription(subscription, cancellationToken); } - public async Task DeregisterDevice(string deviceInstallationId, CancellationToken cancellationToken) + public async Task DeregisterSubscription(string deviceId, CancellationToken cancellationToken) { - await pushNotificationController.DeregisterDevice(deviceInstallationId, cancellationToken); + await pushNotificationController.DeregisterSubscription(deviceId, cancellationToken); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignalRInfinitiesRetryPolicy.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignalRInfinitiesRetryPolicy.cs index 99e519dd8f..3e758969cd 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignalRInfinitiesRetryPolicy.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignalRInfinitiesRetryPolicy.cs @@ -4,7 +4,7 @@ namespace Boilerplate.Client.Core.Services; public class SignalRInfinitiesRetryPolicy : IRetryPolicy { - private static TimeSpan[] delays = new double[] { 1, 3, 5, 10, 15, 20, 30, 45, 59 } + private static TimeSpan[] delays = new double[] { 1, 3, 5, 10, 15, 20, 30 } .Select(TimeSpan.FromSeconds) .ToArray(); @@ -17,6 +17,6 @@ public class SignalRInfinitiesRetryPolicy : IRetryPolicy return delays[index]; } - return TimeSpan.FromMinutes(1); + return TimeSpan.FromSeconds(30); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/AndroidPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/AndroidPushNotificationService.cs index 260a3f13a9..5eadb473b9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/AndroidPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/AndroidPushNotificationService.cs @@ -23,7 +23,7 @@ public async override Task IsPushNotificationSupported(CancellationToken c public string GetDeviceId() => Secure.GetString(Platform.AppContext.ContentResolver, Secure.AndroidId)!; - public override async Task GetDeviceInstallation(CancellationToken cancellationToken) + public override async Task GetSubscription(CancellationToken cancellationToken) { try { @@ -43,13 +43,13 @@ public override async Task GetDeviceInstallation(Cancella throw new InvalidOperationException("Unable to resolve token for FCMv1.", exp); } - var installation = new DeviceInstallationDto + var subscription = new PushNotificationSubscriptionDto { - InstallationId = GetDeviceId(), + DeviceId = GetDeviceId(), Platform = "fcmV1", PushChannel = Token }; - return installation; + return subscription; } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/PushNotificationFirebaseMessagingService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/PushNotificationFirebaseMessagingService.cs index 02840ebf82..95def2bc6e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/PushNotificationFirebaseMessagingService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/PushNotificationFirebaseMessagingService.cs @@ -17,7 +17,7 @@ public override async void OnNewToken(string token) { PushNotificationService.Token = token; - await PushNotificationService.RegisterDevice(default); + await PushNotificationService.RegisterSubscription(default); } catch (Exception exp) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs index 78234c739f..7455006382 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs @@ -48,7 +48,7 @@ public async void RegisteredForRemoteNotifications(UIApplication application, NS try { NotificationService.Token = deviceToken.ToHexString()!; - await NotificationService.RegisterDevice(default); + await NotificationService.RegisterSubscription(default); } catch (Exception exp) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Services/MacCatalystPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Services/MacCatalystPushNotificationService.cs index c286c0771a..9051249542 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Services/MacCatalystPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/Services/MacCatalystPushNotificationService.cs @@ -21,7 +21,7 @@ public async override Task IsPushNotificationSupported(CancellationToken c public string GetDeviceId() => UIDevice.CurrentDevice.IdentifierForVendor.ToString(); - public override async Task GetDeviceInstallation(CancellationToken cancellationToken) + public override async Task GetSubscription(CancellationToken cancellationToken) { using CancellationTokenSource cts = new(TimeSpan.FromSeconds(15)); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cts.Token); @@ -41,13 +41,13 @@ public override async Task GetDeviceInstallation(Cancella throw new InvalidOperationException("Unable to resolve token for APNS.", exp); } - var installation = new DeviceInstallationDto + var subscription = new PushNotificationSubscriptionDto { - InstallationId = GetDeviceId(), + DeviceId = GetDeviceId(), Platform = "apns", PushChannel = Token }; - return installation; + return subscription; } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Services/WindowsPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Services/WindowsPushNotificationService.cs index 3c58938e54..c474371e39 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Services/WindowsPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Windows/Services/WindowsPushNotificationService.cs @@ -4,6 +4,6 @@ namespace Boilerplate.Client.Maui.Platforms.Windows.Services; public partial class WindowsPushNotificationService : PushNotificationServiceBase { - public override Task GetDeviceInstallation(CancellationToken cancellationToken) => + public override Task GetSubscription(CancellationToken cancellationToken) => throw new NotImplementedException(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/AppDelegate.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/AppDelegate.cs index 63b8d75edb..34784a4fe7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/AppDelegate.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/AppDelegate.cs @@ -48,7 +48,7 @@ public async void RegisteredForRemoteNotifications(UIApplication application, NS try { NotificationService.Token = deviceToken.ToHexString()!; - await NotificationService.RegisterDevice(default); + await NotificationService.RegisterSubscription(default); } catch (Exception exp) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Services/iOSPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Services/iOSPushNotificationService.cs index 123d18e129..ab3aa8a942 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Services/iOSPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/Services/iOSPushNotificationService.cs @@ -21,7 +21,7 @@ public async override Task IsPushNotificationSupported(CancellationToken c public string GetDeviceId() => UIDevice.CurrentDevice.IdentifierForVendor.ToString(); - public override async Task GetDeviceInstallation(CancellationToken cancellationToken) + public override async Task GetSubscription(CancellationToken cancellationToken) { using CancellationTokenSource cts = new(TimeSpan.FromSeconds(15)); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, cts.Token); @@ -41,13 +41,13 @@ public override async Task GetDeviceInstallation(Cancella throw new InvalidOperationException("Unable to resolve token for APNS.", exp); } - var installation = new DeviceInstallationDto + var subscription = new PushNotificationSubscriptionDto { - InstallationId = GetDeviceId(), + DeviceId = GetDeviceId(), Platform = "apns", PushChannel = Token }; - return installation; + return subscription; } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebPushNotificationService.cs index 708b522bbb..b087f66beb 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebPushNotificationService.cs @@ -9,9 +9,9 @@ public partial class WebPushNotificationService : PushNotificationServiceBase [AutoInject] private readonly IJSRuntime jSRuntime = default!; [AutoInject] private readonly ClientWebSettings clientWebSettings = default!; - public async override Task GetDeviceInstallation(CancellationToken cancellationToken) + public async override Task GetSubscription(CancellationToken cancellationToken) { - return await jSRuntime.GetDeviceInstallation(clientWebSettings.AdsPushVapid!.PublicKey); + return await jSRuntime.GetPushNotificationSubscription(clientWebSettings.AdsPushVapid!.PublicKey); } public override async Task IsPushNotificationSupported(CancellationToken cancellationToken) => clientWebSettings.WebAppRender.PwaEnabled diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsPushNotificationService.cs index c28d3d98a2..307d6b48db 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsPushNotificationService.cs @@ -4,6 +4,6 @@ namespace Boilerplate.Client.Windows.Services; public partial class WindowsPushNotificationService : PushNotificationServiceBase { - public override Task GetDeviceInstallation(CancellationToken cancellationToken) => + public override Task GetSubscription(CancellationToken cancellationToken) => throw new NotImplementedException(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props index 1075a9f1c5..7f039ccb52 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props @@ -39,7 +39,7 @@ - + @@ -52,7 +52,7 @@ - + @@ -69,7 +69,7 @@ - + @@ -77,7 +77,7 @@ - + @@ -88,8 +88,8 @@ - - + + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props index d50dc52623..8138b10b1d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props @@ -39,7 +39,7 @@ - + @@ -52,7 +52,7 @@ - + @@ -69,7 +69,7 @@ - + @@ -77,7 +77,7 @@ - + @@ -88,8 +88,8 @@ - - + + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs index 536fe678c6..2e39e97516 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs @@ -98,7 +98,8 @@ public async Task Delete(Guid id, string concurrencyStamp, CancellationToken can //#if (signalR == true) private async Task PublishDashboardDataChanged(CancellationToken cancellationToken) { - await appHubContext.Clients.Group("AuthenticatedClients").SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.DASHBOARD_DATA_CHANGED, cancellationToken); + // Checkout AppHubConnectionHandler's comments for more info. + await appHubContext.Clients.GroupExcept("AuthenticatedClients", excludedConnectionIds: [User.GetSessionId().ToString()]).SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.DASHBOARD_DATA_CHANGED, cancellationToken); } //#endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.ResetPassword.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.ResetPassword.cs index 3544df9e41..15b16b7bb9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.ResetPassword.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.ResetPassword.cs @@ -53,11 +53,12 @@ public async Task SendResetPasswordToken(SendResetPasswordTokenRequestDto reques } //#if (signalR == true) + // Checkout AppHubConnectionHandler's comments for more info. sendMessagesTasks.Add(appHubContext.Clients.User(user.Id.ToString()).SendAsync(SignalREvents.SHOW_MESSAGE, message, cancellationToken)); //#endif //#if (notification == true) - sendMessagesTasks.Add(pushNotificationService.RequestPush(message: message, customDeviceFilter: d => d.UserId == user.Id, cancellationToken: cancellationToken)); + sendMessagesTasks.Add(pushNotificationService.RequestPush(message: message, userRelatedPush: true, customSubscriptionFilter: s => s.UserSession!.UserId == user.Id, cancellationToken: cancellationToken)); //#endif await Task.WhenAll(sendMessagesTasks); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs index 5abd60e54d..ba07d3d96a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs @@ -95,7 +95,7 @@ public async Task SignIn(SignInRequestDto request, CancellationToken cancellatio var user = await userManager.FindUserAsync(request) ?? throw new UnauthorizedException(Localizer[nameof(AppStrings.InvalidUserCredentials)]); - var userSession = CreateUserSession(request.DeviceInfo); + var userSession = CreateUserSession(user.Id, request.DeviceInfo); bool isOtpSignIn = string.IsNullOrEmpty(request.Otp) is false; @@ -134,11 +134,12 @@ public async Task SignIn(SignInRequestDto request, CancellationToken cancellatio throw new ResourceValidationException(updateResult.Errors.Select(e => new LocalizedString(e.Code, e.Description)).ToArray()); } - user.Sessions.Add(userSession); + DbContext.UserSessions.Add(userSession); user.TwoFactorTokenRequestedOn = null; var addUserSessionResult = await userManager.UpdateAsync(user); if (addUserSessionResult.Succeeded is false) throw new ResourceValidationException(addUserSessionResult.Errors.Select(e => new LocalizedString(e.Code, e.Description)).ToArray()); + await DbContext.SaveChangesAsync(cancellationToken); } private async Task CheckTwoFactorCode(string code) @@ -161,20 +162,21 @@ public async Task SignIn(SignInRequestDto request, CancellationToken cancellatio /// /// Creates a user session and adds its ID to the access and refresh tokens, but only if the sign-in is successful /// - private UserSession CreateUserSession(string? device) + private UserSession CreateUserSession(Guid userId, string? deviceInfo) { var userSession = new UserSession { - SessionUniqueId = Guid.NewGuid(), + Id = Guid.NewGuid(), + DeviceInfo = deviceInfo, + UserId = userId, + StartedOn = DateTimeOffset.UtcNow, + IP = HttpContext.Connection.RemoteIpAddress?.ToString(), // Relying on Cloudflare cdn to retrieve address. // https://developers.cloudflare.com/rules/transform/managed-transforms/reference/#add-visitor-location-headers Address = $"{Request.Headers["cf-ipcountry"]}, {Request.Headers["cf-ipcity"]}", - Device = device, - IP = HttpContext.Connection.RemoteIpAddress?.ToString(), - StartedOn = DateTimeOffset.UtcNow }; - userClaimsPrincipalFactory.SessionClaims.Add(new("session-id", userSession.SessionUniqueId.ToString())); + userClaimsPrincipalFactory.SessionClaims.Add(new("session-id", userSession.Id.ToString())); return userSession; } @@ -198,7 +200,7 @@ public async Task> Refresh(RefreshRequestDto requ var currentSessionId = Guid.Parse(refreshTicket.Principal.FindFirstValue("session-id") ?? throw new InvalidOperationException("session id could not be found")); user = await userManager.FindByIdAsync(userId) ?? throw new UnauthorizedException(); // User might have been deleted. - userSession = user!.Sessions.Find(s => s.SessionUniqueId == currentSessionId) ?? throw new UnauthorizedException(); // User session might have been deleted. + userSession = await DbContext.UserSessions.FirstOrDefaultAsync(us => us.Id == currentSessionId) ?? throw new UnauthorizedException(); // User session might have been deleted. if (await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not User _) throw new UnauthorizedException(); @@ -213,17 +215,12 @@ public async Task> Refresh(RefreshRequestDto requ } catch when (userSession is not null) { - user!.Sessions.Remove(userSession); + DbContext.UserSessions.Remove(userSession); throw; } finally { - if (user is not null) - { - var result = await userManager.UpdateAsync(user); - if (!result.Succeeded) - throw new ResourceValidationException(result.Errors.Select(err => new LocalizedString(err.Code, err.Description)).ToArray()); - } + await DbContext.SaveChangesAsync(); } } @@ -267,11 +264,12 @@ public async Task SendOtp(IdentityRequestDto request, string? returnUrl = null, //#endif //#if (signalR == true) + // Checkout AppHubConnectionHandler's comments for more info. sendMessagesTasks.Add(appHubContext.Clients.User(user.Id.ToString()).SendAsync(SignalREvents.SHOW_MESSAGE, pushMessage, cancellationToken)); //#endif //#if (notification == true) - sendMessagesTasks.Add(pushNotificationService.RequestPush(message: pushMessage, customDeviceFilter: d => d.UserId == user.Id, cancellationToken: cancellationToken)); + sendMessagesTasks.Add(pushNotificationService.RequestPush(message: pushMessage, userRelatedPush: true, customSubscriptionFilter: s => s.UserSession!.UserId == user.Id, cancellationToken: cancellationToken)); //#endif await Task.WhenAll(sendMessagesTasks); @@ -324,6 +322,7 @@ public async Task SendTwoFactorToken(SignInRequestDto request, CancellationToken //#if (signalR == true) if (firstStepAuthenticationMethod != "SignalR") { + // Checkout AppHubConnectionHandler's comments for more info. sendMessagesTasks.Add(appHubContext.Clients.User(user.Id.ToString()).SendAsync(SignalREvents.SHOW_MESSAGE, message, cancellationToken)); } //#endif @@ -331,7 +330,7 @@ public async Task SendTwoFactorToken(SignInRequestDto request, CancellationToken //#if (notification == true) if (firstStepAuthenticationMethod != "Push") { - sendMessagesTasks.Add(pushNotificationService.RequestPush(message: message, customDeviceFilter: d => d.UserId == user.Id, cancellationToken: cancellationToken)); + sendMessagesTasks.Add(pushNotificationService.RequestPush(message: message, userRelatedPush: true, customSubscriptionFilter: s => s.UserSession!.UserId == user.Id, cancellationToken: cancellationToken)); } //#endif diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs index 497437d052..c24d73bac8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs @@ -1,4 +1,5 @@ -using System.Text; +//+:cnd:noEmit +using System.Text; using System.Text.Encodings.Web; using QRCoder; using Humanizer; @@ -6,6 +7,10 @@ using Boilerplate.Shared.Dtos.Identity; using Boilerplate.Server.Api.Models.Identity; using Boilerplate.Shared.Controllers.Identity; +//#if (signalR == true) +using Microsoft.AspNetCore.SignalR; +using Boilerplate.Server.Api.SignalR; +//#endif namespace Boilerplate.Server.Api.Controllers.Identity; @@ -24,6 +29,10 @@ public partial class UserController : AppControllerBase, IUserController [AutoInject] private UrlEncoder urlEncoder = default!; + //#if (signalR == true) + [AutoInject] private IHubContext appHubContext = default!; + //#endif + [HttpGet] public async Task GetCurrentUser(CancellationToken cancellationToken) { @@ -40,10 +49,7 @@ public async Task> GetUserSessions(CancellationToken cancel { var userId = User.GetUserId(); - var user = await userManager.FindByIdAsync(userId.ToString()) - ?? throw new ResourceNotFoundException(); - - return user.Sessions + return (await DbContext.UserSessions.Where(us => us.UserId == userId).ToArrayAsync(cancellationToken)) .Select(us => { var dto = us.Map(); @@ -61,18 +67,19 @@ public async Task> GetUserSessions(CancellationToken cancel [HttpPost] public async Task SignOut(CancellationToken cancellationToken) { - var userId = User.GetUserId(); - - var user = await userManager.FindByIdAsync(userId.ToString()) - ?? throw new ResourceNotFoundException(); - var currentSessionId = Guid.Parse(User.FindFirstValue("session-id")!); - user.Sessions = user.Sessions.Where(s => s.SessionUniqueId != currentSessionId).ToList(); + var userSession = await DbContext.UserSessions + //#if (notification == true) + .Include(us => us.PushNotificationSubscription) + //#endif + .FirstOrDefaultAsync(us => us.Id == currentSessionId, cancellationToken) ?? throw new ResourceNotFoundException(); - var result = await userManager.UpdateAsync(user); - if (result.Succeeded is false) - throw new ResourceValidationException(result.Errors.Select(err => new LocalizedString(err.Code, err.Description)).ToArray()); + DbContext.UserSessions.Remove(userSession); + await DbContext.SaveChangesAsync(cancellationToken); + + DbContext.UserSessions.Remove(new() { Id = currentSessionId }); + await DbContext.SaveChangesAsync(cancellationToken); SignOut(); } @@ -82,27 +89,24 @@ public async Task RevokeSession(Guid id, CancellationToken cancellationToken) { var userId = User.GetUserId(); - var user = await userManager.FindByIdAsync(userId.ToString()) - ?? throw new ResourceNotFoundException(); - var currentSessionId = Guid.Parse(User.FindFirstValue("session-id")!); if (id == currentSessionId) throw new BadRequestException(); // "Call SignOut instead" - var currentSession = user.Sessions.SingleOrDefault(s => s.SessionUniqueId == currentSessionId) - ?? throw new ResourceNotFoundException(); + var userSession = await DbContext.UserSessions + //#if (notification == true) + .Include(us => us.PushNotificationSubscription) + //#endif + .FirstOrDefaultAsync(us => us.Id == id, cancellationToken) ?? throw new ResourceNotFoundException(); - var revokeUserSessionsDelay = (DateTimeOffset.Now - currentSession.StartedOn) - AppSettings.Identity.RevokeUserSessionsDelay; + DbContext.UserSessions.Remove(userSession); + await DbContext.SaveChangesAsync(cancellationToken); - if (revokeUserSessionsDelay < TimeSpan.Zero) - throw new BadRequestException(Localizer[nameof(AppStrings.WaitForRevokeSessionDelay), revokeUserSessionsDelay.Humanize(culture: CultureInfo.CurrentUICulture)]); - - user.Sessions = user.Sessions.Where(s => s.SessionUniqueId != id).ToList(); - - var result = await userManager.UpdateAsync(user); - if (result.Succeeded is false) - throw new ResourceValidationException(result.Errors.Select(err => new LocalizedString(err.Code, err.Description)).ToArray()); + //#if (signalR == true) + // Checkout AppHubConnectionHandler's comments for more info. + await appHubContext.Clients.Client(userSession.Id.ToString()).SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.SESSION_REVOKED, cancellationToken); + //#endif } [HttpPut] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs index b455070c9b..cbed116114 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs @@ -93,7 +93,8 @@ public async Task Delete(Guid id, string concurrencyStamp, CancellationToken can //#if (signalR == true) private async Task PublishDashboardDataChanged(CancellationToken cancellationToken) { - await appHubContext.Clients.Group("AuthenticatedClients").SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.DASHBOARD_DATA_CHANGED, cancellationToken); + // Checkout AppHubConnectionHandler's comments for more info. + await appHubContext.Clients.GroupExcept("AuthenticatedClients", excludedConnectionIds: [User.GetSessionId().ToString()]).SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.DASHBOARD_DATA_CHANGED, cancellationToken); } //#endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/PushNotification/PushNotificationController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/PushNotification/PushNotificationController.cs index 081138ce2c..122d6bd11d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/PushNotification/PushNotificationController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/PushNotification/PushNotificationController.cs @@ -12,22 +12,22 @@ public partial class PushNotificationController : AppControllerBase, IPushNotifi [AutoInject] PushNotificationService pushNotificationService = default!; [HttpPost] - public async Task RegisterDevice([Required] DeviceInstallationDto deviceInstallation, CancellationToken cancellationToken) + public async Task RegisterSubscription([Required] PushNotificationSubscriptionDto subscription, CancellationToken cancellationToken) { - await pushNotificationService.RegisterDevice(deviceInstallation, cancellationToken); + await pushNotificationService.RegisterSubscription(subscription, cancellationToken); } [HttpPost("{deviceId}")] - public async Task DeregisterDevice([Required] string deviceId, CancellationToken cancellationToken) + public async Task DeregisterSubscription([Required] string deviceId, CancellationToken cancellationToken) { - await pushNotificationService.DeregisterDevice(deviceId, cancellationToken); + await pushNotificationService.DeregisterSubscription(deviceId, cancellationToken); } #if Development // This action is for testing purposes only. [HttpPost] public async Task RequestPush([FromQuery] string? title = null, [FromQuery] string? message = null, [FromQuery] string? action = null, CancellationToken cancellationToken = default) { - await pushNotificationService.RequestPush(title, message, action, null, cancellationToken); + await pushNotificationService.RequestPush(title, message, action, false, null, cancellationToken); } #endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/AppDbContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/AppDbContext.cs index 39f2c1e9e3..8c1eeead6d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/AppDbContext.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/AppDbContext.cs @@ -20,6 +20,8 @@ public partial class AppDbContext(DbContextOptions options) { public DbSet DataProtectionKeys { get; set; } = default!; + public DbSet UserSessions { get; set; } = default!; + //#if (sample == "Todo") public DbSet TodoItems { get; set; } = default!; //#elif (sample == "Admin") @@ -27,7 +29,7 @@ public partial class AppDbContext(DbContextOptions options) public DbSet Products { get; set; } = default!; //#endif //#if (notification == true) - public DbSet DeviceInstallations { get; set; } = default!; + public DbSet PushNotificationSubscriptions { get; set; } = default!; //#endif protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -189,8 +191,8 @@ private void ConfigureContainers(ModelBuilder builder) //#endif //#if (notification == true) - builder.Entity() - .ToContainer("DeviceInstallations").HasPartitionKey(e => e.Platform); + builder.Entity() + .ToContainer("PushNotificationSubscriptions").HasPartitionKey(e => e.Platform); //#endif } //#endif diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Identity/UserConfiguration.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Identity/UserConfiguration.cs index ad71787c83..25d2f86fb4 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Identity/UserConfiguration.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Identity/UserConfiguration.cs @@ -30,11 +30,6 @@ public void Configure(EntityTypeBuilder builder) PasswordHash = "AQAAAAIAAYagAAAAEP0v3wxkdWtMkHA3Pp5/JfS+42/Qto9G05p2mta6dncSK37hPxEHa3PGE4aqN30Aag==", // 123456 }]); - builder.Property(u => u.Sessions) - .HasConversion(v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), - v => JsonSerializer.Deserialize>(v, (JsonSerializerOptions?)null)!); - // You can also use builder.OwnsMany(u => u.Sessions, navBuilder => navBuilder.ToJson()); - //#if (database == "Cosmos") builder.Property(b => b.ConcurrencyStamp) .IsETagConcurrency(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/PushNotification/PushNotificationSubscriptionConfiguration.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/PushNotification/PushNotificationSubscriptionConfiguration.cs new file mode 100644 index 0000000000..10313f02db --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/PushNotification/PushNotificationSubscriptionConfiguration.cs @@ -0,0 +1,31 @@ +using Boilerplate.Server.Api.Models.PushNotification; + +namespace Boilerplate.Server.Api.Data.Configurations.PushNotification; + +public class PushNotificationSubscriptionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .HasOne(sub => sub.UserSession) + .WithOne(us => us.PushNotificationSubscription) + .HasForeignKey(sub => sub.UserSessionId) + .OnDelete(DeleteBehavior.ClientSetNull); + + //#if (database != "PostgreSQL") + builder + .HasIndex(b => b.UserSessionId) + .HasFilter($"[{nameof(PushNotificationSubscription.UserSessionId)}] IS NOT NULL") + .IsUnique(); + //#endif + //#if (IsInsideProjectTemplate == true) + return; + //#endif + //#if (database == "PostgreSQL") + builder + .HasIndex(b => b.UserSessionId) + .HasFilter($"'{nameof(PushNotificationSubscription.UserSessionId)}' IS NOT NULL") + .IsUnique(); + //#endif + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241107182721_InitialMigration.Designer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241122104915_InitialMigration.Designer.cs similarity index 89% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241107182721_InitialMigration.Designer.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241122104915_InitialMigration.Designer.cs index 6d697cb8a1..f73a21583c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241107182721_InitialMigration.Designer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241122104915_InitialMigration.Designer.cs @@ -6,7 +6,7 @@ namespace Boilerplate.Server.Api.Data.Migrations; [DbContext(typeof(AppDbContext))] -[Migration("20241107182721_InitialMigration")] +[Migration("20241122104915_InitialMigration")] partial class InitialMigration { /// @@ -39,7 +39,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "Categories") - .HasAnnotation("Cosmos:PartitionKeyName", "Id"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "Id" }); b.HasData( new @@ -109,7 +109,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "Roles") - .HasAnnotation("Cosmos:PartitionKeyName", "Id"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "Id" }); }); modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.User", b => @@ -184,10 +184,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("SecurityStamp") .HasColumnType("TEXT"); - b.Property("Sessions") - .IsRequired() - .HasColumnType("TEXT"); - b.Property("TwoFactorEnabled") .HasColumnType("INTEGER"); @@ -219,7 +215,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "Users") - .HasAnnotation("Cosmos:PartitionKeyName", "Id"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "Id" }); b.HasData( new @@ -240,12 +236,42 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) PhoneNumber = "+31684207362", PhoneNumberConfirmed = true, SecurityStamp = "959ff4a9-4b07-4cc1-8141-c5fc033daf83", - Sessions = "[]", TwoFactorEnabled = false, UserName = "test" }); }); + modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("DeviceInfo") + .HasColumnType("TEXT"); + + b.Property("IP") + .HasColumnType("TEXT"); + + b.Property("RenewedOn") + .HasColumnType("INTEGER"); + + b.Property("StartedOn") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserSessions"); + }); + modelBuilder.Entity("Boilerplate.Server.Api.Models.Products.Product", b => { b.Property("Id") @@ -282,7 +308,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "Products") - .HasAnnotation("Cosmos:PartitionKeyName", "CategoryId"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "CategoryId" }); b.HasData( new @@ -517,9 +543,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) }); }); - modelBuilder.Entity("Boilerplate.Server.Api.Models.PushNotification.DeviceInstallation", b => + modelBuilder.Entity("Boilerplate.Server.Api.Models.PushNotification.PushNotificationSubscription", b => { - b.Property("InstallationId") + b.Property("DeviceId") .HasColumnType("TEXT"); b.Property("Auth") @@ -542,22 +568,27 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); - b.Property("Tags") + b.Property("RenewedOn") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("Tags") .IsRequired() .HasColumnType("TEXT"); - b.Property("UserId") + b.Property("UserSessionId") .HasColumnType("TEXT"); - b.HasKey("InstallationId"); + b.HasKey("DeviceId"); - b.HasIndex("UserId"); + b.HasIndex("UserSessionId") + .IsUnique() + .HasFilter("[UserSessionId] IS NOT NULL"); - b.ToTable("DeviceInstallations"); + b.ToTable("PushNotificationSubscriptions"); b - .HasAnnotation("Cosmos:ContainerName", "DeviceInstallations") - .HasAnnotation("Cosmos:PartitionKeyName", "Platform"); + .HasAnnotation("Cosmos:ContainerName", "PushNotificationSubscriptions") + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "Platform" }); }); modelBuilder.Entity("Boilerplate.Server.Api.Models.Todo.TodoItem", b => @@ -587,7 +618,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "TodoItems") - .HasAnnotation("Cosmos:PartitionKeyName", "Id"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "Id" }); }); modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => @@ -608,7 +639,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "DataProtectionKeys") - .HasAnnotation("Cosmos:PartitionKeyName", "Id"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "Id" }); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => @@ -634,7 +665,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "RoleClaims") - .HasAnnotation("Cosmos:PartitionKeyName", "RoleId"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "RoleId" }); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => @@ -660,7 +691,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "UserClaims") - .HasAnnotation("Cosmos:PartitionKeyName", "UserId"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "UserId" }); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => @@ -685,7 +716,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "UserLogins") - .HasAnnotation("Cosmos:PartitionKeyName", "UserId"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "UserId" }); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => @@ -704,7 +735,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "UserRoles") - .HasAnnotation("Cosmos:PartitionKeyName", "UserId"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "UserId" }); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => @@ -727,7 +758,18 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "UserTokens") - .HasAnnotation("Cosmos:PartitionKeyName", "UserId"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "UserId" }); + }); + + modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.UserSession", b => + { + b.HasOne("Boilerplate.Server.Api.Models.Identity.User", "User") + .WithMany("Sessions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); }); modelBuilder.Entity("Boilerplate.Server.Api.Models.Products.Product", b => @@ -741,13 +783,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Category"); }); - modelBuilder.Entity("Boilerplate.Server.Api.Models.PushNotification.DeviceInstallation", b => + modelBuilder.Entity("Boilerplate.Server.Api.Models.PushNotification.PushNotificationSubscription", b => { - b.HasOne("Boilerplate.Server.Api.Models.Identity.User", "User") - .WithMany() - .HasForeignKey("UserId"); + b.HasOne("Boilerplate.Server.Api.Models.Identity.UserSession", "UserSession") + .WithOne("PushNotificationSubscription") + .HasForeignKey("Boilerplate.Server.Api.Models.PushNotification.PushNotificationSubscription", "UserSessionId"); - b.Navigation("User"); + b.Navigation("UserSession"); }); modelBuilder.Entity("Boilerplate.Server.Api.Models.Todo.TodoItem", b => @@ -816,6 +858,16 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Navigation("Products"); }); + + modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.User", b => + { + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.UserSession", b => + { + b.Navigation("PushNotificationSubscription"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241107182721_InitialMigration.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241122104915_InitialMigration.cs similarity index 91% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241107182721_InitialMigration.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241122104915_InitialMigration.cs index 5530558a95..e9ba9a2b6c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241107182721_InitialMigration.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241122104915_InitialMigration.cs @@ -61,7 +61,6 @@ protected override void Up(MigrationBuilder migrationBuilder) Gender = table.Column(type: "INTEGER", nullable: true), BirthDate = table.Column(type: "INTEGER", nullable: true), ProfileImageName = table.Column(type: "TEXT", nullable: true), - Sessions = table.Column(type: "TEXT", nullable: false), EmailTokenRequestedOn = table.Column(type: "INTEGER", nullable: true), PhoneNumberTokenRequestedOn = table.Column(type: "INTEGER", nullable: true), ResetPasswordTokenRequestedOn = table.Column(type: "INTEGER", nullable: true), @@ -131,30 +130,6 @@ protected override void Up(MigrationBuilder migrationBuilder) onDelete: ReferentialAction.Cascade); }); - migrationBuilder.CreateTable( - name: "DeviceInstallations", - columns: table => new - { - InstallationId = table.Column(type: "TEXT", nullable: false), - Platform = table.Column(type: "TEXT", nullable: false), - PushChannel = table.Column(type: "TEXT", nullable: false), - P256dh = table.Column(type: "TEXT", nullable: true), - Auth = table.Column(type: "TEXT", nullable: true), - Endpoint = table.Column(type: "TEXT", nullable: true), - UserId = table.Column(type: "TEXT", nullable: true), - Tags = table.Column(type: "TEXT", nullable: false), - ExpirationTime = table.Column(type: "INTEGER", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_DeviceInstallations", x => x.InstallationId); - table.ForeignKey( - name: "FK_DeviceInstallations_Users_UserId", - column: x => x.UserId, - principalTable: "Users", - principalColumn: "Id"); - }); - migrationBuilder.CreateTable( name: "TodoItems", columns: table => new @@ -241,6 +216,29 @@ protected override void Up(MigrationBuilder migrationBuilder) onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "UserSessions", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + IP = table.Column(type: "TEXT", nullable: true), + DeviceInfo = table.Column(type: "TEXT", nullable: true), + Address = table.Column(type: "TEXT", nullable: true), + StartedOn = table.Column(type: "INTEGER", nullable: false), + RenewedOn = table.Column(type: "INTEGER", nullable: true), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserSessions", x => x.Id); + table.ForeignKey( + name: "FK_UserSessions_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "UserTokens", columns: table => new @@ -261,6 +259,31 @@ protected override void Up(MigrationBuilder migrationBuilder) onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "PushNotificationSubscriptions", + columns: table => new + { + DeviceId = table.Column(type: "TEXT", nullable: false), + Platform = table.Column(type: "TEXT", nullable: false), + PushChannel = table.Column(type: "TEXT", nullable: false), + P256dh = table.Column(type: "TEXT", nullable: true), + Auth = table.Column(type: "TEXT", nullable: true), + Endpoint = table.Column(type: "TEXT", nullable: true), + UserSessionId = table.Column(type: "TEXT", nullable: true), + Tags = table.Column(type: "TEXT", nullable: false), + ExpirationTime = table.Column(type: "INTEGER", nullable: false), + RenewedOn = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PushNotificationSubscriptions", x => x.DeviceId); + table.ForeignKey( + name: "FK_PushNotificationSubscriptions_UserSessions_UserSessionId", + column: x => x.UserSessionId, + principalTable: "UserSessions", + principalColumn: "Id"); + }); + migrationBuilder.InsertData( table: "Categories", columns: new[] { "Id", "Color", "ConcurrencyStamp", "Name" }, @@ -275,8 +298,8 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.InsertData( table: "Users", - columns: new[] { "Id", "AccessFailedCount", "BirthDate", "Email", "EmailConfirmed", "EmailTokenRequestedOn", "FullName", "Gender", "LockoutEnabled", "LockoutEnd", "NormalizedEmail", "NormalizedUserName", "OtpRequestedOn", "PasswordHash", "PhoneNumber", "PhoneNumberConfirmed", "PhoneNumberTokenRequestedOn", "ProfileImageName", "ResetPasswordTokenRequestedOn", "SecurityStamp", "Sessions", "TwoFactorEnabled", "TwoFactorTokenRequestedOn", "UserName" }, - values: new object[] { new Guid("8ff71671-a1d6-4f97-abb9-d87d7b47d6e7"), 0, 1306790461440000000L, "test@bitplatform.dev", true, 1306790461440000000L, "Boilerplate test account", 0, true, null, "TEST@BITPLATFORM.DEV", "TEST", null, "AQAAAAIAAYagAAAAEP0v3wxkdWtMkHA3Pp5/JfS+42/Qto9G05p2mta6dncSK37hPxEHa3PGE4aqN30Aag==", "+31684207362", true, null, null, null, "959ff4a9-4b07-4cc1-8141-c5fc033daf83", "[]", false, null, "test" }); + columns: new[] { "Id", "AccessFailedCount", "BirthDate", "Email", "EmailConfirmed", "EmailTokenRequestedOn", "FullName", "Gender", "LockoutEnabled", "LockoutEnd", "NormalizedEmail", "NormalizedUserName", "OtpRequestedOn", "PasswordHash", "PhoneNumber", "PhoneNumberConfirmed", "PhoneNumberTokenRequestedOn", "ProfileImageName", "ResetPasswordTokenRequestedOn", "SecurityStamp", "TwoFactorEnabled", "TwoFactorTokenRequestedOn", "UserName" }, + values: new object[] { new Guid("8ff71671-a1d6-4f97-abb9-d87d7b47d6e7"), 0, 1306790461440000000L, "test@bitplatform.dev", true, 1306790461440000000L, "Boilerplate test account", 0, true, null, "TEST@BITPLATFORM.DEV", "TEST", null, "AQAAAAIAAYagAAAAEP0v3wxkdWtMkHA3Pp5/JfS+42/Qto9G05p2mta6dncSK37hPxEHa3PGE4aqN30Aag==", "+31684207362", true, null, null, null, "959ff4a9-4b07-4cc1-8141-c5fc033daf83", false, null, "test" }); migrationBuilder.InsertData( table: "Products", @@ -308,16 +331,18 @@ protected override void Up(MigrationBuilder migrationBuilder) { new Guid("fb41cc51-9abd-4b45-b0d9-ea8f565ec502"), new Guid("ecf0496f-f1e3-4d92-8fe4-0d7fa2b4ffa4"), new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }, 1306440105984000000L, "", "530i", 55195m } }); - migrationBuilder.CreateIndex( - name: "IX_DeviceInstallations_UserId", - table: "DeviceInstallations", - column: "UserId"); - migrationBuilder.CreateIndex( name: "IX_Products_CategoryId", table: "Products", column: "CategoryId"); + migrationBuilder.CreateIndex( + name: "IX_PushNotificationSubscriptions_UserSessionId", + table: "PushNotificationSubscriptions", + column: "UserSessionId", + unique: true, + filter: "[UserSessionId] IS NOT NULL"); + migrationBuilder.CreateIndex( name: "IX_RoleClaims_RoleId", table: "RoleClaims", @@ -373,6 +398,11 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "Users", column: "NormalizedUserName", unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UserSessions_UserId", + table: "UserSessions", + column: "UserId"); } /// @@ -382,10 +412,10 @@ protected override void Down(MigrationBuilder migrationBuilder) name: "DataProtectionKeys"); migrationBuilder.DropTable( - name: "DeviceInstallations"); + name: "Products"); migrationBuilder.DropTable( - name: "Products"); + name: "PushNotificationSubscriptions"); migrationBuilder.DropTable( name: "RoleClaims"); @@ -408,6 +438,9 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "Categories"); + migrationBuilder.DropTable( + name: "UserSessions"); + migrationBuilder.DropTable( name: "Roles"); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs index f864c7220d..d3dcd8fa49 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs @@ -37,7 +37,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "Categories") - .HasAnnotation("Cosmos:PartitionKeyName", "Id"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "Id" }); b.HasData( new @@ -107,7 +107,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "Roles") - .HasAnnotation("Cosmos:PartitionKeyName", "Id"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "Id" }); }); modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.User", b => @@ -182,10 +182,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("SecurityStamp") .HasColumnType("TEXT"); - b.Property("Sessions") - .IsRequired() - .HasColumnType("TEXT"); - b.Property("TwoFactorEnabled") .HasColumnType("INTEGER"); @@ -217,7 +213,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "Users") - .HasAnnotation("Cosmos:PartitionKeyName", "Id"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "Id" }); b.HasData( new @@ -238,12 +234,42 @@ protected override void BuildModel(ModelBuilder modelBuilder) PhoneNumber = "+31684207362", PhoneNumberConfirmed = true, SecurityStamp = "959ff4a9-4b07-4cc1-8141-c5fc033daf83", - Sessions = "[]", TwoFactorEnabled = false, UserName = "test" }); }); + modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.UserSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Address") + .HasColumnType("TEXT"); + + b.Property("DeviceInfo") + .HasColumnType("TEXT"); + + b.Property("IP") + .HasColumnType("TEXT"); + + b.Property("RenewedOn") + .HasColumnType("INTEGER"); + + b.Property("StartedOn") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserSessions"); + }); + modelBuilder.Entity("Boilerplate.Server.Api.Models.Products.Product", b => { b.Property("Id") @@ -280,7 +306,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "Products") - .HasAnnotation("Cosmos:PartitionKeyName", "CategoryId"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "CategoryId" }); b.HasData( new @@ -515,9 +541,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); }); - modelBuilder.Entity("Boilerplate.Server.Api.Models.PushNotification.DeviceInstallation", b => + modelBuilder.Entity("Boilerplate.Server.Api.Models.PushNotification.PushNotificationSubscription", b => { - b.Property("InstallationId") + b.Property("DeviceId") .HasColumnType("TEXT"); b.Property("Auth") @@ -540,22 +566,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); - b.Property("Tags") + b.Property("RenewedOn") + .HasColumnType("INTEGER"); + + b.PrimitiveCollection("Tags") .IsRequired() .HasColumnType("TEXT"); - b.Property("UserId") + b.Property("UserSessionId") .HasColumnType("TEXT"); - b.HasKey("InstallationId"); + b.HasKey("DeviceId"); - b.HasIndex("UserId"); + b.HasIndex("UserSessionId") + .IsUnique() + .HasFilter("[UserSessionId] IS NOT NULL"); - b.ToTable("DeviceInstallations"); + b.ToTable("PushNotificationSubscriptions"); b - .HasAnnotation("Cosmos:ContainerName", "DeviceInstallations") - .HasAnnotation("Cosmos:PartitionKeyName", "Platform"); + .HasAnnotation("Cosmos:ContainerName", "PushNotificationSubscriptions") + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "Platform" }); }); modelBuilder.Entity("Boilerplate.Server.Api.Models.Todo.TodoItem", b => @@ -585,7 +616,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "TodoItems") - .HasAnnotation("Cosmos:PartitionKeyName", "Id"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "Id" }); }); modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => @@ -606,7 +637,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "DataProtectionKeys") - .HasAnnotation("Cosmos:PartitionKeyName", "Id"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "Id" }); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => @@ -632,7 +663,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "RoleClaims") - .HasAnnotation("Cosmos:PartitionKeyName", "RoleId"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "RoleId" }); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => @@ -658,7 +689,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "UserClaims") - .HasAnnotation("Cosmos:PartitionKeyName", "UserId"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "UserId" }); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => @@ -683,7 +714,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "UserLogins") - .HasAnnotation("Cosmos:PartitionKeyName", "UserId"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "UserId" }); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => @@ -702,7 +733,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "UserRoles") - .HasAnnotation("Cosmos:PartitionKeyName", "UserId"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "UserId" }); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => @@ -725,7 +756,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b .HasAnnotation("Cosmos:ContainerName", "UserTokens") - .HasAnnotation("Cosmos:PartitionKeyName", "UserId"); + .HasAnnotation("Cosmos:PartitionKeyNames", new List { "UserId" }); + }); + + modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.UserSession", b => + { + b.HasOne("Boilerplate.Server.Api.Models.Identity.User", "User") + .WithMany("Sessions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); }); modelBuilder.Entity("Boilerplate.Server.Api.Models.Products.Product", b => @@ -739,13 +781,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Category"); }); - modelBuilder.Entity("Boilerplate.Server.Api.Models.PushNotification.DeviceInstallation", b => + modelBuilder.Entity("Boilerplate.Server.Api.Models.PushNotification.PushNotificationSubscription", b => { - b.HasOne("Boilerplate.Server.Api.Models.Identity.User", "User") - .WithMany() - .HasForeignKey("UserId"); + b.HasOne("Boilerplate.Server.Api.Models.Identity.UserSession", "UserSession") + .WithOne("PushNotificationSubscription") + .HasForeignKey("Boilerplate.Server.Api.Models.PushNotification.PushNotificationSubscription", "UserSessionId"); - b.Navigation("User"); + b.Navigation("UserSession"); }); modelBuilder.Entity("Boilerplate.Server.Api.Models.Todo.TodoItem", b => @@ -814,6 +856,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Navigation("Products"); }); + + modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.User", b => + { + b.Navigation("Sessions"); + }); + + modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.UserSession", b => + { + b.Navigation("PushNotificationSubscription"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/PushNotificationMapper.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/PushNotificationMapper.cs index 34789b0034..986fa3fbae 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/PushNotificationMapper.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Mappers/PushNotificationMapper.cs @@ -10,5 +10,5 @@ namespace Boilerplate.Server.Api.Mappers; [Mapper] public static partial class PushNotificationMapper { - public static partial void Patch(this DeviceInstallationDto source, DeviceInstallation destination); + public static partial void Patch(this PushNotificationSubscriptionDto source, PushNotificationSubscription destination); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/User.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/User.cs index 86a60d36cf..76afafe330 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/User.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/User.cs @@ -16,9 +16,6 @@ public partial class User : IdentityUser [PersonalData] public string? ProfileImageName { get; set; } - [PersonalData] - public List Sessions { get; set; } = []; - /// /// The date and time of the last token request. Ensures the generated token is valid and can only be used once. /// @@ -31,4 +28,6 @@ public partial class User : IdentityUser public DateTimeOffset? TwoFactorTokenRequestedOn { get; set; } public DateTimeOffset? OtpRequestedOn { get; set; } + + public List Sessions { get; set; } = []; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/UserSession.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/UserSession.cs index efe7eedccc..a383178cab 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/UserSession.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/UserSession.cs @@ -1,16 +1,34 @@ -namespace Boilerplate.Server.Api.Models.Identity; +//+:cnd:noEmit +using Boilerplate.Shared.Dtos.Identity; +//#if (notification == true) +using Boilerplate.Server.Api.Models.PushNotification; +//#endif + +namespace Boilerplate.Server.Api.Models.Identity; public partial class UserSession { - public Guid SessionUniqueId { get; set; } + public Guid Id { get; set; } public string? IP { get; set; } - public string? Device { get; set; } + /// + /// + /// + public string? DeviceInfo { get; set; } public string? Address { get; set; } public DateTimeOffset StartedOn { get; set; } public DateTimeOffset? RenewedOn { get; set; } + + public Guid UserId { get; set; } + + [ForeignKey(nameof(UserId))] + public User? User { get; set; } + + //#if (notification == true) + public PushNotificationSubscription? PushNotificationSubscription { get; set; } + //#endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/PushNotification/DeviceInstallation.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/PushNotification/PushNotificationSubscription.cs similarity index 63% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/PushNotification/DeviceInstallation.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/PushNotification/PushNotificationSubscription.cs index cf89c233e9..fb8d9ea255 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/PushNotification/DeviceInstallation.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/PushNotification/PushNotificationSubscription.cs @@ -2,10 +2,10 @@ namespace Boilerplate.Server.Api.Models.PushNotification; -public class DeviceInstallation +public class PushNotificationSubscription { [Required, Key] - public string? InstallationId { get; set; } + public string? DeviceId { get; set; } [Required, AllowedValues("apns", "fcmV1", "browser")] public string? Platform { get; set; } @@ -17,10 +17,10 @@ public class DeviceInstallation public string? Auth { get; set; } public string? Endpoint { get; set; } - public Guid? UserId { get; set; } + public Guid? UserSessionId { get; set; } - [ForeignKey(nameof(UserId))] - public User? User { get; set; } + [ForeignKey(nameof(UserSessionId))] + public UserSession? UserSession { get; set; } public string[] Tags { get; set; } = []; @@ -28,4 +28,9 @@ public class DeviceInstallation /// Unix Time Seconds /// public long ExpirationTime { get; set; } + + /// + /// Unix Time Seconds + /// + public long RenewedOn { get; set; } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs index 5512537acc..8e454caab8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs @@ -23,6 +23,11 @@ using Boilerplate.Server.Api.Controllers; using Boilerplate.Server.Api.Models.Identity; using Boilerplate.Server.Api.Services.Identity; +//#if (signalR == true) +using Microsoft.AspNetCore.SignalR; +using Boilerplate.Server.Api.Signalr; +using Boilerplate.Server.Api.SignalR; +//#endif namespace Boilerplate.Server.Api; @@ -155,6 +160,7 @@ public static void AddServerApiProjectServices(this WebApplicationBuilder builde }); //#if (signalR == true) + services.AddSingleton, AppHubConnectionHandler>(); services.AddSignalR(options => { options.EnableDetailedErrors = env.IsDevelopment(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs index cc93f6cd28..a2c000b8b7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs @@ -121,8 +121,6 @@ public partial class AppIdentityOptions : IdentityOptions /// To sign in with either Otp or magic link. /// public TimeSpan OtpTokenLifetime { get; set; } - - public TimeSpan RevokeUserSessionsDelay { get; set; } } public partial class EmailOptions diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs index 6df512bf92..8b214c679f 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs @@ -9,62 +9,72 @@ namespace Boilerplate.Server.Api.Services; public partial class PushNotificationService { - [AutoInject] private IAdsPushSender adsPushSender = default!; - [AutoInject] private IHttpContextAccessor httpContextAccessor = default!; [AutoInject] private AppDbContext dbContext = default!; + [AutoInject] private IAdsPushSender adsPushSender = default!; [AutoInject] private ILogger logger = default!; + [AutoInject] private IHttpContextAccessor httpContextAccessor = default!; + [AutoInject] private ServerApiSettings serverApiSettings = default!; - public async Task RegisterDevice([Required] DeviceInstallationDto dto, CancellationToken cancellationToken) + public async Task RegisterSubscription([Required] PushNotificationSubscriptionDto dto, CancellationToken cancellationToken) { List tags = [CultureInfo.CurrentUICulture.Name /* To send push notification to all users with specific culture */]; - var userId = httpContextAccessor.HttpContext!.User.IsAuthenticated() ? httpContextAccessor.HttpContext.User.GetUserId() : (Guid?)null; + var userSessionId = httpContextAccessor.HttpContext!.User.IsAuthenticated() ? httpContextAccessor.HttpContext.User.GetSessionId() : (Guid?)null; - var deviceInstallation = await dbContext.DeviceInstallations.FindAsync([dto.InstallationId], cancellationToken); + var subscription = await dbContext.PushNotificationSubscriptions.FindAsync([dto.DeviceId], cancellationToken); - if (deviceInstallation is null) + if (subscription is null) { - dbContext.DeviceInstallations.Add(deviceInstallation = new() + dbContext.PushNotificationSubscriptions.Add(subscription = new() { - InstallationId = dto.InstallationId, + DeviceId = dto.DeviceId, Platform = dto.Platform }); } - dto.Patch(deviceInstallation); + dto.Patch(subscription); - deviceInstallation.UserId = userId; - deviceInstallation.Tags = [.. tags]; - deviceInstallation.ExpirationTime = DateTimeOffset.UtcNow.AddMonths(1).ToUnixTimeSeconds(); + subscription.Tags = [.. tags]; + subscription.UserSessionId = userSessionId; + subscription.RenewedOn = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + subscription.ExpirationTime = DateTimeOffset.UtcNow.AddMonths(1).ToUnixTimeSeconds(); - if (deviceInstallation.Platform is "browser") + if (subscription.Platform is "browser") { - deviceInstallation.PushChannel = VapidSubscription.FromParameters(deviceInstallation.Endpoint, deviceInstallation.P256dh, deviceInstallation.Auth).ToAdsPushToken(); + subscription.PushChannel = VapidSubscription.FromParameters(subscription.Endpoint, subscription.P256dh, subscription.Auth).ToAdsPushToken(); } await dbContext.SaveChangesAsync(cancellationToken); } - public async Task DeregisterDevice(string deviceInstallationId, CancellationToken cancellationToken) + public async Task DeregisterSubscription(string deviceId, CancellationToken cancellationToken) { - dbContext.DeviceInstallations.Remove(new() { InstallationId = deviceInstallationId }); + dbContext.PushNotificationSubscriptions.Remove(new() { DeviceId = deviceId }); await dbContext.SaveChangesAsync(cancellationToken); } - public async Task RequestPush(string? title = null, string? message = null, string? action = null, Expression>? customDeviceFilter = null, CancellationToken cancellationToken = default) + public async Task RequestPush(string? title = null, string? message = null, string? action = null, + bool userRelatedPush = false, + Expression>? customSubscriptionFilter = null, + CancellationToken cancellationToken = default) { var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - var query = dbContext.DeviceInstallations - .Where(dev => dev.ExpirationTime > now) - .WhereIf(customDeviceFilter is not null, customDeviceFilter!); + // userRelatedPush: If the BearerTokenExpiration is 14 days, it’s not practical to send push notifications + // with sensitive information, like an OTP code to a device where the user hasn't used the app for over 14 days. + // This is because, even if the user opens the app, they will be automatically signed out as their session has expired. + + var query = dbContext.PushNotificationSubscriptions + .Where(sub => sub.ExpirationTime > now) + .WhereIf(customSubscriptionFilter is not null, customSubscriptionFilter!) + .WhereIf(userRelatedPush is true, sub => (now - sub.RenewedOn) < serverApiSettings.Identity.BearerTokenExpiration.TotalSeconds); - if (customDeviceFilter is null) + if (customSubscriptionFilter is null) { query = query.OrderBy(_ => EF.Functions.Random()).Take(100); } - var devices = await query.ToListAsync(cancellationToken); + var subscriptions = await query.ToListAsync(cancellationToken); var payload = new AdsPushBasicSendPayload() { @@ -80,14 +90,14 @@ public async Task RequestPush(string? title = null, string? message = null, stri List tasks = []; - foreach (var deviceInstallation in devices) + foreach (var subscription in subscriptions) { - var target = deviceInstallation.Platform is "browser" ? AdsPushTarget.BrowserAndPwa - : deviceInstallation.Platform is "fcmV1" ? AdsPushTarget.Android - : deviceInstallation.Platform is "apns" ? AdsPushTarget.Ios + var target = subscription.Platform is "browser" ? AdsPushTarget.BrowserAndPwa + : subscription.Platform is "fcmV1" ? AdsPushTarget.Android + : subscription.Platform is "apns" ? AdsPushTarget.Ios : throw new NotImplementedException(); - tasks.Add(adsPushSender.BasicSendAsync(target, deviceInstallation.PushChannel, payload, cancellationToken)); + tasks.Add(adsPushSender.BasicSendAsync(target, subscription.PushChannel, payload, cancellationToken)); } try diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHub.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHub.cs index bdcea9cab4..903894402c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHub.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHub.cs @@ -20,6 +20,7 @@ public override async Task OnConnectedAsync() } else { + // Checkout AppHubConnectionHandler's comments for more info. await Groups.AddToGroupAsync(Context.ConnectionId, "AuthenticatedClients"); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHubConnectionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHubConnectionHandler.cs new file mode 100644 index 0000000000..efb814bec9 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHubConnectionHandler.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.SignalR; +using Boilerplate.Server.Api.SignalR; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Connections; + +namespace Boilerplate.Server.Api.Signalr; + +/// +/// SignalR supports basic scenarios like sending messages to all connected clients using `Clients.All()`, +/// which broadcasts to all SignalR connections, whether authenticated or not. Similarly, `Clients.User(userId)` +/// sends messages to all open browser tabs or applications associated with a specific user. +/// +/// In addition to these, the following enhanced scenarios are supported: +/// 1. `Clients.Group("AuthenticatedClients")`: Sends a message to all browser tabs and apps that are signed in. +/// 2. User session IDs can function as an equivalent to SignalR connection IDs. For instance, the application +/// already uses this approach in the `UserController's RevokeSession` method by sending a SignalR message to +/// `Clients.Client(userSession.Id.ToString())`. This ensures that the corresponding browser tab or app clears +/// its access/refresh tokens from storage and navigates to the sign-in page if necessary. +/// +public class AppHubConnectionHandler : HubConnectionHandler +{ + public AppHubConnectionHandler(HubLifetimeManager lifetimeManager, IHubProtocolResolver protocolResolver, IOptions globalHubOptions, IOptions> hubOptions, ILoggerFactory loggerFactory, IUserIdProvider userIdProvider, IServiceScopeFactory serviceScopeFactory) + : base(lifetimeManager, protocolResolver, globalHubOptions, hubOptions, loggerFactory, userIdProvider, serviceScopeFactory) + { + } + + public override async Task OnConnectedAsync(ConnectionContext connection) + { + var user = connection.GetHttpContext()?.User; + if (user?.IsAuthenticated() is true) + { + connection.ConnectionId = user!.GetSessionId().ToString(); + } + + await base.OnConnectedAsync(connection); + } +} + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json index 7aa6759d8a..43a9668cdd 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json @@ -30,7 +30,6 @@ "ResetPasswordTokenLifetime": "0.00:02:00", "TwoFactorTokenLifetime": "0.00:02:00", "OtpTokenLifetime": "0.00:02:00", - "RevokeUserSessionsDelay": "1.00:00:00", "Password": { "RequireDigit": "false", "RequiredLength": "6", diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json index ca4aaf224e..beb7ccecd9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json @@ -23,7 +23,6 @@ "ResetPasswordTokenLifetime": "0.00:02:00", "TwoFactorTokenLifetime": "0.00:02:00", "OtpTokenLifetime": "0.00:02:00", - "RevokeUserSessionsDelay": "1.00:00:00", "Password": { "RequireDigit": "false", "RequiredLength": "6", diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/PushNotification/IPushNotificationController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/PushNotification/IPushNotificationController.cs index f61ea747fe..33b8d77d12 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/PushNotification/IPushNotificationController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/PushNotification/IPushNotificationController.cs @@ -6,8 +6,8 @@ namespace Boilerplate.Shared.Controllers.PushNotification; public interface IPushNotificationController : IAppController { [HttpPost] - Task RegisterDevice([Required] DeviceInstallationDto deviceInstallation, CancellationToken cancellationToken); + Task RegisterSubscription([Required] PushNotificationSubscriptionDto subscription, CancellationToken cancellationToken); [HttpPost("{deviceId}")] - Task DeregisterDevice([Required] string deviceId, CancellationToken cancellationToken); + Task DeregisterSubscription([Required] string deviceId, CancellationToken cancellationToken); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs index 39d4b3a7ad..3672667e13 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs @@ -23,7 +23,7 @@ namespace Boilerplate.Shared.Dtos; [JsonSerializable(typeof(GitHubStats))] [JsonSerializable(typeof(NugetStatsDto))] //#if (notification == true) -[JsonSerializable(typeof(DeviceInstallationDto))] +[JsonSerializable(typeof(PushNotificationSubscriptionDto))] //#endif //#if (sample == "Todo") [JsonSerializable(typeof(TodoItemDto))] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/UserSessionDto.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/UserSessionDto.cs index d2bb317b9a..d7f969f4e3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/UserSessionDto.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/UserSessionDto.cs @@ -2,11 +2,14 @@ public partial class UserSessionDto { - public Guid SessionUniqueId { get; set; } + public Guid Id { get; set; } public string? IP { get; set; } - public string? Device { get; set; } + /// + /// Populated during sign-in using the property. + /// + public string? DeviceInfo { get; set; } public string? Address { get; set; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/PushNotification/DeviceInstallationDto.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/PushNotification/PushNotificationSubscriptionDto.cs similarity index 85% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/PushNotification/DeviceInstallationDto.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/PushNotification/PushNotificationSubscriptionDto.cs index d681913ed2..fa172e1d47 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/PushNotification/DeviceInstallationDto.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/PushNotification/PushNotificationSubscriptionDto.cs @@ -1,10 +1,10 @@ namespace Boilerplate.Shared.Dtos.PushNotification; [DtoResourceType(typeof(AppStrings))] -public partial class DeviceInstallationDto +public partial class PushNotificationSubscriptionDto { [Required(ErrorMessage = nameof(AppStrings.RequiredAttribute_ValidationError))] - public string? InstallationId { get; set; } + public string? DeviceId { get; set; } [Required(ErrorMessage = nameof(AppStrings.RequiredAttribute_ValidationError))] [AllowedValues("apns", "fcmV1", "browser")] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ClaimsPrincipalExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ClaimsPrincipalExtensions.cs index 993787b4a4..cddb11b4e5 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ClaimsPrincipalExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ClaimsPrincipalExtensions.cs @@ -30,10 +30,8 @@ public static string GetDisplayName(this ClaimsPrincipal claimsPrincipal) /// /// Returns the user session id stored in sessions column of user table after user sign in. /// - public static Guid? GetSessionId(this ClaimsPrincipal claimsPrincipal) + public static Guid GetSessionId(this ClaimsPrincipal claimsPrincipal) { - return claimsPrincipal.IsAuthenticated() - ? Guid.Parse(claimsPrincipal.FindFirst("session-id")!.Value) - : null; + return Guid.Parse(claimsPrincipal.FindFirst("session-id")!.Value); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx index 7a58842f3c..dae9a2acfd 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx @@ -992,9 +992,6 @@ اخیرا - - - شما نمیتوانید جلسات کاری دگیر را غیر فعال کنید. مجدد تلاش کنید در {0} بروز رسانی diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx index 89588e6038..8c489c1ee5 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx @@ -992,9 +992,6 @@ Onlangs - - - Je kunt Anderse sessies op dit moment niet intrekken. Probeer het opnieuw in {0} Update diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx index 5e7af87c7a..8b8c8ad353 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx @@ -992,9 +992,6 @@ Recently - - - You can't revoke other sessions right now. Try again in {0} Update diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedPubSubMessages.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedPubSubMessages.cs index d285be7803..b21c690e52 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedPubSubMessages.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedPubSubMessages.cs @@ -10,9 +10,9 @@ public static partial class SharedPubSubMessages { //#if (sample == "Admin") public const string DASHBOARD_DATA_CHANGED = nameof(DASHBOARD_DATA_CHANGED); - //#else - // To see examples of this class, checkout the admin panel sample. //#endif + + public const string SESSION_REVOKED = nameof(SESSION_REVOKED); } public static partial class SignalREvents diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/IdentityPagesTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/IdentityPagesTests.cs index d41d0b335e..3103e48fea 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/IdentityPagesTests.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/IdentityPagesTests.cs @@ -69,13 +69,13 @@ public async Task SignOut() var identityHomePage = await signInPage.SignInWithEmail(email); await identityHomePage.AssertSignInSuccess(email, userFullName: null); - await dbContext.Entry(user).ReloadAsync(); + await dbContext.Entry(user).Reference(u => u.Sessions).LoadAsync(); Assert.AreEqual(1, user.Sessions.Count); var mainHomePage = await identityHomePage.SignOut(); await mainHomePage.AssertSignOut(); - await dbContext.Entry(user).ReloadAsync(); + await dbContext.Entry(user).Reference(u => u.Sessions).LoadAsync(); Assert.AreEqual(0, user.Sessions.Count); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestsInitializer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestsInitializer.cs index 6cfa7f8119..61d123179b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestsInitializer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/TestsInitializer.cs @@ -68,7 +68,7 @@ private static async Task InitializeDatabase(AppTestServer testServer) private static async Task InitializeAuthenticationState(AppTestServer testServer, TestContext testContext) { var playwrightPage = new PageTest() { TestContext = testContext }; - await playwrightPage.Setup(); + await playwrightPage.ContextSetup(); await playwrightPage.BrowserSetup(); var currentMethodFullName = $"{typeof(TestsInitializer).FullName}.{(nameof(InitializeAuthenticationState))}"; From 8614b77898bbf1dd0ca342541d629f94779d8917 Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Sat, 23 Nov 2024 13:57:43 +0100 Subject: [PATCH 04/87] feat(templates): add unique user session check feature to Boilerplate #9308 (#9309) --- .../.template.config/template.json | 1 + .../Components/ClientAppCoordinator.cs | 122 +++++------------- .../Settings/ProfileSection.razor.cs | 16 ++- .../Extensions/HubConnectionExtensions.cs | 31 +++++ .../IClientCoreServiceCollectionExtensions.cs | 44 ++++++- .../Services/AuthenticationManager.cs | 12 +- .../Contracts/IPushNotificationService.cs | 2 +- .../Services/PushNotificationServiceBase.cs | 8 +- ...ushNotificationFirebaseMessagingService.cs | 2 +- .../Platforms/MacCatalyst/AppDelegate.cs | 2 +- .../Platforms/iOS/AppDelegate.cs | 2 +- .../Controllers/Identity/UserController.cs | 4 +- .../PushNotificationController.cs | 8 +- .../Services/PushNotificationService.cs | 4 +- .../Boilerplate.Server.Api/Signalr/AppHub.cs | 9 ++ .../IPushNotificationController.cs | 4 +- .../src/Shared/Resources/AppStrings.fa.resx | 5 + .../src/Shared/Resources/AppStrings.nl.resx | 5 + .../src/Shared/Resources/AppStrings.resx | 5 + .../Shared/Services/SharedPubSubMessages.cs | 16 +++ 20 files changed, 186 insertions(+), 116 deletions(-) create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/HubConnectionExtensions.cs diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json index 811adff765..0f285becb5 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json @@ -440,6 +440,7 @@ "exclude": [ "src/Server/Boilerplate.Server.Api/SignalR/**", "src/Shared/Services/SharedPubSubMessages.cs", + "src/Client/Boilerplate.Client.Core/Extensions/HubConnectionExtensions.cs", "src/Client/Boilerplate.Client.Core/Services/SignalRInfinitiesRetryPolicy.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 f1f0f76398..ff2a830d5c 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 @@ -2,7 +2,6 @@ //#if (signalR == true) using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.AspNetCore.Http.Connections; //#endif //#if (appInsights == true) using BlazorApplicationInsights.Interfaces; @@ -19,11 +18,8 @@ namespace Boilerplate.Client.Core.Components; public partial class ClientAppCoordinator : AppComponentBase { //#if (signalR == true) - private HubConnection? hubConnection; [AutoInject] private Notification notification = default!; - //#endif - //#if (notification == true) - [AutoInject] private IPushNotificationService pushNotificationService = default!; + [AutoInject] private HubConnection hubConnection = default!; //#endif //#if (appInsights == true) [AutoInject] private IApplicationInsights appInsights = default!; @@ -37,6 +33,9 @@ public partial class ClientAppCoordinator : AppComponentBase [AutoInject] private CultureInfoManager cultureInfoManager = default!; [AutoInject] private ILogger authLogger = default!; [AutoInject] private IBitDeviceCoordinator bitDeviceCoordinator = default!; + //#if (notification == true) + [AutoInject] private IPushNotificationService pushNotificationService = default!; + //#endif protected override async Task OnInitAsync() { @@ -70,6 +69,9 @@ protected override async Task OnInitAsync() NavigationManager.LocationChanged += NavigationManager_LocationChanged; AuthenticationManager.AuthenticationStateChanged += AuthenticationStateChanged; + //#if (signalR == true) + SubscribeToSignalREventsMessages(); + //#endif await PropagateUserId(firstRun: true, AuthenticationManager.GetAuthenticationStateAsync()); } @@ -82,7 +84,6 @@ private void NavigationManager_LocationChanged(object? sender, LocationChangedEv navigatorLogger.LogInformation("Navigation's location changed to {Location}", e.Location); } - private SemaphoreSlim semaphore = new(1, 1); /// /// This code manages the association of a user with sensitive services, such as SignalR, push notifications, App Insights, and others, /// ensuring the user is correctly set or cleared as needed. @@ -91,16 +92,7 @@ public async Task PropagateUserId(bool firstRun, Task task) { try { - if (firstRun is false) - { - Abort(); - } - - await semaphore.WaitAsync(CurrentCancellationToken); - // About Semaphore: The following code may take significant time to execute. - // During this period, the authentication state could change. For instance, the app might start with the user authenticated, but they could sign out while this method is running. - // To handle such scenarios, we must ensure the code below runs in a safe and sequential manner, preventing parallel execution. - // Without this safeguard, SignalR or push notifications might incorrectly associate the device with a user who has already signed out. + Abort(); // Cancels ongoing user id propagation, because the new authentication state is available. var user = (await task).User; var isAuthenticated = user.IsAuthenticated(); @@ -135,21 +127,17 @@ public async Task PropagateUserId(bool firstRun, Task task) } //#if (notification == true) - await pushNotificationService.RegisterSubscription(CurrentCancellationToken); + await pushNotificationService.Subscribe(CurrentCancellationToken); //#endif //#if (signalR == true) - await ConnectSignalR(); + await StartSignalR(); //#endif } catch (Exception exp) { ExceptionHandler.Handle(exp); } - finally - { - semaphore.Release(); - } } private void AuthenticationStateChanged(Task task) @@ -158,37 +146,9 @@ private void AuthenticationStateChanged(Task task) } //#if (signalR == true) - private async Task ConnectSignalR() + private void SubscribeToSignalREventsMessages() { - if (hubConnection is not null) - { - await hubConnection.DisposeAsync(); - } - - hubConnection = new HubConnectionBuilder() - .WithAutomaticReconnect(new SignalRInfinitiesRetryPolicy()) - .WithUrl(new Uri(AbsoluteServerAddress, "app-hub"), options => - { - options.SkipNegotiation = true; - options.Transports = HttpTransportType.WebSockets; - // Avoid enabling long polling or Server-Sent Events. Focus on resolving the issue with WebSockets instead. - // WebSockets should be enabled on services like IIS or Cloudflare CDN, offering significantly better performance. - options.AccessTokenProvider = async () => - { - var accessToken = await AuthTokenProvider.GetAccessToken(); - - if (string.IsNullOrEmpty(accessToken) is false && - AuthTokenProvider.ParseAccessToken(accessToken, validateExpiry: true).IsAuthenticated() is false) - { - return await AuthenticationManager.RefreshToken(requestedBy: nameof(HubConnectionBuilder)); - } - - return accessToken; - }; - }) - .Build(); - - hubConnection.On(SignalREvents.SHOW_MESSAGE, async (message) => + signalROnDisposables.Add(hubConnection.On(SignalREvents.SHOW_MESSAGE, async (message) => { if (await notification.IsNotificationAvailable()) { @@ -208,20 +168,24 @@ private async Task ConnectSignalR() });*/ // You can also leverage IPubSubService to notify other components in the application. - }); + })); - hubConnection.On(SignalREvents.PUBLISH_MESSAGE, async (message) => + signalROnDisposables.Add(hubConnection.On(SignalREvents.PUBLISH_MESSAGE, async (message) => { logger.LogInformation("Message {Message} received from server.", message); PubSubService.Publish(message); - }); + })); + hubConnection.Closed += HubConnectionStateChange; + hubConnection.Reconnected += HubConnectionConnected; + hubConnection.Reconnecting += HubConnectionStateChange; + } + + private async Task StartSignalR() + { try { - hubConnection.Closed += HubConnectionStateChange; - hubConnection.Reconnected += HubConnectionConnected; - hubConnection.Reconnecting += HubConnectionStateChange; - + await hubConnection.StopAsync(CurrentCancellationToken); await hubConnection.StartAsync(CurrentCancellationToken); await HubConnectionConnected(null); } @@ -267,45 +231,25 @@ await storageService.GetItem("Culture") ?? // 2- User settings CultureInfo.CurrentUICulture.Name); // 3- OS settings } - var cssClasses = new List { }; + var platformCssClass = AppPlatform.IsWindows ? "bit-windows" : + AppPlatform.IsMacOS ? "bit-macos" : + AppPlatform.IsIOS ? "bit-ios" : + AppPlatform.IsAndroid ? "bit-android" : "bit-unknown"; - if (AppPlatform.IsWindows) - { - cssClasses.Add("bit-windows"); - } - else if (AppPlatform.IsMacOS) - { - cssClasses.Add("bit-macos"); - } - else if (AppPlatform.IsIOS) - { - cssClasses.Add("bit-ios"); - } - else if (AppPlatform.IsAndroid) - { - cssClasses.Add("bit-android"); - } - - var cssVariables = new Dictionary - { - }; - - await jsRuntime.ApplyBodyElementClasses(cssClasses, cssVariables); + await jsRuntime.ApplyBodyElementClasses(cssClasses: [platformCssClass], cssVariables: []); } + private List signalROnDisposables = []; protected override async ValueTask DisposeAsync(bool disposing) { NavigationManager.LocationChanged -= NavigationManager_LocationChanged; AuthenticationManager.AuthenticationStateChanged -= AuthenticationStateChanged; //#if (signalR == true) - if (hubConnection is not null) - { - hubConnection.Closed -= HubConnectionStateChange; - hubConnection.Reconnected -= HubConnectionConnected; - hubConnection.Reconnecting -= HubConnectionStateChange; - await hubConnection.DisposeAsync(); - } + hubConnection.Closed -= HubConnectionStateChange; + hubConnection.Reconnected -= HubConnectionConnected; + hubConnection.Reconnecting -= HubConnectionStateChange; + signalROnDisposables.ForEach(d => d.Dispose()); //#endif await base.DisposeAsync(disposing); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor.cs index 71df207b71..fe9a8ef7b9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor.cs @@ -1,5 +1,9 @@ -using Boilerplate.Shared.Controllers.Identity; +//+:cnd:noEmit +//#if (signalR == true) +using Microsoft.AspNetCore.SignalR.Client; +//#endif using Boilerplate.Shared.Dtos.Identity; +using Boilerplate.Shared.Controllers.Identity; namespace Boilerplate.Client.Core.Components.Pages.Authorized.Settings; @@ -9,6 +13,9 @@ public partial class ProfileSection [Parameter] public UserDto? User { get; set; } + //#if (signalR == true) + [AutoInject] HubConnection hub = default!; + //#endif [AutoInject] private IUserController userController = default!; @@ -49,6 +56,13 @@ private async Task SaveProfile() try { + //#if (signalR == true) + if (await hub.IsUserSessionUnique(CurrentCancellationToken)) + { + throw new ForbiddenException(Localizer[nameof(AppStrings.ConcurrentUserSessionOnTheSameDevice)]); + } + //#endif + editUserDto.Patch(User); (await userController.Update(editUserDto, CurrentCancellationToken)).Patch(User); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/HubConnectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/HubConnectionExtensions.cs new file mode 100644 index 0000000000..40b2fe74ff --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/HubConnectionExtensions.cs @@ -0,0 +1,31 @@ +namespace Microsoft.AspNetCore.SignalR.Client; + +public static class HubConnectionExtensions +{ + /// + /// "/> + /// + public static async Task IsUserSessionUnique(this HubConnection hubConnection, CancellationToken cancellationToken) + { + IDisposable? disposable = null; TaskCompletionSource? pongTcs = new(); + + try + { + if (hubConnection.State is not HubConnectionState.Connected) + throw new ServerConnectionException(AppStrings.ServerConnectionException); + + disposable = hubConnection.On(SignalREvents.PONG, () => + { + pongTcs!.SetResult(); + }); + + await hubConnection.InvokeAsync("Ping", cancellationToken); + + return await Task.WhenAny(Task.Delay(TimeSpan.FromSeconds(3), cancellationToken), pongTcs!.Task) != pongTcs.Task; + } + finally + { + disposable?.Dispose(); + } + } +} 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 b25436cc32..e298ce2c75 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 @@ -11,6 +11,10 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components.WebAssembly.Services; using Boilerplate.Client.Core.Services.HttpMessageHandlers; +//#if (signalR == true) +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.AspNetCore.Http.Connections; +//#endif namespace Microsoft.Extensions.DependencyInjection; @@ -40,7 +44,12 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle services.AddSessioned(); services.AddSessioned(); services.AddSessioned(); - services.AddSessioned(); + services.AddSessioned(sp => + { + var authenticationStateProvider = ActivatorUtilities.CreateInstance(sp); + authenticationStateProvider.OnInit(); + return authenticationStateProvider; + }); services.AddSessioned(sp => (AuthenticationManager)sp.GetRequiredService()); services.AddSingleton(sp => @@ -126,6 +135,39 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle services.AddTypedHttpClients(); + //#if (signalR == true) + services.AddSingleton(); + services.AddSessioned(sp => + { + var absoluteServerAddressProvider = sp.GetRequiredService(); + var authTokenProvider = sp.GetRequiredService(); + var authenticationManager = sp.GetRequiredService(); + var hubConnection = new HubConnectionBuilder() + .WithAutomaticReconnect(sp.GetRequiredService()) + .WithUrl(new Uri(absoluteServerAddressProvider.GetAddress(), "app-hub"), options => + { + options.SkipNegotiation = true; + options.Transports = HttpTransportType.WebSockets; + // Avoid enabling long polling or Server-Sent Events. Focus on resolving the issue with WebSockets instead. + // WebSockets should be enabled on services like IIS or Cloudflare CDN, offering significantly better performance. + options.AccessTokenProvider = async () => + { + var accessToken = await authTokenProvider.GetAccessToken(); + + if (string.IsNullOrEmpty(accessToken) is false && + authTokenProvider.ParseAccessToken(accessToken, validateExpiry: true).IsAuthenticated() is false) + { + return await authenticationManager.RefreshToken(requestedBy: nameof(HubConnectionBuilder)); + } + + return accessToken; + }; + }) + .Build(); + return hubConnection; + }); + //#endif + return services; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs index 43b7ce1f12..775275c59c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs @@ -5,8 +5,11 @@ namespace Boilerplate.Client.Core.Services; public partial class AuthenticationManager : AuthenticationStateProvider, IAsyncDisposable { + private Action? unsubscribe; + [AutoInject] private Cookie cookie = default!; [AutoInject] private IJSRuntime jsRuntime = default!; + [AutoInject] private PubSubService pubSubService = default!; [AutoInject] private IStorageService storageService = default!; [AutoInject] private IUserController userController = default!; [AutoInject] private IAuthTokenProvider tokenProvider = default!; @@ -16,14 +19,9 @@ public partial class AuthenticationManager : AuthenticationStateProvider, IAsync [AutoInject] private IIdentityController identityController = default!; [AutoInject] private ILogger authLogger = default!; - private Action? unsubscribe; - [AutoInject] - private PubSubService pubSubService + public void OnInit() { - set - { - unsubscribe = value.Subscribe(SharedPubSubMessages.SESSION_REVOKED, _ => SignOut(default)); - } + unsubscribe = pubSubService.Subscribe(SharedPubSubMessages.SESSION_REVOKED, _ => SignOut(default)); } /// diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPushNotificationService.cs index 6e1093a766..cc09ba10f2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPushNotificationService.cs @@ -7,5 +7,5 @@ public interface IPushNotificationService string Token { get; set; } Task IsPushNotificationSupported(CancellationToken cancellationToken); Task GetSubscription(CancellationToken cancellationToken); - Task RegisterSubscription(CancellationToken cancellationToken); + Task Subscribe(CancellationToken cancellationToken); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PushNotificationServiceBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PushNotificationServiceBase.cs index 3487d24d78..7ba7445cee 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PushNotificationServiceBase.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PushNotificationServiceBase.cs @@ -12,7 +12,7 @@ public abstract partial class PushNotificationServiceBase : IPushNotificationSer public virtual Task IsPushNotificationSupported(CancellationToken cancellationToken) => Task.FromResult(false); public abstract Task GetSubscription(CancellationToken cancellationToken); - public async Task RegisterSubscription(CancellationToken cancellationToken) + public async Task Subscribe(CancellationToken cancellationToken) { if (await IsPushNotificationSupported(cancellationToken) is false) { @@ -28,11 +28,11 @@ public async Task RegisterSubscription(CancellationToken cancellationToken) return; } - await pushNotificationController.RegisterSubscription(subscription, cancellationToken); + await pushNotificationController.Subscribe(subscription, cancellationToken); } - public async Task DeregisterSubscription(string deviceId, CancellationToken cancellationToken) + public async Task Unsubscribe(string deviceId, CancellationToken cancellationToken) { - await pushNotificationController.DeregisterSubscription(deviceId, cancellationToken); + await pushNotificationController.Unsubscribe(deviceId, cancellationToken); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/PushNotificationFirebaseMessagingService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/PushNotificationFirebaseMessagingService.cs index 95def2bc6e..5e670afaa5 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/PushNotificationFirebaseMessagingService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/Android/Services/PushNotificationFirebaseMessagingService.cs @@ -17,7 +17,7 @@ public override async void OnNewToken(string token) { PushNotificationService.Token = token; - await PushNotificationService.RegisterSubscription(default); + await PushNotificationService.Subscribe(default); } catch (Exception exp) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs index 7455006382..bfc3bef625 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/MacCatalyst/AppDelegate.cs @@ -48,7 +48,7 @@ public async void RegisteredForRemoteNotifications(UIApplication application, NS try { NotificationService.Token = deviceToken.ToHexString()!; - await NotificationService.RegisterSubscription(default); + await NotificationService.Subscribe(default); } catch (Exception exp) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/AppDelegate.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/AppDelegate.cs index 34784a4fe7..a6455f1ce6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/AppDelegate.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Platforms/iOS/AppDelegate.cs @@ -48,7 +48,7 @@ public async void RegisteredForRemoteNotifications(UIApplication application, NS try { NotificationService.Token = deviceToken.ToHexString()!; - await NotificationService.RegisterSubscription(default); + await NotificationService.Subscribe(default); } catch (Exception exp) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs index c24d73bac8..dad3163439 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs @@ -67,7 +67,7 @@ public async Task> GetUserSessions(CancellationToken cancel [HttpPost] public async Task SignOut(CancellationToken cancellationToken) { - var currentSessionId = Guid.Parse(User.FindFirstValue("session-id")!); + var currentSessionId = User.GetSessionId(); var userSession = await DbContext.UserSessions //#if (notification == true) @@ -89,7 +89,7 @@ public async Task RevokeSession(Guid id, CancellationToken cancellationToken) { var userId = User.GetUserId(); - var currentSessionId = Guid.Parse(User.FindFirstValue("session-id")!); + var currentSessionId = User.GetSessionId(); if (id == currentSessionId) throw new BadRequestException(); // "Call SignOut instead" diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/PushNotification/PushNotificationController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/PushNotification/PushNotificationController.cs index 122d6bd11d..667a298514 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/PushNotification/PushNotificationController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/PushNotification/PushNotificationController.cs @@ -12,15 +12,15 @@ public partial class PushNotificationController : AppControllerBase, IPushNotifi [AutoInject] PushNotificationService pushNotificationService = default!; [HttpPost] - public async Task RegisterSubscription([Required] PushNotificationSubscriptionDto subscription, CancellationToken cancellationToken) + public async Task Subscribe([Required] PushNotificationSubscriptionDto subscription, CancellationToken cancellationToken) { - await pushNotificationService.RegisterSubscription(subscription, cancellationToken); + await pushNotificationService.Subscribe(subscription, cancellationToken); } [HttpPost("{deviceId}")] - public async Task DeregisterSubscription([Required] string deviceId, CancellationToken cancellationToken) + public async Task Unsubscribe([Required] string deviceId, CancellationToken cancellationToken) { - await pushNotificationService.DeregisterSubscription(deviceId, cancellationToken); + await pushNotificationService.Unsubscribe(deviceId, cancellationToken); } #if Development // This action is for testing purposes only. diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs index 8b214c679f..42f537dfde 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs @@ -15,7 +15,7 @@ public partial class PushNotificationService [AutoInject] private IHttpContextAccessor httpContextAccessor = default!; [AutoInject] private ServerApiSettings serverApiSettings = default!; - public async Task RegisterSubscription([Required] PushNotificationSubscriptionDto dto, CancellationToken cancellationToken) + public async Task Subscribe([Required] PushNotificationSubscriptionDto dto, CancellationToken cancellationToken) { List tags = [CultureInfo.CurrentUICulture.Name /* To send push notification to all users with specific culture */]; @@ -47,7 +47,7 @@ public async Task RegisterSubscription([Required] PushNotificationSubscriptionDt await dbContext.SaveChangesAsync(cancellationToken); } - public async Task DeregisterSubscription(string deviceId, CancellationToken cancellationToken) + public async Task Unsubscribe(string deviceId, CancellationToken cancellationToken) { dbContext.PushNotificationSubscriptions.Remove(new() { DeviceId = deviceId }); await dbContext.SaveChangesAsync(cancellationToken); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHub.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHub.cs index 903894402c..2857057cae 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHub.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHub.cs @@ -33,4 +33,13 @@ public override async Task OnDisconnectedAsync(Exception? exception) await base.OnDisconnectedAsync(exception); } + + /// + /// + /// + [Authorize] + public async Task Ping() + { + await Clients.Client(Context.User!.GetSessionId().ToString()).SendAsync(SignalREvents.PONG); + } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/PushNotification/IPushNotificationController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/PushNotification/IPushNotificationController.cs index 33b8d77d12..9d9d638389 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/PushNotification/IPushNotificationController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/PushNotification/IPushNotificationController.cs @@ -6,8 +6,8 @@ namespace Boilerplate.Shared.Controllers.PushNotification; public interface IPushNotificationController : IAppController { [HttpPost] - Task RegisterSubscription([Required] PushNotificationSubscriptionDto subscription, CancellationToken cancellationToken); + Task Subscribe([Required] PushNotificationSubscriptionDto subscription, CancellationToken cancellationToken); [HttpPost("{deviceId}")] - Task DeregisterSubscription([Required] string deviceId, CancellationToken cancellationToken); + Task Unsubscribe([Required] string deviceId, CancellationToken cancellationToken); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx index dae9a2acfd..a66f43540e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx @@ -1142,6 +1142,7 @@ داده های تحلیلی شما + پاسخ گوگل ریکپچا نامعتبر است. @@ -1150,5 +1151,9 @@ شما باید چالش گوگل ریکپچا را به سرانجام برسانید. + + + لطفاً اطمینان حاصل کنید که این اپلیکیشن بیش از یک‌بار، به طور هم‌زمان روی یک دستگاه یا مرورگر باز نیست، و دوباره تلاش کنید. + \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx index 8c489c1ee5..5949f0f411 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx @@ -1142,6 +1142,7 @@ Dit zijn jouw analytische gegevens + Ongeldig Google reCAPTCHA-antwoord. @@ -1150,5 +1151,9 @@ U moet slagen voor de Google reCAPTCHA-uitdaging. + + + Zorg ervoor dat je niet meerdere sessies van de app tegelijkertijd uitvoert op hetzelfde apparaat of in dezelfde browser, en probeer het opnieuw. + \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx index 8b8c8ad353..4ca26211f3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx @@ -1151,4 +1151,9 @@ You need to pass the Google reCAPTCHA challenge. + + + Please ensure you're not running multiple sessions of the app simultaneously on the same device or browser, and try again. + + \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedPubSubMessages.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedPubSubMessages.cs index b21c690e52..ed7bf0ab96 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedPubSubMessages.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedPubSubMessages.cs @@ -26,4 +26,20 @@ public static partial class SignalREvents /// Shows message at client side. /// public const string SHOW_MESSAGE = nameof(SHOW_MESSAGE); + + /// + /// While users are allowed to have multiple concurrent user sessions by **signing in** on multiple devices or browsers, + /// copying access and refresh tokens from one device / browser to another is prohibited. + /// + /// This method uses a SignalR mechanism to check for potential misuse of access and refresh tokens across different app instances: + /// - The client calls a `Ping` method on the server. + /// - The server sends a `PONG` to the specific user session. + /// - Because SignalR's connection id is the same is user session's Id (Thanks to AppHubConnectionHandler's implementation), + /// - Only one user session will receive the `PONG` event. + /// - If the validation event is not received within a 3-second timeout, it indicates that there's something wrong. + /// - The user might have illegally copied tokens to another device or browser. + /// - The user may have lost their SignalR connection to the server. + /// - It could also be another open tab in the same browser or another instance of the app. + /// + public const string PONG = nameof(PONG); } From 6859f501acdbf256058518450ac6ad82c7362a61 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Sat, 23 Nov 2024 22:22:25 +0330 Subject: [PATCH 05/87] feat(prerelease): v-9.1.0-pre-01 #9313 (#9314) --- src/Besql/Bit.Besql/wwwroot/bit-besql.js | 2 +- src/Bit.Build.props | 6 +++--- src/BlazorUI/Bit.BlazorUI/Scripts/general.ts | 2 +- .../Bit.BlazorUI.Demo.Server.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Shared.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Client.Core.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Client.Maui.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Client.Web.csproj | 6 +++--- .../wwwroot/service-worker.published.js | 2 +- .../Bit.BlazorUI.Demo.Client.Windows.csproj | 4 ++-- src/BlazorUI/Demo/Directory.Build.props | 2 +- .../Bit.Bswup.Demo/wwwroot/service-worker.js | 2 +- .../wwwroot/service-worker.published.js | 2 +- .../wwwroot/service-worker.js | 2 +- .../wwwroot/service-worker.published.js | 2 +- .../Bit.Bswup/Scripts/bit-bswup.progress.ts | 2 +- src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts | 2 +- src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts | 2 +- .../FullDemo/Client/wwwroot/service-worker.js | 2 +- .../Client/wwwroot/service-worker.published.js | 2 +- src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts | 2 +- src/Bup/Bit.Bup/Scripts/bit-bup.ts | 2 +- src/Butil/Bit.Butil/Scripts/butil.ts | 2 +- .../BlazorEmpty.Client.csproj | 8 ++++---- .../BlazorEmpty/BlazorEmpty.csproj | 8 ++++---- .../wwwroot/service-worker.published.js | 2 +- .../Bit.Boilerplate/src/Directory.Build.props | 2 +- .../src/Directory.Packages.props | 18 +++++++++--------- .../src/Directory.Packages8.props | 18 +++++++++--------- .../Bit.Websites.Careers.Client.csproj | 10 +++++----- .../Bit.Websites.Careers.Server.csproj | 4 ++-- .../Bit.Websites.Careers.Shared.csproj | 4 ++-- src/Websites/Careers/src/Directory.Build.props | 2 +- .../Bit.Websites.Platform.Client.csproj | 12 ++++++------ .../Templates03GettingStartedPage.razor | 4 ++-- .../Templates03GettingStartedPage.razor.cs | 2 +- .../Bit.Websites.Platform.Server.csproj | 4 ++-- .../Bit.Websites.Platform.Shared.csproj | 4 ++-- .../Platform/src/Directory.Build.props | 2 +- .../Bit.Websites.Sales.Client.csproj | 10 +++++----- .../Bit.Websites.Sales.Server.csproj | 4 ++-- .../Bit.Websites.Sales.Shared.csproj | 4 ++-- src/Websites/Sales/src/Directory.Build.props | 2 +- 43 files changed, 94 insertions(+), 94 deletions(-) diff --git a/src/Besql/Bit.Besql/wwwroot/bit-besql.js b/src/Besql/Bit.Besql/wwwroot/bit-besql.js index 80eea970b8..3040440399 100644 --- a/src/Besql/Bit.Besql/wwwroot/bit-besql.js +++ b/src/Besql/Bit.Besql/wwwroot/bit-besql.js @@ -1,5 +1,5 @@ var BitBesql = BitBesql || {}; -BitBesql.version = window['bit-besql version'] = '9.0.1'; +BitBesql.version = window['bit-besql version'] = '9.1.0-pre-01'; async function synchronizeDbWithCache(file) { diff --git a/src/Bit.Build.props b/src/Bit.Build.props index d027cff25e..a2f6405c5f 100644 --- a/src/Bit.Build.props +++ b/src/Bit.Build.props @@ -25,10 +25,10 @@ https://github.com/bitfoundation/bitplatform https://avatars.githubusercontent.com/u/22663390 - 9.0.1 + 9.1.0 - https://github.com/bitfoundation/bitplatform/releases/tag/v-$(ReleaseVersion) - $(ReleaseVersion) + https://github.com/bitfoundation/bitplatform/releases/tag/v-$(ReleaseVersion)-pre-01 + $(ReleaseVersion)-pre-01 $(ReleaseVersion).$([System.DateTime]::Now.ToString(HHmm)) diff --git a/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts b/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts index 2c3b7f298c..31efd657af 100644 --- a/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts +++ b/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts @@ -1,4 +1,4 @@ -(BitBlazorUI as any).version = (window as any)['bit-blazorui version'] = '9.0.1'; +(BitBlazorUI as any).version = (window as any)['bit-blazorui version'] = '9.1.0-pre-01'; interface DotNetObject { invokeMethod(methodIdentifier: string, ...args: any[]): T; diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj index 72919897b2..2073ffd580 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj @@ -5,11 +5,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj index 7f73765f4e..b32520cced 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj @@ -5,11 +5,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj index b0a3aff49b..b1b13a1d12 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj @@ -16,11 +16,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj index 793dde9936..a72bcb241c 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj @@ -85,12 +85,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj index 1e3a1d062f..b738e31461 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj @@ -24,13 +24,13 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js index 85816e325d..926efa3491 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.0.1 +// bit version: 9.1.0-pre-01 // https://github.com/bitfoundation/bitplatform/tree/develop/src/Bswup self.assetsInclude = []; diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj index c2f43c648a..28189fe2f6 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj @@ -29,11 +29,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Directory.Build.props b/src/BlazorUI/Demo/Directory.Build.props index 2edfb46a1b..372ac2f538 100644 --- a/src/BlazorUI/Demo/Directory.Build.props +++ b/src/BlazorUI/Demo/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js index c0266c3a56..c40d668af3 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.0.1 +// bit version: 9.1.0-pre-01 self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; self.caseInsensitiveUrl = true; diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js index 92efbc677b..8fb2162684 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.0.1 +// bit version: 9.1.0-pre-01 self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; self.caseInsensitiveUrl = true; diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js index 59d16a2f49..d5b11de990 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js +++ b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.0.1 +// bit version: 9.1.0-pre-01 // In development, always fetch from the network and do not enable offline support. // This is because caching would make development more difficult (changes would not diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js index 65741cc8e7..634a6c32db 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js +++ b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.0.1 +// bit version: 9.1.0-pre-01 self.assetsInclude = []; self.assetsExclude = [ diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts index b314973b1c..056588b60b 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts @@ -1,4 +1,4 @@ -window['bit-bswup.progress version'] = '9.0.1'; +window['bit-bswup.progress version'] = '9.1.0-pre-01'; ; (function () { (window as any).startBswupProgress = (autoReload: boolean, diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts index 0f153a64b4..84664cd146 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts @@ -1,4 +1,4 @@ -self['bit-bswup.sw version'] = '9.0.1'; +self['bit-bswup.sw version'] = '9.1.0-pre-01'; interface Window { clients: any diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts index 31e9999cae..21599456aa 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts @@ -1,5 +1,5 @@ const BitBswup = {} as any; -BitBswup.version = window['bit-bswup version'] = '9.0.1'; +BitBswup.version = window['bit-bswup version'] = '9.1.0-pre-01'; declare const Blazor: any; diff --git a/src/Bswup/FullDemo/Client/wwwroot/service-worker.js b/src/Bswup/FullDemo/Client/wwwroot/service-worker.js index 67bf7801e0..ce749c4fed 100644 --- a/src/Bswup/FullDemo/Client/wwwroot/service-worker.js +++ b/src/Bswup/FullDemo/Client/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.0.1 +// bit version: 9.1.0-pre-01 // In development, always fetch from the network and do not enable offline support. // This is because caching would make development more difficult (changes would not diff --git a/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js b/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js index e84aa4c2dc..a2d8f59837 100644 --- a/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js +++ b/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.0.1 +// bit version: 9.1.0-pre-01 self.assetsInclude = []; self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; diff --git a/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts b/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts index 553b12cb31..4b9be0005c 100644 --- a/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts +++ b/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts @@ -1,4 +1,4 @@ -window['bit-bup.progress version'] = '9.0.1'; +window['bit-bup.progress version'] = '9.1.0-pre-01'; ; (function () { (window as any).startBupProgress = (showLogs: boolean, showAssets: boolean, appContainerSelector: string, hideApp: boolean, autoHide: boolean) => { diff --git a/src/Bup/Bit.Bup/Scripts/bit-bup.ts b/src/Bup/Bit.Bup/Scripts/bit-bup.ts index 933a5fef3b..74620e190f 100644 --- a/src/Bup/Bit.Bup/Scripts/bit-bup.ts +++ b/src/Bup/Bit.Bup/Scripts/bit-bup.ts @@ -1,5 +1,5 @@ var BitBup = BitBup || {}; -BitBup.version = window['bit-bup version'] = '9.0.1'; +BitBup.version = window['bit-bup version'] = '9.1.0-pre-01'; declare const Blazor: any; diff --git a/src/Butil/Bit.Butil/Scripts/butil.ts b/src/Butil/Bit.Butil/Scripts/butil.ts index d65ce8821c..9fbb2d8344 100644 --- a/src/Butil/Bit.Butil/Scripts/butil.ts +++ b/src/Butil/Bit.Butil/Scripts/butil.ts @@ -1,2 +1,2 @@ var BitButil = BitButil || {}; -BitButil.version = window['bit-butil version'] = '9.0.1'; \ No newline at end of file +BitButil.version = window['bit-butil version'] = '9.1.0-pre-01'; \ No newline at end of file diff --git a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj index 6f865c4156..4e9b6e16f1 100644 --- a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj +++ b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj @@ -1,4 +1,4 @@ - + @@ -17,14 +17,14 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj index 3f0ae4bcf7..0ace85bd55 100644 --- a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj +++ b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj @@ -1,4 +1,4 @@ - + @@ -19,14 +19,14 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js index 6a6fbe8821..3c13f67f11 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js @@ -1,5 +1,5 @@ //+:cnd:noEmit -// bit version: 9.0.1 +// bit version: 9.1.0-pre-01 // https://github.com/bitfoundation/bitplatform/tree/develop/src/Bswup //#if (notification == true) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props index fcd8c5eb32..a4b34048de 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props @@ -1,4 +1,4 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props index 7f039ccb52..3b4d866dca 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props @@ -1,14 +1,14 @@  - - - - - - - - + + + + + + + + @@ -42,7 +42,7 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props index 8138b10b1d..fbce119636 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props @@ -1,14 +1,14 @@  - - - - - - - - + + + + + + + + @@ -42,7 +42,7 @@ - + diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj index 0ca0bc602d..b925f06fa2 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj @@ -22,15 +22,15 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj index b7a3b80a17..6f4f194f8c 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj index edc5a2be54..51f7538f42 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Directory.Build.props b/src/Websites/Careers/src/Directory.Build.props index 05b025af10..f29cb77570 100644 --- a/src/Websites/Careers/src/Directory.Build.props +++ b/src/Websites/Careers/src/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj index b9f50c8ad6..345cd0acd8 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj @@ -22,16 +22,16 @@ - - - + + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor index 46e75c66d7..ecefa1261d 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor @@ -167,8 +167,8 @@ rm $HOME/dotnet.tar.gz }
  • -
    Install Bit Boilerplate project template
    - dotnet new install Bit.Boilerplate::9.0.1 +
    Install Bit Boilerplate project template
    + dotnet new install Bit.Boilerplate::9.1.0-pre-01
  • @if (showCrossPlatform && devOS is "Windows") { diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs index f79e927c35..cb0d898014 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs @@ -38,7 +38,7 @@ public partial class Templates03GettingStartedPage command:"dotnet nuget add source \"https://api.nuget.org/v3/index.json\" --name \"nuget.org\"; dotnet workload install wasm-tools;"), (text:@"echo 'Install the Bit.Boilerplate project template https://www.nuget.org/packages/Boilerplate.Templates';", - command:"dotnet new install Bit.Boilerplate::9.0.1;") + command:"dotnet new install Bit.Boilerplate::9.1.0-pre-01;") ]; if (enableVirtualization) diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj index 4ae0b16dc9..3b52674566 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj index edc5a2be54..51f7538f42 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj @@ -6,11 +6,11 @@
    - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Directory.Build.props b/src/Websites/Platform/src/Directory.Build.props index b33cd4b587..c9655357f7 100644 --- a/src/Websites/Platform/src/Directory.Build.props +++ b/src/Websites/Platform/src/Directory.Build.props @@ -1,4 +1,4 @@ - + preview diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj index 3601172eeb..bea0c763e2 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj @@ -22,15 +22,15 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj index 0bbe2d3008..55271fc091 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj index edc5a2be54..51f7538f42 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Directory.Build.props b/src/Websites/Sales/src/Directory.Build.props index 0562fa123c..6208b63c7b 100644 --- a/src/Websites/Sales/src/Directory.Build.props +++ b/src/Websites/Sales/src/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 From e751efb10b55e7c30f5aa2d2da7ee02691cf61cc Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Sun, 24 Nov 2024 00:45:27 +0330 Subject: [PATCH 06/87] fix(blazorui): resolve BitNav and BitNavBar issues #9316 (#9317) --- .../Components/Navs/Nav/BitNav.razor.cs | 47 +++++++++------ .../Components/Navs/NavBar/BitNavBar.razor.cs | 57 ++++++++++++------- .../Components/Navs/NavBar/BitNavBar.scss | 8 +++ 3 files changed, 74 insertions(+), 38 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Nav/BitNav.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Nav/BitNav.razor.cs index ac96fe05b4..54d8959693 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Nav/BitNav.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Nav/BitNav.razor.cs @@ -30,7 +30,9 @@ public partial class BitNav : BitComponentBase, IDisposable where TItem : /// /// Items to render as children. /// - [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] + [CallOnSet(nameof(OnSetParameters))] + public RenderFragment? ChildContent { get; set; } /// /// Custom CSS classes for different parts of the BitNav component. @@ -94,7 +96,9 @@ public partial class BitNav : BitComponentBase, IDisposable where TItem : /// /// A collection of item to display in the navigation bar. /// - [Parameter] public IList Items { get; set; } = []; + [Parameter] + [CallOnSet(nameof(OnSetParameters))] + public IList Items { get; set; } = []; /// /// Used to customize how content inside the item is rendered. @@ -109,7 +113,9 @@ public partial class BitNav : BitComponentBase, IDisposable where TItem : /// /// Determines how the navigation will be handled. /// - [Parameter] public BitNavMode Mode { get; set; } = BitNavMode.Automatic; + [Parameter] + [CallOnSet(nameof(OnSetMode))] + public BitNavMode Mode { get; set; } /// /// Names and selectors of the custom input type properties. @@ -719,12 +725,11 @@ internal bool GetItemExpanded(TItem item) internal async Task SetSelectedItem(TItem item) { + if (item == SelectedItem && Reselectable is false) return; + if (await AssignSelectedItem(item) is false) return; - if (item != SelectedItem || Reselectable) - { - await OnSelectItem.InvokeAsync(item); - } + await OnSelectItem.InvokeAsync(item); StateHasChanged(); } @@ -732,12 +737,14 @@ internal async Task SetSelectedItem(TItem item) internal void RegisterOption(BitNavOption option) { _items.Add((option as TItem)!); + SetSelectedItemByCurrentUrl(); StateHasChanged(); } internal void UnregisterOption(BitNavOption option) { _items.Remove((option as TItem)!); + SetSelectedItemByCurrentUrl(); StateHasChanged(); } @@ -853,17 +860,6 @@ protected override async Task OnInitializedAsync() await base.OnInitializedAsync(); } - protected override async Task OnParametersSetAsync() - { - if (ChildContent is null && Items != _oldItems) - { - _items = Items?.ToList() ?? new(); - _oldItems = Items; - } - - await base.OnParametersSetAsync(); - } - private List Flatten(IList e) => e.SelectMany(c => Flatten(GetChildItems(c))).Concat(e).ToList(); @@ -921,6 +917,21 @@ private void OnSetSelectedItem() ToggleItemAndParents(_items, SelectedItem, true); } + private void OnSetMode() + { + if (Mode is not BitNavMode.Automatic) return; + + SetSelectedItemByCurrentUrl(); + } + + private void OnSetParameters() + { + if (ChildContent is not null || Options is not null || Items == _oldItems) return; + + _items = Items?.ToList() ?? []; + _oldItems = Items; + } + public void Dispose() diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor.cs index 8a3c1260ed..832cb7a5ec 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor.cs @@ -38,6 +38,18 @@ public partial class BitNavBar : BitComponentBase, IDisposable where TIte /// [Parameter] public TItem? DefaultSelectedItem { get; set; } + /// + /// Renders the nav bat in a width to only fit its content. + /// + [Parameter, ResetClassBuilder] + public bool FitWidth { get; set; } + + /// + /// Renders the nav bar in full width of its container element. + /// + [Parameter, ResetClassBuilder] + public bool FullWidth { get; set; } + /// /// Only renders the icon of each navbar item. /// @@ -106,12 +118,14 @@ public partial class BitNavBar : BitComponentBase, IDisposable where TIte internal void RegisterOption(BitNavBarOption option) { _items.Add((option as TItem)!); + SetSelectedItemByCurrentUrl(); StateHasChanged(); } internal void UnregisterOption(BitNavBarOption option) { _items.Remove((option as TItem)!); + SetSelectedItemByCurrentUrl(); StateHasChanged(); } @@ -123,6 +137,9 @@ protected override void RegisterCssClasses() { ClassBuilder.Register(() => Classes?.Root); + ClassBuilder.Register(() => FitWidth ? "bit-nbr-ftw" : string.Empty); + ClassBuilder.Register(() => FullWidth ? "bit-nbr-flw" : string.Empty); + ClassBuilder.Register(() => IconOnly ? "bit-nbr-ion" : string.Empty); ClassBuilder.Register(() => Color switch @@ -161,9 +178,17 @@ protected override async Task OnInitializedAsync() _oldItems = Items; } - if (Mode != BitNavMode.Automatic && SelectedItemHasBeenSet is false && DefaultSelectedItem is not null) + if (Mode == BitNavMode.Automatic) + { + SetSelectedItemByCurrentUrl(); + _navigationManager.LocationChanged += OnLocationChanged; + } + else { - await AssignSelectedItem(DefaultSelectedItem); + if (DefaultSelectedItem is not null && SelectedItemHasBeenSet is false) + { + await AssignSelectedItem(DefaultSelectedItem); + } } await base.OnInitializedAsync(); @@ -194,34 +219,26 @@ private void SetSelectedItemByCurrentUrl() private void OnSetParameters() { - if (ChildContent is null && Options is null && Items != _oldItems) - { - _items = Items?.ToList() ?? []; - _oldItems = Items; - } + if (ChildContent is not null || Options is not null || Items == _oldItems) return; + + _items = Items?.ToList() ?? []; + _oldItems = Items; } private void OnSetMode() { - if (Mode is BitNavMode.Automatic) - { - SetSelectedItemByCurrentUrl(); - _navigationManager.LocationChanged += OnLocationChanged; - } - else - { - _navigationManager.LocationChanged -= OnLocationChanged; - } + if (Mode is not BitNavMode.Automatic) return; + + SetSelectedItemByCurrentUrl(); } private async Task SetSelectedItem(TItem item) { + if (item == SelectedItem && Reselectable is false) return; + if (await AssignSelectedItem(item) is false) return; - if (item != SelectedItem || Reselectable) - { - await OnSelectItem.InvokeAsync(item); - } + await OnSelectItem.InvokeAsync(item); StateHasChanged(); } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.scss b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.scss index a202ef207a..07576906f2 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.scss @@ -14,6 +14,14 @@ } } +.bit-nbr-ftw { + width: fit-content; +} + +.bit-nbr-flw { + width: 100%; +} + .bit-nbr-cnt { display: flex; justify-content: space-around; From 4f72350cf0d4ab221d557347e7c3857339594ca4 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Sun, 24 Nov 2024 01:00:00 +0330 Subject: [PATCH 07/87] feat(templates): update Boilerplate for latest BlazorUI changes #9318 (#9320) --- .../Components/Layout/NavBar.razor | 57 ++++--------------- .../Components/Layout/NavPanel.razor | 1 + .../Components/Layout/UserMenu.razor | 13 +++-- .../Categories/CategoriesPage.razor | 11 +++- .../Authorized/Products/ProductsPage.razor | 11 +++- .../IClientCoreServiceCollectionExtensions.cs | 2 +- .../src/Shared/Resources/AppStrings.fa.resx | 21 +++++++ .../src/Shared/Resources/AppStrings.nl.resx | 21 +++++++ .../src/Shared/Resources/AppStrings.resx | 21 +++++++ 9 files changed, 105 insertions(+), 53 deletions(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavBar.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavBar.razor index bb680883ea..467e7ba42c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavBar.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavBar.razor @@ -2,49 +2,16 @@ @inherits AppComponentBase;
    - - - - @Localizer[nameof(AppStrings.Home)] - - - - @*#if (sample == "Todo")*@ - - - - @Localizer[nameof(AppStrings.Todo)] - - - @*#endif*@ - - @*#if (sample == "Admin")*@ - - - - @Localizer[nameof(AppStrings.Dashboard)] - - - - - - - @Localizer[nameof(AppStrings.Products)] - - - - - - - @Localizer[nameof(AppStrings.Categories)] - - - @*#endif*@ - - - - - @Localizer[nameof(AppStrings.Terms)] - - + + + @*#if (sample == "Todo")*@ + + @*#endif*@ + @*#if (sample == "Admin")*@ + + + + @*#endif*@ + +
    \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavPanel.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavPanel.razor index bc2abde553..fdd55d4843 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavPanel.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavPanel.razor @@ -63,6 +63,7 @@ + + @Localizer[nameof(AppStrings.ProfileTitle)] + + @Localizer[nameof(AppStrings.Language)] @@ -55,11 +59,10 @@ - - @Localizer[nameof(AppStrings.ProfileTitle)] - - - + @Localizer[nameof(AppStrings.SignOut)] 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 df85275898..dbc86a99ec 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 @@ -66,7 +66,16 @@ - + + + @Localizer[nameof(AppStrings.Page)] @(value.CurrentPageIndex + 1) @Localizer[nameof(AppStrings.Of)] @(value.LastPageIndex + 1) + + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor index 937d6a595d..651c0cf8d1 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor @@ -78,7 +78,16 @@ - + + + @Localizer[nameof(AppStrings.Page)] @(value.CurrentPageIndex + 1) @Localizer[nameof(AppStrings.Of)] @(value.LastPageIndex + 1) + + 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 e298ce2c75..adc05da2fb 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,7 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle services.AddSessioned(); services.AddSessioned(); services.AddSessioned(); - services.AddSessioned(sp => + services.AddSessioned(sp => { var authenticationStateProvider = ActivatorUtilities.CreateInstance(sp); authenticationStateProvider.OnInit(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx index a66f43540e..fbbb0d4adb 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx @@ -1033,6 +1033,27 @@ پنل ادمین + + + {0} آیتم + + + برو به اولین برگه + + + برو به برگه قبل + + + برو به برگه بعد + + + برو به آخرین برگه + + + برگه + + + از تعداد محصولات diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx index 5949f0f411..c2289002da 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx @@ -1033,6 +1033,27 @@ Beheerderspaneel + + + {0} items + + + Ga naar de eerste pagina + + + Ga naar de vorige pagina + + + Ga naar de volgende pagina + + + Ga naar de laatste pagina + + + Pagina + + + tot Aantal producten diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx index 4ca26211f3..6d3bc4291d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx @@ -1033,6 +1033,27 @@ Admin panel + + + {0} items + + + Go to the first page + + + Go to the previous page + + + Go to the next page + + + Go to the last page + + + Page + + + of Products count From 464dc553823f9badaf57d6b085fa308dfff674b2 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Sun, 24 Nov 2024 14:01:38 +0330 Subject: [PATCH 08/87] feat(prerelease): v-9.1.0-pre-02 #9319 (#9321) --- src/Besql/Bit.Besql/wwwroot/bit-besql.js | 2 +- src/Bit.Build.props | 4 ++-- src/BlazorUI/Bit.BlazorUI/Scripts/general.ts | 2 +- .../Bit.BlazorUI.Demo.Server.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Shared.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Client.Core.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Client.Maui.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Client.Web.csproj | 6 +++--- .../wwwroot/service-worker.published.js | 2 +- .../Bit.BlazorUI.Demo.Client.Windows.csproj | 4 ++-- src/BlazorUI/Demo/Directory.Build.props | 2 +- .../Bit.Bswup.Demo/wwwroot/service-worker.js | 2 +- .../wwwroot/service-worker.published.js | 2 +- .../wwwroot/service-worker.js | 2 +- .../wwwroot/service-worker.published.js | 2 +- .../Bit.Bswup/Scripts/bit-bswup.progress.ts | 2 +- src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts | 2 +- src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts | 2 +- .../FullDemo/Client/wwwroot/service-worker.js | 2 +- .../Client/wwwroot/service-worker.published.js | 2 +- src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts | 2 +- src/Bup/Bit.Bup/Scripts/bit-bup.ts | 2 +- src/Butil/Bit.Butil/Scripts/butil.ts | 2 +- .../BlazorEmpty.Client.csproj | 8 ++++---- .../BlazorEmpty/BlazorEmpty.csproj | 8 ++++---- .../wwwroot/service-worker.published.js | 2 +- .../Bit.Boilerplate/src/Directory.Build.props | 2 +- .../src/Directory.Packages.props | 18 +++++++++--------- .../src/Directory.Packages8.props | 18 +++++++++--------- .../Bit.Websites.Careers.Client.csproj | 10 +++++----- .../Bit.Websites.Careers.Server.csproj | 4 ++-- .../Bit.Websites.Careers.Shared.csproj | 4 ++-- src/Websites/Careers/src/Directory.Build.props | 2 +- .../Bit.Websites.Platform.Client.csproj | 12 ++++++------ .../Templates03GettingStartedPage.razor | 4 ++-- .../Templates03GettingStartedPage.razor.cs | 2 +- .../Bit.Websites.Platform.Server.csproj | 4 ++-- .../Bit.Websites.Platform.Shared.csproj | 4 ++-- .../Platform/src/Directory.Build.props | 2 +- .../Bit.Websites.Sales.Client.csproj | 10 +++++----- .../Bit.Websites.Sales.Server.csproj | 4 ++-- .../Bit.Websites.Sales.Shared.csproj | 4 ++-- src/Websites/Sales/src/Directory.Build.props | 2 +- 43 files changed, 93 insertions(+), 93 deletions(-) diff --git a/src/Besql/Bit.Besql/wwwroot/bit-besql.js b/src/Besql/Bit.Besql/wwwroot/bit-besql.js index 3040440399..472dacf6da 100644 --- a/src/Besql/Bit.Besql/wwwroot/bit-besql.js +++ b/src/Besql/Bit.Besql/wwwroot/bit-besql.js @@ -1,5 +1,5 @@ var BitBesql = BitBesql || {}; -BitBesql.version = window['bit-besql version'] = '9.1.0-pre-01'; +BitBesql.version = window['bit-besql version'] = '9.1.0-pre-02'; async function synchronizeDbWithCache(file) { diff --git a/src/Bit.Build.props b/src/Bit.Build.props index a2f6405c5f..7268c5fc24 100644 --- a/src/Bit.Build.props +++ b/src/Bit.Build.props @@ -27,8 +27,8 @@ 9.1.0 - https://github.com/bitfoundation/bitplatform/releases/tag/v-$(ReleaseVersion)-pre-01 - $(ReleaseVersion)-pre-01 + https://github.com/bitfoundation/bitplatform/releases/tag/v-$(ReleaseVersion)-pre-02 + $(ReleaseVersion)-pre-02 $(ReleaseVersion).$([System.DateTime]::Now.ToString(HHmm)) diff --git a/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts b/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts index 31efd657af..6df784a458 100644 --- a/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts +++ b/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts @@ -1,4 +1,4 @@ -(BitBlazorUI as any).version = (window as any)['bit-blazorui version'] = '9.1.0-pre-01'; +(BitBlazorUI as any).version = (window as any)['bit-blazorui version'] = '9.1.0-pre-02'; interface DotNetObject { invokeMethod(methodIdentifier: string, ...args: any[]): T; diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj index 2073ffd580..1eb7f97ac0 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj @@ -5,11 +5,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj index b32520cced..895aff079d 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj @@ -5,11 +5,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj index b1b13a1d12..afee51e54d 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj @@ -16,11 +16,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj index a72bcb241c..56ecde43f7 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj @@ -85,12 +85,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj index b738e31461..f1a9244c2b 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj @@ -24,13 +24,13 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js index 926efa3491..1e2e0677c8 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-01 +// bit version: 9.1.0-pre-02 // https://github.com/bitfoundation/bitplatform/tree/develop/src/Bswup self.assetsInclude = []; diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj index 28189fe2f6..4686a09788 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj @@ -29,11 +29,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Directory.Build.props b/src/BlazorUI/Demo/Directory.Build.props index 372ac2f538..7cb4255517 100644 --- a/src/BlazorUI/Demo/Directory.Build.props +++ b/src/BlazorUI/Demo/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js index c40d668af3..6fb7c93e1b 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-01 +// bit version: 9.1.0-pre-02 self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; self.caseInsensitiveUrl = true; diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js index 8fb2162684..fe66bc8bf6 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-01 +// bit version: 9.1.0-pre-02 self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; self.caseInsensitiveUrl = true; diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js index d5b11de990..4e68967b9d 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js +++ b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-01 +// bit version: 9.1.0-pre-02 // In development, always fetch from the network and do not enable offline support. // This is because caching would make development more difficult (changes would not diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js index 634a6c32db..7ebf4ccfe2 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js +++ b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-01 +// bit version: 9.1.0-pre-02 self.assetsInclude = []; self.assetsExclude = [ diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts index 056588b60b..4a6d36b7ec 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts @@ -1,4 +1,4 @@ -window['bit-bswup.progress version'] = '9.1.0-pre-01'; +window['bit-bswup.progress version'] = '9.1.0-pre-02'; ; (function () { (window as any).startBswupProgress = (autoReload: boolean, diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts index 84664cd146..a67f442a7b 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts @@ -1,4 +1,4 @@ -self['bit-bswup.sw version'] = '9.1.0-pre-01'; +self['bit-bswup.sw version'] = '9.1.0-pre-02'; interface Window { clients: any diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts index 21599456aa..89db63d028 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts @@ -1,5 +1,5 @@ const BitBswup = {} as any; -BitBswup.version = window['bit-bswup version'] = '9.1.0-pre-01'; +BitBswup.version = window['bit-bswup version'] = '9.1.0-pre-02'; declare const Blazor: any; diff --git a/src/Bswup/FullDemo/Client/wwwroot/service-worker.js b/src/Bswup/FullDemo/Client/wwwroot/service-worker.js index ce749c4fed..3f891c2957 100644 --- a/src/Bswup/FullDemo/Client/wwwroot/service-worker.js +++ b/src/Bswup/FullDemo/Client/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-01 +// bit version: 9.1.0-pre-02 // In development, always fetch from the network and do not enable offline support. // This is because caching would make development more difficult (changes would not diff --git a/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js b/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js index a2d8f59837..c50db063e7 100644 --- a/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js +++ b/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-01 +// bit version: 9.1.0-pre-02 self.assetsInclude = []; self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; diff --git a/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts b/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts index 4b9be0005c..935b9f166c 100644 --- a/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts +++ b/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts @@ -1,4 +1,4 @@ -window['bit-bup.progress version'] = '9.1.0-pre-01'; +window['bit-bup.progress version'] = '9.1.0-pre-02'; ; (function () { (window as any).startBupProgress = (showLogs: boolean, showAssets: boolean, appContainerSelector: string, hideApp: boolean, autoHide: boolean) => { diff --git a/src/Bup/Bit.Bup/Scripts/bit-bup.ts b/src/Bup/Bit.Bup/Scripts/bit-bup.ts index 74620e190f..039a82676c 100644 --- a/src/Bup/Bit.Bup/Scripts/bit-bup.ts +++ b/src/Bup/Bit.Bup/Scripts/bit-bup.ts @@ -1,5 +1,5 @@ var BitBup = BitBup || {}; -BitBup.version = window['bit-bup version'] = '9.1.0-pre-01'; +BitBup.version = window['bit-bup version'] = '9.1.0-pre-02'; declare const Blazor: any; diff --git a/src/Butil/Bit.Butil/Scripts/butil.ts b/src/Butil/Bit.Butil/Scripts/butil.ts index 9fbb2d8344..f587195edb 100644 --- a/src/Butil/Bit.Butil/Scripts/butil.ts +++ b/src/Butil/Bit.Butil/Scripts/butil.ts @@ -1,2 +1,2 @@ var BitButil = BitButil || {}; -BitButil.version = window['bit-butil version'] = '9.1.0-pre-01'; \ No newline at end of file +BitButil.version = window['bit-butil version'] = '9.1.0-pre-02'; \ No newline at end of file diff --git a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj index 4e9b6e16f1..8375085437 100644 --- a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj +++ b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj @@ -1,4 +1,4 @@ - + @@ -17,14 +17,14 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj index 0ace85bd55..4cabe7d1c1 100644 --- a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj +++ b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj @@ -1,4 +1,4 @@ - + @@ -19,14 +19,14 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js index 3c13f67f11..5bc8091fb0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js @@ -1,5 +1,5 @@ //+:cnd:noEmit -// bit version: 9.1.0-pre-01 +// bit version: 9.1.0-pre-02 // https://github.com/bitfoundation/bitplatform/tree/develop/src/Bswup //#if (notification == true) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props index a4b34048de..684f2791fe 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props @@ -1,4 +1,4 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props index 3b4d866dca..ca787376ac 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props @@ -1,14 +1,14 @@  - - - - - - - - + + + + + + + + @@ -42,7 +42,7 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props index fbce119636..55035e87a6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props @@ -1,14 +1,14 @@  - - - - - - - - + + + + + + + + @@ -42,7 +42,7 @@ - + diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj index b925f06fa2..6da1187624 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj @@ -22,15 +22,15 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj index 6f4f194f8c..e19f87c400 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj index 51f7538f42..ca8e07ec0b 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Directory.Build.props b/src/Websites/Careers/src/Directory.Build.props index f29cb77570..14b2a657ec 100644 --- a/src/Websites/Careers/src/Directory.Build.props +++ b/src/Websites/Careers/src/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj index 345cd0acd8..b2811ad032 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj @@ -22,16 +22,16 @@ - - - + + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor index ecefa1261d..6bc62ae484 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor @@ -167,8 +167,8 @@ rm $HOME/dotnet.tar.gz }
  • -
    Install Bit Boilerplate project template
    - dotnet new install Bit.Boilerplate::9.1.0-pre-01 +
    Install Bit Boilerplate project template
    + dotnet new install Bit.Boilerplate::9.1.0-pre-02
  • @if (showCrossPlatform && devOS is "Windows") { diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs index cb0d898014..9f9df3717e 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs @@ -38,7 +38,7 @@ public partial class Templates03GettingStartedPage command:"dotnet nuget add source \"https://api.nuget.org/v3/index.json\" --name \"nuget.org\"; dotnet workload install wasm-tools;"), (text:@"echo 'Install the Bit.Boilerplate project template https://www.nuget.org/packages/Boilerplate.Templates';", - command:"dotnet new install Bit.Boilerplate::9.1.0-pre-01;") + command:"dotnet new install Bit.Boilerplate::9.1.0-pre-02;") ]; if (enableVirtualization) diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj index 3b52674566..3dae7902c1 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj index 51f7538f42..ca8e07ec0b 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj @@ -6,11 +6,11 @@
    - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Directory.Build.props b/src/Websites/Platform/src/Directory.Build.props index c9655357f7..83a98487ca 100644 --- a/src/Websites/Platform/src/Directory.Build.props +++ b/src/Websites/Platform/src/Directory.Build.props @@ -1,4 +1,4 @@ - + preview diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj index bea0c763e2..5edd1fa86f 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj @@ -22,15 +22,15 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj index 55271fc091..367898d2c8 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj index 51f7538f42..ca8e07ec0b 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Directory.Build.props b/src/Websites/Sales/src/Directory.Build.props index 6208b63c7b..8405b3443b 100644 --- a/src/Websites/Sales/src/Directory.Build.props +++ b/src/Websites/Sales/src/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 From bedd9ef04979d996c8ce7c179be8c555f95a284d Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Sun, 24 Nov 2024 18:18:55 +0330 Subject: [PATCH 09/87] feat(templates): Add ModalService to Boilerplate #9323 (#9324) --- .../Components/Layout/MessageBox.razor | 41 ++++-------- .../Components/Layout/MessageBox.razor.cs | 59 ++--------------- .../Components/Layout/MessageBox.razor.scss | 46 ++------------ .../Components/Layout/Modal.razor | 27 ++++++++ .../Components/Layout/Modal.razor.cs | 63 +++++++++++++++++++ .../Components/Layout/Modal.razor.scss | 26 ++++++++ .../Components/Layout/Prompt.razor | 16 +++++ .../Components/Layout/Prompt.razor.cs | 16 +++++ .../Components/Layout/Prompt.razor.scss | 7 +++ .../Components/Layout/RootLayout.razor | 2 +- .../IClientCoreServiceCollectionExtensions.cs | 2 + .../Services/ClientPubSubMessages.cs | 3 +- .../Services/MessageBoxData.cs | 10 --- .../Services/MessageBoxService.cs | 42 +++---------- .../Services/ModalData.cs | 12 ++++ .../Services/ModalService.cs | 49 +++++++++++++++ .../Services/PromptService.cs | 22 +++++++ .../compilerconfig.json | 12 ++++ 18 files changed, 286 insertions(+), 169 deletions(-) create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.cs create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.scss create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.cs create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.scss delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxData.cs create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ModalData.cs create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ModalService.cs create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PromptService.cs diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor index 6abaf8a1bb..36023bf53e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor @@ -1,32 +1,13 @@ @inherits AppComponentBase -
    - -
    - - - - @title - - - - - - - - - @body - - - - - @Localizer[AppStrings.Ok] - - - -
    -
    -
    \ No newline at end of file +
    + + @Body + +
    + + + + @Localizer[AppStrings.Ok] + + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor.cs index 6b38366c85..8ef16e20c7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor.cs @@ -2,63 +2,12 @@ public partial class MessageBox { - private bool isOpen; - private string? body; - private string? title; - private Action? unsubscribe; - private bool disposed = false; + [Parameter] public string? Body { get; set; } + [Parameter] public Action? OnOk { get; set; } - private TaskCompletionSource? tcs; - protected override Task OnInitAsync() + private void OnOkClick() { - unsubscribe = PubSubService.Subscribe(ClientPubSubMessages.SHOW_MESSAGE, async args => - { - var data = (MessageBoxData)args!; - - tcs = data.TaskCompletionSource; - - await ShowMessageBox(data.Message, data.Title); - }); - - return base.OnInitAsync(); - } - - private async Task ShowMessageBox(string message, string title = "") - { - await InvokeAsync(() => - { - isOpen = true; - this.title = title; - body = message; - - StateHasChanged(); - }); - } - - private async Task OnCloseClick() - { - isOpen = false; - tcs?.SetResult(false); - } - - private async Task OnOkClick() - { - isOpen = false; - tcs?.SetResult(true); - } - - protected override async ValueTask DisposeAsync(bool disposing) - { - await base.DisposeAsync(true); - - if (disposed || disposing is false) return; - - tcs?.TrySetResult(false); - tcs = null; - - unsubscribe?.Invoke(); - - disposed = true; + OnOk?.Invoke(); } } 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 663287364a..ac94c637e7 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 @@ -1,41 +1,7 @@ -@import '../../Styles/abstracts/_media-queries.scss'; - -section { - padding: 1rem; - min-width: 20rem; - max-height: var(--app-height); - - @include lt-md { - min-width: unset; - } -} - -::deep { - .root { - width: var(--app-width); - height: var(--app-height); - top: var(--app-inset-top); - left: var(--app-inset-left); - right: var(--app-inset-right); - bottom: var(--app-inset-bottom); - } - - .content { - @include lt-md { - min-width: 95%; - max-width: 95%; - } - } - - .stack { - max-height: calc(var(--app-height) - 3rem); - } - - .body { - width: 100%; - flex-grow: 1; - display: flex; - overflow: auto; - white-space: pre; - } +.body { + width: 100%; + flex-grow: 1; + display: flex; + overflow: auto; + white-space: pre; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor new file mode 100644 index 0000000000..ae47327841 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor @@ -0,0 +1,27 @@ +@inherits AppComponentBase + +
    + +
    + + + + @title + + + + + + + + @if (componentType is not null) + { + + } + +
    +
    +
    \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.cs new file mode 100644 index 0000000000..a9f8f6c42b --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.cs @@ -0,0 +1,63 @@ +namespace Boilerplate.Client.Core.Components.Layout; + +public partial class Modal +{ + private bool isOpen; + private string? title; + private Type? componentType; + private bool disposed = false; + private Action? unsubscribeShow; + private Action? unsubscribeClose; + private IDictionary? componentParameters; + + private TaskCompletionSource? tcs; + + protected override Task OnInitAsync() + { + unsubscribeShow = PubSubService.Subscribe(ClientPubSubMessages.SHOW_MODAL, async args => + { + var data = (ModalData)args!; + + tcs = data.TaskCompletionSource; + + await InvokeAsync(() => + { + isOpen = true; + title = data.Title; + componentType = data.ComponentType; + componentParameters = data.Parameters; + + StateHasChanged(); + }); + }); + + unsubscribeClose = PubSubService.Subscribe(ClientPubSubMessages.CLOSE_MODAL, async _ => + { + await CloseModal(); + await InvokeAsync(StateHasChanged); + }); + + return base.OnInitAsync(); + } + + private async Task CloseModal() + { + isOpen = false; + tcs?.SetResult(); + } + + protected override async ValueTask DisposeAsync(bool disposing) + { + await base.DisposeAsync(disposing); + + if (disposed || disposing is false) return; + + tcs?.TrySetResult(); + tcs = null; + + unsubscribeShow?.Invoke(); + unsubscribeClose?.Invoke(); + + disposed = true; + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.scss new file mode 100644 index 0000000000..d412824f7a --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.scss @@ -0,0 +1,26 @@ +@import '../../Styles/abstracts/_media-queries.scss'; + +section { + padding: 1rem; + min-width: 20rem; + max-height: var(--app-height); + + @include lt-md { + min-width: unset; + } +} + +::deep { + .root { + width: var(--app-width); + height: var(--app-height); + top: var(--app-inset-top); + left: var(--app-inset-left); + right: var(--app-inset-right); + bottom: var(--app-inset-bottom); + } + + .stack { + max-height: calc(var(--app-height) - 3rem); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor new file mode 100644 index 0000000000..1584c38964 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor @@ -0,0 +1,16 @@ +@inherits AppComponentBase + +
    + + + @Body + + + +
    + + + + @Localizer[AppStrings.Ok] + + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.cs new file mode 100644 index 0000000000..3e85e73657 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace Boilerplate.Client.Core.Components.Layout; + +public partial class Prompt +{ + private string? value; + + [Parameter] public string? Body { get; set; } + [Parameter] public Action? OnOk { get; set; } + + private void OnOkClick() + { + OnOk?.Invoke(value); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.scss new file mode 100644 index 0000000000..ac94c637e7 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.scss @@ -0,0 +1,7 @@ +.body { + width: 100%; + flex-grow: 1; + display: flex; + overflow: auto; + white-space: pre; +} 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 7b0a8a96eb..0577dae99b 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 @@ -44,8 +44,8 @@ + - \ No newline at end of file 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 adc05da2fb..2d2a3d0f98 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 @@ -39,7 +39,9 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle // Defining them as singletons would result in them being shared across all users in Blazor Server and during pre-rendering. // To address this, we use the AddSessioned extension method. // AddSessioned applies AddSingleton in BlazorHybrid and AddScoped in Blazor WebAssembly and Blazor Server, ensuring correct service lifetimes for each environment. + services.AddSessioned(); services.AddSessioned(); + services.AddSessioned(); services.AddSessioned(); services.AddSessioned(); services.AddSessioned(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientPubSubMessages.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientPubSubMessages.cs index 1fd6a767c5..3a7e05b86b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientPubSubMessages.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientPubSubMessages.cs @@ -6,7 +6,8 @@ namespace Boilerplate.Client.Core.Services; public static partial class ClientPubSubMessages { public const string SHOW_SNACK = nameof(SHOW_SNACK); - public const string SHOW_MESSAGE = nameof(SHOW_MESSAGE); + public const string SHOW_MODAL = nameof(SHOW_MODAL); + public const string CLOSE_MODAL = nameof(CLOSE_MODAL); public const string THEME_CHANGED = nameof(THEME_CHANGED); public const string OPEN_NAV_PANEL = nameof(OPEN_NAV_PANEL); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxData.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxData.cs deleted file mode 100644 index fb1af21ec2..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxData.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Boilerplate.Client.Core.Services; - -public partial class MessageBoxData(string message, string title, TaskCompletionSource taskCompletionSource) -{ - public string Message { get; set; } = message; - - public string Title { get; set; } = title; - - public TaskCompletionSource TaskCompletionSource { get; set; } = taskCompletionSource; -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxService.cs index 7f9add12eb..15ba511267 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxService.cs @@ -2,43 +2,21 @@ public partial class MessageBoxService { - private bool isRunning = false; - private readonly ConcurrentQueue queue = new(); - - - [AutoInject] private readonly PubSubService pubSubService = default!; - + [AutoInject] private ModalService modalService = default!; public Task Show(string message, string title = "") { TaskCompletionSource tcs = new(); - - queue.Enqueue(new(message, title, tcs)); - - if (isRunning is false) + Dictionary parameters = new() { - isRunning = true; - _ = ProcessQueue(); - } - - return tcs.Task; - } - - private async Task ProcessQueue() - { - if (queue.IsEmpty) - { - isRunning = false; - return; - } - - if (queue.TryDequeue(out var data)) + { nameof(MessageBox.Body), message }, + { nameof(MessageBox.OnOk), () => { tcs.SetResult(true); modalService.Close(); } } + }; + modalService.Show(parameters, title).ContinueWith(async task => { - pubSubService.Publish(ClientPubSubMessages.SHOW_MESSAGE, data, persistent: true); - - await data.TaskCompletionSource.Task; - } - - _ = ProcessQueue(); + await task; + tcs.SetResult(false); + }); + return tcs.Task; } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ModalData.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ModalData.cs new file mode 100644 index 0000000000..0c33cba671 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ModalData.cs @@ -0,0 +1,12 @@ +namespace Boilerplate.Client.Core.Services; + +public partial class ModalData(Type type, IDictionary? parameters, string? title, TaskCompletionSource taskCompletionSource) +{ + public Type ComponentType { get; set; } = type; + + public IDictionary? Parameters { get; set; } = parameters; + + public string? Title { get; set; } = title; + + public TaskCompletionSource TaskCompletionSource { get; set; } = taskCompletionSource; +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ModalService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ModalService.cs new file mode 100644 index 0000000000..695eae7950 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ModalService.cs @@ -0,0 +1,49 @@ +namespace Boilerplate.Client.Core.Services; + +public partial class ModalService +{ + private bool isRunning = false; + private readonly ConcurrentQueue queue = new(); + + + [AutoInject] private readonly PubSubService pubSubService = default!; + + + public void Close() + { + pubSubService.Publish(ClientPubSubMessages.CLOSE_MODAL); + } + + public Task Show(IDictionary? parameters = null, string title = "") + { + TaskCompletionSource tcs = new(); + + queue.Enqueue(new(typeof(T), parameters, title, tcs)); + + if (isRunning is false) + { + isRunning = true; + _ = ProcessQueue(); + } + + return tcs.Task; + } + + private async Task ProcessQueue() + { + if (queue.IsEmpty) + { + isRunning = false; + return; + } + + if (queue.TryDequeue(out var data)) + { + pubSubService.Publish(ClientPubSubMessages.SHOW_MODAL, data, persistent: true); + + await data.TaskCompletionSource.Task; + } + + _ = ProcessQueue(); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PromptService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PromptService.cs new file mode 100644 index 0000000000..b3237fc9ff --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PromptService.cs @@ -0,0 +1,22 @@ +namespace Boilerplate.Client.Core.Services; + +public partial class PromptService +{ + [AutoInject] private ModalService modalService = default!; + + public Task Show(string message, string title = "") + { + TaskCompletionSource tcs = new(); + Dictionary parameters = new() + { + { nameof(Prompt.Body), message }, + { nameof(Prompt.OnOk), (string value) => { tcs.SetResult(value); modalService.Close(); } } + }; + modalService.Show(parameters, title).ContinueWith(async task => + { + await task; + tcs.SetResult(null); + }); + return tcs.Task; + } +} 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 b836243a4e..3b4ebe9f7d 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 @@ -36,6 +36,12 @@ "minify": { "enabled": false }, "options": { "sourceMap": false } }, + { + "outputFile": "Components/Layout/Modal.razor.css", + "inputFile": "Components/Layout/Modal.razor.scss", + "minify": { "enabled": false }, + "options": { "sourceMap": false } + }, { "outputFile": "Components/Layout/NavBar.razor.css", "inputFile": "Components/Layout/NavBar.razor.scss", @@ -48,6 +54,12 @@ "minify": { "enabled": false }, "options": { "sourceMap": false } }, + { + "outputFile": "Components/Layout/Prompt.razor.css", + "inputFile": "Components/Layout/Prompt.razor.scss", + "minify": { "enabled": false }, + "options": { "sourceMap": false } + }, { "outputFile": "Components/Layout/RootContainer.razor.css", "inputFile": "Components/Layout/RootContainer.razor.scss", From 3811e5287b6462b4be8f9188e424ca20ffd00627 Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Sun, 24 Nov 2024 23:55:46 +0100 Subject: [PATCH 10/87] feat(templates): Add feature to ask for security code before dangerous tasks to Boilerplate #9310 (#9311) --- .github/workflows/bit.full.ci.yml | 10 +- .../.azure-devops/workflows/cd.yml | 2 +- .../Bit.Boilerplate/.github/workflows/cd.yml | 2 +- .../.template.config/template.json | 1 + .../Components/AppComponentBase.cs | 7 +- .../Components/ClientAppCoordinator.cs | 16 +-- .../Components/Layout/Prompt.razor | 4 +- .../Components/Layout/RootLayout.razor.cs | 2 +- .../Layout/SignOutConfirmDialog.razor.cs | 2 +- .../Categories/AddOrEditCategoryPage.razor | 2 +- .../Categories/CategoriesPage.razor | 2 +- .../Authorized/Dashboard/DashboardPage.razor | 2 +- .../Authorized/Products/ProductsPage.razor | 2 +- .../Settings/DeleteAccountSection.razor.cs | 9 +- .../Authorized/Settings/SessionsSection.razor | 4 +- .../Settings/SessionsSection.razor.cs | 38 ++++--- .../Pages/Authorized/Todo/TodoPage.razor | 2 +- .../Pages/Identity/ConfirmPage.razor.cs | 19 +++- .../Pages/Identity/SignIn/SignInPage.razor | 2 +- .../Pages/Identity/SignIn/SignInPage.razor.cs | 14 +-- .../Components/Pages/NotAuthorizedPage.razor | 17 +++- .../Pages/NotAuthorizedPage.razor.cs | 12 ++- .../Boilerplate.Client.Core/Data/README.md | 4 +- .../IClientCoreServiceCollectionExtensions.cs | 11 ++- ...uthenticationManager.cs => AuthManager.cs} | 60 +++++++++--- .../AuthDelegatingHandler.cs | 2 +- .../Boilerplate.Client.Windows/Program.cs | 4 - .../Services/WindowsStorageService.cs | 2 +- .../ElevatedAccessTokenTemplate.razor | 64 ++++++++++++ .../Categories/CategoryController.cs | 2 +- .../Dashboard/DashboardController.cs | 2 +- .../IdentityController.EmailConfirmation.cs | 2 +- .../IdentityController.PhoneConfirmation.cs | 2 +- .../Identity/IdentityController.cs | 62 ++++++++++-- .../Controllers/Identity/UserController.cs | 98 ++++++++++++++++--- .../Controllers/Products/ProductController.cs | 2 +- .../Controllers/Todo/TodoItemController.cs | 2 +- .../Configurations/Todo/TodoConfiguration.cs | 14 +++ ...241124194037_InitialMigration.Designer.cs} | 12 ++- ....cs => 20241124194037_InitialMigration.cs} | 6 +- .../Migrations/AppDbContextModelSnapshot.cs | 10 +- .../ElevatedAccessTokenTemplateModel.cs | 8 ++ .../Models/Identity/User.cs | 16 ++- .../Models/Identity/UserSession.cs | 5 + .../Resources/EmailStrings.fa.resx | 11 ++- .../Resources/EmailStrings.nl.resx | 11 ++- .../Resources/EmailStrings.resx | 11 ++- .../ServerApiSettings.cs | 5 + .../Services/EmailService.cs | 18 ++++ .../Services/PushNotificationService.cs | 69 ++++++++----- .../{Signalr => SignalR}/AppHub.cs | 0 .../AppHubConnectionHandler.cs | 0 .../Boilerplate.Server.Api/appsettings.json | 4 +- .../Boilerplate.Server.Web/appsettings.json | 3 +- .../Controllers/Identity/IUserController.cs | 3 + .../Dtos/Identity/ConfirmEmailRequestDto.cs | 3 + .../Dtos/Identity/ConfirmPhoneRequestDto.cs | 3 + .../Shared/Dtos/Identity/RefreshRequestDto.cs | 7 +- .../Extensions/ClaimsPrincipalExtensions.cs | 2 +- .../ISharedServiceCollectionExtensions.cs | 27 +++-- .../src/Shared/Resources/AppStrings.fa.resx | 30 ++++-- .../src/Shared/Resources/AppStrings.nl.resx | 14 ++- .../src/Shared/Resources/AppStrings.resx | 16 ++- .../src/Shared/Services/AuthPolicies.cs | 42 ++++++++ .../Services/RootServiceScopeProvider.cs | 6 ++ .../src/Shared/appsettings.json | 5 +- .../src/Tests/IdentityApiTests.cs | 2 +- 67 files changed, 675 insertions(+), 178 deletions(-) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/{AuthenticationManager.cs => AuthManager.cs} (74%) create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/ElevatedAccessTokenTemplate.razor create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Todo/TodoConfiguration.cs rename src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/{20241122104915_InitialMigration.Designer.cs => 20241124194037_InitialMigration.Designer.cs} (99%) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/{20241122104915_InitialMigration.cs => 20241124194037_InitialMigration.cs} (95%) create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Emailing/ElevatedAccessTokenTemplateModel.cs rename src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/{Signalr => SignalR}/AppHub.cs (100%) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/{Signalr => SignalR}/AppHubConnectionHandler.cs (100%) create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AuthPolicies.cs create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/RootServiceScopeProvider.cs diff --git a/.github/workflows/bit.full.ci.yml b/.github/workflows/bit.full.ci.yml index f6799fe9ad..82d9561daf 100644 --- a/.github/workflows/bit.full.ci.yml +++ b/.github/workflows/bit.full.ci.yml @@ -1,4 +1,4 @@ -name: bit platform full CI +name: bit platform full CI on: workflow_dispatch: @@ -62,7 +62,7 @@ jobs: dotnet new bit-bp --name SimpleTest --database Sqlite --framework net8.0 cd SimpleTest/src/Server/SimpleTest.Server.Api/ dotnet tool restore - dotnet ef migrations add InitialMigration + dotnet ef migrations add InitialMigration --verbose dotnet ef database update cd ../../Tests dotnet build @@ -83,7 +83,7 @@ jobs: dotnet new bit-bp --name TestSqlite --database Sqlite --advancedTests --framework net9.0 cd TestSqlite/src/Server/TestSqlite.Server.Api/ dotnet tool restore - dotnet ef migrations add InitialMigration + dotnet ef migrations add InitialMigration --verbose dotnet ef database update cd ../../Tests dotnet build @@ -105,7 +105,7 @@ jobs: dotnet new bit-bp --name TestSqlServer --database SqlServer --advancedTests --framework net8.0 cd TestSqlServer/src/Server/TestSqlServer.Server.Api/ dotnet tool restore - dotnet ef migrations add InitialMigration + dotnet ef migrations add InitialMigration --verbose dotnet ef database update cd ../../Tests dotnet test --logger GitHubActions --filter "${{ env.BLAZOR_SERVER_TEST_FILTER }}" @@ -125,7 +125,7 @@ jobs: dotnet new bit-bp --name MultilingualDisabled --database Sqlite --advancedTests --framework net8.0 cd MultilingualDisabled/src/Server/MultilingualDisabled.Server.Api/ dotnet tool restore - dotnet ef migrations add InitialMigration + dotnet ef migrations add InitialMigration --verbose dotnet ef database update cd ../../Tests dotnet test -p:MultilingualEnabled=false --logger GitHubActions --filter "${{ env.MULTILINGUAL_DISABLED_TEST_FILTER }}" -- MSTest.Parallelize.Workers=1 diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.azure-devops/workflows/cd.yml b/src/Templates/Boilerplate/Bit.Boilerplate/.azure-devops/workflows/cd.yml index 3aaa1af716..0a8be9b251 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.azure-devops/workflows/cd.yml +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.azure-devops/workflows/cd.yml @@ -68,7 +68,7 @@ jobs: displayName: 'Build migrations bundle' inputs: targetType: 'inline' - script: 'cd src/Server/Boilerplate.Server.Api/ && dotnet tool restore && dotnet ef migrations bundle --self-contained -r linux-x64 --project Boilerplate.Server.Api.csproj' + script: 'cd src/Server/Boilerplate.Server.Api/ && dotnet tool restore && dotnet ef migrations bundle --self-contained -r linux-x64 --project Boilerplate.Server.Api.csproj --verbose' - task: PublishPipelineArtifact@1 displayName: Upload ef migrations bundle diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.github/workflows/cd.yml b/src/Templates/Boilerplate/Bit.Boilerplate/.github/workflows/cd.yml index 9b16fa6966..01fe96a2a7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.github/workflows/cd.yml +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.github/workflows/cd.yml @@ -56,7 +56,7 @@ jobs: - name: Build migrations bundle run: | - cd src/Server/Boilerplate.Server.Api/ && dotnet tool restore && dotnet ef migrations bundle --self-contained -r linux-x64 --project Boilerplate.Server.Api.csproj + cd src/Server/Boilerplate.Server.Api/ && dotnet tool restore && dotnet ef migrations bundle --self-contained -r linux-x64 --project Boilerplate.Server.Api.csproj --verbose - name: Upload ef migrations bundle uses: actions/upload-artifact@v4 diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json index 0f285becb5..7749d0523a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json @@ -418,6 +418,7 @@ "src/Server/Boilerplate.Server.Api/Controllers/Todo/**", "src/Server/Boilerplate.Server.Api/Mappers/TodoMapper.cs", "src/Server/Boilerplate.Server.Api/Models/Todo/**", + "src/Server/Boilerplate.Server.Api/Data/Configurations/Todo/**", "src/Shared/Controllers/Todo/**", "src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/**" ] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs index a37859f207..da1c477132 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs @@ -38,12 +38,17 @@ public partial class AppComponentBase : ComponentBase, IAsyncDisposable [AutoInject] protected IExceptionHandler ExceptionHandler = default!; - [AutoInject] protected AuthenticationManager AuthenticationManager = default!; + [AutoInject] protected AuthManager AuthManager = default!; [AutoInject] protected SnackBarService SnackBarService = default!; [AutoInject] protected ITelemetryContext TelemetryContext = default!; + /// + /// + /// + [AutoInject] protected IAuthorizationService AuthorizationService = default!; + /// /// /// 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 ff2a830d5c..5aaa93ca77 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 @@ -27,11 +27,10 @@ public partial class ClientAppCoordinator : AppComponentBase [AutoInject] private Navigator navigator = default!; [AutoInject] private IJSRuntime jsRuntime = default!; [AutoInject] private IStorageService storageService = default!; - [AutoInject] private AuthenticationManager authManager = default!; + [AutoInject] private ILogger authLogger = default!; [AutoInject] private ILogger navigatorLogger = default!; [AutoInject] private ILogger logger = default!; [AutoInject] private CultureInfoManager cultureInfoManager = default!; - [AutoInject] private ILogger authLogger = default!; [AutoInject] private IBitDeviceCoordinator bitDeviceCoordinator = default!; //#if (notification == true) [AutoInject] private IPushNotificationService pushNotificationService = default!; @@ -68,11 +67,11 @@ protected override async Task OnInitAsync() //#endif NavigationManager.LocationChanged += NavigationManager_LocationChanged; - AuthenticationManager.AuthenticationStateChanged += AuthenticationStateChanged; + AuthManager.AuthenticationStateChanged += AuthenticationStateChanged; //#if (signalR == true) SubscribeToSignalREventsMessages(); //#endif - await PropagateUserId(firstRun: true, AuthenticationManager.GetAuthenticationStateAsync()); + await PropagateUserId(firstRun: true, AuthManager.GetAuthenticationStateAsync()); } await base.OnInitAsync(); @@ -150,11 +149,12 @@ private void SubscribeToSignalREventsMessages() { signalROnDisposables.Add(hubConnection.On(SignalREvents.SHOW_MESSAGE, async (message) => { + logger.LogInformation("SignalR Message {Message} received from server to show.", message); if (await notification.IsNotificationAvailable()) { // Show local notification // Note that this code has nothing to do with push notification. - await notification.Show("Boilerplate", new() { Body = message }); + await notification.Show("Boilerplate SignalR", new() { Body = message }); } else { @@ -172,7 +172,7 @@ private void SubscribeToSignalREventsMessages() signalROnDisposables.Add(hubConnection.On(SignalREvents.PUBLISH_MESSAGE, async (message) => { - logger.LogInformation("Message {Message} received from server.", message); + logger.LogInformation("SignalR Message {Message} received from server to publish.", message); PubSubService.Publish(message); })); @@ -215,7 +215,7 @@ private async Task HubConnectionStateChange(Exception? exception) if (exception is HubException && exception.Message.EndsWith(nameof(AppStrings.UnauthorizedException))) { - await AuthenticationManager.RefreshToken(requestedBy: nameof(HubException)); + await AuthManager.RefreshToken(requestedBy: nameof(HubException)); } } } @@ -243,7 +243,7 @@ await storageService.GetItem("Culture") ?? // 2- User settings protected override async ValueTask DisposeAsync(bool disposing) { NavigationManager.LocationChanged -= NavigationManager_LocationChanged; - AuthenticationManager.AuthenticationStateChanged -= AuthenticationStateChanged; + AuthManager.AuthenticationStateChanged -= AuthenticationStateChanged; //#if (signalR == true) hubConnection.Closed -= HubConnectionStateChange; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor index 1584c38964..a090e4828a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor @@ -1,11 +1,11 @@ @inherits AppComponentBase
    - + @Body - +
    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 03bfdc88d3..c845c5ec78 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 @@ -24,9 +24,9 @@ public partial class RootLayout : IDisposable [AutoInject] private Keyboard keyboard = default!; + [AutoInject] private AuthManager authManager = default!; [AutoInject] private ThemeService themeService = default!; [AutoInject] private PubSubService pubSubService = default!; - [AutoInject] private AuthenticationManager authManager = default!; [AutoInject] private IExceptionHandler exceptionHandler = default!; [AutoInject] private ITelemetryContext telemetryContext = default!; [AutoInject] private NavigationManager navigationManager = default!; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/SignOutConfirmDialog.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/SignOutConfirmDialog.razor.cs index 30401fd810..ee16513dac 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/SignOutConfirmDialog.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/SignOutConfirmDialog.razor.cs @@ -15,7 +15,7 @@ private async Task CloseModal() private async Task SignOut() { - await AuthenticationManager.SignOut(CurrentCancellationToken); + await AuthManager.SignOut(CurrentCancellationToken); await CloseModal(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/AddOrEditCategoryPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/AddOrEditCategoryPage.razor index b6af81d804..505cfd180b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/AddOrEditCategoryPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/AddOrEditCategoryPage.razor @@ -1,6 +1,6 @@ @attribute [Route(Urls.AddOrEditCategoryPage + "/{Id:guid?}")] @attribute [Route("{culture?}" + Urls.AddOrEditCategoryPage + "/{Id:guid?}")] -@attribute [Authorize] +@attribute [Authorize(Policy = AuthPolicies.PRIVILEGED_ACCESS)] @inherits AppPageBase 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 dbc86a99ec..f836f0a79a 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 @@ -1,6 +1,6 @@ @attribute [Route(Urls.CategoriesPage)] @attribute [Route("{culture?}" + Urls.CategoriesPage)] -@attribute [Authorize] +@attribute [Authorize(Policy = AuthPolicies.PRIVILEGED_ACCESS)] @inherits AppPageBase @Localizer[nameof(AppStrings.CategoriesPageTitle)] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Dashboard/DashboardPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Dashboard/DashboardPage.razor index 02c23e89ce..e984b94b58 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Dashboard/DashboardPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Dashboard/DashboardPage.razor @@ -1,6 +1,6 @@ @attribute [Route(Urls.DashboardPage)] @attribute [Route("{culture?}" + Urls.DashboardPage)] -@attribute [Authorize] +@attribute [Authorize(Policy = AuthPolicies.PRIVILEGED_ACCESS)] @inherits AppPageBase @Localizer[nameof(AppStrings.DashboardPageTitle)] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor index 651c0cf8d1..e143b66ec8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor @@ -1,6 +1,6 @@ @attribute [Route(Urls.ProductsPage)] @attribute [Route("{culture?}" + Urls.ProductsPage)] -@attribute [Authorize] +@attribute [Authorize(Policy = AuthPolicies.PRIVILEGED_ACCESS)] @inherits AppPageBase @Localizer[nameof(AppStrings.ProductsPageTitle)] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/DeleteAccountSection.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/DeleteAccountSection.razor.cs index 5e31176cd5..090a3789e2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/DeleteAccountSection.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/DeleteAccountSection.razor.cs @@ -6,14 +6,15 @@ public partial class DeleteAccountSection { private bool isDialogOpen; - [AutoInject] IUserController userController = default!; - private async Task DeleteAccount() { - await userController.Delete(CurrentCancellationToken); + if (await AuthManager.TryEnterElevatedAccessMode(CurrentCancellationToken)) + { + await userController.Delete(CurrentCancellationToken); - await AuthenticationManager.SignOut(CurrentCancellationToken); + await AuthManager.SignOut(CurrentCancellationToken); + } } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor index 8f5abe8273..8df1629e59 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor @@ -1,5 +1,7 @@ @inherits AppComponentBase + +
    @if (currentSession is not null) @@ -37,7 +39,7 @@ + IconName="@(isWaiting ? BitIconName.Process : BitIconName.Delete)" /> diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor.cs index 8e37e9e191..f427e1c1ee 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor.cs @@ -1,11 +1,13 @@ -using Boilerplate.Shared.Controllers.Identity; -using Boilerplate.Shared.Dtos.Identity; +using Boilerplate.Shared.Dtos.Identity; +using Boilerplate.Shared.Controllers.Identity; +using Microsoft.AspNetCore.Components.Routing; namespace Boilerplate.Client.Core.Components.Pages.Authorized.Settings; public partial class SessionsSection { private bool isWaiting; + private bool hasRevokedAnySession = false; private Guid? currentSessionId; private UserSessionDto? currentSession; private UserSessionDto[] otherSessions = []; @@ -26,15 +28,9 @@ private async Task LoadSessions() List userSessions = []; currentSessionId = await PrerenderStateService.GetValue(async () => (await AuthenticationStateTask).User.GetSessionId()); - try - { - userSessions = await userController.GetUserSessions(CurrentCancellationToken); - } - finally - { - otherSessions = userSessions.Where(s => s.Id != currentSessionId).ToArray(); - currentSession = userSessions.Single(s => s.Id == currentSessionId); - } + userSessions = await userController.GetUserSessions(CurrentCancellationToken); + otherSessions = userSessions.Where(s => s.Id != currentSessionId).ToArray(); + currentSession = userSessions.Single(s => s.Id == currentSessionId); } private async Task RevokeSession(UserSessionDto session) @@ -45,10 +41,13 @@ private async Task RevokeSession(UserSessionDto session) try { - await userController.RevokeSession(session.Id, CurrentCancellationToken); - - SnackBarService.Success(Localizer[nameof(AppStrings.RemoveSessionSuccessMessage)]); - await LoadSessions(); + if (await AuthManager.TryEnterElevatedAccessMode(CurrentCancellationToken)) + { + await userController.RevokeSession(session.Id, CurrentCancellationToken); + hasRevokedAnySession = true; + SnackBarService.Success(Localizer[nameof(AppStrings.RemoveSessionSuccessMessage)]); + await LoadSessions(); + } } catch (KnownException e) { @@ -88,4 +87,13 @@ private string GetLastSeenOn(DateTimeOffset renewedOn) : DateTimeOffset.UtcNow - renewedOn < TimeSpan.FromMinutes(15) ? Localizer[nameof(AppStrings.Recently)] : renewedOn.ToLocalTime().ToString("g"); } + + private async Task OnBeforeInternalNavigation(LocationChangingContext context) + { + if (hasRevokedAnySession && await AuthorizationService.AuthorizeAsync((await AuthenticationStateTask).User, AuthPolicies.PRIVILEGED_ACCESS) is { Succeeded: false }) + { + // Refreshing the token to check if the user session can now be privileged. + await AuthManager.RefreshToken("CheckPrivilege"); + } + } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor index da6c6d9b4e..15927794b6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor @@ -1,6 +1,6 @@ @attribute [Route(Urls.TodoPage)] @attribute [Route("{culture?}" + Urls.TodoPage)] -@attribute [Authorize] +@attribute [Authorize(Policy = AuthPolicies.PRIVILEGED_ACCESS)] @inherits AppPageBase @Localizer[nameof(AppStrings.TodoPageTitle)] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ConfirmPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ConfirmPage.razor.cs index 0a30948be9..fb58e0ea25 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ConfirmPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ConfirmPage.razor.cs @@ -13,6 +13,7 @@ public partial class ConfirmPage private readonly ConfirmEmailRequestDto emailModel = new(); private readonly ConfirmPhoneRequestDto phoneModel = new(); + [AutoInject] private ITelemetryContext telemetryContext = default!; [AutoInject] private IIdentityController identityController = default!; @@ -75,9 +76,14 @@ private async Task ConfirmEmail() await WrapRequest(async () => { - var signInResponse = await identityController.ConfirmEmail(new() { Email = emailModel.Email, Token = emailModel.Token }, CurrentCancellationToken); + var signInResponse = await identityController.ConfirmEmail(new() + { + Email = emailModel.Email, + Token = emailModel.Token, + DeviceInfo = telemetryContext.OS + }, CurrentCancellationToken); - await AuthenticationManager.StoreTokens(signInResponse, true); + await AuthManager.StoreTokens(signInResponse, true); NavigationManager.NavigateTo(Urls.HomePage, replace: true); @@ -101,9 +107,14 @@ private async Task ConfirmPhone() await WrapRequest(async () => { - var signInResponse = await identityController.ConfirmPhone(new() { PhoneNumber = phoneModel.PhoneNumber, Token = phoneModel.Token }, CurrentCancellationToken); + var signInResponse = await identityController.ConfirmPhone(new() + { + Token = phoneModel.Token, + DeviceInfo = telemetryContext.OS, + PhoneNumber = phoneModel.PhoneNumber + }, CurrentCancellationToken); - await AuthenticationManager.StoreTokens(signInResponse, true); + await AuthManager.StoreTokens(signInResponse, true); NavigationManager.NavigateTo(Urls.HomePage, replace: true); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor index 18ecebf61c..63e997e6f3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor @@ -13,7 +13,7 @@ @if (requiresTwoFactor is false) { - @if (isOtpRequested is false) + @if (isOtpSent is false) { } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs index 90728d88c6..d0cbc4b76a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs @@ -32,7 +32,7 @@ public partial class SignInPage : IDisposable private bool isWaiting; - private bool isOtpRequested; + private bool isOtpSent; private bool requiresTwoFactor; private SignInPanelTab currentSignInPanelTab; private readonly SignInRequestDto model = new(); @@ -76,7 +76,7 @@ protected override async Task OnInitAsync() if (source == OtpPayload) { - isOtpRequested = false; + isOtpSent = false; model.Otp = null; } @@ -112,7 +112,7 @@ private async Task SocialSignIn(string provider) private async Task DoSignIn() { if (isWaiting) return; - if (isOtpRequested && string.IsNullOrWhiteSpace(model.Otp)) return; + if (isOtpSent && string.IsNullOrWhiteSpace(model.Otp)) return; isWaiting = true; @@ -122,7 +122,7 @@ private async Task DoSignIn() CleanModel(); - requiresTwoFactor = await AuthenticationManager.SignIn(model, CurrentCancellationToken); + requiresTwoFactor = await AuthManager.SignIn(model, CurrentCancellationToken); if (requiresTwoFactor is false) { @@ -165,14 +165,14 @@ private async Task SendOtp(bool resend) var request = new IdentityRequestDto { UserName = model.UserName, Email = model.Email, PhoneNumber = model.PhoneNumber }; + await identityController.SendOtp(request, ReturnUrlQueryString, CurrentCancellationToken); + if (resend is false) { - isOtpRequested = true; + isOtpSent = true; PubSubService.Publish(ClientPubSubMessages.UPDATE_IDENTITY_HEADER_BACK_LINK, OtpPayload); } - - await identityController.SendOtp(request, ReturnUrlQueryString, CurrentCancellationToken); } catch (KnownException e) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor index 658228b47a..4c79eb88fe 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor @@ -20,9 +20,22 @@ else @Localizer[nameof(AppStrings.ForbiddenException)] - @Localizer[nameof(AppStrings.YouAreSignInAs)] @user.GetDisplayName() + @Localizer[nameof(AppStrings.YouAreSignedInAs)] @user.GetDisplayName() + + @if (lacksValidPrivilege) + { + + @Localizer[nameof(AppStrings.TryRemovingOtherSessions)] + + } + + + @Localizer[nameof(AppStrings.SignInAsDifferentUser)] + - @Localizer[nameof(AppStrings.SignInAsDifferentUser)]
    diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs index 68e7a716b9..d3d13badac 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs @@ -3,6 +3,7 @@ public partial class NotAuthorizedPage { private bool isRefreshingToken; + private bool lacksValidPrivilege; private ClaimsPrincipal user = default!; [SupplyParameterFromQuery(Name = "return-url"), Parameter] public string? ReturnUrl { get; set; } @@ -28,7 +29,7 @@ protected override async Task OnAfterFirstRenderAsync() StateHasChanged(); try { - var accessToken = await AuthenticationManager.RefreshToken(requestedBy: nameof(NotAuthorizedPage)); + var accessToken = await AuthManager.RefreshToken(requestedBy: nameof(NotAuthorizedPage)); if (string.IsNullOrEmpty(accessToken) is false && ReturnUrl is not null) { var @char = ReturnUrl.Contains('?') ? '&' : '?'; // The RedirectUrl may already include a query string. @@ -43,17 +44,22 @@ protected override async Task OnAfterFirstRenderAsync() } } - if ((await AuthenticationStateTask).User.IsAuthenticated() is false) + var user = (await AuthenticationStateTask).User; + + if (user.IsAuthenticated() is false) { await SignOut(); } + lacksValidPrivilege = await AuthorizationService.AuthorizeAsync(user, AuthPolicies.PRIVILEGED_ACCESS) is { Succeeded: false }; + StateHasChanged(); + await base.OnAfterFirstRenderAsync(); } private async Task SignOut() { - await AuthenticationManager.SignOut(CurrentCancellationToken); + await AuthManager.SignOut(CurrentCancellationToken); var returnUrl = ReturnUrl ?? NavigationManager.ToBaseRelativePath(NavigationManager.Uri); NavigationManager.NavigateTo(Urls.SignInPage + (string.IsNullOrEmpty(returnUrl) ? string.Empty : $"?return-url={returnUrl}")); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/README.md b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/README.md index 660082f762..cafd54e35d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/README.md +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Data/README.md @@ -16,11 +16,11 @@ URL.createObjectURL(blob); Set `Server.Web` as the Startup Project in solution explorer and set `Client.Core` it as the Default Project in Package Manager Console and run the following commands: ```powershell -Add-Migration InitialMigration -OutputDir Data\Migrations -Context OfflineDbContext +Add-Migration InitialMigration -OutputDir Data\Migrations -Context OfflineDbContext -Verbobse ``` Or open a terminal in your Server.Web project directory and run followings: ```bash -dotnet ef migrations add InitialMigration --context OfflineDbContext --output-dir Data/Migrations --project ../Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj +dotnet ef migrations add InitialMigration --context OfflineDbContext --output-dir Data/Migrations --project ../Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj --verbose ``` *Note*: If you encounter any problem in running these commands, first make sure that the solution builds successfully. 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 2d2a3d0f98..2b79bab437 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 @@ -48,11 +48,11 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle services.AddSessioned(); services.AddSessioned(sp => { - var authenticationStateProvider = ActivatorUtilities.CreateInstance(sp); + var authenticationStateProvider = ActivatorUtilities.CreateInstance(sp); authenticationStateProvider.OnInit(); return authenticationStateProvider; }); - services.AddSessioned(sp => (AuthenticationManager)sp.GetRequiredService()); + services.AddSessioned(sp => (AuthManager)sp.GetRequiredService()); services.AddSingleton(sp => { @@ -141,9 +141,10 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle services.AddSingleton(); services.AddSessioned(sp => { - var absoluteServerAddressProvider = sp.GetRequiredService(); + var authManager = sp.GetRequiredService(); var authTokenProvider = sp.GetRequiredService(); - var authenticationManager = sp.GetRequiredService(); + var absoluteServerAddressProvider = sp.GetRequiredService(); + var hubConnection = new HubConnectionBuilder() .WithAutomaticReconnect(sp.GetRequiredService()) .WithUrl(new Uri(absoluteServerAddressProvider.GetAddress(), "app-hub"), options => @@ -159,7 +160,7 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle if (string.IsNullOrEmpty(accessToken) is false && authTokenProvider.ParseAccessToken(accessToken, validateExpiry: true).IsAuthenticated() is false) { - return await authenticationManager.RefreshToken(requestedBy: nameof(HubConnectionBuilder)); + return await authManager.RefreshToken(requestedBy: nameof(HubConnectionBuilder)); } return accessToken; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthManager.cs similarity index 74% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthManager.cs index 775275c59c..d7793e8e10 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthManager.cs @@ -3,25 +3,31 @@ namespace Boilerplate.Client.Core.Services; -public partial class AuthenticationManager : AuthenticationStateProvider, IAsyncDisposable +public partial class AuthManager : AuthenticationStateProvider, IAsyncDisposable { private Action? unsubscribe; [AutoInject] private Cookie cookie = default!; [AutoInject] private IJSRuntime jsRuntime = default!; [AutoInject] private PubSubService pubSubService = default!; + [AutoInject] private PromptService promptService = default!; [AutoInject] private IStorageService storageService = default!; [AutoInject] private IUserController userController = default!; + [AutoInject] private ILogger authLogger = default!; [AutoInject] private IAuthTokenProvider tokenProvider = default!; - [AutoInject] private IPrerenderStateService prerenderStateService; [AutoInject] private IExceptionHandler exceptionHandler = default!; [AutoInject] private IStringLocalizer localizer = default!; [AutoInject] private IIdentityController identityController = default!; - [AutoInject] private ILogger authLogger = default!; + [AutoInject] private IAuthorizationService authorizationService = default!; + [AutoInject] private IPrerenderStateService prerenderStateService = default!; public void OnInit() { + // Example for method call after object instantiation with dependency injection. + + //#if (signalR == true) unsubscribe = pubSubService.Subscribe(SharedPubSubMessages.SESSION_REVOKED, _ => SignOut(default)); + //#endif } /// @@ -71,7 +77,7 @@ public async Task SignOut(CancellationToken cancellationToken) { await userController.SignOut(cancellationToken); } - catch (Exception exp) when (exp is ServerConnectionException or UnauthorizedException) + catch (Exception exp) when (exp is ServerConnectionException or UnauthorizedException or ResourceNotFoundException) { // The user might sign out while the app is offline, making token refresh attempts fail. // These exceptions are intentionally ignored in this case. @@ -85,17 +91,17 @@ public async Task SignOut(CancellationToken cancellationToken) /// /// To prevent multiple simultaneous refresh token requests. /// - private TaskCompletionSource? refreshTokenTsc = null; + private TaskCompletionSource? accessTokenTsc = null; - public Task RefreshToken(string requestedBy) + public Task RefreshToken(string requestedBy, string? elevatedAccessToken = null) { - if (refreshTokenTsc is null) + if (accessTokenTsc is null) { - refreshTokenTsc = new(); + accessTokenTsc = new(); _ = RefreshTokenImplementation(); } - return refreshTokenTsc.Task; + return accessTokenTsc.Task; async Task RefreshTokenImplementation() { @@ -106,9 +112,9 @@ async Task RefreshTokenImplementation() if (string.IsNullOrEmpty(refreshToken)) throw new UnauthorizedException(localizer[nameof(AppStrings.YouNeedToSignIn)]); - var refreshTokenResponse = await identityController.Refresh(new() { RefreshToken = refreshToken }, default); + var refreshTokenResponse = await identityController.Refresh(new() { RefreshToken = refreshToken, ElevatedAccessToken = elevatedAccessToken }, default); await StoreTokens(refreshTokenResponse); - refreshTokenTsc.SetResult(refreshTokenResponse.AccessToken!); + accessTokenTsc.SetResult(refreshTokenResponse.AccessToken!); } catch (Exception exp) { @@ -123,11 +129,11 @@ async Task RefreshTokenImplementation() await ClearTokens(); } - refreshTokenTsc.SetResult(null); + accessTokenTsc.SetResult(null); } finally { - refreshTokenTsc = null; + accessTokenTsc = null; } } } @@ -156,6 +162,34 @@ public override async Task GetAuthenticationStateAsync() } } + public async Task TryEnterElevatedAccessMode(CancellationToken cancellationToken) + { + var user = tokenProvider.ParseAccessToken(await tokenProvider.GetAccessToken(), validateExpiry: true); + var hasElevatedAccess = await authorizationService.AuthorizeAsync(user, AuthPolicies.ELEVATED_ACCESS) is { Succeeded: true }; + if (hasElevatedAccess) + return true; + + try + { + await userController.SendElevatedAccessToken(cancellationToken); + } + catch (TooManyRequestsExceptions exp) + { + exceptionHandler.Handle(exp); // Let's show prompt anyway. + } + + var token = await promptService.Show(localizer[AppStrings.EnterElevatedAccessToken], title: "Boilerplate"); + if (string.IsNullOrEmpty(token)) + return false; + + if (accessTokenTsc != null) + { + await accessTokenTsc.Task; // Wait for any ongoing token refresh to complete. + } + var accessToken = await RefreshToken(requestedBy: "RequestElevatedAccess", token); + return string.IsNullOrEmpty(accessToken) is false; + } + private async Task ClearTokens() { await storageService.RemoveItem("access_token"); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs index c2ef31f84b..cf4774e3dc 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs @@ -58,7 +58,7 @@ protected override async Task SendAsync(HttpRequestMessage var refreshToken = await storageService.GetItem("refresh_token"); if (string.IsNullOrEmpty(refreshToken)) throw; - var authManager = serviceProvider.GetRequiredService(); + var authManager = serviceProvider.GetRequiredService(); logScopeData["RefreshTokenRequested"] = true; var accessToken = await authManager.RefreshToken(requestedBy: nameof(AuthDelegatingHandler)); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs index 5e977beca0..d93b136348 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs @@ -117,12 +117,8 @@ public static void Main(string[] args) settings.IsZoomControlEnabled = false; settings.AreBrowserAcceleratorKeysEnabled = false; } - bool hasBlazorStarted = false; blazorWebView.WebView.NavigationCompleted += async delegate { - if (hasBlazorStarted) - return; - hasBlazorStarted = true; await blazorWebView.WebView.ExecuteScriptAsync("Blazor.start()"); }; }; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsStorageService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsStorageService.cs index 89f82a275e..55dccac820 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsStorageService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsStorageService.cs @@ -19,7 +19,7 @@ public async ValueTask IsPersistent(string key) public async ValueTask RemoveItem(string key) { - Application.UserAppDataRegistry.DeleteValue(key); + Application.UserAppDataRegistry.DeleteValue(key, throwOnMissingValue: false); tempStorage.Remove(key); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/ElevatedAccessTokenTemplate.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/ElevatedAccessTokenTemplate.razor new file mode 100644 index 0000000000..69ca465bd9 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/ElevatedAccessTokenTemplate.razor @@ -0,0 +1,64 @@ +@using Boilerplate.Server.Api.Models.Emailing + +@code { + [Parameter] public ElevatedAccessTokenTemplateModel Model { get; set; } = default!; + [Parameter] public HttpContext HttpContext { get; set; } = default!; + [Inject] public IStringLocalizer EmailLocalizer { get; set; } = default!; +} + + + + + + + @EmailLocalizer[nameof(EmailStrings.ElevatedAccessTokenEmailSubject), Model.Token] + + +
    + + + + + + + + + + + + + +
    + @EmailLocalizer[nameof(EmailStrings.ElevatedAccessTokenHello), Model.DisplayName!] +
    + @EmailLocalizer[nameof(EmailStrings.ElevatedAccessTokenMessage)] +
    + + + + +
    + @Model.Token +
    +
    + @EmailLocalizer[nameof(EmailStrings.CopyTokenNote)] +
    + + + + + + + +
    + Company Logo +
    + + @EmailLocalizer[nameof(EmailStrings.AppName)] + +
    +
    + + \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs index 2e39e97516..2f8443550a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs @@ -8,7 +8,7 @@ namespace Boilerplate.Server.Api.Controllers.Categories; -[ApiController, Route("api/[controller]/[action]")] +[ApiController, Route("api/[controller]/[action]"), Authorize(Policy = AuthPolicies.PRIVILEGED_ACCESS)] public partial class CategoryController : AppControllerBase, ICategoryController { //#if (signalR == true) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Dashboard/DashboardController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Dashboard/DashboardController.cs index 7895f782bc..6867e82515 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Dashboard/DashboardController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Dashboard/DashboardController.cs @@ -3,7 +3,7 @@ namespace Boilerplate.Server.Api.Controllers.Dashboard; -[ApiController, Route("api/[controller]/[action]")] +[ApiController, Route("api/[controller]/[action]"), Authorize(Policy = AuthPolicies.PRIVILEGED_ACCESS)] public partial class DashboardController : AppControllerBase, IDashboardController { [HttpGet] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.EmailConfirmation.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.EmailConfirmation.cs index 94eb8ed520..bb6150abd9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.EmailConfirmation.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.EmailConfirmation.cs @@ -57,7 +57,7 @@ public async Task ConfirmEmail(ConfirmEmailRequestDto request, CancellationToken var token = await userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, FormattableString.Invariant($"Otp_Email,{user.OtpRequestedOn?.ToUniversalTime()}")); - await SignIn(new() { Email = request.Email, Otp = token }, cancellationToken); + await SignIn(new() { Email = request.Email, Otp = token, DeviceInfo = request.DeviceInfo }, cancellationToken); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.PhoneConfirmation.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.PhoneConfirmation.cs index eb8bdcff8a..a1b368ad16 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.PhoneConfirmation.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.PhoneConfirmation.cs @@ -58,7 +58,7 @@ public async Task ConfirmPhone(ConfirmPhoneRequestDto request, CancellationToken var token = await userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, FormattableString.Invariant($"Otp_Sms,{user.OtpRequestedOn?.ToUniversalTime()}")); - await SignIn(new() { PhoneNumber = request.PhoneNumber, Otp = token }, cancellationToken); + await SignIn(new() { PhoneNumber = request.PhoneNumber, Otp = token, DeviceInfo = request.DeviceInfo }, cancellationToken); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs index ba07d3d96a..064a0d3672 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs @@ -10,6 +10,7 @@ using Boilerplate.Server.Api.Models.Identity; using Boilerplate.Shared.Controllers.Identity; using Boilerplate.Server.Api.Services.Identity; +using System.Threading; namespace Boilerplate.Server.Api.Controllers.Identity; @@ -19,11 +20,11 @@ public partial class IdentityController : AppControllerBase, IIdentityController { [AutoInject] private IUserStore userStore = default!; [AutoInject] private IUserEmailStore userEmailStore = default!; - [AutoInject] private IUserPhoneNumberStore userPhoneNumberStore = default!; [AutoInject] private UserManager userManager = default!; [AutoInject] private SignInManager signInManager = default!; [AutoInject] private ILogger logger = default!; [AutoInject] private IUserConfirmation userConfirmation = default!; + [AutoInject] private IUserPhoneNumberStore userPhoneNumberStore = default!; [AutoInject] private IOptionsMonitor bearerTokenOptions = default!; [AutoInject] private AppUserClaimsPrincipalFactory userClaimsPrincipalFactory = default!; //#if (signalR == true) @@ -95,7 +96,13 @@ public async Task SignIn(SignInRequestDto request, CancellationToken cancellatio var user = await userManager.FindUserAsync(request) ?? throw new UnauthorizedException(Localizer[nameof(AppStrings.InvalidUserCredentials)]); - var userSession = CreateUserSession(user.Id, request.DeviceInfo); + var userSession = await CreateUserSession(user.Id, request.DeviceInfo, cancellationToken); + userClaimsPrincipalFactory.SessionClaims.Add(new(AppClaimTypes.SESSION_ID, userSession.Id.ToString())); + userClaimsPrincipalFactory.SessionClaims.Add(new(AppClaimTypes.ELEVATED_SESSION, "true")); // This only applies to the current, short-lived access token. + if (userSession.Privileged) + { + userClaimsPrincipalFactory.SessionClaims.Add(new(AppClaimTypes.PRIVILEGED_SESSION, "true")); + } bool isOtpSignIn = string.IsNullOrEmpty(request.Otp) is false; @@ -162,7 +169,7 @@ public async Task SignIn(SignInRequestDto request, CancellationToken cancellatio /// /// Creates a user session and adds its ID to the access and refresh tokens, but only if the sign-in is successful /// - private UserSession CreateUserSession(Guid userId, string? deviceInfo) + private async Task CreateUserSession(Guid userId, string? deviceInfo, CancellationToken cancellationToken) { var userSession = new UserSession { @@ -176,13 +183,25 @@ private UserSession CreateUserSession(Guid userId, string? deviceInfo) Address = $"{Request.Headers["cf-ipcountry"]}, {Request.Headers["cf-ipcity"]}", }; - userClaimsPrincipalFactory.SessionClaims.Add(new("session-id", userSession.Id.ToString())); + userSession.Privileged = await IsUserSessionPrivileged(userSession, cancellationToken); return userSession; } + /// + /// + /// + private async Task IsUserSessionPrivileged(UserSession userSession, CancellationToken cancellationToken) + { + var maxConcurrentPrivilegedSessions = AppSettings.Identity.MaxConcurrentPrivilegedSessions; + + return maxConcurrentPrivilegedSessions == -1 || // -1 means no limit + userSession.Privileged is true || // Once session gets privileged, it stays privileged until gets deleted. + await DbContext.UserSessions.CountAsync(us => us.UserId == userSession.UserId && us.Privileged == true, cancellationToken) < maxConcurrentPrivilegedSessions; + } + [HttpPost] - public async Task> Refresh(RefreshRequestDto request) + public async Task> Refresh(RefreshRequestDto request, CancellationToken cancellationToken) { UserSession? userSession = null; User? user = null; @@ -197,17 +216,40 @@ public async Task> Refresh(RefreshRequestDto requ throw new UnauthorizedException(); var userId = refreshTicket!.Principal.GetUserId().ToString() ?? throw new InvalidOperationException("User id could not be found"); - var currentSessionId = Guid.Parse(refreshTicket.Principal.FindFirstValue("session-id") ?? throw new InvalidOperationException("session id could not be found")); + var currentSessionId = Guid.Parse(refreshTicket.Principal.FindFirstValue(AppClaimTypes.SESSION_ID) ?? throw new InvalidOperationException("session id could not be found")); user = await userManager.FindByIdAsync(userId) ?? throw new UnauthorizedException(); // User might have been deleted. - userSession = await DbContext.UserSessions.FirstOrDefaultAsync(us => us.Id == currentSessionId) ?? throw new UnauthorizedException(); // User session might have been deleted. + userSession = await DbContext.UserSessions + .FirstOrDefaultAsync(us => us.Id == currentSessionId) ?? throw new UnauthorizedException(); // User session might have been deleted. if (await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not User _) throw new UnauthorizedException(); userSession.RenewedOn = DateTimeOffset.UtcNow; - userClaimsPrincipalFactory.SessionClaims.Add(new("session-id", currentSessionId.ToString())); + userClaimsPrincipalFactory.SessionClaims.Add(new(AppClaimTypes.SESSION_ID, currentSessionId.ToString())); + + userSession.Privileged = await IsUserSessionPrivileged(userSession, cancellationToken); + if (userSession.Privileged is true) + { + userClaimsPrincipalFactory.SessionClaims.Add(new(AppClaimTypes.PRIVILEGED_SESSION, "true")); + } + + if (string.IsNullOrEmpty(request.ElevatedAccessToken) is false) + { + var tokenIsValid = await userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, FormattableString.Invariant($"ElevatedAccess:{userSession.Id},{user.ElevatedAccessTokenRequestedOn?.ToUniversalTime()}"), request.ElevatedAccessToken); + if (tokenIsValid is false) + { + await userManager.AccessFailedAsync(user); + throw new UnauthorizedException(nameof(AppStrings.InvalidToken)); + } + else + { + user.ElevatedAccessTokenRequestedOn = null; // invalidates token + await ((IUserLockoutStore)userStore).ResetAccessFailedCountAsync(user, cancellationToken); + userClaimsPrincipalFactory.SessionClaims.Add(new(AppClaimTypes.ELEVATED_SESSION, "true")); + } + } var newPrincipal = await signInManager.CreateUserPrincipalAsync(user!); @@ -215,6 +257,10 @@ public async Task> Refresh(RefreshRequestDto requ } catch when (userSession is not null) { + //#if (notification == true) + // In order to have the code that works with databases without cascade delete support, we're loading subscriptions, so ef core will set their UserSessionId to null + await DbContext.Entry(userSession).Reference(us => us.PushNotificationSubscription).LoadAsync(cancellationToken); + //#endif DbContext.UserSessions.Remove(userSession); throw; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs index dad3163439..cf1aa43308 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs @@ -17,17 +17,16 @@ namespace Boilerplate.Server.Api.Controllers.Identity; [ApiController, Route("api/[controller]/[action]")] public partial class UserController : AppControllerBase, IUserController { - [AutoInject] private UserManager userManager = default!; - - [AutoInject] private IUserStore userStore = default!; - - [AutoInject] private IUserEmailStore userEmailStore = default!; - + [AutoInject] private UrlEncoder urlEncoder = default!; [AutoInject] private PhoneService phoneService = default!; - [AutoInject] private EmailService emailService = default!; + [AutoInject] private IUserStore userStore = default!; + [AutoInject] private UserManager userManager = default!; + [AutoInject] private IUserEmailStore userEmailStore = default!; - [AutoInject] private UrlEncoder urlEncoder = default!; + //#if (notification == true) + [AutoInject] private PushNotificationService pushNotificationService = default!; + //#endif //#if (signalR == true) [AutoInject] private IHubContext appHubContext = default!; @@ -71,6 +70,7 @@ public async Task SignOut(CancellationToken cancellationToken) var userSession = await DbContext.UserSessions //#if (notification == true) + // In order to have the code that works with databases without cascade delete support, we're loading subscriptions, so ef core will set their UserSessionId to null .Include(us => us.PushNotificationSubscription) //#endif .FirstOrDefaultAsync(us => us.Id == currentSessionId, cancellationToken) ?? throw new ResourceNotFoundException(); @@ -78,13 +78,10 @@ public async Task SignOut(CancellationToken cancellationToken) DbContext.UserSessions.Remove(userSession); await DbContext.SaveChangesAsync(cancellationToken); - DbContext.UserSessions.Remove(new() { Id = currentSessionId }); - await DbContext.SaveChangesAsync(cancellationToken); - SignOut(); } - [HttpPost("{id}")] + [HttpPost("{id}"), Authorize(Policy = AuthPolicies.ELEVATED_ACCESS)] public async Task RevokeSession(Guid id, CancellationToken cancellationToken) { var userId = User.GetUserId(); @@ -96,6 +93,7 @@ public async Task RevokeSession(Guid id, CancellationToken cancellationToken) var userSession = await DbContext.UserSessions //#if (notification == true) + // In order to have the code that works with databases without cascade delete support, we're loading subscriptions, so ef core will set their UserSessionId to null .Include(us => us.PushNotificationSubscription) //#endif .FirstOrDefaultAsync(us => us.Id == id, cancellationToken) ?? throw new ResourceNotFoundException(); @@ -250,9 +248,16 @@ public async Task ChangePhoneNumber(ChangePhoneNumberRequestDto request, Cancell if (result.Succeeded is false) throw new ResourceValidationException(result.Errors.Select(e => new LocalizedString(e.Code, e.Description)).ToArray()); + + await ((IUserLockoutStore)userStore).ResetAccessFailedCountAsync(user, cancellationToken); + user.PhoneNumberTokenRequestedOn = null; // invalidates phone token + var updateResult = await userManager.UpdateAsync(user); + + if (updateResult.Succeeded is false) + throw new ResourceValidationException(updateResult.Errors.Select(e => new LocalizedString(e.Code, e.Description)).ToArray()); } - [HttpDelete] + [HttpDelete, Authorize(Policy = AuthPolicies.ELEVATED_ACCESS)] public async Task Delete(CancellationToken cancellationToken) { var userId = User.GetUserId(); @@ -260,6 +265,20 @@ public async Task Delete(CancellationToken cancellationToken) var user = await userManager.FindByIdAsync(userId.ToString()) ?? throw new ResourceNotFoundException(); + var currentSessionId = User.GetSessionId(); + + foreach (var userSession in await GetUserSessions(cancellationToken)) + { + if (userSession.Id == currentSessionId) + { + await SignOut(cancellationToken); + } + else + { + await RevokeSession(userSession.Id, cancellationToken); + } + } + var result = await userManager.DeleteAsync(user); if (result.Succeeded is false) @@ -346,6 +365,59 @@ await userAuthenticatorKeyStore.SetAuthenticatorKeyAsync(user, }; } + [HttpPost] + public async Task SendElevatedAccessToken(CancellationToken cancellationToken) + { + var user = await userManager.FindByIdAsync(User.GetUserId().ToString()); + + var resendDelay = (DateTimeOffset.Now - user!.ElevatedAccessTokenRequestedOn) - AppSettings.Identity.BearerTokenExpiration; + // Elevated access token claim gets added to access token upon refresh token request call, so their lifetime would be the same + + if (resendDelay < TimeSpan.Zero) + throw new TooManyRequestsExceptions(Localizer[nameof(AppStrings.WaitForElevatedAccessTokenRequestResendDelay) , resendDelay.Value.Humanize(culture: CultureInfo.CurrentUICulture)]); + + user.ElevatedAccessTokenRequestedOn = DateTimeOffset.Now; + var result = await userManager.UpdateAsync(user); + if (result.Succeeded is false) + throw new ResourceValidationException(result.Errors.Select(e => new LocalizedString(e.Code, e.Description)).ToArray()); + + var currentUserSessionId = User.GetSessionId(); + + var token = await userManager.GenerateUserTokenAsync( + user, + TokenOptions.DefaultPhoneProvider, + FormattableString.Invariant($"ElevatedAccess:{currentUserSessionId},{user.ElevatedAccessTokenRequestedOn?.ToUniversalTime()}")); + + List sendMessagesTasks = []; + + var messageText = Localizer[nameof(AppStrings.ElevatedAccessToken), token].ToString(); + + if (await userManager.IsEmailConfirmedAsync(user)) + { + sendMessagesTasks.Add(emailService.SendElevatedAccessToken(user, token, cancellationToken)); + } + + if (await userManager.IsPhoneNumberConfirmedAsync(user)) + { + sendMessagesTasks.Add(phoneService.SendSms(messageText, user.PhoneNumber!, cancellationToken)); + } + + //#if (signalR == true) + // Checkout AppHubConnectionHandler's comments for more info. + var userSessionIdsExceptCurrentUserSessionId = await DbContext.UserSessions + .Where(us => us.UserId == user.Id && us.Id != currentUserSessionId) + .Select(us => us.Id) + .ToArrayAsync(cancellationToken); + sendMessagesTasks.Add(appHubContext.Clients.Clients(userSessionIdsExceptCurrentUserSessionId.Select(us => us.ToString()).ToArray()).SendAsync(SignalREvents.SHOW_MESSAGE, messageText, cancellationToken)); + //#endif + + //#if (notification == true) + sendMessagesTasks.Add(pushNotificationService.RequestPush(message: messageText, userRelatedPush: true, customSubscriptionFilter: us => us.UserSession!.UserId == user.Id && us.UserSessionId != currentUserSessionId, cancellationToken: cancellationToken)); + //#endif + + await Task.WhenAll(sendMessagesTasks); + } + private static string FormatKey(string unformattedKey) { var result = new StringBuilder(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs index cbed116114..807fed1c29 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs @@ -8,7 +8,7 @@ namespace Boilerplate.Server.Api.Controllers.Products; -[ApiController, Route("api/[controller]/[action]")] +[ApiController, Route("api/[controller]/[action]"), Authorize(Policy = AuthPolicies.PRIVILEGED_ACCESS)] public partial class ProductController : AppControllerBase, IProductController { //#if (signalR == true) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Todo/TodoItemController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Todo/TodoItemController.cs index cb6ca4f71c..83f05cd53e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Todo/TodoItemController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Todo/TodoItemController.cs @@ -3,7 +3,7 @@ namespace Boilerplate.Server.Api.Controllers.Todo; -[ApiController, Route("api/[controller]/[action]")] +[ApiController, Route("api/[controller]/[action]"), Authorize(Policy = AuthPolicies.PRIVILEGED_ACCESS)] public partial class TodoItemController : AppControllerBase, ITodoItemController { [HttpGet, EnableQuery] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Todo/TodoConfiguration.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Todo/TodoConfiguration.cs new file mode 100644 index 0000000000..6e6fd0099f --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Configurations/Todo/TodoConfiguration.cs @@ -0,0 +1,14 @@ +using Boilerplate.Server.Api.Models.Todo; + +namespace Boilerplate.Server.Api.Data.Configurations.Todo; + +public class TodoConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasOne(t => t.User) + .WithMany(u => u.TodoItems) + .HasForeignKey(t => t.UserId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241122104915_InitialMigration.Designer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241124194037_InitialMigration.Designer.cs similarity index 99% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241122104915_InitialMigration.Designer.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241124194037_InitialMigration.Designer.cs index f73a21583c..ac8da26932 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241122104915_InitialMigration.Designer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241124194037_InitialMigration.Designer.cs @@ -6,7 +6,7 @@ namespace Boilerplate.Server.Api.Data.Migrations; [DbContext(typeof(AppDbContext))] -[Migration("20241122104915_InitialMigration")] +[Migration("20241124194037_InitialMigration")] partial class InitialMigration { /// @@ -130,6 +130,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasAnnotation("Cosmos:PropertyName", "_etag"); + b.Property("ElevatedAccessTokenRequestedOn") + .HasColumnType("INTEGER"); + b.Property("Email") .HasMaxLength(256) .HasColumnType("TEXT"); @@ -256,6 +259,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("IP") .HasColumnType("TEXT"); + b.Property("Privileged") + .HasColumnType("INTEGER"); + b.Property("RenewedOn") .HasColumnType("INTEGER"); @@ -795,7 +801,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Boilerplate.Server.Api.Models.Todo.TodoItem", b => { b.HasOne("Boilerplate.Server.Api.Models.Identity.User", "User") - .WithMany() + .WithMany("TodoItems") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -862,6 +868,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.User", b => { b.Navigation("Sessions"); + + b.Navigation("TodoItems"); }); modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.UserSession", b => diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241122104915_InitialMigration.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241124194037_InitialMigration.cs similarity index 95% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241122104915_InitialMigration.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241124194037_InitialMigration.cs index e9ba9a2b6c..e5b87ed4ac 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241122104915_InitialMigration.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241124194037_InitialMigration.cs @@ -66,6 +66,7 @@ protected override void Up(MigrationBuilder migrationBuilder) ResetPasswordTokenRequestedOn = table.Column(type: "INTEGER", nullable: true), TwoFactorTokenRequestedOn = table.Column(type: "INTEGER", nullable: true), OtpRequestedOn = table.Column(type: "INTEGER", nullable: true), + ElevatedAccessTokenRequestedOn = table.Column(type: "INTEGER", nullable: true), UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), @@ -224,6 +225,7 @@ protected override void Up(MigrationBuilder migrationBuilder) IP = table.Column(type: "TEXT", nullable: true), DeviceInfo = table.Column(type: "TEXT", nullable: true), Address = table.Column(type: "TEXT", nullable: true), + Privileged = table.Column(type: "INTEGER", nullable: false), StartedOn = table.Column(type: "INTEGER", nullable: false), RenewedOn = table.Column(type: "INTEGER", nullable: true), UserId = table.Column(type: "TEXT", nullable: false) @@ -298,8 +300,8 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.InsertData( table: "Users", - columns: new[] { "Id", "AccessFailedCount", "BirthDate", "Email", "EmailConfirmed", "EmailTokenRequestedOn", "FullName", "Gender", "LockoutEnabled", "LockoutEnd", "NormalizedEmail", "NormalizedUserName", "OtpRequestedOn", "PasswordHash", "PhoneNumber", "PhoneNumberConfirmed", "PhoneNumberTokenRequestedOn", "ProfileImageName", "ResetPasswordTokenRequestedOn", "SecurityStamp", "TwoFactorEnabled", "TwoFactorTokenRequestedOn", "UserName" }, - values: new object[] { new Guid("8ff71671-a1d6-4f97-abb9-d87d7b47d6e7"), 0, 1306790461440000000L, "test@bitplatform.dev", true, 1306790461440000000L, "Boilerplate test account", 0, true, null, "TEST@BITPLATFORM.DEV", "TEST", null, "AQAAAAIAAYagAAAAEP0v3wxkdWtMkHA3Pp5/JfS+42/Qto9G05p2mta6dncSK37hPxEHa3PGE4aqN30Aag==", "+31684207362", true, null, null, null, "959ff4a9-4b07-4cc1-8141-c5fc033daf83", false, null, "test" }); + columns: new[] { "Id", "AccessFailedCount", "BirthDate", "ElevatedAccessTokenRequestedOn", "Email", "EmailConfirmed", "EmailTokenRequestedOn", "FullName", "Gender", "LockoutEnabled", "LockoutEnd", "NormalizedEmail", "NormalizedUserName", "OtpRequestedOn", "PasswordHash", "PhoneNumber", "PhoneNumberConfirmed", "PhoneNumberTokenRequestedOn", "ProfileImageName", "ResetPasswordTokenRequestedOn", "SecurityStamp", "TwoFactorEnabled", "TwoFactorTokenRequestedOn", "UserName" }, + values: new object[] { new Guid("8ff71671-a1d6-4f97-abb9-d87d7b47d6e7"), 0, 1306790461440000000L, null, "test@bitplatform.dev", true, 1306790461440000000L, "Boilerplate test account", 0, true, null, "TEST@BITPLATFORM.DEV", "TEST", null, "AQAAAAIAAYagAAAAEP0v3wxkdWtMkHA3Pp5/JfS+42/Qto9G05p2mta6dncSK37hPxEHa3PGE4aqN30Aag==", "+31684207362", true, null, null, null, "959ff4a9-4b07-4cc1-8141-c5fc033daf83", false, null, "test" }); migrationBuilder.InsertData( table: "Products", diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs index d3dcd8fa49..193e49789a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs @@ -128,6 +128,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasAnnotation("Cosmos:PropertyName", "_etag"); + b.Property("ElevatedAccessTokenRequestedOn") + .HasColumnType("INTEGER"); + b.Property("Email") .HasMaxLength(256) .HasColumnType("TEXT"); @@ -254,6 +257,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IP") .HasColumnType("TEXT"); + b.Property("Privileged") + .HasColumnType("INTEGER"); + b.Property("RenewedOn") .HasColumnType("INTEGER"); @@ -793,7 +799,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Boilerplate.Server.Api.Models.Todo.TodoItem", b => { b.HasOne("Boilerplate.Server.Api.Models.Identity.User", "User") - .WithMany() + .WithMany("TodoItems") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); @@ -860,6 +866,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.User", b => { b.Navigation("Sessions"); + + b.Navigation("TodoItems"); }); modelBuilder.Entity("Boilerplate.Server.Api.Models.Identity.UserSession", b => diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Emailing/ElevatedAccessTokenTemplateModel.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Emailing/ElevatedAccessTokenTemplateModel.cs new file mode 100644 index 0000000000..d1e194c616 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Emailing/ElevatedAccessTokenTemplateModel.cs @@ -0,0 +1,8 @@ +namespace Boilerplate.Server.Api.Models.Emailing; + +public partial class ElevatedAccessTokenTemplateModel +{ + public required string DisplayName { get; set; } + + public required string Token { get; set; } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/User.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/User.cs index 76afafe330..3e3036fc62 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/User.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/User.cs @@ -1,4 +1,9 @@ -namespace Boilerplate.Server.Api.Models.Identity; +//+:cnd:noEmit +//#if (sample == "Todo") +using Boilerplate.Server.Api.Models.Todo; +//#endif + +namespace Boilerplate.Server.Api.Models.Identity; public partial class User : IdentityUser { @@ -29,5 +34,14 @@ public partial class User : IdentityUser public DateTimeOffset? OtpRequestedOn { get; set; } + /// + /// + /// + public DateTimeOffset? ElevatedAccessTokenRequestedOn { get; set; } + public List Sessions { get; set; } = []; + + //#if (sample == "Todo") + public List TodoItems { get; set; } = []; + //#endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/UserSession.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/UserSession.cs index a383178cab..d8dddb11d0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/UserSession.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/UserSession.cs @@ -19,6 +19,11 @@ public partial class UserSession public string? Address { get; set; } + /// + /// + /// + public bool Privileged { get; set; } + public DateTimeOffset StartedOn { get; set; } public DateTimeOffset? RenewedOn { get; set; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Resources/EmailStrings.fa.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Resources/EmailStrings.fa.resx index 63e2a045a3..0a824501ed 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Resources/EmailStrings.fa.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Resources/EmailStrings.fa.resx @@ -183,9 +183,18 @@ توکن 2FA جدید شما برای ورود: + + + Boilerplate {0} - کد دسترسی ویژه + + + سلام {0} عزیز + + + کد دسترسی ویژه شما: - این کد را کپی و در صفحه ورود استفاده کنید. + این کد را کپی و در برنامه استفاده کنید. برنامه Boilerplate diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Resources/EmailStrings.nl.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Resources/EmailStrings.nl.resx index b494678387..2a5f5524a8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Resources/EmailStrings.nl.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Resources/EmailStrings.nl.resx @@ -183,9 +183,18 @@ Hier is uw nieuwe 2FA-token om in te loggen: + + + Boilerplate {0} - Elevated access token + + + Hallo lieve {0} + + + Hier is uw nieuwe bevoorrechte toegangstoken: - Kopieer deze code en gebruik deze in de inlogpagina. + Kopieer deze code en gebruik deze in de app. Boilerplate-app diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Resources/EmailStrings.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Resources/EmailStrings.resx index ac4fa6e6e3..90620547aa 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Resources/EmailStrings.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Resources/EmailStrings.resx @@ -183,9 +183,18 @@ Here is your new 2FA token to login: + + + Boilerplate {0} - Elevated access token + + + Hello dear {0} + + + Here is your new elevated access token: - Copy this code and use it in the login page. + Copy this code and use it in the app. Boilerplate app diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs index a2c000b8b7..7d377016ac 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs @@ -121,6 +121,11 @@ public partial class AppIdentityOptions : IdentityOptions /// To sign in with either Otp or magic link. ///
    public TimeSpan OtpTokenLifetime { get; set; } + + /// + /// + /// + public int MaxConcurrentPrivilegedSessions { get; set; } } public partial class EmailOptions diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/EmailService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/EmailService.cs index 8538b0024d..f8512610fe 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/EmailService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/EmailService.cs @@ -99,6 +99,24 @@ public async Task SendEmailToken(User user, string toEmailAddress, string token, await SendEmail(body, toEmailAddress!, user.DisplayName!, subject, cancellationToken); } + public async Task SendElevatedAccessToken(User user, string token, CancellationToken cancellationToken) + { + var subject = emailLocalizer[EmailStrings.ElevatedAccessTokenEmailSubject, token]; + + if (hostEnvironment.IsDevelopment()) + { + LogSendEmail(logger, subject, user.Email!, "ElevatedAccess"); + } + + var body = await BuildBody(new Dictionary() + { + [nameof(ElevatedAccessTokenTemplate.Model)] = new ElevatedAccessTokenTemplateModel { DisplayName = user.DisplayName!, Token = token }, + [nameof(ElevatedAccessTokenTemplate.HttpContext)] = httpContextAccessor.HttpContext + }); + + await SendEmail(body, user.Email!, user.DisplayName!, subject, cancellationToken); + } + private async Task BuildBody(Dictionary parameters) where TTemplate : IComponent { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs index 42f537dfde..c910970b62 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs @@ -2,6 +2,7 @@ using AdsPush.Vapid; using AdsPush.Abstraction; using System.Linq.Expressions; +using System.Collections.Concurrent; using Boilerplate.Shared.Dtos.PushNotification; using Boilerplate.Server.Api.Models.PushNotification; @@ -10,10 +11,9 @@ namespace Boilerplate.Server.Api.Services; public partial class PushNotificationService { [AutoInject] private AppDbContext dbContext = default!; - [AutoInject] private IAdsPushSender adsPushSender = default!; - [AutoInject] private ILogger logger = default!; - [AutoInject] private IHttpContextAccessor httpContextAccessor = default!; [AutoInject] private ServerApiSettings serverApiSettings = default!; + [AutoInject] private IHttpContextAccessor httpContextAccessor = default!; + [AutoInject] private RootServiceScopeProvider rootServiceScopeProvider = default!; public async Task Subscribe([Required] PushNotificationSubscriptionDto dto, CancellationToken cancellationToken) { @@ -76,37 +76,52 @@ public async Task RequestPush(string? title = null, string? message = null, stri var subscriptions = await query.ToListAsync(cancellationToken); - var payload = new AdsPushBasicSendPayload() + _ = Task.Run(async () => // Let's not wait for the push notification to be sent { - Title = AdsPushText.CreateUsingString(title ?? "Boilerplate"), - Detail = AdsPushText.CreateUsingString(message ?? string.Empty), - Parameters = new Dictionary() + await using var scope = rootServiceScopeProvider.Invoke(); + var adsPushSender = scope.ServiceProvider.GetRequiredService(); + var logger = scope.ServiceProvider.GetRequiredService>(); + + var payload = new AdsPushBasicSendPayload() { + Title = AdsPushText.CreateUsingString(title ?? "Boilerplate push"), + Detail = AdsPushText.CreateUsingString(message ?? string.Empty), + Parameters = new Dictionary() { - "action", action ?? string.Empty + { + "action", action ?? string.Empty + } } - } - }; + }; - List tasks = []; + ConcurrentBag exceptions = []; - foreach (var subscription in subscriptions) - { - var target = subscription.Platform is "browser" ? AdsPushTarget.BrowserAndPwa - : subscription.Platform is "fcmV1" ? AdsPushTarget.Android - : subscription.Platform is "apns" ? AdsPushTarget.Ios - : throw new NotImplementedException(); + await Parallel.ForEachAsync(subscriptions, parallelOptions: new() + { + MaxDegreeOfParallelism = 10, + CancellationToken = default + }, async (subscription, _) => + { + try + { + var target = subscription.Platform is "browser" ? AdsPushTarget.BrowserAndPwa + : subscription.Platform is "fcmV1" ? AdsPushTarget.Android + : subscription.Platform is "apns" ? AdsPushTarget.Ios + : throw new NotImplementedException(); - tasks.Add(adsPushSender.BasicSendAsync(target, subscription.PushChannel, payload, cancellationToken)); - } + await adsPushSender.BasicSendAsync(target, subscription.PushChannel, payload, default); + } + catch (Exception exp) + { + exceptions.Add(exp); + } + }); - try - { - await Task.WhenAll(tasks); - } - catch (Exception exp) - { - logger.LogWarning(exp, "Failed to send push notification."); - } + if (exceptions.Any()) + { + logger.LogError(new AggregateException(exceptions), "Failed to send push notifications"); + } + + }, default); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHub.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/SignalR/AppHub.cs similarity index 100% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHub.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/SignalR/AppHub.cs diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHubConnectionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/SignalR/AppHubConnectionHandler.cs similarity index 100% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Signalr/AppHubConnectionHandler.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/SignalR/AppHubConnectionHandler.cs diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json index 43a9668cdd..9ceb2aa987 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json @@ -1,4 +1,4 @@ -{ +{ "ConnectionStrings": { //#if (database == "SqlServer") "SqlServerConnectionString": "Data Source=(localdb)\\mssqllocaldb; Initial Catalog=BoilerplateDb;Integrated Security=true;Application Name=Boilerplate;TrustServerCertificate=True;", @@ -30,6 +30,8 @@ "ResetPasswordTokenLifetime": "0.00:02:00", "TwoFactorTokenLifetime": "0.00:02:00", "OtpTokenLifetime": "0.00:02:00", + "MaxConcurrentPrivilegedSessions": 3, + "MaxConcurrentPrivilegedSessions_Comment": "Is the maximum number of concurrent privileged sessions a user can have.", "Password": { "RequireDigit": "false", "RequiredLength": "6", diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json index beb7ccecd9..4ffcc2c291 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json @@ -1,4 +1,4 @@ -{ +{ //#if (IsInsideProjectTemplate == true) "ConnectionStrings": { "SqlServerConnectionString": "Data Source=(localdb)\\mssqllocaldb; Initial Catalog=BoilerplateDb;Integrated Security=true;Application Name=Boilerplate;TrustServerCertificate=True;", @@ -23,6 +23,7 @@ "ResetPasswordTokenLifetime": "0.00:02:00", "TwoFactorTokenLifetime": "0.00:02:00", "OtpTokenLifetime": "0.00:02:00", + "MaxConcurrentPrivilegedSessions": 3, "Password": { "RequireDigit": "false", "RequiredLength": "6", diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs index 2e72e977a8..d0ed51fb16 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs @@ -44,4 +44,7 @@ public interface IUserController : IAppController [HttpPost] [Route("~/api/[controller]/2fa")] Task TwoFactorAuth(TwoFactorAuthRequestDto request, CancellationToken cancellationToken) => default!; + + [HttpPost] + Task SendElevatedAccessToken(CancellationToken cancellationToken); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/ConfirmEmailRequestDto.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/ConfirmEmailRequestDto.cs index b77a56ab74..9998cf374c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/ConfirmEmailRequestDto.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/ConfirmEmailRequestDto.cs @@ -11,6 +11,9 @@ public partial class ConfirmEmailRequestDto [Required(ErrorMessage = nameof(AppStrings.RequiredAttribute_ValidationError))] public string? Token { get; set; } + + /// Samsung Android 14 + public string? DeviceInfo { get; set; } } public partial class ChangeEmailRequestDto : ConfirmEmailRequestDto diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/ConfirmPhoneRequestDto.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/ConfirmPhoneRequestDto.cs index 7ebab421d3..7ad403c168 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/ConfirmPhoneRequestDto.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/ConfirmPhoneRequestDto.cs @@ -10,6 +10,9 @@ public partial class ConfirmPhoneRequestDto [Required(ErrorMessage = nameof(AppStrings.RequiredAttribute_ValidationError))] public string? Token { get; set; } + + /// Samsung Android 14 + public string? DeviceInfo { get; set; } } public partial class ChangePhoneNumberRequestDto : ConfirmPhoneRequestDto diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/RefreshRequestDto.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/RefreshRequestDto.cs index ebf4fc39a2..8deabc8433 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/RefreshRequestDto.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Identity/RefreshRequestDto.cs @@ -5,4 +5,9 @@ public partial class RefreshRequestDto { [Required(ErrorMessage = nameof(AppStrings.RequiredAttribute_ValidationError))] public string? RefreshToken { get; set; } -} \ No newline at end of file + + /// + /// + /// + public string? ElevatedAccessToken { get; set; } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ClaimsPrincipalExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ClaimsPrincipalExtensions.cs index cddb11b4e5..62719b4959 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ClaimsPrincipalExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ClaimsPrincipalExtensions.cs @@ -32,6 +32,6 @@ public static string GetDisplayName(this ClaimsPrincipal claimsPrincipal) ///
    public static Guid GetSessionId(this ClaimsPrincipal claimsPrincipal) { - return Guid.Parse(claimsPrincipal.FindFirst("session-id")!.Value); + return Guid.Parse(claimsPrincipal.FindFirst(AppClaimTypes.SESSION_ID)!.Value); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ISharedServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ISharedServiceCollectionExtensions.cs index 81404dda86..7edea8f875 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ISharedServiceCollectionExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Extensions/ISharedServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using Boilerplate.Shared; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components.Web; namespace Microsoft.Extensions.DependencyInjection; @@ -27,23 +28,33 @@ public static IServiceCollection AddSharedProjectServices(this IServiceCollectio return options; }); + // Creates async service scope from the `root` service scope. + services.AddSingleton(sp => new RootServiceScopeProvider(() => sp.CreateAsyncScope())); services.AddOptions() .Bind(configuration) .ValidateDataAnnotations() .ValidateOnStart(); - // Define authorization policies here to seamlessly integrate them across various components, - // including web api actions and razor pages using Authorize attribute, AuthorizeView in razor pages, - // and programmatically in C# by injecting IAuthorizationService for enhanced security and access control. - services.AddAuthorizationCore(options => - { - options.AddPolicy("AdminsOnly", authPolicyBuilder => authPolicyBuilder.RequireRole("Admin")); - options.AddPolicy("TwoFactorEnabled", x => x.RequireClaim("amr", "mfa")); // For those who have two-factor authentication enabled. - }); + services.ConfigureAuthorizationCore(); services.AddLocalization(); return services; } + + /// + /// Define authorization policies here to seamlessly integrate them across various components, + /// including web api actions and razor pages using Authorize attribute, AuthorizeView in razor pages, + /// and programmatically in C# by injecting for enhanced security and access control. + /// + public static void ConfigureAuthorizationCore(this IServiceCollection services) + { + services.AddAuthorizationCore(options => + { + options.AddPolicy(AuthPolicies.TFA_ENABLED, x => x.RequireClaim("amr", "mfa")); + options.AddPolicy(AuthPolicies.PRIVILEGED_ACCESS, x => x.RequireClaim(AppClaimTypes.PRIVILEGED_SESSION, "true")); + options.AddPolicy(AuthPolicies.ELEVATED_ACCESS, x => x.RequireClaim(AppClaimTypes.ELEVATED_SESSION, "true")); + }); + } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx index fbbb0d4adb..561e59b690 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx @@ -236,25 +236,28 @@ توکن برای شما ارسال شد - جلسه ها + جلسات - سشن‌ها - جلسه های فعال شما در دستگاههای مختلف + جلسه (سشن) های فعال شما در دستگاههای مختلف - سشن‌های شما + جلسه (سشن) های شما - سشن جاری + جلسه - سشن جاری - سایر سشن‌ها + سایر جلسات (سشن‌ها) - حذف سشن + حذف جلسه - سشن + + + حذف سایر جلسات (سشن) - حذف سشن با موفقیت انجام شد + حذف جلسه - سشن با موفقیت انجام شد 404 @@ -759,8 +762,8 @@ ورود به سیستم به عنوان کاربر متفاوت - - شما در حال ورود به سیستم به عنوان + + شما به سیستم وارد شده اید با رمز عبور را فراموش کرده اید؟ @@ -923,6 +926,12 @@ توکن 2FA تولید و برای شما ارسال شده است. + + + لطفا توکن دریافت دسترسی ویژه را وارد کنید + + + توکن {0} شما قبلا ایمیل تایید را درخواست کرده اید. دوباره امتحان کنید در {0} @@ -935,6 +944,9 @@ شما قبلا رمز عبور یک‌بار مصرف را درخواست کرده اید. دوباره امتحان کنید در {0} + + + شما قبلا توکن دسترسی ویژه را درخواست کرده اید. دوباره امتحان کنید در {0} رمز یک‌بار مصرف diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx index c2289002da..3fc67b101d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx @@ -252,6 +252,9 @@ Sessie verwijderen + + + Probeer andere sessies te verwijderen Sessie succesvol verwijderd @@ -759,7 +762,7 @@ Inloggen als Anderse gebruiker - + U meldt zich aan als @@ -923,6 +926,12 @@ Het 2FA-token is gegenereerd en naar u verzonden. + + + Voer het verhoogde toegangstoken in om door te gaan. + + + Token {0} Je hebt de bevestigingsmail al aangevraagd. Probeer het opnieuw in {0} @@ -935,6 +944,9 @@ Je hebt al een OTP aangevraagd. Probeer het opnieuw in {0} + + + U hebt al een elevated access token aangevraagd. Probeer het opnieuw in {0} OTP (Computergebruik) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx index 6d3bc4291d..07e643b90d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx @@ -252,6 +252,9 @@ Remove session + + + Try removing other sessions Session removed successfully @@ -759,8 +762,8 @@ Sign in as different user - - You're sign in as + + You're signed in as Forgot password? @@ -923,6 +926,12 @@ The 2FA token has been generated and sent to you. + + + Please enter the elevated access token to continue. + + + Token {0} You have already requested the confirmation email. Try again in {0} @@ -935,6 +944,9 @@ You have already requested an OTP. Try again in {0} + + + You have already requested a elevated access token. Try again in {0} OTP diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AuthPolicies.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AuthPolicies.cs new file mode 100644 index 0000000000..0150780d8a --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/AuthPolicies.cs @@ -0,0 +1,42 @@ +namespace Boilerplate.Shared.Services; + +public class AuthPolicies +{ + /// + /// Having this policy means the user has enabled 2 factor authentication. + /// + public const string TFA_ENABLED = nameof(TFA_ENABLED); + + /// + /// By default, each user is limited to 3 active sessions. + /// This policy can be disabled or configured to adjust the session limit dynamically, + /// such as by reading from application settings, the user's subscription plan, or other criteria. + /// Currently, this policy applies only to the Todo and AdminPanel related sample pages. + /// However, it can be extended to cover additional pages as needed. + /// + /// Important: Do not apply this policy to the settings page, as users need access to manage and revoke their sessions there. + /// + public const string PRIVILEGED_ACCESS = nameof(PRIVILEGED_ACCESS); + + /// + /// Enables the user to execute potentially harmful operations, like account removal. + /// This limited-time policy is activated upon successful verification via a secure 6-digit code or + /// during the initial minutes of a sign-in session. + /// + public const string ELEVATED_ACCESS = nameof(ELEVATED_ACCESS); +} + +public class AppClaimTypes +{ + public const string SESSION_ID = "session-id"; + + /// + /// + /// + public const string PRIVILEGED_SESSION = "privileged-session"; + + /// + /// + /// + public const string ELEVATED_SESSION = "elevated-session"; +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/RootServiceScopeProvider.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/RootServiceScopeProvider.cs new file mode 100644 index 0000000000..35d035be83 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/RootServiceScopeProvider.cs @@ -0,0 +1,6 @@ +namespace Boilerplate.Shared.Services; + +/// +/// Creates async service scope from the `root` service scope. +/// +public delegate AsyncServiceScope RootServiceScopeProvider(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json index bc4b2d2dfe..e0f74b2150 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json @@ -31,8 +31,8 @@ "AppCenterLoggerProvider": { "LogLevel": { "Default": "Warning", - "Microsoft.EntityFrameworkCore.Database.Command": "Information", - "Boilerplate.Client.Core.Services.AuthenticationManager": "Information" + "Boilerplate.Client.Core.Services.AuthManager": "Information", + "Microsoft.EntityFrameworkCore.Database.Command": "Information" }, "IncludeScopes": true }, @@ -69,6 +69,7 @@ "LogLevel": { "Default": "Information", "Microsoft.AspNetCore*": "Warning", + "Microsoft.AspNetCore.Authorization": "Information", "System.Net.Http.HttpClient.*.LogicalHandler": "Warning" }, "IncludeScopes": true diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityApiTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityApiTests.cs index dde2251bdb..d764995cf8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityApiTests.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/IdentityApiTests.cs @@ -22,7 +22,7 @@ await server.Build(services => await using var scope = server.WebApp.Services.CreateAsyncScope(); - var authenticationManager = scope.ServiceProvider.GetRequiredService(); + var authenticationManager = scope.ServiceProvider.GetRequiredService(); await authenticationManager.SignIn(new() { From 8e87b7d03b7823d2e5c9bdca167ecb121f80a9e4 Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Mon, 25 Nov 2024 09:53:58 +0100 Subject: [PATCH 11/87] fix(templates): resolve minor issues in Boilerplate #9327 (#9328) --- .../Authorized/Settings/SessionsSection.razor | 37 +++++++++++-------- .../Boilerplate.Client.Core/Data/README.md | 2 +- .../ElevatedAccessTokenTemplate.razor | 2 +- .../Components/EmailTokenTemplate.razor | 2 +- .../Components/OtpTemplate.razor | 2 +- .../ResetPasswordTokenTemplate.razor | 2 +- .../Components/SocialSignedInPage.razor | 2 +- .../Components/TwoFactorTokenTemplate.razor | 2 +- .../Shared/Dtos/Identity/UserSessionDto.cs | 8 ++++ 9 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor index 8df1629e59..cdfb0bdd77 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor @@ -8,14 +8,16 @@ { @Localizer[nameof(AppStrings.CurrentSession)] - + + + } @@ -28,14 +30,17 @@ - + + + - + @EmailLocalizer[nameof(EmailStrings.ElevatedAccessTokenEmailSubject), Model.Token] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/EmailTokenTemplate.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/EmailTokenTemplate.razor index e70c6d0dc6..738a4efd37 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/EmailTokenTemplate.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/EmailTokenTemplate.razor @@ -10,7 +10,7 @@ - + @EmailLocalizer[nameof(EmailStrings.ConfirmationEmailSubject), Model.Token!] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/OtpTemplate.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/OtpTemplate.razor index 8b7d16c4b9..1c06372f99 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/OtpTemplate.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/OtpTemplate.razor @@ -10,7 +10,7 @@ - + @EmailLocalizer[nameof(EmailStrings.OtpEmailSubject), Model.Token] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/ResetPasswordTokenTemplate.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/ResetPasswordTokenTemplate.razor index 9a31b16388..d070632e9b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/ResetPasswordTokenTemplate.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/ResetPasswordTokenTemplate.razor @@ -10,7 +10,7 @@ - + @EmailLocalizer[nameof(EmailStrings.ResetPasswordEmailSubject), Model.Token] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/SocialSignedInPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/SocialSignedInPage.razor index d6ff895fc6..2676399732 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/SocialSignedInPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Components/SocialSignedInPage.razor @@ -6,7 +6,7 @@ - + @Localizer[nameof(AppStrings.SocialSignedInTitle)] + +
    +
    + +
    + + + + + bit BlazorUI + + + +
    +
    + + + + @advanceWaySelectedItem?.Title + + +
    +
    + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> +
    +
    +
    +
    "; + private readonly string example10CsharpCode = @" +public class MenuItem +{ + public string? Title { get; set; } + public string? Icon { get; set; } +} + +private static readonly List basicNavBarCustoms = +[ + new() { Title = ""Home"", Icon = BitIconName.Home }, + new() { Title = ""Products"", Icon = BitIconName.ProductVariant }, + new() { Title = ""Academy"", Icon = BitIconName.LearningTools }, + new() { Title = ""Profile"", Icon = BitIconName.Contact }, +];"; + + private readonly string example11RazorCode = @" + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" />"; + private readonly string example11CsharpCode = @" +public class MenuItem +{ + public string? Title { get; set; } + public string? Icon { get; set; } +} + +private static readonly List basicNavBarCustoms = +[ + new() { Title = ""Home"", Icon = BitIconName.Home }, + new() { Title = ""Products"", Icon = BitIconName.ProductVariant }, + new() { Title = ""Academy"", Icon = BitIconName.LearningTools }, + new() { Title = ""Profile"", Icon = BitIconName.Contact }, +];"; + + private readonly string example12RazorCode = @" + + + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + + item.Title }, + IconName = { Selector = item => item.Icon }, + Class = { Selector = item => item.CssClass }, + Style = { Selector = item => item.Style }})"" /> + + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> + item.Title }, + IconName = { Selector = item => item.Icon } })"" />"; + private readonly string example12CsharpCode = @" +public class MenuItem +{ + public string? Title { get; set; } + public string? Icon { get; set; } + public string? CssClass { get; set; } + public string? Style { get; set; } +} + +private static readonly List basicNavBarCustoms = +[ + new() { Title = ""Home"", Icon = BitIconName.Home }, + new() { Title = ""Products"", Icon = BitIconName.ProductVariant }, + new() { Title = ""Academy"", Icon = BitIconName.LearningTools }, + new() { Title = ""Profile"", Icon = BitIconName.Contact }, +]; + +private static readonly List basicNavBarCustomsClassStyle = +[ + new() { Title = ""Home"", Icon = BitIconName.Home }, + new() { Title = ""Products"", Icon = BitIconName.ProductVariant, CssClass = ""custom-item"" }, + new() { Title = ""Academy"", Icon = BitIconName.LearningTools, Style = ""color: #b6ff00;font-weight: 600;"" }, + new() { Title = ""Profile"", Icon = BitIconName.Contact }, +];"; + + + private readonly string example13RazorCode = @" + item.Title }, + IconName = { Selector = item => item.Icon } })"" />"; + private readonly string example13CsharpCode = @" +private static readonly List rtlCustomsItems = +[ + new() { Title = ""خانه"", Icon = BitIconName.Home }, + new() { Title = ""محصولات"", Icon = BitIconName.ProductVariant }, + new() { Title = ""آکادمی"", Icon = BitIconName.LearningTools }, + new() { Title = ""پروفایل"", Icon = BitIconName.Contact }, +];"; +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor index 82e9886e1a..30a088d1b0 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor @@ -16,6 +16,8 @@ +
    Defines the mode in which navigation is handled by the nav bar component.
    +

    @@ -24,17 +26,197 @@ +
    Only renders the icon of each navbar item.
    +

    - + +
    Enables fit-content width for the nav bar that sets the width of the component based on its content size.
    +

    + +
    +
    + + + +
    Explore BitNavBar's customization capabilities.
    +

    - @item.Text - + + @item.Text
    +
    + + + +
    Managing NavBar click event (OnClick).
    +

    + +
    + Clicked item: @(eventsClickedItem?.Text) +
    +
    + + + +
    This example demonstrates different ways to handle item selection and changes within the component.
    +

    +
    DefaultSelectedItem:
    +
    + +
    + Selected item: @(selectedItem?.Text) +

    +
    Two-way SelectedItem:
    +
    + +
    + +
    +
    + + + +
    Enables the ability to trigger select event even when the same item is selected again.
    +

    + +
    + +
    + Count clicked item: @(countClick) +
    +
    + + + +
    An illustrative example of integrating this component into a straightforward mobile application.
    +

    +
    +
    + +
    + + + + + bit BlazorUI + + + +
    +
    + + + + @advanceWaySelectedItem?.Text + + +
    +
    + +
    +
    +
    +
    +
    +
    + + + +
    Offering a range of specialized colors, providing visual cues for specific states within your application.
    +

    +
    Primary
    + +

    +
    Secondary
    + +

    +
    Tertiary
    + +

    +
    Info
    + +

    +
    Success
    + +

    +
    Warning
    + +

    +
    SevereWarning
    + +

    +
    Error
    + +

    +
    +
    PrimaryBackground
    + +

    +
    SecondaryBackground
    + +

    +
    TertiaryBackground
    + +
    +

    +
    PrimaryForeground
    + +

    +
    SecondaryForeground
    + +

    +
    TertiaryForeground
    + +

    +
    PrimaryBorder
    + +

    +
    SecondaryBorder
    + +

    +
    TertiaryBorder
    + +
    +
    + + + +
    Empower customization by overriding default styles and classes, allowing tailored design modifications to suit specific UI requirements.
    +

    +
    +
    Component's style & class:
    + +
    + +
    +

    +
    +
    Item's style & class:
    + +
    +

    +
    +
    Styles & Classes:
    + +
    + +
    +
    +
    + + + +
    Use BitNavBar in right-to-left (RTL).
    +

    +
    + +
    +
    \ No newline at end of file diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.cs index 7099cf4470..03c5e3809d 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.cs @@ -18,77 +18,29 @@ public partial class _BitNavBarItemDemo new() { Text = "Profile", IconName = BitIconName.Contact }, ]; + private static readonly List styleClassItems = + [ + new() { Text = "Home", IconName = BitIconName.Home }, + new() { Text = "Products", IconName = BitIconName.ProductVariant, Class = "custom-item" }, + new() { Text = "Academy", IconName = BitIconName.LearningTools, Style = "color: #b6ff00;font-weight: 600;" }, + new() { Text = "Profile", IconName = BitIconName.Contact }, + ]; + private static readonly List rtlItems = + [ + new() { Text = "خانه", IconName = BitIconName.Home }, + new() { Text = "محصولات", IconName = BitIconName.ProductVariant }, + new() { Text = "آکادمی", IconName = BitIconName.LearningTools }, + new() { Text = "پروفایل", IconName = BitIconName.Contact }, + ]; - private readonly string example1RazorCode = @" -"; - private readonly string example1CsharpCode = @" -private static readonly List basicNavBarItems = -[ - new() { Text = ""Home"", IconName = BitIconName.Home }, - new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, - new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, - new() { Text = ""Profile"", IconName = BitIconName.Contact }, -];"; - - private readonly string example2RazorCode = @" - - -"; - private readonly string example2CsharpCode = @" -private static readonly List basicNavBarItems = -[ - new() { Text = ""Home"", IconName = BitIconName.Home }, - new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, - new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, - new() { Text = ""Profile"", IconName = BitIconName.Contact }, -]; - -private static readonly List basicNavBarItemsDisabled = -[ - new() { Text = ""Home"", IconName = BitIconName.Home }, - new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, - new() { Text = ""Academy"", IconName = BitIconName.LearningTools, IsEnabled = false }, - new() { Text = ""Profile"", IconName = BitIconName.Contact }, -];"; - - private readonly string example3RazorCode = @" -"; - private readonly string example3CsharpCode = @" -private static readonly List basicNavBarItems = -[ - new() { Text = ""Home"", IconName = BitIconName.Home }, - new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, - new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, - new() { Text = ""Profile"", IconName = BitIconName.Contact }, -];"; - - private readonly string example4RazorCode = @" -"; - private readonly string example4CsharpCode = @" -private static readonly List basicNavBarItems = -[ - new() { Text = ""Home"", IconName = BitIconName.Home }, - new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, - new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, - new() { Text = ""Profile"", IconName = BitIconName.Contact }, -];"; + private static IEnumerable> choiceGroupItems = + basicNavBarItems.Select(i => new BitChoiceGroupItem() { Id = i.Text, Text = i.Text, IsEnabled = i.IsEnabled, Value = i }); - private readonly string example5RazorCode = @" - - - @item.Text - - -"; - private readonly string example5CsharpCode = @" -private static readonly List basicNavBarItems = -[ - new() { Text = ""Home"", IconName = BitIconName.Home }, - new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, - new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, - new() { Text = ""Profile"", IconName = BitIconName.Contact }, -];"; + private int countClick; + private bool reselectable = true; + private BitNavBarItem? selectedItem; + private BitNavBarItem? eventsClickedItem; + private BitNavBarItem? twoWaySelectedItem; + private BitNavBarItem? advanceWaySelectedItem = basicNavBarItems[1]; } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.samples.cs new file mode 100644 index 0000000000..34a312c6ae --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.samples.cs @@ -0,0 +1,302 @@ +namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Navs.NavBar; + +public partial class _BitNavBarItemDemo +{ + private readonly string example1RazorCode = @" +"; + private readonly string example1CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example2RazorCode = @" + + +"; + private readonly string example2CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +]; + +private static readonly List basicNavBarItemsDisabled = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools, IsEnabled = false }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example3RazorCode = @" +"; + private readonly string example3CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example4RazorCode = @" +"; + private readonly string example4CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example5RazorCode = @" +"; + private readonly string example5CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example6RazorCode = @" + + + + @item.Text + +"; + private readonly string example6CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example7RazorCode = @" + eventsClickedItem = item"" /> + +Clicked item: @(eventsClickedItem?.Text)"; + private readonly string example7CsharpCode = @" +private BitNavBarItem? eventsClickedItem; + +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example8RazorCode = @" + +Selected item: @(selectedItem?.Text) + + +"; + private readonly string example8CsharpCode = @" +private BitNavBarItem? selectedItem; +private BitNavBarItem? twoWaySelectedItem; + +private static IEnumerable> choiceGroupItems = + basicNavBarItems.Select(i => new BitChoiceGroupItem() { Id = i.Text, Text = i.Text, IsEnabled = i.IsEnabled, Value = i }); + +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example9RazorCode = @" + + countClick++"" Reselectable=""reselectable"" /> +Count clicked item: @(countClick)"; + private readonly string example9CsharpCode = @" +private int countClick; +private bool reselectable = true; + +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example10RazorCode = @" + + +
    +
    + +
    + + + + + bit BlazorUI + + + +
    +
    + + + + @advanceWaySelectedItem?.Text + + +
    +
    + +
    +
    +
    +
    "; + private readonly string example10CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example11RazorCode = @" + + + + + + + + + + + + + + + + + + +"; + private readonly string example11CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + private readonly string example12RazorCode = @" + + + + + + + + +"; + private readonly string example12CsharpCode = @" +private static readonly List basicNavBarItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +]; + +private static readonly List styleClassItems = +[ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", IconName = BitIconName.ProductVariant, Class = ""custom-item"" }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools, Style = ""color: #b6ff00;font-weight: 600;"" }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, +];"; + + + private readonly string example13RazorCode = @" +"; + private readonly string example13CsharpCode = @" +private static readonly List rtlItems = +[ + new() { Text = ""خانه"", IconName = BitIconName.Home }, + new() { Text = ""محصولات"", IconName = BitIconName.ProductVariant }, + new() { Text = ""آکادمی"", IconName = BitIconName.LearningTools }, + new() { Text = ""پروفایل"", IconName = BitIconName.Contact }, +];"; +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor index 593f015eca..3420a659c6 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor @@ -31,6 +31,8 @@ +
    Defines the mode in which navigation is handled by the nav bar component.
    +

    @@ -42,6 +44,8 @@ +
    Only renders the icon of each navbar item.
    +

    @@ -51,8 +55,23 @@
    - + +
    Enables fit-content width for the nav bar that sets the width of the component based on its content size.
    +

    + + + + + + +
    +
    + + + +
    Explore BitNavBar's customization capabilities.
    +

    @@ -61,9 +80,323 @@ - @option.Text - + + @option.Text
    +
    + + + +
    Managing NavBar click event (OnClick).
    +

    + + + + + + +
    + Clicked item: @(eventsClickedOption?.Text) +
    +
    + + + +
    This example demonstrates different ways to handle item selection and changes within the component.
    +

    +
    DefaultSelectedItem:
    +
    + Coming soon... + + + + + + +
    + Selected item: @(selectedOption?.Text) +

    +
    Two-way SelectedItem:
    +
    + + + + + + +
    + + + + + + +
    +
    + + + +
    Enables the ability to trigger select event even when the same item is selected again.
    +

    + +
    + + + + + + +
    + Count clicked item: @(countClick) +
    +
    + + + +
    An illustrative example of integrating this component into a straightforward mobile application.
    +

    +
    +
    + +
    + + + + + bit BlazorUI + + + +
    +
    + + + + @advanceSelectedOption?.Text + + +
    +
    + + + + + + +
    +
    +
    +
    +
    +
    + + + +
    Offering a range of specialized colors, providing visual cues for specific states within your application.
    +

    +
    Primary
    + + + + + + +

    +
    Secondary
    + + + + + + +

    +
    Tertiary
    + + + + + + +

    +
    Info
    + + + + + + +

    +
    Success
    + + + + + + +

    +
    Warning
    + + + + + + +

    +
    SevereWarning
    + + + + + + +

    +
    Error
    + + + + + + +

    +
    +
    PrimaryBackground
    + + + + + + +

    +
    SecondaryBackground
    + + + + + + +

    +
    TertiaryBackground
    + + + + + + +
    +

    +
    PrimaryForeground
    + + + + + + +

    +
    SecondaryForeground
    + + + + + + +

    +
    TertiaryForeground
    + + + + + + +

    +
    PrimaryBorder
    + + + + + + +

    +
    SecondaryBorder
    + + + + + + +

    +
    TertiaryBorder
    + + + + + + +
    +
    + + + +
    Empower customization by overriding default styles and classes, allowing tailored design modifications to suit specific UI requirements.
    +

    +
    +
    Component's style & class:
    + + + + + + +
    + + + + + + +
    +

    +
    +
    Item's style & class:
    + + + + + + +
    +

    +
    +
    Styles & Classes:
    + + + + + + +
    + + + + + + +
    +
    +
    + + + +
    Use BitNavBar in right-to-left (RTL).
    +

    +
    + + + + + + +
    +
    \ No newline at end of file diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.cs index 2c19f01295..9e105edeb5 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.cs @@ -2,56 +2,15 @@ public partial class _BitNavBarOptionDemo { - private readonly string example1RazorCode = @" - - - - - -"; + private int countClick; + private bool reselectable = true; + private BitNavBarOption? selectedOption; + private BitNavBarOption? eventsClickedOption; + private BitNavBarOption? twoWaySelectedOption; + private BitNavBarOption? advanceSelectedOption; - private readonly string example2RazorCode = @" - - - - - - - - - - - - -"; - - private readonly string example3RazorCode = @" - - - - - -"; - - private readonly string example4RazorCode = @" - - - - - -"; - - private readonly string example5RazorCode = @" - - - - - - - - - @option.Text - - -"; + private BitNavBarOption optionHome = default!; + private BitNavBarOption optionProducts = default!; + private BitNavBarOption optionAcademy = default!; + private BitNavBarOption optionProfile = default!; } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.samples.cs new file mode 100644 index 0000000000..c0d13c8668 --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.samples.cs @@ -0,0 +1,359 @@ +namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Navs.NavBar; + +public partial class _BitNavBarOptionDemo +{ + private readonly string example1RazorCode = @" + + + + + +"; + + private readonly string example2RazorCode = @" + + + + + + + + + + + + +"; + + private readonly string example3RazorCode = @" + + + + + +"; + + private readonly string example4RazorCode = @" + + + + + +"; + + private readonly string example5RazorCode = @" + + + + + +"; + + private readonly string example6RazorCode = @" + + + + + + + + + + @option.Text + +"; + + private readonly string example7RazorCode = @" + eventsClickedOption = option""> + + + + + +Clicked item: @(eventsClickedOption?.Text)"; + private readonly string example7CsharpCode = @" +private BitNavBarOption? eventsClickedOption; +"; + + private readonly string example8RazorCode = @" + + + + + + + +"" TValue=""BitNavBarOption"" @bind-Value=""@twoWaySelectedOption""> + + + + +"; + private readonly string example8CsharpCode = @" +private BitNavBarOption? twoWaySelectedOption; + +private BitNavBarOption optionHome = default!; +private BitNavBarOption optionProducts = default!; +private BitNavBarOption optionAcademy = default!; +private BitNavBarOption optionProfile = default!; +"; + + private readonly string example9RazorCode = @" + + countClick++"" Reselectable=""reselectable""> + + + + + +Count clicked item: @(countClick)"; + private readonly string example9CsharpCode = @" +private int countClick; +private bool reselectable = true; +"; + + private readonly string example10RazorCode = @" + + +
    +
    + +
    + + + + + bit BlazorUI + + + +
    +
    + + + + @advanceSelectedOption?.Text + + +
    +
    + + + + + + +
    +
    +
    +
    "; + private readonly string example10CsharpCode = @" +private BitNavBarOption? advanceSelectedOption; +"; + + private readonly string example11RazorCode = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + + private readonly string example12RazorCode = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + + private readonly string example13RazorCode = @" + + + + + +"; +} From e366999c46afac7f1ee195f747ece3c2131fc7c4 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Wed, 27 Nov 2024 12:12:56 +0330 Subject: [PATCH 18/87] feat(templates): correct min value of Y axis of BitChart in Boilerplate #9343 (#9344) --- .../Authorized/Dashboard/ProductsCountPerCategoryWidget.razor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Dashboard/ProductsCountPerCategoryWidget.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Dashboard/ProductsCountPerCategoryWidget.razor.cs index a9fec72f26..358a569433 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Dashboard/ProductsCountPerCategoryWidget.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Dashboard/ProductsCountPerCategoryWidget.razor.cs @@ -15,6 +15,7 @@ protected override async Task OnInitAsync() { Options = new BitChartBarOptions { + Scales = new() { YAxes = [new BitChartLinearCartesianAxis() { Ticks = new() { Min = 0 } }] }, Responsive = true, Legend = new BitChartLegend() { From d7a276ea7093dcf262cdf55380442317e26ebbcf Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Wed, 27 Nov 2024 15:16:01 +0330 Subject: [PATCH 19/87] feat(blazorui): improve BitNavBar demo page further #9341 (#9342) --- .../Components/Navs/NavBar/BitNavBar.razor | 3 +- .../Components/Navs/NavBar/BitNavBar.razor.cs | 8 +- .../Navs/NavBar/BitNavBarDemo.razor | 2 +- .../Navs/NavBar/BitNavBarDemo.razor.scss | 22 +- .../Pages/Components/Navs/NavBar/MenuItem.cs | 1 + .../Navs/NavBar/_BitNavBarCustomDemo.razor | 500 ++++++++++------- .../Navs/NavBar/_BitNavBarCustomDemo.razor.cs | 6 +- .../_BitNavBarCustomDemo.razor.samples.cs | 63 ++- .../Navs/NavBar/_BitNavBarItemDemo.razor | 282 ++++++---- .../Navs/NavBar/_BitNavBarItemDemo.razor.cs | 6 +- .../_BitNavBarItemDemo.razor.samples.cs | 49 +- .../Navs/NavBar/_BitNavBarOptionDemo.razor | 522 ++++++++++-------- .../Navs/NavBar/_BitNavBarOptionDemo.razor.cs | 4 +- .../_BitNavBarOptionDemo.razor.samples.cs | 41 +- 14 files changed, 910 insertions(+), 599 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor index 1d98fbc641..4d7f294cb6 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor @@ -21,8 +21,9 @@ var url = GetUrl(item); var template = GetTemplate(item); var isEnabled = GetIsEnabled(item); + var href = (isEnabled && (SelectedItem != itm || Reselectable)) ? url : null; - <_BitNavBarChild Href="@url" + <_BitNavBarChild Href="@href" Title="@GetTitle(item)" Target="@GetTarget(item)" Style="@GetItemCssStyle(item)" diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor.cs index 832cb7a5ec..edf68f0816 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/NavBar/BitNavBar.razor.cs @@ -252,14 +252,14 @@ private async Task HandleOnClick(TItem item) { if (GetIsEnabled(item) is false) return; - if (Mode == BitNavMode.Manual) + if (SelectedItem != item || Reselectable) { - await SetSelectedItem(item); + await OnItemClick.InvokeAsync(item); } - if (SelectedItem != item || Reselectable) + if (Mode == BitNavMode.Manual) { - await OnItemClick.InvokeAsync(item); + await SetSelectedItem(item); } } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor index f45420adca..8a6b89d788 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/BitNavBarDemo.razor @@ -6,7 +6,7 @@
    ? Fragment { get; set; } public string? CssClass { get; set; } public string? Style { get; set; } public bool Disabled { get; set; } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor index ff14894911..2fdcd0c125 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor @@ -1,35 +1,43 @@  - +
    + +
    Disabled:

    - +
    + +




    Disabled item:

    - +
    + +
    - + -
    Defines the mode in which navigation is handled by the nav bar component.
    +
    Defines the mode in which the selected item is handled manually, instead of automatically using the current url.


    - +
    + +
    @@ -37,10 +45,12 @@
    Only renders the icon of each navbar item.


    - +
    + +
    @@ -48,10 +58,12 @@
    Enables fit-content width for the nav bar that sets the width of the component based on its content size.


    - +
    + +
    @@ -59,27 +71,40 @@
    Explore BitNavBar's customization capabilities.


    - - - - @custom.Title - - +
    ItemTemplate:

    +
    + + + @custom.Title + + + +
    +



    +
    Item's template:

    +
    + +
    -
    Managing NavBar click event (OnClick).
    +
    Managing NavBar item click event (OnItemClick).


    - -
    - Clicked item: @(eventsClickedItem?.Title) +
    + +
    +

    + Clicked item: @eventsClickedItem?.Title
    @@ -87,24 +112,26 @@
    This example demonstrates different ways to handle item selection and changes within the component.


    -
    DefaultSelectedItem:
    -
    - -
    - Selected item: @(selectedItem?.Title) -

    -
    Two-way SelectedItem:
    +
    DefaultSelectedItem:

    +
    + +

    - + Selected item: @selectedItem.Title +



    +
    Two-way SelectedItem:

    +
    + +

    @@ -116,14 +143,16 @@


    - +
    + +

    - Count clicked item: @(countClick) + Item click count: @countClick @@ -147,18 +176,19 @@
    - - @advanceWaySelectedItem?.Title + + @advancedSelectedItem?.Title
    - +
    @@ -170,143 +200,177 @@
    Offering a range of specialized colors, providing visual cues for specific states within your application.


    -
    Primary
    - -

    -
    Secondary
    - -

    -
    Tertiary
    - -

    -
    Info
    - -

    -
    Success
    - -

    -
    Warning
    - -

    -
    SevereWarning
    - -

    -
    Error
    - -

    +
    Primary:

    +
    + +
    +



    +
    Secondary:

    +
    + +
    +



    +
    Tertiary:

    +
    + +
    +



    +
    Info:

    +
    + +
    +



    +
    Success:

    +
    + +
    +



    +
    Warning:

    +
    + +
    +



    +
    SevereWarning:

    +
    + +
    +



    +
    Error:

    +
    + +
    +



    -
    PrimaryBackground
    - PrimaryBackground:

    +
    + +
    +



    +
    SecondaryBackground:

    +
    + +
    +



    +
    TertiaryBackground:

    +
    + +
    + +



    +
    PrimaryForeground:

    +
    + +
    +



    +
    SecondaryForeground:

    +
    + -

    -
    SecondaryBackground
    - item.Icon } })" /> +
    +



    +
    TertiaryForeground:

    +
    + +
    +



    +
    PrimaryBorder:

    +
    + -

    -
    TertiaryBackground
    - item.Icon } })" /> +
    +



    +
    SecondaryBorder:

    +
    + +
    +



    +
    TertiaryBorder:

    +
    + + IconName = { Selector = item => item.Icon } })" />
    -

    -
    PrimaryForeground
    - -

    -
    SecondaryForeground
    - -

    -
    TertiaryForeground
    - -

    -
    PrimaryBorder
    - -

    -
    SecondaryBorder
    - -

    -
    TertiaryBorder
    -
    @@ -314,37 +378,43 @@
    Empower customization by overriding default styles and classes, allowing tailored design modifications to suit specific UI requirements.


    -
    -
    Component's style & class:
    +
    Component's Style:

    +
    -
    + IconName = { Selector = item => item.Icon } })" /> +
    +

    +
    Component's Class:

    +
    -

    -
    -
    Item's style & class:
    +



    +
    Item's style & class:

    +
    -

    -
    -
    Styles & Classes:
    +



    +
    Styles:

    +
    -
    +
    +

    +
    Classes:

    +
    @@ -355,11 +425,21 @@
    Use BitNavBar in right-to-left (RTL).


    -
    +
    - \ No newline at end of file + + +@code { + private static readonly List templateNavBarCustoms = + [ + new() { Title = "Home", Icon = BitIconName.Home }, + new() { Title = "Products", Fragment = (item) => @
    @item.Title🎁
    }, + new() { Title = "Academy", Icon = BitIconName.LearningTools }, + new() { Title = "Profile", Icon = BitIconName.Contact }, + ]; +} \ No newline at end of file diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor.cs index e7b17e1e75..1d621eba28 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor.cs @@ -39,8 +39,8 @@ public partial class _BitNavBarCustomDemo private int countClick; private bool reselectable = true; - private MenuItem? selectedItem; + private MenuItem selectedItem = basicNavBarCustoms[0]; + private MenuItem twoWaySelectedItem = basicNavBarCustoms[0]; private MenuItem? eventsClickedItem; - private MenuItem? twoWaySelectedItem; - private MenuItem? advanceWaySelectedItem = basicNavBarCustoms[1]; + private MenuItem advancedSelectedItem = basicNavBarCustoms[1]; } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor.samples.cs index 4b2dc2e8d1..7d0cbd462b 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor.samples.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarCustomDemo.razor.samples.cs @@ -120,15 +120,31 @@ public class MenuItem NameSelectors=""@(new() { Text = { Selector = item => item.Title }, IconName = { Selector = item => item.Icon } })""> + @custom.Title - @custom.Title -"; + + + item.Title }, + IconName = { Selector = item => item.Icon }, + Template = { Selector = item => item.Fragment } })"" /> + +@code { + private static readonly List templateNavBarCustoms = + [ + new() { Title = ""Home"", Icon = BitIconName.Home }, + new() { Title = ""Products"", Fragment = (item) => @
    @item.Title🎁
    }, + new() { Title = ""Academy"", Icon = BitIconName.LearningTools }, + new() { Title = ""Profile"", Icon = BitIconName.Contact }, + ]; +}"; private readonly string example6CsharpCode = @" public class MenuItem { public string? Title { get; set; } public string? Icon { get; set; } + public RenderFragment? Fragment { get; set; } } private static readonly List basicNavBarCustoms = @@ -141,11 +157,11 @@ public class MenuItem private readonly string example7RazorCode = @" eventsClickedItem = item"" NameSelectors=""@(new() { Text = { Selector = item => item.Title }, - IconName = { Selector = item => item.Icon } })"" - OnItemClick=""(MenuItem item) => eventsClickedItem = item"" /> + IconName = { Selector = item => item.Icon } })"" /> -Clicked item: @(eventsClickedItem?.Title)"; +Clicked item: @eventsClickedItem?.Title"; private readonly string example7CsharpCode = @" private MenuItem? eventsClickedItem; @@ -170,7 +186,7 @@ public class MenuItem DefaultSelectedItem=""basicNavBarCustoms[1]"" NameSelectors=""@(new() { Text = { Selector = item => item.Title }, IconName = { Selector = item => item.Icon } })"" /> -Selected item: @(selectedItem?.Title) +Selected item: @selectedItem.Title item.Icon } })"" /> "; private readonly string example8CsharpCode = @" -private MenuItem? selectedItem; -private MenuItem? twoWaySelectedItem; +private MenuItem selectedItem = basicNavBarCustoms[0]; +private MenuItem twoWaySelectedItem = basicNavBarCustoms[0]; private static IEnumerable> choiceGroupItems = basicNavBarCustoms.Select(i => new BitChoiceGroupItem() { Id = i.Title, Text = i.Title, IsEnabled = true, Value = i }); @@ -201,13 +217,15 @@ public class MenuItem private readonly string example9RazorCode = @" + countClick++"" Reselectable=""reselectable"" + OnItemClick=""(MenuItem item) => countClick++"" NameSelectors=""@(new() { Text = { Selector = item => item.Title }, IconName = { Selector = item => item.Icon } })"" /> -Count clicked item: @(countClick)"; + +Item click count: @countClick"; private readonly string example9CsharpCode = @" private int countClick; private bool reselectable = true; @@ -279,23 +297,26 @@ bit BlazorUI
    - - @advanceWaySelectedItem?.Title + + @advancedSelectedItem?.Title
    - item.Title }, - IconName = { Selector = item => item.Icon } })"" /> +
    + item.Title }, + IconName = { Selector = item => item.Icon } })"" /> +
    "; private readonly string example10CsharpCode = @" +private MenuItem advancedSelectedItem = basicNavBarCustoms[1]; + public class MenuItem { public string? Title { get; set; } @@ -471,11 +492,11 @@ public class MenuItem Style = { Selector = item => item.Style }})"" /> item.Title }, IconName = { Selector = item => item.Icon } })"" /> item.Title }, IconName = { Selector = item => item.Icon } })"" />"; private readonly string example12CsharpCode = @" @@ -508,7 +529,7 @@ public class MenuItem item.Title }, - IconName = { Selector = item => item.Icon } })"" />"; + IconName = { Selector = item => item.Icon } })"" />"; private readonly string example13CsharpCode = @" private static readonly List rtlCustomsItems = [ diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor index 30a088d1b0..d6095c1301 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor @@ -1,26 +1,34 @@  - +
    + +
    Disabled:

    - +
    + +




    Disabled item:

    - +
    + +
    - + -
    Defines the mode in which navigation is handled by the nav bar component.
    +
    Defines the mode in which the selected item is handled manually, instead of automatically using the current url.


    - +
    + +
    @@ -28,7 +36,9 @@
    Only renders the icon of each navbar item.


    - +
    + +
    @@ -36,7 +46,9 @@
    Enables fit-content width for the nav bar that sets the width of the component based on its content size.


    - +
    + +
    @@ -44,22 +56,32 @@
    Explore BitNavBar's customization capabilities.


    - - - - @item.Text - - +
    ItemTemplate:

    +
    + + + @item.Text + + + +
    +



    +
    Item's template:

    +
    + +
    -
    Managing NavBar click event (OnClick).
    +
    Managing NavBar item click event (OnItemClick).


    - -
    - Clicked item: @(eventsClickedItem?.Text) +
    + +
    +

    + Clicked item: @eventsClickedItem?.Text
    @@ -67,15 +89,22 @@
    This example demonstrates different ways to handle item selection and changes within the component.


    -
    DefaultSelectedItem:
    -
    - -
    - Selected item: @(selectedItem?.Text) -

    -
    Two-way SelectedItem:
    +
    DefaultSelectedItem:

    +
    + +

    - + Selected item: @selectedItem.Text +



    +
    Two-way SelectedItem:

    +
    + +

    @@ -87,9 +116,14 @@


    - +
    + +

    - Count clicked item: @(countClick) + Item click count: @countClick @@ -113,13 +147,17 @@
    - - @advanceWaySelectedItem?.Text + + @advancedSelectedItem.Text
    - +
    @@ -131,58 +169,92 @@
    Offering a range of specialized colors, providing visual cues for specific states within your application.


    -
    Primary
    - -

    -
    Secondary
    - -

    -
    Tertiary
    - -

    -
    Info
    - -

    -
    Success
    - -

    -
    Warning
    - -

    -
    SevereWarning
    - -

    -
    Error
    - -

    +
    Primary:

    +
    + +
    +



    +
    Secondary:

    +
    + +
    +



    +
    Tertiary:

    +
    + +
    +



    +
    Info:

    +
    + +
    +



    +
    Success:

    +
    + +
    +



    +
    Warning:

    +
    + +
    +



    +
    SevereWarning:

    +
    + +
    +



    +
    Error:

    +
    + +
    +



    -
    PrimaryBackground
    - -

    -
    SecondaryBackground
    - -

    -
    TertiaryBackground
    - +
    PrimaryBackground:

    +
    + +
    +



    +
    SecondaryBackground:

    +
    + +
    +



    +
    TertiaryBackground:

    +
    + +
    +
    +



    +
    PrimaryForeground:

    +
    + +
    +



    +
    SecondaryForeground:

    +
    + +
    +



    +
    TertiaryForeground:

    +
    + +
    +



    +
    PrimaryBorder:

    +
    + +
    +



    +
    SecondaryBorder:

    +
    + +
    +



    +
    TertiaryBorder:

    +
    +
    -

    -
    PrimaryForeground
    - -

    -
    SecondaryForeground
    - -

    -
    TertiaryForeground
    - -

    -
    PrimaryBorder
    - -

    -
    SecondaryBorder
    - -

    -
    TertiaryBorder
    -
    @@ -190,23 +262,31 @@
    Empower customization by overriding default styles and classes, allowing tailored design modifications to suit specific UI requirements.


    -
    -
    Component's style & class:
    - -
    - +
    Component's Style:

    +
    +


    -
    -
    Item's style & class:
    +
    Component's Class:

    +
    + +
    +



    +
    Item's style & class:

    +
    +



    +
    Styles:

    +
    + +


    -
    -
    Styles & Classes:
    +
    Classes:

    +
    -
    -
    @@ -215,8 +295,18 @@
    Use BitNavBar in right-to-left (RTL).


    -
    +
    - \ No newline at end of file + + +@code { + private static readonly List templateNavBarItems = + [ + new() { Text = "Home", IconName = BitIconName.Home }, + new() { Text = "Products", Template = (item) => @
    @item.Text🎁
    }, + new() { Text = "Academy", IconName = BitIconName.LearningTools }, + new() { Text = "Profile", IconName = BitIconName.Contact }, + ]; +} \ No newline at end of file diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.cs index 03c5e3809d..5c589677a9 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.cs @@ -39,8 +39,8 @@ public partial class _BitNavBarItemDemo private int countClick; private bool reselectable = true; - private BitNavBarItem? selectedItem; + private BitNavBarItem selectedItem = basicNavBarItems[0]; + private BitNavBarItem twoWaySelectedItem = basicNavBarItems[0]; private BitNavBarItem? eventsClickedItem; - private BitNavBarItem? twoWaySelectedItem; - private BitNavBarItem? advanceWaySelectedItem = basicNavBarItems[1]; + private BitNavBarItem advancedSelectedItem = basicNavBarItems[1]; } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.samples.cs index 34a312c6ae..a13bbdfdc2 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.samples.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarItemDemo.razor.samples.cs @@ -72,10 +72,22 @@ public partial class _BitNavBarItemDemo private readonly string example6RazorCode = @" + @item.Text - @item.Text -"; + + + + +@code { + private static readonly List templateNavBarItems = + [ + new() { Text = ""Home"", IconName = BitIconName.Home }, + new() { Text = ""Products"", Template = (item) => @
    @item.Text🎁
    }, + new() { Text = ""Academy"", IconName = BitIconName.LearningTools }, + new() { Text = ""Profile"", IconName = BitIconName.Contact }, + ]; +}"; private readonly string example6CsharpCode = @" private static readonly List basicNavBarItems = [ @@ -88,7 +100,7 @@ public partial class _BitNavBarItemDemo private readonly string example7RazorCode = @" eventsClickedItem = item"" /> -Clicked item: @(eventsClickedItem?.Text)"; +Clicked item: @eventsClickedItem?.Text"; private readonly string example7CsharpCode = @" private BitNavBarItem? eventsClickedItem; @@ -102,13 +114,13 @@ public partial class _BitNavBarItemDemo private readonly string example8RazorCode = @" -Selected item: @(selectedItem?.Text) +Selected item: @selectedItem.Text "; private readonly string example8CsharpCode = @" -private BitNavBarItem? selectedItem; -private BitNavBarItem? twoWaySelectedItem; +private BitNavBarItem selectedItem = basicNavBarItems[0]; +private BitNavBarItem twoWaySelectedItem = basicNavBarItems[0]; private static IEnumerable> choiceGroupItems = basicNavBarItems.Select(i => new BitChoiceGroupItem() { Id = i.Text, Text = i.Text, IsEnabled = i.IsEnabled, Value = i }); @@ -123,8 +135,13 @@ public partial class _BitNavBarItemDemo private readonly string example9RazorCode = @" - countClick++"" Reselectable=""reselectable"" /> -Count clicked item: @(countClick)"; + + countClick++"" /> + +Item click count: @countClick"; private readonly string example9CsharpCode = @" private int countClick; private bool reselectable = true; @@ -190,18 +207,24 @@ bit BlazorUI
    - - @advanceWaySelectedItem?.Text + + @advancedSelectedItem.Text
    - +
    + +
    "; private readonly string example10CsharpCode = @" +private BitNavBarItem advancedSelectedItem = basicNavBarItems[1]; + private static readonly List basicNavBarItems = [ new() { Text = ""Home"", IconName = BitIconName.Home }, @@ -269,8 +292,8 @@ bit BlazorUI - -"; + +"; private readonly string example12CsharpCode = @" private static readonly List basicNavBarItems = [ diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor index 3420a659c6..48241f151c 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor @@ -1,44 +1,52 @@  - - - - - - +
    + + + + + + +
    Disabled:

    - - - - - - +
    + + + + + + +




    Disabled item:

    - - - - - - +
    + + + + + + +
    - + -
    Defines the mode in which navigation is handled by the nav bar component.
    +
    Defines the mode in which the selected item is handled manually, instead of automatically using the current url.


    - - - - - - +
    + + + + + + +
    @@ -46,12 +54,14 @@
    Only renders the icon of each navbar item.


    - - - - - - +
    + + + + + + +
    @@ -59,12 +69,14 @@
    Enables fit-content width for the nav bar that sets the width of the component based on its content size.


    - - - - - - +
    + + + + + + +
    @@ -72,33 +84,52 @@
    Explore BitNavBar's customization capabilities.


    - - +
    ItemTemplate:

    +
    + + + + + + + + + @option.Text + + + +
    +



    +
    Item's template:

    +
    + - + + + - - - - @option.Text - - + +
    -
    Managing NavBar click event (OnClick).
    +
    Managing NavBar item click event (OnItemClick).


    - - - - - - -
    - Clicked item: @(eventsClickedOption?.Text) +
    + + + + + + +
    +

    + Clicked item: @eventsClickedOption?.Text
    @@ -106,26 +137,27 @@
    This example demonstrates different ways to handle item selection and changes within the component.


    -
    DefaultSelectedItem:
    -
    - Coming soon... - - - - - - -
    - Selected item: @(selectedOption?.Text) -

    -
    Two-way SelectedItem:
    +
    DefaultSelectedItem (not available):

    +
    + + + + + + +

    - - - - - - + Selected option: @selectedOption?.Text +



    +
    Two-way SelectedItem:

    +
    + + + + + + +

    @@ -142,14 +174,16 @@


    - - - - - - +
    + + + + + + +

    - Count clicked item: @(countClick) + Item click count: @countClick
    @@ -173,18 +207,20 @@
    - - @advanceSelectedOption?.Text + + @advancedSelectedOption?.Text
    - - - - - - +
    @@ -196,143 +232,177 @@
    Offering a range of specialized colors, providing visual cues for specific states within your application.


    -
    Primary
    - - - - - - -

    -
    Secondary
    - - - - - - -

    -
    Tertiary
    - - - - - - -

    -
    Info
    - - - - - - -

    -
    Success
    - - - - - - -

    -
    Warning
    - - - - - - -

    -
    SevereWarning
    - - - - - - -

    -
    Error
    - - - - - - -

    +
    Primary:

    +
    + + + + + + +
    +



    +
    Secondary:

    +
    + + + + + + +
    +



    +
    Tertiary:

    +
    + + + + + + +
    +



    +
    Info:

    +
    + + + + + + +
    +



    +
    Success:

    +
    + + + + + + +
    +



    +
    Warning:

    +
    + + + + + + +
    +



    +
    SevereWarning:

    +
    + + + + + + +
    +



    +
    Error:

    +
    + + + + + + +
    +



    -
    PrimaryBackground
    - +
    PrimaryBackground:

    +
    + + + + + + +
    +



    +
    SecondaryBackground:

    +
    + + + + + + +
    +



    +
    TertiaryBackground:

    +
    + + + + + + +
    +
    +



    +
    PrimaryForeground:

    +
    + -

    -
    SecondaryBackground
    - +
    +



    +
    SecondaryForeground:

    +
    + -

    -
    TertiaryBackground
    - +
    +



    +
    TertiaryForeground:

    +
    + + + + + + +
    +



    +
    PrimaryBorder:

    +
    + + + + + + +
    +



    +
    SecondaryBorder:

    +
    + + + + + + +
    +



    +
    TertiaryBorder:

    +
    +
    -

    -
    PrimaryForeground
    - - - - - - -

    -
    SecondaryForeground
    - - - - - - -

    -
    TertiaryForeground
    - - - - - - -

    -
    PrimaryBorder
    - - - - - - -

    -
    SecondaryBorder
    - - - - - - -

    -
    TertiaryBorder
    - - - - - -
    @@ -340,15 +410,18 @@
    Empower customization by overriding default styles and classes, allowing tailored design modifications to suit specific UI requirements.


    -
    -
    Component's style & class:
    +
    Component's Style:

    +
    -
    +
    +

    +
    Component's Class:

    +
    @@ -356,9 +429,9 @@
    -

    -
    -
    Item's style & class:
    +



    +
    Item's style & class:

    +
    @@ -366,17 +439,20 @@
    -

    -
    -
    Styles & Classes:
    - +



    +
    Styles:

    +
    + -
    - +
    +

    +
    Classes:

    +
    + @@ -390,7 +466,7 @@
    Use BitNavBar in right-to-left (RTL).


    -
    +
    diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.cs index 9e105edeb5..412a423d86 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.cs @@ -4,10 +4,10 @@ public partial class _BitNavBarOptionDemo { private int countClick; private bool reselectable = true; - private BitNavBarOption? selectedOption; private BitNavBarOption? eventsClickedOption; + private BitNavBarOption? selectedOption; private BitNavBarOption? twoWaySelectedOption; - private BitNavBarOption? advanceSelectedOption; + private BitNavBarOption? advancedSelectedOption; private BitNavBarOption optionHome = default!; private BitNavBarOption optionProducts = default!; diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.samples.cs index c0d13c8668..4bca4ba2c9 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.samples.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/NavBar/_BitNavBarOptionDemo.razor.samples.cs @@ -58,9 +58,20 @@ public partial class _BitNavBarOptionDemo + @option.Text - @option.Text + + + + + + + + + "; private readonly string example7RazorCode = @" @@ -70,7 +81,8 @@ public partial class _BitNavBarOptionDemo -Clicked item: @(eventsClickedOption?.Text)"; + +Clicked item: @eventsClickedOption?.Text"; private readonly string example7CsharpCode = @" private BitNavBarOption? eventsClickedOption; "; @@ -95,8 +107,7 @@ public partial class _BitNavBarOptionDemo private BitNavBarOption optionHome = default!; private BitNavBarOption optionProducts = default!; private BitNavBarOption optionAcademy = default!; -private BitNavBarOption optionProfile = default!; -"; +private BitNavBarOption optionProfile = default!;"; private readonly string example9RazorCode = @" @@ -106,7 +117,7 @@ public partial class _BitNavBarOptionDemo -Count clicked item: @(countClick)"; +Item click count: @countClick"; private readonly string example9CsharpCode = @" private int countClick; private bool reselectable = true; @@ -165,24 +176,26 @@ bit BlazorUI
    - - @advanceSelectedOption?.Text + + @advancedSelectedOption?.Text
    - - - - - - +
    + + + + + + +
    "; private readonly string example10CsharpCode = @" -private BitNavBarOption? advanceSelectedOption; +private BitNavBarOption? advancedSelectedOption; "; private readonly string example11RazorCode = @" From bcd5bb321294d540f658f8c62384cf883f45641d Mon Sep 17 00:00:00 2001 From: Mohammad Aminsafaei Date: Wed, 27 Nov 2024 18:41:09 +0330 Subject: [PATCH 20/87] fix(blazorui): resolve icon issues of BitSpinButton #9334 (#9345) --- .../Components/Inputs/SpinButton/BitSpinButton.razor | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/SpinButton/BitSpinButton.razor b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/SpinButton/BitSpinButton.razor index 68f92e7ce3..8ac4660c06 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/SpinButton/BitSpinButton.razor +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/SpinButton/BitSpinButton.razor @@ -59,7 +59,7 @@ aria-label="@DecrementAriaLabel"> + class="bit-spb-ico bit-spn-sbi bit-icon bit-icon--@(DecrementIconName ?? "ChevronDownSmall") @Classes?.DecrementIcon" /> } @@ -148,7 +148,7 @@ aria-label="@DecrementAriaLabel"> + class="bit-spb-ico bit-spn-sbi bit-icon bit-icon--@(DecrementIconName ?? "ChevronDownSmall") @Classes?.DecrementIcon" /> @@ -166,7 +166,7 @@ aria-label="@IncrementAriaLabel"> + class="bit-spb-ico bit-spn-sbi bit-icon bit-icon--@(IncrementIconName ?? "ChevronDownSmall bit-ico-r180") @Classes?.IncrementIcon" /> } @@ -187,7 +187,7 @@ aria-label="@IncrementAriaLabel"> + class="bit-spb-ico bit-spn-sbi bit-icon bit-icon--@(IncrementIconName ?? "ChevronDownSmall bit-ico-r180") @Classes?.IncrementIcon" /> } From c1a3921aef86f1363bf13a6075d7c0ff056284d5 Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Wed, 27 Nov 2024 21:48:00 +0100 Subject: [PATCH 21/87] feat(templates): repalce AppCenter with Sentry in Boilerplate #9347 (#9348) --- .github/workflows/admin-sample.cd.yml | 28 ++++------ .github/workflows/bit.full.ci.yml | 4 +- .github/workflows/todo-sample.cd.yml | 32 +++++------ .../.template.config/ide.host.json | 2 +- .../.template.config/template.json | 4 +- .../Boilerplate.Client.Core.csproj | 3 +- .../Layout/AppErrorBoundary.razor.scss | 1 + .../Components/Layout/DiagnosticModal.razor | 4 ++ .../Layout/DiagnosticModal.razor.cs | 6 +++ .../Components/Layout/RootContainer.razor | 6 +-- .../Categories/CategoriesPage.razor.cs | 4 ++ .../Components/Pages/TermsPage.razor | 6 +-- .../Components/Routes.razor | 54 ++++++++++--------- .../IClientCoreServiceCollectionExtensions.cs | 8 ++- .../IConfigurationBuilderExtensions.cs | 4 +- .../Extensions/ILoggingBuilderExtensions.cs | 18 +++++-- .../Services/MessageBoxService.cs | 2 +- .../Services/PromptService.cs | 2 +- .../Boilerplate.Client.Maui.csproj | 4 +- .../MauiProgram.Services.cs | 30 ++++------- .../Boilerplate.Client.Maui/MauiProgram.cs | 35 ++++++++---- .../Services/MauiExceptionHandler.cs | 2 +- .../Program.Services.cs | 6 +-- .../Boilerplate.Client.Windows.csproj | 3 -- .../Program.Services.cs | 33 ++++-------- .../Boilerplate.Client.Windows/Program.cs | 13 +---- .../Services/WindowsExceptionHandler.cs | 6 --- .../src/Directory.Packages.props | 6 +-- .../src/Directory.Packages8.props | 6 +-- .../Boilerplate.Server.Api.csproj | 1 + .../Program.Services.cs | 14 ++--- .../Server/Boilerplate.Server.Api/Program.cs | 4 ++ .../Server/Boilerplate.Server.Web/Program.cs | 4 ++ .../src/Shared/appsettings.Development.json | 28 ++++------ .../src/Shared/appsettings.json | 35 ++++++------ .../Templates05CreateProjectPage.razor | 8 +-- .../Templates05CreateProjectPage.razor.cs | 10 ++-- 37 files changed, 207 insertions(+), 229 deletions(-) diff --git a/.github/workflows/admin-sample.cd.yml b/.github/workflows/admin-sample.cd.yml index 91aecdfe7b..0994ffc478 100644 --- a/.github/workflows/admin-sample.cd.yml +++ b/.github/workflows/admin-sample.cd.yml @@ -36,18 +36,19 @@ 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 --sample Admin --appInsights --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --api Standalone --notification --captcha reCaptcha --signalR --framework net9.0 + cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --appInsights --sentry --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --api Standalone --notification --captcha reCaptcha --signalR --framework net9.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 with: files: 'AdminPanel/src/Shared/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Core/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Web/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Web/appsettings.Production.json' env: + WebAppRender.BlazorMode: BlazorWebAssembly ServerAddress: ${{ env.API_SERVER_ADDRESS }} + Logging.Sentry.Dsn: ${{ secrets.ADMINPANEL_SENTRY_DSN }} GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} - WebAppRender.BlazorMode: BlazorWebAssembly - ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} AdsPushVapid.PublicKey: ${{ secrets.ADMINPANEL_PUBLIC_VAPIDKEY }} + ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - uses: actions/setup-node@v4 with: @@ -144,7 +145,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 --sample Admin --windows --appInsights --appCenter --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --offlineDb --framework net9.0 + cd ..\..\..\ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --windows --appInsights --sentry --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --offlineDb --framework net9.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 @@ -152,13 +153,10 @@ jobs: files: 'AdminPanel\src\Shared\appsettings.json, AdminPanel\src\Client\AdminPanel.Client.Core\appsettings.json, AdminPanel\src\Client\AdminPanel.Client.Windows\appsettings.json' env: ServerAddress: ${{ env.API_SERVER_ADDRESS }} + Logging.Sentry.Dsn: ${{ secrets.ADMINPANEL_SENTRY_DSN }} GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} WindowsUpdate.FilesUrl: https://windows-adminpanel.bitplatform.dev ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - - - name: Set app center secret - run: (Get-Content AdminPanel\src\Client\AdminPanel.Client.Windows\Program.cs) -Replace 'appCenterSecret = null;', 'appCenterSecret = "a9ed2257-fb82-496a-ba10-78c2d9ef33a6";' | Out-File -Encoding utf8 AdminPanel\src\Client\AdminPanel.Client.Windows\Program.cs - shell: pwsh - name: Generate CSS/JS files run: dotnet build AdminPanel\src\Client\AdminPanel.Client.Core\AdminPanel.Client.Core.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release @@ -195,7 +193,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 --sample Admin --appInsights --appCenter --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 + cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --appInsights --sentry --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 - uses: actions/setup-node@v4 with: @@ -221,13 +219,10 @@ jobs: files: 'AdminPanel/src/Shared/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Core/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Maui/appsettings.json' env: ServerAddress: ${{ env.API_SERVER_ADDRESS }} + Logging.Sentry.Dsn: ${{ secrets.ADMINPANEL_SENTRY_DSN }} GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - - name: Set app center secret - run: | - sed -i 's/appCenterSecret = null;/appCenterSecret = "ea9b98ea-93a0-48c7-982a-0a72f4ad6d04";/g' AdminPanel/src/Client/AdminPanel.Client.Maui/MauiProgram.cs - - name: Install maui run: cd src && dotnet workload install maui-android @@ -275,7 +270,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 --sample Admin --appInsights --appCenter --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 + cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --appInsights --sentry --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 @@ -283,13 +278,10 @@ jobs: files: 'AdminPanel/src/Shared/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Core/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Maui/appsettings.json' env: ServerAddress: ${{ env.API_SERVER_ADDRESS }} + Logging.Sentry.Dsn: ${{ secrets.ADMINPANEL_SENTRY_DSN }} GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - - name: Set app center secret - run: | - brew install gnu-sed && gsed -i 's/appCenterSecret = null;/appCenterSecret = "0bc0d910-dc84-4887-a3a0-eee6b1b55797";/g' AdminPanel/src/Client/AdminPanel.Client.Maui/MauiProgram.cs - - name: Install maui run: cd src && dotnet workload install maui diff --git a/.github/workflows/bit.full.ci.yml b/.github/workflows/bit.full.ci.yml index 10a9617f32..aa1c90c845 100644 --- a/.github/workflows/bit.full.ci.yml +++ b/.github/workflows/bit.full.ci.yml @@ -177,12 +177,12 @@ jobs: - name: Test sample configuration 1 run: | - dotnet new bit-bp --name TestProject --database SqlServer --filesStorage AzureBlobStorage --api Integrated --captcha reCaptcha --pipeline Azure --sample Admin --offlineDb --windows --appInsights --appCenter --signalR --notification --framework net9.0 + dotnet new bit-bp --name TestProject --database SqlServer --filesStorage AzureBlobStorage --api Integrated --captcha reCaptcha --pipeline Azure --sample Admin --offlineDb --windows --appInsights --sentry --signalR --notification --framework net9.0 dotnet build TestProject/TestProject.sln -p:MultilingualEnabled=true -p:PwaEnabled=true -p:Environment=Staging - name: Test sample configuration 2 run: | - dotnet new bit-bp --name TestProject2 --database Other --filesStorage Other --api Standalone --captcha None --pipeline None --sample None --offlineDb false --windows false --appInsights false --appCenter false --signalR false --notification false --framework net8.0 + dotnet new bit-bp --name TestProject2 --database Other --filesStorage Other --api Standalone --captcha None --pipeline None --sample None --offlineDb false --windows false --appInsights false --sentry false --signalR false --notification false --framework net8.0 dotnet build TestProject2/TestProject2.sln -p:MultilingualEnabled=false -p:PwaEnabled=false -p:Environment=Development - name: Run BeforeBuildTasks diff --git a/.github/workflows/todo-sample.cd.yml b/.github/workflows/todo-sample.cd.yml index ab68c74182..89cf7a2f2a 100644 --- a/.github/workflows/todo-sample.cd.yml +++ b/.github/workflows/todo-sample.cd.yml @@ -42,19 +42,20 @@ 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 Todo --appInsights --appCenter --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net8.0 + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample Todo --appInsights --sentry --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net8.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 with: files: 'TodoSample/src/Shared/appsettings.json, TodoSample/src/Client/TodoSample.Client.Core/appsettings.json, TodoSample/src/Client/TodoSample.Client.Web/appsettings.json, TodoSample/src/Client/TodoSample.Client.Web/appsettings.Production.json' env: + WebAppRender.PrerenderEnabled: true ServerAddress: ${{ env.SERVER_ADDRESS }} - GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} WebAppRender.BlazorMode: BlazorWebAssembly - WebAppRender.PrerenderEnabled: true - ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} + Logging.Sentry.Dsn: ${{ secrets.TODO_SENTRY_DSN }} AdsPushVapid.PublicKey: ${{ secrets.TODO_PUBLIC_VAPIDKEY }} + GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} + ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - name: Install wasm run: cd src && dotnet workload install wasm-tools @@ -145,7 +146,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 Todo --windows --appInsights --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --framework net8.0 + cd ..\..\..\ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample Todo --windows --appInsights --sentry --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --framework net8.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 @@ -153,13 +154,10 @@ jobs: files: 'TodoSample\src\Shared\appsettings.json, TodoSample\src\Client\TodoSample.Client.Core\appsettings.json, TodoSample\src\Client\TodoSample.Client.Windows\appsettings.json' env: ServerAddress: ${{ env.SERVER_ADDRESS }} - GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} + Logging.Sentry.Dsn: ${{ secrets.TODO_SENTRY_DSN }} WindowsUpdate.FilesUrl: https://windows-todo.bitplatform.dev + GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - - - name: Set app center secret - run: (Get-Content TodoSample\src\Client\TodoSample.Client.Windows\Program.cs) -Replace 'appCenterSecret = null;', 'appCenterSecret = "39f576f2-7c16-4990-af3f-7b70509d41e2";' | Out-File -Encoding utf8 TodoSample\src\Client\TodoSample.Client.Windows\Program.cs - shell: pwsh - name: Generate CSS/JS files run: dotnet build TodoSample\src\Client\TodoSample.Client.Core\TodoSample.Client.Core.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release @@ -204,7 +202,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 Todo --appInsights --appCenter --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net8.0 + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample Todo --appInsights --sentry --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net8.0 - name: Extract Android signing key from env uses: timheuer/base64-to-file@v1.2 @@ -226,13 +224,10 @@ jobs: files: 'TodoSample/src/Shared/appsettings.json, TodoSample/src/Client/TodoSample.Client.Core/appsettings.json, TodoSample/src/Client/TodoSample.Client.Maui/appsettings.json' env: ServerAddress: ${{ env.SERVER_ADDRESS }} + Logging.Sentry.Dsn: ${{ secrets.TODO_SENTRY_DSN }} GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - - name: Set app center secret - run: | - sed -i 's/appCenterSecret = null;/appCenterSecret = "de0219a6-fdcd-44f7-8c28-c108331ed27c";/g' TodoSample/src/Client/TodoSample.Client.Maui/MauiProgram.cs - - name: Install maui run: cd src && dotnet workload install maui-android @@ -304,7 +299,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 Todo --appInsights --appCenter --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net8.0 + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample Todo --appInsights --sentry --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net8.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 @@ -312,13 +307,10 @@ jobs: files: 'TodoSample/src/Shared/appsettings.json, TodoSample/src/Client/TodoSample.Client.Core/appsettings.json, TodoSample/src/Client/TodoSample.Client.Maui/appsettings.json' env: ServerAddress: ${{ env.SERVER_ADDRESS }} + Logging.Sentry.Dsn: ${{ secrets.TODO_SENTRY_DSN }} GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - - name: Set app center secret - run: | - brew install gnu-sed && gsed -i 's/appCenterSecret = null;/appCenterSecret = "f72e6774-1c83-404c-bca8-6e5198fb8e0e";/g' TodoSample/src/Client/TodoSample.Client.Maui/MauiProgram.cs - - name: Install maui run: cd src && dotnet workload install maui 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 1c353fa855..2a8b8763ec 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/ide.host.json @@ -45,7 +45,7 @@ "id": "appInsights" }, { - "id": "appCenter" + "id": "sentry" }, { "id": "notification" diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json index b1d1b39af3..e5a6064e42 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json @@ -213,8 +213,8 @@ "datatype": "bool", "defaultValue": "false" }, - "appCenter": { - "displayName": "Add Visual Studio AppCenter to project?", + "sentry": { + "displayName": "Add Sentry logging to project?", "type": "parameter", "datatype": "bool", "defaultValue": "false" 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 960529a516..da6e6b829f 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 @@ -25,8 +25,9 @@ - + + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppErrorBoundary.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppErrorBoundary.razor.scss index 60e6469f77..fc5dcc74ec 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppErrorBoundary.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppErrorBoundary.razor.scss @@ -3,6 +3,7 @@ main { width: 100%; height: 100%; + background-color: $bit-color-background-primary } .exception { 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 index a4741d0f4a..7418c17099 100644 --- 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 @@ -57,6 +57,10 @@ OnClick="ReloadLogs" IconName="@BitIconName.Refresh" Color="BitColor.SecondaryBackground" /> + + @@ -10,9 +10,7 @@
    - - @ChildContent - + @ChildContent
    diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor.cs index 4e0e93df6a..4a139091b9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Categories/CategoriesPage.razor.cs @@ -101,6 +101,10 @@ private async Task DeleteCategory() await RefreshData(); } + catch (KnownException exp) + { + SnackBarService.Error(exp.Message); + } finally { deletingCategory = null; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/TermsPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/TermsPage.razor index cbb91172e5..a10436fc2b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/TermsPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/TermsPage.razor @@ -43,11 +43,11 @@ Providing certain information is optional, and bit platform will only use it for the purposes of the Boilerplate Demo Version. If the user requests, bit platform will delete this data within one day, using one of the contact methods provided at the end of this EULA. In addition, the user is required to provide an email address for account verification purposes, but bit platform will not use this email for any other purposes. - For more information on data collection and usage by AppInsights and AppCenter, please refer to their documents: + For more information on data collection and usage by AppInsights and Sentry, please refer to their documents:
    Application Insights overview
    - Visual Studio App Center documentation + Sentry documentation
    bit platform also provides a feature in the settings page for users to delete their accounts and their data completely (Account settings). @@ -61,7 +61,7 @@
    Analytics and Cookies - bit platform uses AppInsights and AppCenter to collect data on app performance and user interactions. these data consists of the + bit platform uses AppInsights and Sentry to collect data on app performance and user interactions. these data consists of the User id (generated by the app), User-Session id (an id generated by the app), App-Session id (an id generated by the app), Operating System name and version, and App version. bit platform will not use these data for any purposes other than analytics and in-app features (such as showing all active sessions of the user in the settings page). diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor index acd9f543b3..878de0a5f2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Routes.razor @@ -1,26 +1,28 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 2b79bab437..7756a019eb 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 @@ -127,12 +127,10 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle //#if (appInsights == true) services.Add(ServiceDescriptor.Describe(typeof(IApplicationInsights), typeof(AppInsightsJsSdkService), AppPlatform.IsBrowser ? ServiceLifetime.Singleton : ServiceLifetime.Scoped)); - services.AddBlazorApplicationInsights(x => + services.AddBlazorApplicationInsights(options => { - ClientCoreSettings settings = new(); - configuration.Bind(settings); - x.ConnectionString = settings.ApplicationInsights?.ConnectionString; - }); + configuration.GetRequiredSection("ApplicationInsights").Bind(options); + }, loggingOptions: options => configuration.GetRequiredSection("Logging:ApplicationInsightsLoggerProvider").Bind(options)); //#endif services.AddTypedHttpClients(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IConfigurationBuilderExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IConfigurationBuilderExtensions.cs index f9e5310988..26deef8747 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IConfigurationBuilderExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IConfigurationBuilderExtensions.cs @@ -27,7 +27,7 @@ public static partial class IConfigurationBuilderExtensions /// Client/Client.Windows/appsettings.json /// Client/Client.Windows/appsettings.{environment}.json (If present) ///
    - public static void AddClientConfigurations(this IConfigurationBuilder builder, string clientEntryAssemblyName) + public static IConfigurationBuilder AddClientConfigurations(this IConfigurationBuilder builder, string clientEntryAssemblyName) { IConfigurationBuilder configBuilder = AppPlatform.IsBrowser ? new WebAssemblyHostConfiguration() : new ConfigurationBuilder(); @@ -85,5 +85,7 @@ public static void AddClientConfigurations(this IConfigurationBuilder builder, s builder.Sources.Add(source); } } + + return builder; } } 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 aa9b0d9a84..9affa37b04 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 @@ -1,4 +1,5 @@ -using Boilerplate.Client.Core.Services.DiagnosticLog; +//-:cnd:noEmit +using Boilerplate.Client.Core.Services.DiagnosticLog; namespace Microsoft.Extensions.Logging; @@ -11,8 +12,10 @@ public static ILoggingBuilder AddDiagnosticLogger(this ILoggingBuilder builder) return builder; } - public static ILoggingBuilder ConfigureLoggers(this ILoggingBuilder loggingBuilder) + public static ILoggingBuilder ConfigureLoggers(this ILoggingBuilder loggingBuilder, IConfiguration configuration) { + loggingBuilder.AddConfiguration(configuration.GetSection("Logging")); + if (AppEnvironment.IsDev()) { loggingBuilder.AddDebug(); @@ -20,11 +23,20 @@ public static ILoggingBuilder ConfigureLoggers(this ILoggingBuilder loggingBuild if (!AppPlatform.IsBrowser) // Browser has its own WebAssemblyConsoleLoggerProvider. { - loggingBuilder.AddConsole(); // Device Log / logcat + loggingBuilder.AddConsole(options => configuration.GetRequiredSection("Logging:Console").Bind(options)); // Device Log / logcat } loggingBuilder.AddDiagnosticLogger(); + //#if (sentry == true) + loggingBuilder.AddSentry(options => + { + options.Debug = AppEnvironment.IsDev(); + options.Environment = AppEnvironment.Current; + configuration.GetRequiredSection("Logging:Sentry").Bind(options); + }); + //#endif + return loggingBuilder; } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxService.cs index 15ba511267..d071764907 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxService.cs @@ -15,7 +15,7 @@ public Task Show(string message, string title = "") modalService.Show(parameters, title).ContinueWith(async task => { await task; - tcs.SetResult(false); + tcs.TrySetResult(false); }); return tcs.Task; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PromptService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PromptService.cs index f6d6958223..518ac61ba0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PromptService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PromptService.cs @@ -16,7 +16,7 @@ public partial class PromptService modalService.Show(parameters, title).ContinueWith(async task => { await task; - tcs.SetResult(null); + tcs.TrySetResult(null); }); return tcs.Task; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj index b130e2742a..725fdf6be1 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Boilerplate.Client.Maui.csproj @@ -161,9 +161,7 @@ - - - + 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 7d247368e3..a0b61cae98 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 @@ -28,7 +28,12 @@ public static void ConfigureServices(this MauiAppBuilder builder) }); services.AddSingleton(); - services.AddSingleton(sp => configuration.Get()!); + var settings = new ClientMauiSettings(); + configuration.Bind(settings); + services.AddSingleton(sp => + { + return settings; + }); services.AddSingleton(ITelemetryContext.Current!); if (AppPlatform.IsWindows || AppPlatform.IsMacOS) { @@ -38,37 +43,24 @@ public static void ConfigureServices(this MauiAppBuilder builder) services.AddMauiBlazorWebView(); services.AddBlazorWebViewDeveloperTools(); - builder.Logging.ConfigureLoggers(); - builder.Logging.AddConfiguration(configuration.GetSection("Logging")); + builder.Logging.ConfigureLoggers(configuration); builder.Logging.AddEventSourceLogger(); if (AppPlatform.IsWindows) { - builder.Logging.AddEventLog(); + builder.Logging.AddEventLog(options => configuration.GetRequiredSection("Logging:EventLog").Bind(options)); } //+:cnd:noEmit - - //#if (appCenter == true) - if (Microsoft.AppCenter.AppCenter.Configured) - { - builder.Logging.AddAppCenter(options => options.IncludeScopes = true); - } - //#endif - //#if (appInsights == true) - var connectionString = configuration.Get()!.ApplicationInsights?.ConnectionString; - if (string.IsNullOrEmpty(connectionString) is false) + if (string.IsNullOrEmpty(settings.ApplicationInsights?.ConnectionString) is false) { builder.Logging.AddApplicationInsights(config => { config.TelemetryInitializers.Add(new MauiAppInsightsTelemetryInitializer()); - config.ConnectionString = connectionString; - }, options => - { - options.IncludeScopes = true; - }); + configuration.GetRequiredSection("ApplicationInsights").Bind(config); + }, options => configuration.GetRequiredSection("Logging:ApplicationInsights").Bind(options)); } //#endif //-:cnd:noEmit diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.cs index eaf601ee64..fca485351c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/MauiProgram.cs @@ -20,15 +20,14 @@ public static partial class MauiProgram { public static MauiApp CreateMauiApp() { - //+:cnd:noEmit - //#if (appCenter == true) - string? appCenterSecret = null; - if (appCenterSecret is not null) + AppDomain.CurrentDomain.UnhandledException += (_, e) => LogException(e.ExceptionObject); + TaskScheduler.UnobservedTaskException += (_, e) => { - Microsoft.AppCenter.AppCenter.Start(appCenterSecret, typeof(Microsoft.AppCenter.Crashes.Crashes), typeof(Microsoft.AppCenter.Analytics.Analytics)); - } - //#endif - //-:cnd:noEmit + if (LogException(e.Exception)) + { + e.SetObserved(); + } + }; AppPlatform.IsBlazorHybrid = true; #if iOS @@ -38,11 +37,18 @@ public static MauiApp CreateMauiApp() var builder = MauiApp.CreateBuilder(); + //+:cnd:noEmit builder .UseMauiApp() + //#if (sentry == true) + .UseSentry(options => + { + var configuration = new ConfigurationBuilder().AddClientConfigurations(clientEntryAssemblyName: "Boilerplate.Client.Maui").Build(); + configuration.GetRequiredSection("Logging:Sentry").Bind(options); + }) + //#endif .Configuration.AddClientConfigurations(clientEntryAssemblyName: "Boilerplate.Client.Maui"); - //+:cnd:noEmit //#if (notification == true) if (AppPlatform.IsWindows is false) { @@ -189,4 +195,15 @@ public override void DecidePolicy(WKWebView webView, WKNavigationAction navigati } } #endif + + private static bool LogException(object? error) + { + var errorMessage = error?.ToString() ?? "Unknown error"; + if (IPlatformApplication.Current?.Services is IServiceProvider services && error is Exception exp) + { + services.GetRequiredService().Handle(exp); + return true; + } + return false; + } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiExceptionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiExceptionHandler.cs index b1bc4d3fc7..b134234281 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiExceptionHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiExceptionHandler.cs @@ -2,7 +2,7 @@ namespace Boilerplate.Client.Maui.Services; /// -/// Instead of Client.Core, install AppCenter, Firebase etc in Client.Maui, so the web version of the app won't download unnecessary packages. +/// Instead of Client.Core, install Firebase and similar packages in Client.Maui, so the web version of the app won't download unnecessary packages. /// You can call their APIs such as Crashes.TrackError in MauiExceptionHandler to monitor all exceptions across Android, iOS, Windows, and macOS. /// Employing Microsoft.Extensions.Logging implementations (like Sentry.Extensions.Logging) will result in /// automatic exception logging due to the logger.LogError method call within the ExceptionHandlerBase class. diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Program.Services.cs index e2fb6b9f4e..a23526a4a3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Program.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Program.Services.cs @@ -1,7 +1,6 @@ //+:cnd:noEmit -using Microsoft.Extensions.Logging.Configuration; -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Boilerplate.Client.Web.Services; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; namespace Boilerplate.Client.Web; @@ -13,8 +12,7 @@ public static void ConfigureServices(this WebAssemblyHostBuilder builder) var configuration = builder.Configuration; // The following services are blazor web assembly only. - builder.Logging.ConfigureLoggers(); - builder.Logging.AddConfiguration(configuration.GetSection("Logging")); + builder.Logging.ConfigureLoggers(configuration); services.AddClientWebProjectServices(configuration); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Boilerplate.Client.Windows.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Boilerplate.Client.Windows.csproj index 81dc2b9b60..11e5411ec2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Boilerplate.Client.Windows.csproj +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Boilerplate.Client.Windows.csproj @@ -47,9 +47,6 @@ - - - PreserveNewest 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 a019a1ed72..0132bfcbde 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 @@ -27,10 +27,10 @@ public static void AddClientWindowsProjectServices(this IServiceCollection servi services.AddSingleton(sp => configuration); services.AddSingleton(); services.AddSingleton(); + ClientWindowsSettings settings = new(); + configuration.Bind(settings); services.AddSingleton(sp => { - ClientWindowsSettings settings = new(); - configuration.Bind(settings); return settings; }); services.AddSingleton(ITelemetryContext.Current!); @@ -43,32 +43,19 @@ public static void AddClientWindowsProjectServices(this IServiceCollection servi services.AddLogging(loggingBuilder => { - loggingBuilder.ConfigureLoggers(); - loggingBuilder.AddConfiguration(configuration.GetSection("Logging")); + loggingBuilder.ConfigureLoggers(configuration); loggingBuilder.AddEventSourceLogger(); - loggingBuilder.AddEventLog(); - //#if (appCenter == true) - if (Microsoft.AppCenter.AppCenter.Configured) - { - loggingBuilder.AddAppCenter(options => options.IncludeScopes = true); - } - //#endif + loggingBuilder.AddEventLog(options => configuration.GetRequiredSection("Logging:EventLog").Bind(options)); //#if (appInsights == true) - loggingBuilder.AddApplicationInsights(config => + if (string.IsNullOrEmpty(settings.ApplicationInsights?.ConnectionString) is false) { - config.TelemetryInitializers.Add(new WindowsAppInsightsTelemetryInitializer()); - ClientWindowsSettings settings = new(); - configuration.Bind(settings); - var connectionString = settings.ApplicationInsights?.ConnectionString; - if (string.IsNullOrEmpty(connectionString) is false) + loggingBuilder.AddApplicationInsights(config => { - config.ConnectionString = connectionString; - } - }, options => - { - options.IncludeScopes = true; - }); + config.TelemetryInitializers.Add(new WindowsAppInsightsTelemetryInitializer()); + configuration.GetRequiredSection("ApplicationInsights").Bind(config); + }, options => configuration.GetRequiredSection("Logging:ApplicationInsights").Bind(options)); + } //#endif }); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs index d93b136348..0d2db5f2bd 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs @@ -12,16 +12,9 @@ public partial class Program [STAThread] public static void Main(string[] args) { - //#if (appCenter == true) - string? appCenterSecret = null; - if (appCenterSecret is not null) - { - Microsoft.AppCenter.AppCenter.Start(appCenterSecret, typeof(Microsoft.AppCenter.Crashes.Crashes), typeof(Microsoft.AppCenter.Analytics.Analytics)); - } - //#endif - Application.ThreadException += (_, e) => LogException(e.Exception); AppDomain.CurrentDomain.UnhandledException += (_, e) => LogException(e.ExceptionObject); + TaskScheduler.UnobservedTaskException += (_, e) => { LogException(e.Exception); e.SetObserved(); }; ApplicationConfiguration.Initialize(); @@ -32,10 +25,8 @@ public static void Main(string[] args) Application.SetColorMode(SystemColorMode.System); //#endif + var configuration = new ConfigurationBuilder().AddClientConfigurations(clientEntryAssemblyName: "Boilerplate.Client.Windows").Build(); var services = new ServiceCollection(); - ConfigurationBuilder configurationBuilder = new(); - configurationBuilder.AddClientConfigurations(clientEntryAssemblyName: "Boilerplate.Client.Windows"); - var configuration = configurationBuilder.Build(); services.AddClientWindowsProjectServices(configuration); Services = services.BuildServiceProvider(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsExceptionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsExceptionHandler.cs index caa4289309..784793c994 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsExceptionHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsExceptionHandler.cs @@ -1,11 +1,5 @@ namespace Boilerplate.Client.Windows.Services; -/// -/// Instead of Client.Core, install AppCenter, Firebase etc in Client.Windows, so the web version of the app won't download unnecessary packages. -/// You can call their APIs such as Crashes.TrackError inside WindowsExceptionHandler to monitor all exceptions. -/// Employing Microsoft.Extensions.Logging implementations (like Sentry.Extensions.Logging) will result in -/// automatic exception logging due to the logger.LogError method call within the class. -/// public partial class WindowsExceptionHandler : ClientExceptionHandlerBase { protected override void Handle(Exception exception, bool nonInterrupting, Dictionary parameters) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props index 161c020157..96e9262973 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props @@ -28,6 +28,9 @@ + + + @@ -45,9 +48,6 @@ - - - diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props index 3a05b91f95..371175fc86 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props @@ -28,6 +28,9 @@ + + + @@ -45,9 +48,6 @@ - - - diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj index 73cac2ce67..4557a568f0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs index 9252cfefb5..39c2d33004 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs @@ -113,7 +113,7 @@ public static void AddServerApiProjectServices(this WebApplicationBuilder builde .Configure(opt => opt.Level = CompressionLevel.Fastest); //#if (appInsights == true) - services.AddApplicationInsightsTelemetry(configuration); + services.AddApplicationInsightsTelemetry(options => configuration.GetRequiredSection("ApplicationInsights").Bind(options)); //#endif services.AddCors(builder => @@ -315,8 +315,7 @@ private static void AddIdentity(WebApplicationBuilder builder) }) .AddBearerToken(IdentityConstants.BearerScheme, options => { - options.BearerTokenExpiration = identityOptions.BearerTokenExpiration; - options.RefreshTokenExpiration = identityOptions.RefreshTokenExpiration; + configuration.GetRequiredSection("Identity").Bind(options); var validationParameters = new TokenValidationParameters { @@ -355,9 +354,8 @@ private static void AddIdentity(WebApplicationBuilder builder) { authenticationBuilder.AddGoogle(options => { - options.ClientId = configuration["Authentication:Google:ClientId"]!; - options.ClientSecret = configuration["Authentication:Google:ClientSecret"]!; options.SignInScheme = IdentityConstants.ExternalScheme; + configuration.GetRequiredSection("Authentication:Google").Bind(options); }); } @@ -365,9 +363,8 @@ private static void AddIdentity(WebApplicationBuilder builder) { authenticationBuilder.AddGitHub(options => { - options.ClientId = configuration["Authentication:GitHub:ClientId"]!; - options.ClientSecret = configuration["Authentication:GitHub:ClientSecret"]!; options.SignInScheme = IdentityConstants.ExternalScheme; + configuration.GetRequiredSection("Authentication:GitHub").Bind(options); }); } @@ -375,10 +372,9 @@ private static void AddIdentity(WebApplicationBuilder builder) { authenticationBuilder.AddTwitter(options => { - options.ConsumerKey = configuration["Authentication:Twitter:ConsumerKey"]!; - options.ConsumerSecret = configuration["Authentication:Twitter:ConsumerSecret"]!; options.RetrieveUserDetails = true; options.SignInScheme = IdentityConstants.ExternalScheme; + configuration.GetRequiredSection("Authentication:Twitter").Bind(options); }); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.cs index 298f437122..5f051f2b91 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.cs @@ -11,6 +11,10 @@ public static async Task Main(string[] args) builder.Configuration.AddSharedConfigurations(); + //#if (sentry == true) + builder.WebHost.UseSentry(configureOptions: options => builder.Configuration.GetRequiredSection("Logging:Sentry").Bind(options)); + //#endif + // The following line (using the * in the URL), allows the emulators and mobile devices to access the app using the host IP address. if (builder.Environment.IsDevelopment() && OperatingSystem.IsWindows()) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.cs index a762339406..3e29f8faae 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.cs @@ -21,6 +21,10 @@ public static async Task Main(string[] args) builder.Configuration.AddClientConfigurations(clientEntryAssemblyName: "Boilerplate.Client.Web"); + //#if (sentry == true) + builder.WebHost.UseSentry(configureOptions: options => builder.Configuration.GetRequiredSection("Logging:Sentry").Bind(options)); + //#endif + // The following line (using the * in the URL), allows the emulators and mobile devices to access the app using the host IP address. if (builder.Environment.IsDevelopment() && OperatingSystem.IsWindows()) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.Development.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.Development.json index d2b0879764..17a842ca05 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.Development.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.Development.json @@ -12,8 +12,7 @@ "Default": "Information", "Microsoft.AspNetCore*": "Warning", "System.Net.Http.HttpClient.*.LogicalHandler": "Warning" - }, - "IncludeScopes": true + } }, "ApplicationInsightsLoggerProvider": { "LogLevel": { @@ -21,18 +20,16 @@ "Microsoft.AspNetCore*": "Warning", "System.Net.Http.HttpClient": "Warning", "System.Net.Http.HttpClient.*.LogicalHandler": "Warning" - }, - "IncludeScopes": true + } }, //#endif - //#if (appCenter == true) - "AppCenterLoggerProvider": { + //#if (sentry == true) + "Sentry": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore*": "Warning", "System.Net.Http.HttpClient.*.LogicalHandler": "Warning" - }, - "IncludeScopes": true + } }, //#endif "Console": { @@ -40,8 +37,7 @@ "Default": "Information", "Microsoft.AspNetCore*": "Warning", "System.Net.Http.HttpClient.*.LogicalHandler": "Warning" - }, - "IncludeScopes": true + } }, "WebAssemblyConsoleLoggerProvider": { "LogLevel": { @@ -49,32 +45,28 @@ "Microsoft.AspNetCore*": "Warning", "System.Net.Http.HttpClient": "Warning", "System.Net.Http.HttpClient.*.LogicalHandler": "Warning" - }, - "IncludeScopes": true + } }, "EventLog": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore*": "Warning", "System.Net.Http.HttpClient.*.LogicalHandler": "Warning" - }, - "IncludeScopes": true + } }, "EventSource": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore*": "Warning", "System.Net.Http.HttpClient.*.LogicalHandler": "Warning" - }, - "IncludeScopes": true + } }, "Debug": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore*": "Warning", "System.Net.Http.HttpClient.*.LogicalHandler": "Warning" - }, - "IncludeScopes": true + } } }, "$schema": "https://json.schemastore.org/appsettings.json" diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json index e0f74b2150..d8281cfda8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json @@ -16,54 +16,51 @@ "LogLevel": { "Default": "Warning", "Microsoft.EntityFrameworkCore.Database.Command": "Information" - }, - "IncludeScopes": true + } }, "ApplicationInsightsLoggerProvider": { "LogLevel": { "Default": "Warning", "Microsoft.EntityFrameworkCore.Database.Command": "Information" - }, - "IncludeScopes": true + } }, //#endif - //#if (appCenter == true) - "AppCenterLoggerProvider": { + //#if (sentry == true) + "Sentry": { + "Sentry_Comment": "https://docs.sentry.io/platforms/dotnet/guides/extensions-logging/", + "Dsn": null, + "SendDefaultPii": true, + "EnableScopeSync": true, "LogLevel": { "Default": "Warning", "Boilerplate.Client.Core.Services.AuthManager": "Information", "Microsoft.EntityFrameworkCore.Database.Command": "Information" - }, - "IncludeScopes": true + } }, //#endif "Console": { "LogLevel": { "Default": "Warning", "Microsoft.EntityFrameworkCore.Database.Command": "Information" - }, - "IncludeScopes": true + } }, "WebAssemblyConsoleLoggerProvider": { "LogLevel": { "Default": "Warning", "Microsoft.EntityFrameworkCore.Database.Command": "Information" - }, - "IncludeScopes": true + } }, "EventLog": { "LogLevel": { "Default": "Warning", "Microsoft.EntityFrameworkCore.Database.Command": "Information" - }, - "IncludeScopes": true + } }, "EventSource": { "LogLevel": { "Default": "Warning", "Microsoft.EntityFrameworkCore.Database.Command": "Information" - }, - "IncludeScopes": true + } }, "DiagnosticLogger": { "LogLevel": { @@ -71,14 +68,12 @@ "Microsoft.AspNetCore*": "Warning", "Microsoft.AspNetCore.Authorization": "Information", "System.Net.Http.HttpClient.*.LogicalHandler": "Warning" - }, - "IncludeScopes": true + } }, "Debug": { "LogLevel": { "Default": "None" - }, - "IncludeScopes": true + } } }, "$schema": "https://json.schemastore.org/appsettings.json" 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 c06ecdf545..788865dfef 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 @@ -136,14 +136,14 @@
    - App Center - + Sentry +

    - @GetAppCenterCommand() + @GetSentryCommand() - Adding --appCenter true parameter to the dotnet new command allows you to capture errors, crashes, and analytics data + Adding --sentry true parameter to the dotnet new command allows you to capture errors, crashes, and analytics data seamlessly across all supported platforms.
    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 0b982bf32c..6cfe0ee6cd 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 @@ -7,7 +7,7 @@ public partial class Templates05CreateProjectPage private string name = "MyFirstProject"; private Parameter windows = new() { Value = true, Default = true }; - private Parameter appCenter = new() { Value = false, Default = false }; + private Parameter sentry = new() { Value = false, Default = false }; private Parameter offlineDb = new() { Value = false, Default = false }; private Parameter notification = new() { Value = false, Default = false }; private Parameter appInsight = new() { Value = false, Default = false }; @@ -125,9 +125,9 @@ private string GetFinalCommand() finalCommand.Append(GetWindowsCommand()); } - if (appCenter.IsModified) + if (sentry.IsModified) { - finalCommand.Append(GetAppCenterCommand()); + finalCommand.Append(GetSentryCommand()); } if (database.IsModified) @@ -198,9 +198,9 @@ private string GetWindowsCommand() return $"--windows {windows.Value.ToString().ToLowerInvariant()} "; } - private string GetAppCenterCommand() + private string GetSentryCommand() { - return $"--appCenter {appCenter.Value.ToString().ToLowerInvariant()} "; + return $"--sentry {sentry.Value.ToString().ToLowerInvariant()} "; } private string GetDatabaseCommand() From 2ebbf9f2728b78d0869257a6fa5e117b24f04f73 Mon Sep 17 00:00:00 2001 From: Mohammad Ebrahimi Date: Thu, 28 Nov 2024 13:13:36 +0330 Subject: [PATCH 22/87] feat(templates): add missing tests for pre-rendered html in Boilerplate #9277 (#9278) --- .../Extensions/PlaywrightHydrationExtensions.cs | 4 ++++ .../PageTests/BlazorServerPreRenderingTests.cs | 16 ++++++++++++++++ .../BlazorWebAssemblyPreRenderingTests.cs | 10 +++++++++- .../Tests/PageTests/BlazorWebAssemblyTests.cs | 8 ++++---- .../src/Tests/PageTests/LocalizationTests.cs | 4 ++++ .../src/Tests/PageTests/PageModels/HomePage.cs | 16 +++++++++++----- 6 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightHydrationExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightHydrationExtensions.cs index 6f02accab3..a277f27742 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightHydrationExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Extensions/PlaywrightHydrationExtensions.cs @@ -13,6 +13,10 @@ public static async Task WaitForHydrationToComplete(this IPage page) public static Task EnableHydrationCheck(this IPage page) => page.RouteAsync("**/*", ChangeTitleHandler); + public static Task DisableHydrationCheck(this IBrowserContext context) => context.UnrouteAsync("**/*", ChangeTitleHandler); + + public static Task DisableHydrationCheck(this IPage page) => page.UnrouteAsync("**/*", ChangeTitleHandler); + private static async Task ChangeTitleHandler(IRoute route) { var request = route.Request; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorServerPreRenderingTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorServerPreRenderingTests.cs index dcaed1f5ca..c27137b111 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorServerPreRenderingTests.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorServerPreRenderingTests.cs @@ -11,3 +11,19 @@ public partial class LocalizationTests : BlazorServer.LocalizationTests { public override bool PreRenderEnabled => true; } + +[TestClass] +public partial class PreRenderingTests : PageTestBase +{ + public override bool PreRenderEnabled => true; + + [TestMethod] + [AutoAuthenticate] + public async Task PreRenderedHtml() + { + await Page.GotoAsync($"view-source:{WebAppServerAddress}"); + + await Assertions.Expect(Page.GetByText(TestData.DefaultTestEmail).First).ToBeVisibleAsync(); + await Assertions.Expect(Page.GetByText(TestData.DefaultTestFullName).First).ToBeVisibleAsync(); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyPreRenderingTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyPreRenderingTests.cs index 0b1bb2c1c0..c157c5bdad 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyPreRenderingTests.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyPreRenderingTests.cs @@ -1,4 +1,6 @@ -namespace Boilerplate.Tests.PageTests.BlazorWebAssembly.PreRendering; +using Boilerplate.Client.Web; + +namespace Boilerplate.Tests.PageTests.BlazorWebAssembly.PreRendering; [TestClass] public partial class IdentityPagesTests : BlazorWebAssembly.IdentityPagesTests @@ -11,3 +13,9 @@ public partial class LocalizationTests : BlazorWebAssembly.LocalizationTests { public override bool PreRenderEnabled => true; } + +[TestClass] +public partial class PreRenderingTests : BlazorServer.PreRendering.PreRenderingTests +{ + public override BlazorWebAppMode BlazorRenderMode => BlazorWebAppMode.BlazorWebAssembly; +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyTests.cs index 8184ab661c..761dd822bf 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyTests.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/BlazorWebAssemblyTests.cs @@ -21,10 +21,10 @@ public partial class LocalizationTests : BlazorServer.LocalizationTests [TestCategory("MultilingualDisabled")] public async Task MultilingualDisabled() { - if (CultureInfoManager.MultilingualEnabled is false) + if (CultureInfoManager.MultilingualEnabled) { - Assert.Inconclusive("Multilingual is disabled. " + - "You can enable it via true setting in Directiory.Build.props."); + Assert.Inconclusive("Multilingual is enabled. " + + "You can disable it via false setting in Directiory.Build.props."); return; } @@ -32,7 +32,7 @@ public async Task MultilingualDisabled() await homePage.Open(); await homePage.AssertOpen(); - var contains = PlaywrightAssetCachingExtensions.ContainsAsset("icudt_hybrid.dat"); + var contains = PlaywrightAssetCachingExtensions.ContainsAsset("icudt_hybrid"); Assert.IsFalse(contains, "The 'icudt_hybrid.dat' file must not be loaded when Multilingual is disabled."); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/LocalizationTests.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/LocalizationTests.cs index dd5954a1c1..f22f5e8eb5 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/LocalizationTests.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/LocalizationTests.cs @@ -17,6 +17,7 @@ public async Task AcceptLanguageHeader(string cultureName, string cultureDisplay var homePage = new MainHomePage(Page, WebAppServerAddress); await homePage.Open(); await homePage.AssertLocalized(localizer, cultureName, cultureDisplayName); + await homePage.AssertCultureCombobox(cultureName, cultureDisplayName); } public async Task SetCultureInBrowserContext(BrowserNewContextOptions options, string cultureName, string _) => options.Locale = cultureName; @@ -29,6 +30,7 @@ public async Task LanguageDropDown(string cultureName, string cultureDisplayName await homePage.Open(); await homePage.ChangeCulture(cultureDisplayName); await homePage.AssertLocalized(localizer, cultureName, cultureDisplayName); + await homePage.AssertCultureCombobox(cultureName, cultureDisplayName); } [TestMethod] @@ -40,6 +42,7 @@ public async Task QueryString(string cultureName, string cultureDisplayName) await Page.GotoAsync($"{WebAppServerAddress}?culture={cultureName}"); await Page.WaitForHydrationToComplete(); await homePage.AssertLocalized(localizer, cultureName, cultureDisplayName); + await homePage.AssertCultureCombobox(cultureName, cultureDisplayName); } [TestMethod] @@ -51,6 +54,7 @@ public async Task UrlSegment(string cultureName, string cultureDisplayName) await Page.GotoAsync(new Uri(WebAppServerAddress, cultureName).ToString()); await Page.WaitForHydrationToComplete(); await homePage.AssertLocalized(localizer, cultureName, cultureDisplayName); + await homePage.AssertCultureCombobox(cultureName, cultureDisplayName); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/HomePage.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/HomePage.cs index b39c9a2495..d24cdff50b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/HomePage.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/PageTests/PageModels/HomePage.cs @@ -1,4 +1,5 @@ using Boilerplate.Tests.PageTests.PageModels.Layout; +using Boilerplate.Tests.Services; namespace Boilerplate.Tests.PageTests.PageModels; @@ -14,24 +15,29 @@ public async Task ChangeCulture(string cultureDisplayName) await Page.Locator($"button[role='option']:has-text('{cultureDisplayName}')").ClickAsync(); } + public override async Task AssertOpen() + { + var (displayName, culture) = CultureInfoManager.SupportedCultures.Single(x => x.Culture.Name == CultureInfoManager.DefaultCulture.Name); + var localizer = StringLocalizerFactory.Create(culture.Name); + await AssertLocalized(localizer, culture.Name, displayName); + } + public async Task AssertLocalized(IStringLocalizer localizer, string cultureName, string cultureDisplayName) { await Assertions.Expect(Page).ToHaveTitleAsync(localizer[nameof(AppStrings.HomePageTitle)]); await Assertions.Expect(Page.GetByRole(AriaRole.Heading, new() { Level = 4, Name = localizer[nameof(AppStrings.HomePanelTitle)] + " " + localizer[nameof(AppStrings.HomePanelSubtitle)] })).ToBeVisibleAsync(); - await Assertions.Expect(Page.GetByText(localizer[nameof(AppStrings.HomeMessage)])).ToBeVisibleAsync(); await Assertions.Expect(Page.GetByRole(AriaRole.Link, new() { Name = localizer[nameof(AppStrings.SignIn)] })).ToBeVisibleAsync(); - await Assertions.Expect(Page.GetByRole(AriaRole.Link, new() { Name = localizer[nameof(AppStrings.SignUp)] })).ToBeVisibleAsync(); + } + public async Task AssertCultureCombobox(string cultureName, string cultureDisplayName) + { var cultureDropdown = Page.GetByRole(AriaRole.Combobox).Locator($"//img[contains(@src, 'flags/{cultureName}')]"); - await Assertions.Expect(cultureDropdown).ToBeVisibleAsync(); - await cultureDropdown.ClickAsync(); - await Assertions.Expect(Page.Locator($"button[role='option']:has-text('{cultureDisplayName}')")).ToHaveAttributeAsync("aria-selected", "true"); } } From cac62b965a85facb3fee8cf29df627bffea246d2 Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Fri, 29 Nov 2024 21:51:28 +0100 Subject: [PATCH 23/87] fix(templates): resolve Sentry issues of logging on iOS in Boilerplate #9349 (#9350) --- .github/workflows/admin-sample.cd.yml | 2 +- .github/workflows/todo-sample.cd.yml | 2 +- .../Components/ClientAppCoordinator.cs | 10 +-- .../Layout/AppErrorBoundary.razor.cs | 4 +- .../Components/Layout/DiagnosticModal.razor | 8 ++- .../Layout/DiagnosticModal.razor.cs | 21 +++++- .../Components/Layout/MessageBox.razor | 2 +- .../Components/Layout/Prompt.razor | 4 +- .../Products/AddOrEditProductModal.razor | 4 +- .../Products/AddOrEditProductModal.razor.cs | 2 +- .../Authorized/Products/ProductsPage.razor | 4 +- .../Settings/TwoFactorSection.razor.cs | 2 + .../Pages/Identity/ConfirmPage.razor.cs | 4 +- .../Pages/Identity/SignIn/SignInPage.razor.cs | 3 +- .../Components/Pages/NotAuthorizedPage.razor | 5 +- .../Pages/NotAuthorizedPage.razor.cs | 61 +++++++++--------- .../Extensions/ILoggingBuilderExtensions.cs | 2 +- .../Boilerplate.Client.Core/Scripts/app.ts | 17 ++++- .../Services/AppInsightsJsSdkService.cs | 8 +-- .../Services/AppTelemetryContext.cs | 2 +- .../Services/ClientExceptionHandlerBase.cs | 1 + .../Services/Contracts/ITelemetryContext.cs | 4 +- .../LoggingDelegatingHandler.cs | 2 +- .../Components/Pages/AboutPage.razor | 4 +- .../Components/Pages/AboutPage.razor.cs | 4 +- .../MauiAppInsightsTelemetryInitializer.cs | 2 +- .../Services/MauiTelemetryContext.cs | 2 +- .../ClientWebSettings.cs | 2 +- .../Boilerplate.Client.Web/appsettings.json | 4 +- .../.well-known/apple-app-site-association | 4 +- .../Components/Pages/AboutPage.razor | 4 +- .../Components/Pages/AboutPage.razor.cs | 4 +- .../Boilerplate.Client.Windows/Program.cs | 3 + .../WindowsAppInsightsTelemetryInitializer.cs | 2 +- .../Diagnostics/DiagnosticsController.cs | 64 +++++++++++++++++++ .../IdentityController.SocialSignIn.cs | 8 +-- .../Identity/IdentityController.cs | 3 +- .../ServerApiSettings.cs | 2 +- .../Services/EmailService.cs | 51 ++++++++++----- .../Services/PhoneService.cs | 41 ++++++++---- .../Services/PushNotificationService.cs | 2 +- .../Services/ServerExceptionHandler.cs | 9 +++ .../Boilerplate.Server.Api/appsettings.json | 4 +- .../Boilerplate.Server.Web/appsettings.json | 4 +- .../Diagnostics/IDiagnosticsController.cs | 8 +++ .../Bit.Boilerplate/src/Shared/Mapper.cs | 5 ++ .../src/Shared/Resources/AppStrings.fa.resx | 8 ++- .../src/Shared/Resources/AppStrings.nl.resx | 8 ++- .../src/Shared/Resources/AppStrings.resx | 8 ++- .../src/Tests/Services/FakePhoneService.cs | 12 ++-- 50 files changed, 315 insertions(+), 131 deletions(-) create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Diagnostics/IDiagnosticsController.cs diff --git a/.github/workflows/admin-sample.cd.yml b/.github/workflows/admin-sample.cd.yml index 0994ffc478..8ffa774e73 100644 --- a/.github/workflows/admin-sample.cd.yml +++ b/.github/workflows/admin-sample.cd.yml @@ -270,7 +270,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 --sample Admin --appInsights --sentry --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 + cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --appInsights --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 diff --git a/.github/workflows/todo-sample.cd.yml b/.github/workflows/todo-sample.cd.yml index 89cf7a2f2a..ad5400a129 100644 --- a/.github/workflows/todo-sample.cd.yml +++ b/.github/workflows/todo-sample.cd.yml @@ -299,7 +299,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 Todo --appInsights --sentry --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net8.0 + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample Todo --appInsights --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net8.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 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 5aaa93ca77..eb09997a36 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 @@ -51,7 +51,7 @@ protected override async Task OnInitAsync() TelemetryContext.PageUrl = NavigationManager.Uri; if (AppPlatform.IsBlazorHybrid is false) { - TelemetryContext.OS = await jsRuntime.GetBrowserPlatform(); + TelemetryContext.Platform = await jsRuntime.GetBrowserPlatform(); } //#if (appInsights == true) @@ -71,7 +71,7 @@ protected override async Task OnInitAsync() //#if (signalR == true) SubscribeToSignalREventsMessages(); //#endif - await PropagateUserId(firstRun: true, AuthManager.GetAuthenticationStateAsync()); + await PropagateUserId(firstRun: true, AuthenticationStateTask); } await base.OnInitAsync(); @@ -80,7 +80,7 @@ protected override async Task OnInitAsync() private void NavigationManager_LocationChanged(object? sender, LocationChangedEventArgs e) { TelemetryContext.PageUrl = e.Location; - navigatorLogger.LogInformation("Navigation's location changed to {Location}", e.Location); + navigatorLogger.LogInformation("Navigator's location changed to {Location}", e.Location); } /// @@ -122,7 +122,7 @@ public async Task PropagateUserId(bool firstRun, Task task) var data = TelemetryContext.ToDictionary(); using var scope = authLogger.BeginScope(data); { - authLogger.LogInformation($"Propagating {(firstRun ? "initial" : "changed")} authentication state."); + authLogger.LogInformation("Propagating {AuthStateType} {AuthState} authentication state.", firstRun ? "Initial" : "Updated", user.IsAuthenticated() ? "Authenticated" : "Anonymous"); } //#if (notification == true) @@ -207,7 +207,7 @@ private async Task HubConnectionStateChange(Exception? exception) if (exception is null) { - logger.LogInformation("SignalR state changed {State}", hubConnection!.State); + logger.LogInformation("SignalR state changed to {State}", hubConnection!.State); } else { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppErrorBoundary.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppErrorBoundary.razor.cs index 76896d95f5..64b03cedaf 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppErrorBoundary.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/AppErrorBoundary.razor.cs @@ -34,8 +34,10 @@ private void GoHome() navigationManager.NavigateTo(Urls.HomePage, forceLoad: true); } - private void ShowDiagnostic() + private async Task ShowDiagnostic() { + Recover(); + await Task.Yield(); pubSubService.Publish(ClientPubSubMessages.SHOW_DIAGNOSTIC_MODAL); } } 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 index 7418c17099..6c0a45a262 100644 --- 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 @@ -1,7 +1,7 @@ @inherits AppComponentBase
    - + Diagnostic @@ -58,9 +58,13 @@ IconName="@BitIconName.Refresh" Color="BitColor.SecondaryBackground" /> + - @Localizer[AppStrings.Ok] + @Localizer[nameof(AppStrings.Ok)] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor index a861395750..575837fd03 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor @@ -2,7 +2,7 @@
    - + @Body @if (OtpInput) @@ -24,6 +24,6 @@ - @Localizer[AppStrings.Ok] + @Localizer[nameof(AppStrings.Ok)] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/AddOrEditProductModal.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/AddOrEditProductModal.razor index 7da5968105..6d176d1122 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/AddOrEditProductModal.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/AddOrEditProductModal.razor @@ -43,8 +43,8 @@ diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/AddOrEditProductModal.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/AddOrEditProductModal.razor.cs index a49e5ea436..c2f97648dd 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/AddOrEditProductModal.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/AddOrEditProductModal.razor.cs @@ -28,7 +28,7 @@ public async Task ShowModal(ProductDto productToShow) await InvokeAsync(() => { isOpen = true; - product = productToShow; + productToShow.Patch(product); selectedCategoryId = (product.CategoryId ?? default).ToString(); StateHasChanged(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor index e143b66ec8..81671f1d3f 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor @@ -55,12 +55,12 @@ - + Format="N0" /> { Email = emailModel.Email, Token = emailModel.Token, - DeviceInfo = telemetryContext.OS + DeviceInfo = telemetryContext.Platform }, CurrentCancellationToken); await AuthManager.StoreTokens(signInResponse, true); @@ -110,7 +110,7 @@ await WrapRequest(async () => var signInResponse = await identityController.ConfirmPhone(new() { Token = phoneModel.Token, - DeviceInfo = telemetryContext.OS, + DeviceInfo = telemetryContext.Platform, PhoneNumber = phoneModel.PhoneNumber }, CurrentCancellationToken); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs index d0cbc4b76a..5bfd0acac0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.cs @@ -50,7 +50,6 @@ protected override async Task OnInitAsync() model.UserName = UserNameQueryString; model.Email = EmailQueryString; model.PhoneNumber = PhoneNumberQueryString; - model.DeviceInfo = telemetryContext.OS; if (string.IsNullOrEmpty(OtpQueryString) is false) { @@ -122,6 +121,8 @@ private async Task DoSignIn() CleanModel(); + model.DeviceInfo = telemetryContext.Platform; + requiresTwoFactor = await AuthManager.SignIn(model, CurrentCancellationToken); if (requiresTwoFactor is false) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor index 4c79eb88fe..099663cb4d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor @@ -4,7 +4,7 @@ @Localizer[nameof(AppStrings.NotAuthorizedPageTitle)] -@if (isRefreshingToken) +@if (isUpdatingAuthState) { } @@ -39,5 +39,8 @@ else + + + } \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs index d3d13badac..c9116573b9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs @@ -2,58 +2,41 @@ public partial class NotAuthorizedPage { - private bool isRefreshingToken; private bool lacksValidPrivilege; + private bool isUpdatingAuthState = true; private ClaimsPrincipal user = default!; [SupplyParameterFromQuery(Name = "return-url"), Parameter] public string? ReturnUrl { get; set; } - protected override async Task OnParamsSetAsync() - { - user = (await AuthenticationStateTask).User; - - await base.OnParamsSetAsync(); - } - protected override async Task OnAfterFirstRenderAsync() { - var refreshToken = await StorageService.GetItem("refresh_token"); - - // Let's update the access token by refreshing it when a refresh token is available. - // Following this procedure, the newly acquired access token may now include the necessary roles or claims. - // To prevent infinities redirect loop, let's append try_refreshing_token=false to the url, so we only redirect in case no try_refreshing_token=false is present - - if (string.IsNullOrEmpty(refreshToken) is false && ReturnUrl?.Contains("try_refreshing_token=false", StringComparison.InvariantCulture) is null or false) + try { - isRefreshingToken = true; - StateHasChanged(); - try + var refreshToken = await StorageService.GetItem("refresh_token"); + + // Let's update the access token by refreshing it when a refresh token is available. + // Following this procedure, the newly acquired access token may now include the necessary roles or claims. + if (string.IsNullOrEmpty(refreshToken) is false) { var accessToken = await AuthManager.RefreshToken(requestedBy: nameof(NotAuthorizedPage)); - if (string.IsNullOrEmpty(accessToken) is false && ReturnUrl is not null) + if (string.IsNullOrEmpty(accessToken) is false && ReturnUrl is not null && ReturnUrl.Contains("try_refreshing_token=false", StringComparison.InvariantCulture) is false) { + // To prevent infinities redirect loop, let's append try_refreshing_token=false to the url, so we only redirect in case no try_refreshing_token=false is present var @char = ReturnUrl.Contains('?') ? '&' : '?'; // The RedirectUrl may already include a query string. NavigationManager.NavigateTo($"{ReturnUrl}{@char}try_refreshing_token=false", replace: true); - return; } } - finally - { - isRefreshingToken = false; - StateHasChanged(); - } - } - var user = (await AuthenticationStateTask).User; + user = (await AuthenticationStateTask).User; - if (user.IsAuthenticated() is false) + lacksValidPrivilege = await AuthorizationService.AuthorizeAsync(user, AuthPolicies.PRIVILEGED_ACCESS) is { Succeeded: false }; + } + finally { - await SignOut(); + isUpdatingAuthState = false; + StateHasChanged(); } - lacksValidPrivilege = await AuthorizationService.AuthorizeAsync(user, AuthPolicies.PRIVILEGED_ACCESS) is { Succeeded: false }; - StateHasChanged(); - await base.OnAfterFirstRenderAsync(); } @@ -64,3 +47,17 @@ private async Task SignOut() NavigationManager.NavigateTo(Urls.SignInPage + (string.IsNullOrEmpty(returnUrl) ? string.Empty : $"?return-url={returnUrl}")); } } + +public partial class RedirectToSignInPage : AppComponentBase +{ + [Parameter] public string? ReturnUrl { get; set; } + + protected override async Task OnAfterFirstRenderAsync() + { + await base.OnAfterFirstRenderAsync(); + + await AuthManager.SignOut(CurrentCancellationToken); + var returnUrl = ReturnUrl ?? NavigationManager.ToBaseRelativePath(NavigationManager.Uri); + NavigationManager.NavigateTo(Urls.SignInPage + (string.IsNullOrEmpty(returnUrl) ? string.Empty : $"?return-url={returnUrl}")); + } +} 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 9affa37b04..4b20eaa868 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 @@ -1,4 +1,4 @@ -//-:cnd:noEmit +//+:cnd:noEmit using Boilerplate.Client.Core.Services.DiagnosticLog; namespace Microsoft.Extensions.Logging; 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 e8e2053ed3..9992da2431 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 @@ -17,7 +17,22 @@ class App { } public static getPlatform(): string { - return (navigator as any).userAgentData?.platform || navigator?.platform; + let data = [(navigator as any).userAgentData?.platform ?? navigator?.platform]; + + if (navigator.userAgent.includes('Firefox')) + data.push('Firefox browser'); + else if (navigator.userAgent.includes('Edg')) + data.push('Edge browser'); + else if (navigator.userAgent.includes('OPR')) + data.push('Opera browser'); + else if (navigator.userAgent.includes('Chrome')) + data.push('Chrome browser'); + else if (navigator.userAgent.includes('Safari')) + data.push('Safari browser'); + else + data.push('Unknown browser'); + + return data.filter(d => d != null).join(' '); } public static getTimeZone(): string { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppInsightsJsSdkService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppInsightsJsSdkService.cs index 5562b4111d..a070630091 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppInsightsJsSdkService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppInsightsJsSdkService.cs @@ -128,11 +128,11 @@ public async Task AddTelemetryInitializer(TelemetryItem telemetryItem) try { await applicationInsights.AddTelemetryInitializer(telemetryItem); - telemetryInitializerIsAddedTcs!.SetResult(); + telemetryInitializerIsAddedTcs!.TrySetResult(); } catch (Exception exp) { - telemetryInitializerIsAddedTcs!.SetException(exp); + telemetryInitializerIsAddedTcs!.TrySetException(exp); } } @@ -158,7 +158,7 @@ private async Task EnsureAppInsightsJsFilesAreLoaded() if (await jsRuntime.InvokeAsync("window.hasOwnProperty", "appInsights") && await jsRuntime.InvokeAsync("window.hasOwnProperty", "blazorApplicationInsights")) { - appInsightsJsFilesAreLoaded.SetResult(); + appInsightsJsFilesAreLoaded.TrySetResult(); break; } await Task.Delay(250, cts.Token); @@ -166,7 +166,7 @@ await jsRuntime.InvokeAsync("window.hasOwnProperty", "blazorApplicationIns } catch (Exception exp) { - appInsightsJsFilesAreLoaded.SetException(exp); + appInsightsJsFilesAreLoaded.TrySetException(exp); } } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppTelemetryContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppTelemetryContext.cs index 18e241c1fa..1ecb936ce9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppTelemetryContext.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AppTelemetryContext.cs @@ -11,7 +11,7 @@ public class AppTelemetryContext : ITelemetryContext public Guid AppSessionId { get; set; } = Guid.NewGuid(); - public virtual string? OS { get; set; } = RuntimeInformation.OSDescription; + public virtual string? Platform { get; set; } = RuntimeInformation.OSDescription; public virtual string? AppVersion { get; set; } = typeof(AppTelemetryContext).Assembly.GetName().Version?.ToString(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientExceptionHandlerBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientExceptionHandlerBase.cs index dac046f24f..38dccd5fa8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientExceptionHandlerBase.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientExceptionHandlerBase.cs @@ -24,6 +24,7 @@ public void Handle(Exception exception, parameters[nameof(filePath)] = filePath; parameters[nameof(memberName)] = memberName; parameters[nameof(lineNumber)] = lineNumber; + parameters["exceptionId"] = Guid.NewGuid(); // This will remain consistent across different registered loggers, such as Sentry, Application Insights, etc. Handle(exception, nonInterrupting, parameters.ToDictionary(i => i.Key, i => i.Value ?? string.Empty)); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/ITelemetryContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/ITelemetryContext.cs index 9e471ce55b..cc58092e7a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/ITelemetryContext.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/ITelemetryContext.cs @@ -34,7 +34,7 @@ public static ITelemetryContext? Current public Guid AppSessionId { get; set; } - public string? OS { get; set; } + public string? Platform { get; set; } public string? AppVersion { get; set; } public string? WebView { get; set; } @@ -60,7 +60,7 @@ public static ITelemetryContext? Current { nameof(UserId), UserId }, { nameof(UserSessionId), UserSessionId }, { nameof(AppSessionId), AppSessionId }, - { nameof(OS), OS }, + { nameof(Platform), Platform }, { nameof(AppVersion), AppVersion }, { nameof(PageUrl), PageUrl }, { nameof(UserAgent), UserAgent }, diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/LoggingDelegatingHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/LoggingDelegatingHandler.cs index ee015d9cf0..9e236ee029 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/LoggingDelegatingHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/LoggingDelegatingHandler.cs @@ -24,7 +24,7 @@ protected override async Task SendAsync(HttpRequestMessage using var scope = logger.BeginScope(logScopeData); logger.Log(logLevel, "Received HTTP response for {Uri} after {Duration}ms", request.RequestUri, - stopwatch.ElapsedMilliseconds); + stopwatch.ElapsedMilliseconds.ToString("N0")); } } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Components/Pages/AboutPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Components/Pages/AboutPage.razor index 3035e0431e..020cd029f7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Components/Pages/AboutPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Components/Pages/AboutPage.razor @@ -1,4 +1,4 @@ -@attribute [Route(Urls.AboutPage)] +@attribute [Route(Urls.AboutPage)] @attribute [Route("{culture?}" + Urls.AboutPage)] @inherits AppPageBase @@ -14,7 +14,7 @@ App Name: @appName App Version: @appVersion - OS: @os + OS: @platform Web View: @webView OEM: @oem Environment: @AppEnvironment.Current diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Components/Pages/AboutPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Components/Pages/AboutPage.razor.cs index 0346bc950c..699fbf5340 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Components/Pages/AboutPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Components/Pages/AboutPage.razor.cs @@ -13,7 +13,7 @@ public partial class AboutPage private string appName = default!; private string appVersion = default!; private string processId = default!; - private string os = default!; + private string platform = default!; private string webView = default!; private string oem = default!; @@ -24,7 +24,7 @@ protected async override Task OnInitAsync() // https://stackoverflow.com/a/2941199/2720104 appName = AppInfo.Name; appVersion = telemetryContext.AppVersion!; - os = telemetryContext.OS!; + platform = telemetryContext.Platform!; webView = telemetryContext.WebView!; processId = Environment.ProcessId.ToString(); oem = DeviceInfo.Current.Manufacturer; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiAppInsightsTelemetryInitializer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiAppInsightsTelemetryInitializer.cs index c0eb69ccfd..70e34e4400 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiAppInsightsTelemetryInitializer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiAppInsightsTelemetryInitializer.cs @@ -12,7 +12,7 @@ public void Initialize(ITelemetry telemetry) { telemetry.Context.Session.Id = ITelemetryContext.Current.AppSessionId.ToString(); telemetry.Context.Component.Version = ITelemetryContext.Current.AppVersion; - telemetry.Context.Device.OperatingSystem = ITelemetryContext.Current.OS; + telemetry.Context.Device.OperatingSystem = ITelemetryContext.Current.Platform; telemetry.Context.User.AuthenticatedUserId = ITelemetryContext.Current.UserId?.ToString(); telemetry.Context.GlobalProperties[nameof(ITelemetryContext.UserSessionId)] = ITelemetryContext.Current.UserSessionId?.ToString(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiTelemetryContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiTelemetryContext.cs index a47ca63a89..961e06fdd3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiTelemetryContext.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiTelemetryContext.cs @@ -2,7 +2,7 @@ public class MauiTelemetryContext : AppTelemetryContext { - public override string? OS { get; set; } = $"{DeviceInfo.Current.Manufacturer} {(AppPlatform.IsIosOnMacOS ? DevicePlatform.macOS : DeviceInfo.Current.Platform)} {DeviceInfo.Current.Version}"; + public override string? Platform { get; set; } = $"{DeviceInfo.Current.Manufacturer} {(AppPlatform.IsIosOnMacOS ? DevicePlatform.macOS : DeviceInfo.Current.Platform)} {DeviceInfo.Current.Version}"; public override string? AppVersion { get; set; } = VersionTracking.CurrentVersion; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/ClientWebSettings.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/ClientWebSettings.cs index 204be99a4c..a272a61e46 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/ClientWebSettings.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/ClientWebSettings.cs @@ -94,7 +94,7 @@ public enum BlazorWebAppMode public class AdsPushVapidOptions { /// - /// Web push's vapid. More info at https://vapidkeys.com/ + /// Web push's vapid. More info at https://tools.reactpwa.com/vapid /// [Required] public string PublicKey { get; set; } = default!; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json index d2235cccdd..8991eab6ec 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json @@ -1,4 +1,4 @@ -{ +{ "WebAppRender": { "PrerenderEnabled": false, "PrerenderEnabled_Comment": "for apps with Prerender enabled, follow the instructions in the service-worker.published.js file", @@ -8,7 +8,7 @@ //#if (notification == true) "AdsPushVapid_Comment": "https://github.com/adessoTurkey-dotNET/AdsPush", "AdsPushVapid": { - "AdsPushVapid_Comment": "Web push's vapid. More info at https://vapidkeys.com/", + "AdsPushVapid_Comment": "Web push's vapid. More info at https://tools.reactpwa.com/vapid", "PublicKey": "BDSNUvuIISD8NQVByQANEtZ2foKaENIcIGUxsiQs9kDz11fQik8c9WeiMwUHs3iTgNNH4nvXioNQIEsn4OAjTKc" }, //#endif diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/.well-known/apple-app-site-association b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/.well-known/apple-app-site-association index 979b6f7d64..104f307abb 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/.well-known/apple-app-site-association +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/.well-known/apple-app-site-association @@ -16,7 +16,7 @@ "NOT /healthchecks-ui/*", "NOT /healthz/*", "NOT /swagger/*", - "NOT /signin/*", + "NOT /signin-*", "NOT /.well-known/*", "NOT /sitemap.xml", "*" @@ -33,7 +33,7 @@ "NOT /healthchecks-ui/*", "NOT /healthz/*", "NOT /swagger/*", - "NOT /signin/*", + "NOT /signin-*", "NOT /.well-known/*", "NOT /sitemap.xml", "*" diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Components/Pages/AboutPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Components/Pages/AboutPage.razor index e44d576374..dabbd94cbc 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Components/Pages/AboutPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Components/Pages/AboutPage.razor @@ -1,4 +1,4 @@ -@attribute [Route(Urls.AboutPage)] +@attribute [Route(Urls.AboutPage)] @attribute [Route("{culture?}" + Urls.AboutPage)] @inherits AppPageBase @@ -14,7 +14,7 @@ App Name: @appName App Version: @appVersion - OS: @os + OS: @platform Web View: @webView Environment: @AppEnvironment.Current Process Id: @processId diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Components/Pages/AboutPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Components/Pages/AboutPage.razor.cs index 23e11134a1..b1bc5622cc 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Components/Pages/AboutPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Components/Pages/AboutPage.razor.cs @@ -12,7 +12,7 @@ public partial class AboutPage private string appName = default!; private string appVersion = default!; - private string os = default!; + private string platform = default!; private string webView = default!; private string processId = default!; @@ -22,7 +22,7 @@ protected override async Task OnInitAsync() var asm = typeof(AboutPage).Assembly; appName = asm.GetCustomAttribute()!.Title; appVersion = telemetryContext.AppVersion!; - os = telemetryContext.OS!; + platform = telemetryContext.Platform!; webView = telemetryContext.WebView!; processId = Environment.ProcessId.ToString(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs index 0d2db5f2bd..dd642c5893 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.cs @@ -72,6 +72,9 @@ public static void Main(string[] args) var form = new Form() { Text = "Boilerplate", + Height = 768, + Width = 1024, + MinimumSize = new Size(375, 667), WindowState = FormWindowState.Maximized, BackColor = ColorTranslator.FromHtml("#0D2960"), Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsAppInsightsTelemetryInitializer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsAppInsightsTelemetryInitializer.cs index b6d9ac626c..a8da3ff43d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsAppInsightsTelemetryInitializer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsAppInsightsTelemetryInitializer.cs @@ -12,7 +12,7 @@ public void Initialize(ITelemetry telemetry) { telemetry.Context.Session.Id = ITelemetryContext.Current.AppSessionId.ToString(); telemetry.Context.Component.Version = ITelemetryContext.Current.AppVersion; - telemetry.Context.Device.OperatingSystem = ITelemetryContext.Current.OS; + telemetry.Context.Device.OperatingSystem = ITelemetryContext.Current.Platform; telemetry.Context.User.AuthenticatedUserId = ITelemetryContext.Current.UserId?.ToString(); telemetry.Context.GlobalProperties[nameof(ITelemetryContext.UserSessionId)] = ITelemetryContext.Current.UserSessionId?.ToString(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs new file mode 100644 index 0000000000..afa1cf384e --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs @@ -0,0 +1,64 @@ +//+:cnd:noEmit +using System.Text; +//#if (signalR == true) +using Microsoft.AspNetCore.SignalR; +using Boilerplate.Server.Api.SignalR; +//#endif +using Boilerplate.Server.Api.Services; +using Boilerplate.Shared.Controllers.Diagnostics; + +namespace Boilerplate.Server.Api.Controllers.Diagnostics; + +[ApiController, AllowAnonymous] +[Route("api/[controller]/[action]")] +public partial class DiagnosticsController : AppControllerBase, IDiagnosticsController +{ + //#if (notification == true) + [AutoInject] private PushNotificationService pushNotificationService = default!; + //#endif + //#if (signalR == true) + [AutoInject] private IHubContext appHubContext = default!; + //#endif + + public async Task DoDiagnostics(CancellationToken cancellationToken) + { + StringBuilder result = new(); + + foreach (var header in Request.Headers.Where(h => h.Key.StartsWith("X-", StringComparison.InvariantCulture))) + { + result.AppendLine($"{header.Key}: {header.Value}"); + } + + result.AppendLine($"Client IP: {HttpContext.Connection.RemoteIpAddress}"); + + result.AppendLine($"Trace => {Request.HttpContext.TraceIdentifier}"); + + var isAuthenticated = User.IsAuthenticated(); + Guid? userSessionId = isAuthenticated ? User.GetSessionId() : null; + + result.AppendLine($"IsAuthenticated: {isAuthenticated.ToString().ToLowerInvariant()}"); + + //#if (notification == true) + if (isAuthenticated) + { + var subscription = await DbContext.UserSessions.Include(us => us.PushNotificationSubscription) + .FirstOrDefaultAsync(us => us.Id == userSessionId, cancellationToken); + + result.AppendLine($"Subscription exists: {(subscription?.PushNotificationSubscription is not null).ToString().ToLowerInvariant()}"); + + await pushNotificationService.RequestPush("Test Push", DateTimeOffset.Now.ToString("HH:mm:ss"), "Test action", userRelatedPush: true, u => u.UserSessionId == userSessionId, cancellationToken); + } + //#endif + + //#if (signalR == true) + if (isAuthenticated) + { + await appHubContext.Clients.Client(userSessionId.ToString()!).SendAsync(SignalREvents.SHOW_MESSAGE, DateTimeOffset.Now.ToString("HH:mm:ss"), cancellationToken); + } + //#endif + + result.AppendLine($"Culture => C: {CultureInfo.CurrentCulture.Name}, UC: {CultureInfo.CurrentUICulture.Name}"); + + return result.ToString(); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.SocialSignIn.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.SocialSignIn.cs index a86aca127c..a66fd513ae 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.SocialSignIn.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.SocialSignIn.cs @@ -27,11 +27,11 @@ public async Task SocialSignIn(string provider, string? returnUrl public async Task SocialSignInCallback(string? returnUrl = null, int? localHttpPort = null, CancellationToken cancellationToken = default) { string? url; - - var info = await signInManager.GetExternalLoginInfoAsync() ?? throw new BadRequestException(); + ExternalLoginInfo? info = null; try { + info = await signInManager.GetExternalLoginInfoAsync() ?? throw new BadRequestException(); var email = info.Principal.GetEmail(); var phoneNumber = phoneService.NormalizePhoneNumber(info.Principal.Claims.FirstOrDefault(c => c.Type is ClaimTypes.HomePhone or ClaimTypes.MobilePhone or ClaimTypes.OtherPhone)?.Value); @@ -86,7 +86,7 @@ public async Task SocialSignInCallback(string? returnUrl = null, i } catch (Exception exp) { - LogSocialSignInCallbackFailed(logger, exp, info.LoginProvider, info.Principal.GetDisplayName()); + LogSocialSignInCallbackFailed(logger, exp, info?.LoginProvider, info?.Principal?.GetDisplayName()); url = $"{Urls.SignInPage}?error={Uri.EscapeDataString(exp is KnownException ? Localizer[exp.Message] : Localizer[nameof(AppStrings.UnknownException)])}"; } finally @@ -101,5 +101,5 @@ public async Task SocialSignInCallback(string? returnUrl = null, i } [LoggerMessage(Level = LogLevel.Error, Message = "Failed to perform {loginProvider} social sign in for {principal}")] - private static partial void LogSocialSignInCallbackFailed(ILogger logger, Exception exp, string loginProvider, string principal); + private static partial void LogSocialSignInCallbackFailed(ILogger logger, Exception exp, string? loginProvider, string? principal); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs index ac96ce92b4..100195e018 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs @@ -232,7 +232,8 @@ public async Task> Refresh(RefreshRequestDto requ if (string.IsNullOrEmpty(request.ElevatedAccessToken) is false) { - var tokenIsValid = await userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, FormattableString.Invariant($"ElevatedAccess:{userSession.Id},{user.ElevatedAccessTokenRequestedOn?.ToUniversalTime()}"), request.ElevatedAccessToken); + var tokenIsValid = await userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultPhoneProvider, FormattableString.Invariant($"ElevatedAccess:{userSession.Id},{user.ElevatedAccessTokenRequestedOn?.ToUniversalTime()}"), request.ElevatedAccessToken) + || await userManager.VerifyTwoFactorTokenAsync(user, userManager.Options.Tokens.AuthenticatorTokenProvider, request.ElevatedAccessToken); if (tokenIsValid is false) { await userManager.AccessFailedAsync(user); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs index 7d377016ac..c810c39852 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/ServerApiSettings.cs @@ -81,7 +81,7 @@ public override IEnumerable Validate(ValidationContext validat //#endif //#if (notification == true) - if (AdsPushVapid?.PrivateKey is "dMIR1ICj-lDWYZ-ZYCwXKyC2ShYayYYkEL-oOPnpq9c" || AdsPushVapid?.Subject is "mailto: ") + if (AdsPushVapid?.PrivateKey is "dMIR1ICj-lDWYZ-ZYCwXKyC2ShYayYYkEL-oOPnpq9c" || AdsPushVapid?.Subject is "mailto:test@bitplatform.dev") { throw new InvalidOperationException("The AdsPushVapid's PrivateKey and Subject are not set. Please set them in the server's appsettings.json file."); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/EmailService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/EmailService.cs index f8512610fe..e510b613c4 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/EmailService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/EmailService.cs @@ -1,6 +1,6 @@ -using Microsoft.AspNetCore.Components; +using FluentEmail.Core; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; -using FluentEmail.Core; using Boilerplate.Server.Api.Models.Emailing; using Boilerplate.Server.Api.Models.Identity; @@ -9,13 +9,12 @@ namespace Boilerplate.Server.Api.Services; public partial class EmailService { [AutoInject] private HtmlRenderer htmlRenderer = default!; - [AutoInject] private IStringLocalizer localizer = default!; - [AutoInject] private IFluentEmail fluentEmail = default!; - [AutoInject] private IStringLocalizer emailLocalizer = default!; - [AutoInject] private IHttpContextAccessor httpContextAccessor = default!; [AutoInject] private ILogger logger = default!; - [AutoInject] private IHostEnvironment hostEnvironment = default!; [AutoInject] private ServerApiSettings appSettings = default!; + [AutoInject] private IHostEnvironment hostEnvironment = default!; + [AutoInject] private IHttpContextAccessor httpContextAccessor = default!; + [AutoInject] private IStringLocalizer emailLocalizer = default!; + [AutoInject] private RootServiceScopeProvider rootServiceScopeProvider = default!; public async Task SendResetPasswordToken(User user, string token, Uri link, CancellationToken cancellationToken) { @@ -132,16 +131,38 @@ private async Task BuildBody(Dictionary para private async Task SendEmail(string body, string toEmailAddress, string toName, string subject, CancellationToken cancellationToken) { - var emailResult = await fluentEmail.To(toEmailAddress, toName) - .Subject(subject) - .SetFrom(appSettings.Email!.DefaultFromEmail, emailLocalizer[nameof(EmailStrings.DefaultFromName)]) - .Body(body, isHtml: true) - .SendAsync(cancellationToken); - - if (emailResult.Successful is false) - throw new ResourceValidationException(emailResult.ErrorMessages.Select(err => localizer[err]).ToArray()); + var defaultFromName = emailLocalizer[nameof(EmailStrings.DefaultFromName)]; + var defaultFromEmail = appSettings.Email!.DefaultFromEmail; + + _ = Task.Run(async () => // Let's not wait for the email to be sent. Consider using a proper message queue or background job system like Hangfire. + { + await using var scope = rootServiceScopeProvider.Invoke(); + var logger = scope.ServiceProvider.GetRequiredService>(); + + try + { + var fluentEmail = scope.ServiceProvider.GetRequiredService(); + var localizer = scope.ServiceProvider.GetRequiredService>(); + var emailResult = await fluentEmail.To(toEmailAddress, toName) + .Subject(subject) + .SetFrom(defaultFromEmail, defaultFromName) + .Body(body, isHtml: true) + .SendAsync(default); + + if (emailResult.Successful is false) + throw new ResourceValidationException(emailResult.ErrorMessages.Select(err => localizer[err]).ToArray()); + } + catch (Exception exp) + { + LogSendEmailFailed(logger, exp, subject, toEmailAddress); + } + + }, default); } + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to send e-mail with subject '{Subject}' to {ToEmailAddress}.")] + private static partial void LogSendEmailFailed(ILogger logger, Exception exp, string subject, string toEmailAddress); + [LoggerMessage(Level = LogLevel.Information, Message = "{type} e-mail with subject '{subject}' to {toEmailAddress}. {link}")] private static partial void LogSendEmail(ILogger logger, string subject, string toEmailAddress, string type, string? link = null); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PhoneService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PhoneService.cs index 6fc360a93c..ba23d29f51 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PhoneService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PhoneService.cs @@ -6,10 +6,11 @@ namespace Boilerplate.Server.Api.Services; public partial class PhoneService { [AutoInject] private readonly ServerApiSettings appSettings = default!; - [AutoInject] private readonly ILogger logger = default!; + [AutoInject] private readonly PhoneNumberUtil phoneNumberUtil = default!; [AutoInject] private readonly IHostEnvironment hostEnvironment = default!; + [AutoInject] private readonly ILogger phoneLogger = default!; [AutoInject] private readonly IHttpContextAccessor httpContextAccessor = default!; - [AutoInject] private readonly PhoneNumberUtil phoneNumberUtil = default!; + [AutoInject] private readonly RootServiceScopeProvider rootServiceScopeProvider = default!; public virtual string? NormalizePhoneNumber(string? phoneNumber) { @@ -30,27 +31,41 @@ public virtual async Task SendSms(string messageText, string phoneNumber, Cancel { if (hostEnvironment.IsDevelopment()) { - LogSendSms(logger, messageText, phoneNumber); + LogSendSms(phoneLogger, messageText, phoneNumber); } if (appSettings.Sms?.Configured is false) return; - var messageOptions = new CreateMessageOptions(new(phoneNumber)) - { - From = new(appSettings.Sms!.FromPhoneNumber), - Body = messageText - }; + var from = appSettings.Sms!.FromPhoneNumber; - var smsMessage = MessageResource.Create(messageOptions); + _ = Task.Run(async () => // Let's not wait for the sms to be sent. Consider using a proper message queue or background job system like Hangfire. + { + await using var scope = rootServiceScopeProvider.Invoke(); + var logger = scope.ServiceProvider.GetRequiredService>(); + MessageResource? smsMessage = null; + try + { + var messageOptions = new CreateMessageOptions(new(phoneNumber)) + { + From = new(from), + Body = messageText + }; - if (smsMessage.ErrorCode is null) return; + smsMessage = MessageResource.Create(messageOptions); - LogSendSmsFailed(logger, phoneNumber, smsMessage.ErrorCode, smsMessage.ErrorMessage); + if (smsMessage.ErrorCode is not null) + throw new InvalidOperationException(smsMessage.ErrorMessage); + } + catch (Exception exp) + { + LogSendSmsFailed(logger, exp, phoneNumber, smsMessage?.ErrorCode); + } + }, default); } [LoggerMessage(Level = LogLevel.Information, Message = "SMS: {message} to {phoneNumber}.")] private static partial void LogSendSms(ILogger logger, string message, string phoneNumber); - [LoggerMessage(Level = LogLevel.Error, Message = "Failed to send Sms to {phoneNumber}. Code: {errorCode}, Error message: {errorMessage}")] - private static partial void LogSendSmsFailed(ILogger logger, string phoneNumber, int? errorCode, string errorMessage); + [LoggerMessage(Level = LogLevel.Error, Message = "Failed to send Sms to {phoneNumber}. Code: {errorCode}")] + private static partial void LogSendSmsFailed(ILogger logger, Exception exp, string phoneNumber, int? errorCode); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs index c910970b62..33469d0166 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/PushNotificationService.cs @@ -76,7 +76,7 @@ public async Task RequestPush(string? title = null, string? message = null, stri var subscriptions = await query.ToListAsync(cancellationToken); - _ = Task.Run(async () => // Let's not wait for the push notification to be sent + _ = Task.Run(async () => // Let's not wait for the push notification to be sent. Consider using a proper message queue or background job system like Hangfire. { await using var scope = rootServiceScopeProvider.Invoke(); var adsPushSender = scope.ServiceProvider.GetRequiredService(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerExceptionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerExceptionHandler.cs index c5b1b653ea..30179b7d53 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerExceptionHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerExceptionHandler.cs @@ -1,6 +1,7 @@ using System.Net; using Microsoft.Net.Http.Headers; using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Authentication; namespace Boilerplate.Server.Api.Services; @@ -15,6 +16,14 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e httpContext.Response.Headers.Append(HeaderNames.RequestId, httpContext.TraceIdentifier); var exception = UnWrapException(e); + + if (exception is AuthenticationFailureException) + { + httpContext.Response.Redirect($"{Urls.SignInPage}?error={Uri.EscapeDataString(exception.Message)}"); + + return true; + } + var knownException = exception as KnownException; // The details of all of the exceptions are returned only in dev mode. in any other modes like production, only the details of the known exceptions are returned. diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json index 771c051304..0c7ca54c08 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json @@ -61,8 +61,8 @@ //#if (notification == true) "AdsPushVapid_Comment": "https://github.com/adessoTurkey-dotNET/AdsPush", "AdsPushVapid": { - "AdsPushVapid_Comment": "Web push's vapid. More info at https://vapidkeys.com/", - "Subject": "mailto: ", + "AdsPushVapid_Comment": "Web push's vapid. More info at https://tools.reactpwa.com/vapid", + "Subject": "mailto:test@bitplatform.dev", "PrivateKey": "dMIR1ICj-lDWYZ-ZYCwXKyC2ShYayYYkEL-oOPnpq9c", "PublicKey_Comment": "Set public key in Client.Core's appsettings.json" }, diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json index 55463fad28..4d1dea9cd8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json @@ -51,8 +51,8 @@ "GoogleRecaptchaSecretKey": "6LdMKr4pAAAAANvngWNam_nlHzEDJ2t6SfV6L_DS", "AdsPushVapid_Comment": "https://github.com/adessoTurkey-dotNET/AdsPush", "AdsPushVapid": { - "AdsPushVapid_Comment": "Web push's vapid. More info at https://vapidkeys.com/", - "Subject": "mailto: ", + "AdsPushVapid_Comment": "Web push's vapid. More info at https://tools.reactpwa.com/vapid/", + "Subject": "mailto:test@bitplatform.dev", "PrivateKey": "dMIR1ICj-lDWYZ-ZYCwXKyC2ShYayYYkEL-oOPnpq9c", "PublicKey_Comment": "Set public key in Client.Core's appsettings.json" }, diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Diagnostics/IDiagnosticsController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Diagnostics/IDiagnosticsController.cs new file mode 100644 index 0000000000..c82185e5e7 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Diagnostics/IDiagnosticsController.cs @@ -0,0 +1,8 @@ +namespace Boilerplate.Shared.Controllers.Diagnostics; + +[Route("api/[controller]/[action]/")] +public interface IDiagnosticsController : IAppController +{ + [HttpPost] + Task DoDiagnostics(CancellationToken cancellationToken); +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Mapper.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Mapper.cs index 6f803dcfb9..5a97dd8bc0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Mapper.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Mapper.cs @@ -1,7 +1,10 @@ //+:cnd:noEmit using Boilerplate.Shared.Dtos.Identity; + //#if (sample == "Todo") using Boilerplate.Shared.Dtos.Todo; +//#elif (sample == "Admin") +using Boilerplate.Shared.Dtos.Products; //#endif using Riok.Mapperly.Abstractions; @@ -21,6 +24,8 @@ public static partial class Mapper { //#if (sample == "Todo") public static partial void Patch(this TodoItemDto source, TodoItemDto destination); + //#elif (sample == "Admin") + public static partial void Patch(this ProductDto source, ProductDto destination); //#endif public static partial void Patch(this UserDto source, UserDto destination); public static partial void Patch(this EditUserDto source, UserDto destination); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx index b44c0267bb..c5084fba7b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.fa.resx @@ -848,6 +848,12 @@ هنگامی که کد QR را اسکن کردید یا کلید بالا را وارد کردید، برنامه احراز هویت دو عاملی شما یک کد منحصر به فرد را در اختیار شما قرار می دهد. کد را در کادر تایید زیر وارد کنید. + + + احراز هویت دو مرحله‌ای فعال شد. + + + احراز هویت دو مرحله‌ای غیرفعال شد. کد تایید: @@ -931,7 +937,7 @@ توکن 2FA تولید و برای شما ارسال شده است. - لطفا توکن دریافت دسترسی ویژه را وارد کنید + لطفا کد ۶ رقمی که ارسال کردیم یا کد Authenticator app خود را وارد کنید توکن {0} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx index 1739c6760f..11de1bd6ee 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.nl.resx @@ -848,6 +848,12 @@ Nadat je de QR-code hebt gescand of de bovenstaande sleutel hebt ingevoerd, geeft je tweefactorauthenticatie-app je een unieke code. Voer de code in het bevestigingsvak hieronder in. + + + Twee-factor-authenticatie is ingeschakeld. + + + Twee-factor-authenticatie is uitgeschakeld. Verificatie code: @@ -931,7 +937,7 @@ Het 2FA-token is gegenereerd en naar u verzonden. - Voer het verhoogde toegangstoken in om door te gaan. + Voer de verhoogde toegangstoken in die we u zojuist hebben gestuurd of de code van uw authenticator-app om door te gaan. Token {0} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx index 43a401f549..28fa73ae10 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Resources/AppStrings.resx @@ -848,6 +848,12 @@ Once you have scanned the QR code or input the key above, your two factor authentication app will provide you with a unique code. Enter the code in the confirmation box below. + + + Two-factor authentication has been enabled. + + + Two-factor authentication has been disabled. Verification Code: @@ -931,7 +937,7 @@ The 2FA token has been generated and sent to you. - Please enter the elevated access token to continue. + Please enter the elevated access token we just sent you or your authenticator app code to continue. Token {0} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/FakePhoneService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/FakePhoneService.cs index d6be667b77..c43953b909 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/FakePhoneService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Tests/Services/FakePhoneService.cs @@ -1,15 +1,15 @@ -using System.Collections.Concurrent; -using System.Text.RegularExpressions; +using PhoneNumbers; using Boilerplate.Server.Api; -using Boilerplate.Server.Api.Services; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using PhoneNumbers; +using System.Collections.Concurrent; +using System.Text.RegularExpressions; +using Boilerplate.Server.Api.Services; namespace Boilerplate.Tests.Services; -public partial class FakePhoneService(ServerApiSettings appSettings, IHostEnvironment hostEnvironment, IHttpContextAccessor httpContextAccessor, ILogger logger, PhoneNumberUtil phoneNumberUtil) - : PhoneService(appSettings, hostEnvironment, httpContextAccessor, logger, phoneNumberUtil) +public partial class FakePhoneService(ServerApiSettings appSettings, IHostEnvironment hostEnvironment, IHttpContextAccessor httpContextAccessor, ILogger logger, PhoneNumberUtil phoneNumberUtil, RootServiceScopeProvider rootServiceScopeProvider) + : PhoneService(appSettings, hostEnvironment, httpContextAccessor, logger, phoneNumberUtil, rootServiceScopeProvider) { private static readonly ConcurrentDictionary LastSmsPerPhone = new(); From e696dd54b582a6fdfda5fa2e02625357c907039e Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Sat, 30 Nov 2024 00:31:20 +0330 Subject: [PATCH 24/87] feat(prerelease): v-9.1.0-pre-04 #9354 (#9357) --- src/Besql/Bit.Besql/wwwroot/bit-besql.js | 2 +- src/Bit.Build.props | 4 ++-- src/BlazorUI/Bit.BlazorUI/Scripts/general.ts | 2 +- .../Bit.BlazorUI.Demo.Server.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Shared.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Client.Core.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Client.Maui.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Client.Web.csproj | 6 +++--- .../wwwroot/service-worker.published.js | 2 +- .../Bit.BlazorUI.Demo.Client.Windows.csproj | 4 ++-- src/BlazorUI/Demo/Directory.Build.props | 2 +- .../Bit.Bswup.Demo/wwwroot/service-worker.js | 2 +- .../wwwroot/service-worker.published.js | 2 +- .../wwwroot/service-worker.js | 2 +- .../wwwroot/service-worker.published.js | 2 +- .../Bit.Bswup/Scripts/bit-bswup.progress.ts | 2 +- src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts | 2 +- src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts | 2 +- .../FullDemo/Client/wwwroot/service-worker.js | 2 +- .../Client/wwwroot/service-worker.published.js | 2 +- src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts | 2 +- src/Bup/Bit.Bup/Scripts/bit-bup.ts | 2 +- src/Butil/Bit.Butil/Scripts/butil.ts | 2 +- .../BlazorEmpty.Client.csproj | 8 ++++---- .../BlazorEmpty/BlazorEmpty.csproj | 8 ++++---- .../wwwroot/service-worker.published.js | 2 +- .../Bit.Boilerplate/src/Directory.Build.props | 2 +- .../src/Directory.Packages.props | 18 +++++++++--------- .../src/Directory.Packages8.props | 18 +++++++++--------- .../Bit.Websites.Careers.Client.csproj | 10 +++++----- .../Bit.Websites.Careers.Server.csproj | 4 ++-- .../Bit.Websites.Careers.Shared.csproj | 4 ++-- src/Websites/Careers/src/Directory.Build.props | 2 +- .../Bit.Websites.Platform.Client.csproj | 12 ++++++------ .../Templates03GettingStartedPage.razor | 4 ++-- .../Templates03GettingStartedPage.razor.cs | 2 +- .../Bit.Websites.Platform.Server.csproj | 4 ++-- .../Bit.Websites.Platform.Shared.csproj | 4 ++-- .../Platform/src/Directory.Build.props | 2 +- .../Bit.Websites.Sales.Client.csproj | 10 +++++----- .../Bit.Websites.Sales.Server.csproj | 4 ++-- .../Bit.Websites.Sales.Shared.csproj | 4 ++-- src/Websites/Sales/src/Directory.Build.props | 2 +- 43 files changed, 93 insertions(+), 93 deletions(-) diff --git a/src/Besql/Bit.Besql/wwwroot/bit-besql.js b/src/Besql/Bit.Besql/wwwroot/bit-besql.js index f2eb857a70..c335510454 100644 --- a/src/Besql/Bit.Besql/wwwroot/bit-besql.js +++ b/src/Besql/Bit.Besql/wwwroot/bit-besql.js @@ -1,5 +1,5 @@ var BitBesql = BitBesql || {}; -BitBesql.version = window['bit-besql version'] = '9.1.0-pre-03'; +BitBesql.version = window['bit-besql version'] = '9.1.0-pre-04'; async function synchronizeDbWithCache(file) { diff --git a/src/Bit.Build.props b/src/Bit.Build.props index 60ae82f8b4..6dbff0c1b8 100644 --- a/src/Bit.Build.props +++ b/src/Bit.Build.props @@ -27,8 +27,8 @@ 9.1.0 - https://github.com/bitfoundation/bitplatform/releases/tag/v-$(ReleaseVersion)-pre-03 - $(ReleaseVersion)-pre-03 + https://github.com/bitfoundation/bitplatform/releases/tag/v-$(ReleaseVersion)-pre-04 + $(ReleaseVersion)-pre-04 $(ReleaseVersion).$([System.DateTime]::Now.ToString(HHmm)) diff --git a/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts b/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts index 7e765f04cd..49d7673bd6 100644 --- a/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts +++ b/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts @@ -1,4 +1,4 @@ -(BitBlazorUI as any).version = (window as any)['bit-blazorui version'] = '9.1.0-pre-03'; +(BitBlazorUI as any).version = (window as any)['bit-blazorui version'] = '9.1.0-pre-04'; interface DotNetObject { invokeMethod(methodIdentifier: string, ...args: any[]): T; diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj index bd8369f5a7..fc5606e7c2 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj @@ -5,11 +5,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj index 1c28303de2..47d044058c 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj @@ -5,11 +5,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj index 0e65a0f0fc..2941ff7ca7 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj @@ -16,11 +16,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj index fade96ae13..97cd9f8713 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj @@ -85,12 +85,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj index c022347fe9..01a014b1eb 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj @@ -24,13 +24,13 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js index fe7297ed3c..1de622cabe 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-03 +// bit version: 9.1.0-pre-04 // https://github.com/bitfoundation/bitplatform/tree/develop/src/Bswup self.assetsInclude = []; diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj index 012cd9413c..a677d72859 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj @@ -29,11 +29,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Directory.Build.props b/src/BlazorUI/Demo/Directory.Build.props index 85b5d9961c..1e28858485 100644 --- a/src/BlazorUI/Demo/Directory.Build.props +++ b/src/BlazorUI/Demo/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js index 96fae42b5e..3d17920d94 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-03 +// bit version: 9.1.0-pre-04 self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; self.caseInsensitiveUrl = true; diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js index 4074439054..606af822e6 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-03 +// bit version: 9.1.0-pre-04 self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; self.caseInsensitiveUrl = true; diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js index 6a5b38d129..f53be0a51e 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js +++ b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-03 +// bit version: 9.1.0-pre-04 // In development, always fetch from the network and do not enable offline support. // This is because caching would make development more difficult (changes would not diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js index 92bf83bdc8..852d206ba6 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js +++ b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-03 +// bit version: 9.1.0-pre-04 self.assetsInclude = []; self.assetsExclude = [ diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts index 4ccb0144bd..f709822b3c 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts @@ -1,4 +1,4 @@ -window['bit-bswup.progress version'] = '9.1.0-pre-03'; +window['bit-bswup.progress version'] = '9.1.0-pre-04'; ; (function () { (window as any).startBswupProgress = (autoReload: boolean, diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts index 0319ccbf3e..267aa70693 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts @@ -1,4 +1,4 @@ -self['bit-bswup.sw version'] = '9.1.0-pre-03'; +self['bit-bswup.sw version'] = '9.1.0-pre-04'; interface Window { clients: any diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts index 5aafc10555..d618ee2279 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts @@ -1,5 +1,5 @@ const BitBswup = {} as any; -BitBswup.version = window['bit-bswup version'] = '9.1.0-pre-03'; +BitBswup.version = window['bit-bswup version'] = '9.1.0-pre-04'; declare const Blazor: any; diff --git a/src/Bswup/FullDemo/Client/wwwroot/service-worker.js b/src/Bswup/FullDemo/Client/wwwroot/service-worker.js index 0df920b0e1..746afaaf53 100644 --- a/src/Bswup/FullDemo/Client/wwwroot/service-worker.js +++ b/src/Bswup/FullDemo/Client/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-03 +// bit version: 9.1.0-pre-04 // In development, always fetch from the network and do not enable offline support. // This is because caching would make development more difficult (changes would not diff --git a/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js b/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js index eaee3a7e71..4e9a196507 100644 --- a/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js +++ b/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-03 +// bit version: 9.1.0-pre-04 self.assetsInclude = []; self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; diff --git a/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts b/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts index 8750b01e5f..81f67a2615 100644 --- a/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts +++ b/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts @@ -1,4 +1,4 @@ -window['bit-bup.progress version'] = '9.1.0-pre-03'; +window['bit-bup.progress version'] = '9.1.0-pre-04'; ; (function () { (window as any).startBupProgress = (showLogs: boolean, showAssets: boolean, appContainerSelector: string, hideApp: boolean, autoHide: boolean) => { diff --git a/src/Bup/Bit.Bup/Scripts/bit-bup.ts b/src/Bup/Bit.Bup/Scripts/bit-bup.ts index 8f076ce745..94b621475a 100644 --- a/src/Bup/Bit.Bup/Scripts/bit-bup.ts +++ b/src/Bup/Bit.Bup/Scripts/bit-bup.ts @@ -1,5 +1,5 @@ var BitBup = BitBup || {}; -BitBup.version = window['bit-bup version'] = '9.1.0-pre-03'; +BitBup.version = window['bit-bup version'] = '9.1.0-pre-04'; declare const Blazor: any; diff --git a/src/Butil/Bit.Butil/Scripts/butil.ts b/src/Butil/Bit.Butil/Scripts/butil.ts index be0945572d..4149eaac32 100644 --- a/src/Butil/Bit.Butil/Scripts/butil.ts +++ b/src/Butil/Bit.Butil/Scripts/butil.ts @@ -1,2 +1,2 @@ var BitButil = BitButil || {}; -BitButil.version = window['bit-butil version'] = '9.1.0-pre-03'; \ No newline at end of file +BitButil.version = window['bit-butil version'] = '9.1.0-pre-04'; \ No newline at end of file diff --git a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj index fe51c99abd..36c6b40480 100644 --- a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj +++ b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj @@ -1,4 +1,4 @@ - + @@ -17,14 +17,14 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj index e012d9e724..55351eae52 100644 --- a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj +++ b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj @@ -1,4 +1,4 @@ - + @@ -19,14 +19,14 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js index 52f55a4059..80443e737d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js @@ -1,5 +1,5 @@ //+:cnd:noEmit -// bit version: 9.1.0-pre-03 +// bit version: 9.1.0-pre-04 // https://github.com/bitfoundation/bitplatform/tree/develop/src/Bswup //#if (notification == true) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props index 9c012b0be9..60d15f9530 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props @@ -1,4 +1,4 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props index 96e9262973..c87d83cf25 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props @@ -1,14 +1,14 @@  - - - - - - - - + + + + + + + + @@ -45,7 +45,7 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props index 371175fc86..55c094a8d0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props @@ -1,14 +1,14 @@  - - - - - - - - + + + + + + + + @@ -45,7 +45,7 @@ - + diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj index e3b418ade1..59a84dc572 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj @@ -22,15 +22,15 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj index 7b6554607f..2c44ab8911 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj index 13b569df1a..ea7b0f0d47 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Directory.Build.props b/src/Websites/Careers/src/Directory.Build.props index 0e9f27554f..d5bea38995 100644 --- a/src/Websites/Careers/src/Directory.Build.props +++ b/src/Websites/Careers/src/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj index 294dd3babd..70fbbb4c08 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj @@ -22,16 +22,16 @@ - - - + + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor index 656ef0cc4b..f3eaa7d3e9 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor @@ -167,8 +167,8 @@ rm $HOME/dotnet.tar.gz }
  • -
    Install Bit Boilerplate project template
    - dotnet new install Bit.Boilerplate::9.1.0-pre-03 +
    Install Bit Boilerplate project template
    + dotnet new install Bit.Boilerplate::9.1.0-pre-04
  • @if (showCrossPlatform && devOS is "Windows") { diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs index 8922e68853..f74dec8b6b 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs @@ -38,7 +38,7 @@ public partial class Templates03GettingStartedPage command:"dotnet nuget add source \"https://api.nuget.org/v3/index.json\" --name \"nuget.org\"; dotnet workload install wasm-tools;"), (text:@"echo 'Install the Bit.Boilerplate project template https://www.nuget.org/packages/Boilerplate.Templates';", - command:"dotnet new install Bit.Boilerplate::9.1.0-pre-03;") + command:"dotnet new install Bit.Boilerplate::9.1.0-pre-04;") ]; if (enableVirtualization) diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj index 13a328590d..1c6469761c 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj index 13b569df1a..ea7b0f0d47 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj @@ -6,11 +6,11 @@
    - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Directory.Build.props b/src/Websites/Platform/src/Directory.Build.props index 82e05d814e..115b00a13e 100644 --- a/src/Websites/Platform/src/Directory.Build.props +++ b/src/Websites/Platform/src/Directory.Build.props @@ -1,4 +1,4 @@ - + preview diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj index bf081ffcea..b74f3b0462 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj @@ -22,15 +22,15 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj index 357d65350d..769a26898e 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj index 13b569df1a..ea7b0f0d47 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Directory.Build.props b/src/Websites/Sales/src/Directory.Build.props index fa240bd8d0..6b4f7a7660 100644 --- a/src/Websites/Sales/src/Directory.Build.props +++ b/src/Websites/Sales/src/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 From 14d26640b2c2ded4d6c2b6dd04a4caf0353509aa Mon Sep 17 00:00:00 2001 From: Mohammad Aminsafaei Date: Sat, 30 Nov 2024 09:46:00 +0330 Subject: [PATCH 25/87] feat(blazorui): add Float feature to BitButton #9352 (#9355) --- .../BitBadgePosition.cs => BitPosition.cs} | 2 +- .../Buttons/BitButton/BitButton.razor.cs | 35 +++++++ .../Buttons/BitButton/BitButton.scss | 55 +++++++++++ .../Notifications/Badge/BitBadge.razor.cs | 20 ++-- .../Components/Buttons/BitButtonDemo.razor | 35 +++++-- .../Components/Buttons/BitButtonDemo.razor.cs | 91 ++++++++++++++++++- .../Buttons/BitButtonDemo.razor.samples.cs | 46 +++++++--- .../Notifications/Badge/BitBadgeDemo.razor.cs | 20 ++-- 8 files changed, 263 insertions(+), 41 deletions(-) rename src/BlazorUI/Bit.BlazorUI/Components/{Notifications/Badge/BitBadgePosition.cs => BitPosition.cs} (85%) diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadgePosition.cs b/src/BlazorUI/Bit.BlazorUI/Components/BitPosition.cs similarity index 85% rename from src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadgePosition.cs rename to src/BlazorUI/Bit.BlazorUI/Components/BitPosition.cs index e137d21dd6..b0547bfe6b 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadgePosition.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/BitPosition.cs @@ -1,6 +1,6 @@ namespace Bit.BlazorUI; -public enum BitBadgePosition +public enum BitPosition { TopLeft, TopCenter, diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.razor.cs index 0cc8381d65..bd611eed18 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.razor.cs @@ -65,6 +65,24 @@ public partial class BitButton : BitComponentBase [Parameter, ResetClassBuilder] public bool FixedColor { get; set; } + /// + /// Apply floating behavior. + /// + [Parameter, ResetClassBuilder] + public bool Float { get; set; } + + /// + /// Apply position absolute when the button is in floating mode. + /// + [Parameter, ResetClassBuilder] + public bool FloatAbsolute { get; set; } + + /// + /// The position of the button in floating mode. + /// + [Parameter, ResetClassBuilder] + public BitPosition? FloatPosition { get; set; } + /// /// Expand the button width to 100% of the available width. /// @@ -229,6 +247,23 @@ protected override void RegisterCssClasses() ClassBuilder.Register(() => FixedColor ? "bit-btn-ftc" : string.Empty); ClassBuilder.Register(() => FullWidth ? "bit-btn-flw" : string.Empty); + + ClassBuilder.Register(() => Float && FloatAbsolute ? "bit-btn-fab" : + Float ? "bit-btn-flt" : string.Empty); + + ClassBuilder.Register(() => Float ? FloatPosition switch + { + BitPosition.TopRight => "bit-btn-trg", + BitPosition.TopCenter => "bit-btn-tcr", + BitPosition.TopLeft => "bit-btn-tlf", + BitPosition.CenterLeft => "bit-btn-clf", + BitPosition.BottomLeft => "bit-btn-blf", + BitPosition.BottomCenter => "bit-btn-bcr", + BitPosition.BottomRight => "bit-btn-brg", + BitPosition.CenterRight => "bit-btn-crg", + BitPosition.Center => "bit-btn-ctr", + _ => "bit-btn-brg" + } : string.Empty); } protected override void RegisterCssStyles() diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss index abbf42f882..f4c489da04 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss @@ -159,6 +159,16 @@ width: 100%; } +.bit-btn-flt { + position: fixed; + z-index: $zindex-base; +} + +.bit-btn-fab { + position: absolute; + z-index: $zindex-base; +} + .bit-btn-pri { --bit-btn-clr: #{$clr-pri}; --bit-btn-clr-txt: #{$clr-pri-text}; @@ -347,3 +357,48 @@ padding: var(--bit-btn-pad-ntx); --bit-btn-icn-margintop: 0; } + +.bit-btn-trg { + top: spacing(1); + right: spacing(1); +} + +.bit-btn-tcr { + top: spacing(1); + left: calc(50% - var(--bit-btn-spn-size)); +} + +.bit-btn-tlf { + top: spacing(1); + left: spacing(1); +} + +.bit-btn-clf { + left: spacing(1); + top: calc(50% - var(--bit-btn-spn-size)); +} + +.bit-btn-blf { + left: spacing(1); + bottom: spacing(1); +} + +.bit-btn-bcr { + bottom: spacing(1); + left: calc(50% - var(--bit-btn-spn-size)); +} + +.bit-btn-brg { + right: spacing(1); + bottom: spacing(1); +} + +.bit-btn-crg { + right: spacing(1); + top: calc(50% - var(--bit-btn-spn-size)); +} + +.bit-btn-ctr { + top: calc(50% - var(--bit-btn-spn-size)); + left: calc(50% - var(--bit-btn-spn-size)); +} \ No newline at end of file diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadge.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadge.razor.cs index 83c04214f5..1f2cd6be14 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadge.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadge.razor.cs @@ -67,7 +67,7 @@ public partial class BitBadge : BitComponentBase /// The position of the badge. ///
    [Parameter, ResetClassBuilder] - public BitBadgePosition? Position { get; set; } + public BitPosition? Position { get; set; } /// /// The size of badge, Possible values: Small | Medium | Large @@ -113,15 +113,15 @@ protected override void RegisterCssClasses() ClassBuilder.Register(() => Position switch { - BitBadgePosition.TopRight => "bit-bdg-trg", - BitBadgePosition.TopCenter => "bit-bdg-tcr", - BitBadgePosition.TopLeft => "bit-bdg-tlf", - BitBadgePosition.CenterLeft => "bit-bdg-clf", - BitBadgePosition.BottomLeft => "bit-bdg-blf", - BitBadgePosition.BottomCenter => "bit-bdg-bcr", - BitBadgePosition.BottomRight => "bit-bdg-brg", - BitBadgePosition.CenterRight => "bit-bdg-crg", - BitBadgePosition.Center => "bit-bdg-ctr", + BitPosition.TopRight => "bit-bdg-trg", + BitPosition.TopCenter => "bit-bdg-tcr", + BitPosition.TopLeft => "bit-bdg-tlf", + BitPosition.CenterLeft => "bit-bdg-clf", + BitPosition.BottomLeft => "bit-bdg-blf", + BitPosition.BottomCenter => "bit-bdg-bcr", + BitPosition.BottomRight => "bit-bdg-brg", + BitPosition.CenterRight => "bit-bdg-crg", + BitPosition.Center => "bit-bdg-ctr", _ => "bit-bdg-trg" }); diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor index e9acf05018..21ea2dd5d3 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor @@ -209,7 +209,26 @@ - + + +
    Demonstrates a floating button with adjustable float modes and customizable positions.
    +
    +
    + + +
    +
    + +
    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras eu ligula quis orci accumsan pharetra. Fusce mattis sit amet enim vitae imperdiet. Maecenas hendrerit sapien nisl, quis consectetur mi bibendum vel. Pellentesque vel rhoncus quam, non bibendum arcu. Vivamus euismod tellus non felis finibus, dictum finibus eros elementum. Vivamus a massa sit amet leo volutpat blandit at vel tortor. Praesent posuere, nulla eu tempus accumsan, nibh elit rhoncus mauris, eu semper tellus risus et nisi. Duis felis ipsum, luctus eget ultrices sit amet, scelerisque quis metus.
    Suspendisse blandit erat ac lobortis pulvinar. Donec nunc leo, tempus sit amet accumsan in, sagittis sed odio. Pellentesque tristique felis sed purus pellentesque, ac dictum ex fringilla. Integer a tincidunt eros, non porttitor turpis. Sed gravida felis massa, in viverra massa aliquam sit amet. Etiam vitae dolor in velit sodales tristique id nec turpis. Proin sit amet urna sollicitudin, malesuada enim et, lacinia mi. Fusce nisl massa, efficitur sit amet elementum convallis, porttitor vel turpis. Fusce congue dui sit amet mollis pulvinar. Suspendisse vulputate leo quis nunc tincidunt, nec dictum risus congue.

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras eu ligula quis orci accumsan pharetra. Fusce mattis sit amet enim vitae imperdiet. Maecenas hendrerit sapien nisl, quis consectetur mi bibendum vel. Pellentesque vel rhoncus quam, non bibendum arcu. Vivamus euismod tellus non felis finibus, dictum finibus eros elementum. Vivamus a massa sit amet leo volutpat blandit at vel tortor. Praesent posuere, nulla eu tempus accumsan, nibh elit rhoncus mauris, eu semper tellus risus et nisi. Duis felis ipsum, luctus eget ultrices sit amet, scelerisque quis metus.
    Suspendisse blandit erat ac lobortis pulvinar. Donec nunc leo, tempus sit amet accumsan in, sagittis sed odio. Pellentesque tristique felis sed purus pellentesque, ac dictum ex fringilla. Integer a tincidunt eros, non porttitor turpis. Sed gravida felis massa, in viverra massa aliquam sit amet. Etiam vitae dolor in velit sodales tristique id nec turpis. Proin sit amet urna sollicitudin, malesuada enim et, lacinia mi. Fusce nisl massa, efficitur sit amet elementum convallis, porttitor vel turpis. Fusce congue dui sit amet mollis pulvinar. Suspendisse vulputate leo quis nunc tincidunt, nec dictum risus congue.

    +
    +
    +
    +
    +
    + +
    BitButton supports three different types, 'Submit' for sending form data, 'Reset' to clear form inputs, and 'Button' to provide flexibility for different interaction purposes.

    @@ -241,7 +260,7 @@
    - +
    Here are some examples of customizing the button content.

    @@ -291,7 +310,7 @@
    - +
    Managing button click event (OnClick). @@ -303,7 +322,7 @@ - +
    Varying sizes for buttons tailored to meet diverse design needs, ensuring flexibility and visual hierarchy within your interface.

    @@ -345,7 +364,7 @@
    - +
    Setting the FullWidth makes the button occupy 100% of its container's width.

    @@ -355,7 +374,7 @@
    - +
    Offering a range of specialized color variants with Primary being the default, providing visual cues for specific actions or states within your application.

    @@ -469,7 +488,7 @@
    - +
    Empower customization by overriding default styles and classes, allowing tailored design modifications to suit specific UI requirements.


    @@ -512,7 +531,7 @@
    - +
    Use BitButton in right-to-left (RTL).

    diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.cs index 5bc909c0d0..0f94dcaad2 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.cs @@ -74,6 +74,29 @@ public partial class BitButtonDemo Description = "Preserves the foreground color of the button through hover and focus.", }, new() + { + Name = "Float", + Type = "bool", + DefaultValue = "false", + Description = "Apply floating behavior.", + }, + new() + { + Name = "FloatAbsolute", + Type = "bool", + DefaultValue = "false", + Description = "Apply position absolute when the button is in floating mode.", + }, + new() + { + Name = "FloatPosition", + Type = "bool", + DefaultValue = "false", + Description = "The position of the button in floating mode.", + LinkType = LinkType.Link, + Href = "#button-position" + }, + new() { Name = "FullWidth", Type = "bool", @@ -599,7 +622,61 @@ public partial class BitButtonDemo Description = "A tag (keyword) for the current document." } ] - } + }, + new() + { + Id = "button-position", + Name = "BitPosition", + Description = "", + Items = + [ + new() + { + Name = "TopLeft", + Value = "0" + }, + new() + { + Name = "TopCenter", + Value = "1" + }, + new() + { + Name = "TopRight", + Value = "2" + }, + new() + { + Name = "CenterLeft", + Value = "3" + }, + new() + { + Name = "Center", + Value = "4" + }, + new() + { + Name = "CenterRight", + Value = "5" + }, + new() + { + Name = "BottomLeft", + Value = "6" + }, + new() + { + Name = "BottomCenter", + Value = "7" + }, + new() + { + Name = "BottomRight", + Value = "8" + } + ] + }, ]; private bool fillIsLoading; @@ -611,6 +688,18 @@ public partial class BitButtonDemo private bool templateIsLoading; + private BitPosition floatPosition; + private bool floatingTypeIsViewPort = true; + + private List> floatPositionList = Enum.GetValues(typeof(BitPosition)) + .Cast() + .Select(enumValue => new BitDropdownItem + { + Value = enumValue, + Text = enumValue.ToString() + }) + .ToList(); + private async Task LoadingFillClick() { fillIsLoading = true; diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.samples.cs index 7d34795322..87bf256c53 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.samples.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.samples.cs @@ -161,6 +161,30 @@ Open bitplatform.dev with a rel attribute (nofollow & noreferrer) "; private readonly string example9RazorCode = @" + + + +
    + +
    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras eu ligula quis orci accumsan pharetra. Fusce mattis sit amet enim vitae imperdiet. Maecenas hendrerit sapien nisl, quis consectetur mi bibendum vel. Pellentesque vel rhoncus quam, non bibendum arcu. Vivamus euismod tellus non felis finibus, dictum finibus eros elementum. Vivamus a massa sit amet leo volutpat blandit at vel tortor. Praesent posuere, nulla eu tempus accumsan, nibh elit rhoncus mauris, eu semper tellus risus et nisi. Duis felis ipsum, luctus eget ultrices sit amet, scelerisque quis metus.
    Suspendisse blandit erat ac lobortis pulvinar. Donec nunc leo, tempus sit amet accumsan in, sagittis sed odio. Pellentesque tristique felis sed purus pellentesque, ac dictum ex fringilla. Integer a tincidunt eros, non porttitor turpis. Sed gravida felis massa, in viverra massa aliquam sit amet. Etiam vitae dolor in velit sodales tristique id nec turpis. Proin sit amet urna sollicitudin, malesuada enim et, lacinia mi. Fusce nisl massa, efficitur sit amet elementum convallis, porttitor vel turpis. Fusce congue dui sit amet mollis pulvinar. Suspendisse vulputate leo quis nunc tincidunt, nec dictum risus congue.

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras eu ligula quis orci accumsan pharetra. Fusce mattis sit amet enim vitae imperdiet. Maecenas hendrerit sapien nisl, quis consectetur mi bibendum vel. Pellentesque vel rhoncus quam, non bibendum arcu. Vivamus euismod tellus non felis finibus, dictum finibus eros elementum. Vivamus a massa sit amet leo volutpat blandit at vel tortor. Praesent posuere, nulla eu tempus accumsan, nibh elit rhoncus mauris, eu semper tellus risus et nisi. Duis felis ipsum, luctus eget ultrices sit amet, scelerisque quis metus.
    Suspendisse blandit erat ac lobortis pulvinar. Donec nunc leo, tempus sit amet accumsan in, sagittis sed odio. Pellentesque tristique felis sed purus pellentesque, ac dictum ex fringilla. Integer a tincidunt eros, non porttitor turpis. Sed gravida felis massa, in viverra massa aliquam sit amet. Etiam vitae dolor in velit sodales tristique id nec turpis. Proin sit amet urna sollicitudin, malesuada enim et, lacinia mi. Fusce nisl massa, efficitur sit amet elementum convallis, porttitor vel turpis. Fusce congue dui sit amet mollis pulvinar. Suspendisse vulputate leo quis nunc tincidunt, nec dictum risus congue.

    +
    +
    "; + private readonly string example9CsharpCode = @" +private BitPosition floatPosition; +private bool floatingTypeIsViewPort = true; + +private List> floatPositionList = Enum.GetValues(typeof(BitPosition)) + .Cast() + .Select(enumValue => new BitDropdownItem + { + Value = enumValue, + Text = enumValue.ToString() + }) + .ToList();"; + + private readonly string example10RazorCode = @" @@ -172,7 +196,7 @@ Open bitplatform.dev with a rel attribute (nofollow & noreferrer) Button
    "; - private readonly string example9CsharpCode = @" + private readonly string example10CsharpCode = @" public class ButtonValidationModel { [Required] @@ -191,7 +215,7 @@ private async Task HandleValidSubmit() StateHasChanged(); }"; - private readonly string example10RazorCode = @" + private readonly string example11RazorCode = @" + + +
    + @foreach (var (idx, i) in basicItems) + { +
    @(idx.ToString().PadLeft(2, '0')). Item @i
    + } +
    +
    "; + private readonly string example1CsharpCode = @" +private (int, int)[] basicItems = GenerateRandomNumbers(1, 51); +private async Task HandleOnRefreshBasic() +{ + await Task.Delay(2000); + basicItems = GenerateRandomNumbers(1, 51); + _ = Task.Delay(1000).ContinueWith(_ => InvokeAsync(StateHasChanged)); +} + +private static (int, int)[] GenerateRandomNumbers(int min, int max) +{ + var random = new Random(); + return Enumerable.Range(min, max - min).Select(i => (i, random.Next(min, max))).ToArray(); +}"; + + private readonly string example2RazorCode = @" + + + + +
    + @foreach (var (idx, i) in customItems) + { +
    @(idx.ToString().PadLeft(2, '0')). Item @i
    + } +
    +
    + + + + + + + + +
    "; + private readonly string example2CsharpCode = @" +private (int, int)[] customItems = GenerateRandomNumbers(1, 51); +private async Task HandleOnRefreshCustom() +{ + await Task.Delay(2000); + customItems = GenerateRandomNumbers(1, 51); + _ = Task.Delay(1000).ContinueWith(_ => InvokeAsync(StateHasChanged)); +} + +private static (int, int)[] GenerateRandomNumbers(int min, int max) +{ + var random = new Random(); + return Enumerable.Range(min, max - min).Select(i => (i, random.Next(min, max))).ToArray(); +}"; + + private readonly string example3RazorCode = @" + + +
    +
    + +
    + @foreach (var (idx, i) in multiItems1) + { +
    @(idx.ToString().PadLeft(2, '0')). Item @i
    + } +
    +
    +
    + +
    + +
    + @foreach (var (idx, i) in multiItems2) + { +
    @(idx.ToString().PadLeft(2, '0')). Item @i
    + } +
    +
    +
    +
    "; + private readonly string example3CsharpCode = @" +private (int, int)[] multiItems1 = GenerateRandomNumbers(0, 50); +private async Task HandleOnRefresh1() +{ + await Task.Delay(2000); + multiItems1 = GenerateRandomNumbers(1, 51); + _ = Task.Delay(1000).ContinueWith(_ => InvokeAsync(StateHasChanged)); +} +private (int, int)[] multiItems2 = GenerateRandomNumbers(51, 101); +private async Task HandleOnRefresh2() +{ + await Task.Delay(2000); + multiItems2 = GenerateRandomNumbers(51, 101); + _ = Task.Delay(1000).ContinueWith(_ => InvokeAsync(StateHasChanged)); +} + +private static (int, int)[] GenerateRandomNumbers(int min, int max) +{ + var random = new Random(); + return Enumerable.Range(min, max - min).Select(i => (i, random.Next(min, max))).ToArray(); +}"; + +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/PullToRefresh/BitPullToRefreshDemo.razor.scss b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/PullToRefresh/BitPullToRefreshDemo.razor.scss new file mode 100644 index 0000000000..c11b6eeb5d --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/PullToRefresh/BitPullToRefreshDemo.razor.scss @@ -0,0 +1,12 @@ +.anchor { + width: 150px; + padding: 4px; + cursor: grab; + height: 300px; + overflow: auto; + user-select: none; + border: 1px gray solid; +} + +::deep { +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor index 3b0daddf91..619dd830b3 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor @@ -226,6 +226,9 @@ Overlay + + PullToRefresh + Separator diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs index 73a93a565f..e16622235b 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs @@ -141,6 +141,7 @@ public partial class NavMenu : IDisposable new() { Text = "Label", Url = "/components/label" }, new() { Text = "Link", Url = "/components/link", Description = "Anchor" }, new() { Text = "Overlay", Url = "/components/overlay" }, + new() { Text = "PullToRefresh", Url = "/components/pulltorefresh" }, new() { Text = "Separator", Url = "/components/separator" }, new() { Text = "Sticky", Url = "/components/sticky" }, new() { Text = "Text", Url = "/components/text" }, diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json index f83a0d1b06..b0b71bb475 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json @@ -389,6 +389,12 @@ "minify": { "enabled": false }, "options": { "sourceMap": false } }, + { + "outputFile": "Pages/Components/Utilities/PullToRefresh/BitPullToRefreshDemo.razor.css", + "inputFile": "Pages/Components/Utilities/PullToRefresh/BitPullToRefreshDemo.razor.scss", + "minify": { "enabled": false }, + "options": { "sourceMap": false } + }, { "outputFile": "Pages/Components/Utilities/Separator/BitSeparatorDemo.razor.css", "inputFile": "Pages/Components/Utilities/Separator/BitSeparatorDemo.razor.scss", From f25d0de98e448f17ea9255baec806e421edca22c Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Sun, 1 Dec 2024 20:03:15 +0330 Subject: [PATCH 28/87] feat(blazorui): add BitModalService #9322 (#9325) --- .../Bit.BlazorUI.Extras.csproj | 11 +- .../ModalService/BitModalContainer.razor | 10 ++ .../ModalService/BitModalContainer.razor.cs | 63 ++++++++ .../ModalService/BitModalReference.cs | 50 ++++++ .../ModalService/BitModalService.cs | 150 ++++++++++++++++++ .../IServiceCollectionExtensions.cs | 21 +++ .../Surfaces/Modal/BitModalTests.cs | 42 ++--- .../Components/Surfaces/Modal/BitModal.razor | 24 +-- .../Surfaces/Modal/BitModal.razor.cs | 92 +++++++---- .../Components/Surfaces/Modal/BitModal.ts | 3 + .../Surfaces/Modal/BitModalClassStyles.cs | 11 ++ .../Surfaces/Modal/BitModalParameters.cs | 119 ++++++++++++++ .../Surfaces/Modal/BitModalPosition.cs | 13 -- src/BlazorUI/Bit.BlazorUI/Utils/BitShortId.cs | 2 +- .../Utils/IServiceCollectionExtensions.cs | 12 +- .../Components/ComponentDemo.razor | 8 +- .../Components/ComponentDemo.razor.cs | 2 +- .../IServiceCollectionExtensions.cs | 3 +- .../ModalService/BitModalServiceDemo.razor | 20 +++ .../ModalService/BitModalServiceDemo.razor.cs | 71 +++++++++ .../BitModalServiceDemo.razor.scss | 0 .../Extras/ModalService/ModalContent.razor | 18 +++ .../PdfReader/BitPdfReaderDemo.razor.cs | 3 +- .../Surfaces/Modal/BitModalDemo.razor | 18 +-- .../Surfaces/Modal/BitModalDemo.razor.cs | 30 ++-- .../Pages/Home/ComponentsSection.razor | 3 + .../Shared/NavMenu.razor.cs | 8 + 27 files changed, 695 insertions(+), 112 deletions(-) create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalReference.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalService.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/IServiceCollectionExtensions.cs create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModalParameters.cs delete mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModalPosition.cs create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/BitModalServiceDemo.razor create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/BitModalServiceDemo.razor.cs create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/BitModalServiceDemo.razor.scss create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/ModalContent.razor diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj b/src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj index be0b23b41b..f6cc045e49 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj @@ -4,10 +4,8 @@ net9.0;net8.0 - enable - Bit.BlazorUI true - 0 + enable BeforeBuildTasks; $(ResolveStaticWebAssetsInputsDependsOn) @@ -35,6 +33,7 @@ + @@ -57,9 +56,9 @@ - - - + + + diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor new file mode 100644 index 0000000000..18595aad3b --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor @@ -0,0 +1,10 @@ +@namespace Bit.BlazorUI + +@foreach (var modalReference in _modalRefs) +{ + + + @modalReference.Modal + + +} \ No newline at end of file diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor.cs new file mode 100644 index 0000000000..49d89b3568 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor.cs @@ -0,0 +1,63 @@ +using System.Collections.Concurrent; +using System.Threading.Tasks; + +namespace Bit.BlazorUI; + +public partial class BitModalContainer : IDisposable +{ + private readonly List _modalRefs = []; + + + + [Parameter] public BitModalParameters ModalParameters { get; set; } = new(); + + + + [Inject] private BitModalService _modalService { get; set; } = default!; + + + + internal void InjectPersistentModals(ConcurrentQueue queue) + { + while (queue.TryDequeue(out var modalRef)) + { + _modalRefs.Add(modalRef); + } + } + + + + protected override void OnInitialized() + { + base.OnInitialized(); + + _modalService.InitContainer(this); + + _modalService.OnAddModal += OnModalAdd; + _modalService.OnCloseModal += OnCloseModal; + } + + + + private Task OnModalAdd(BitModalReference modalRef) + { + if (_modalRefs.Contains(modalRef)) return Task.CompletedTask; + + _modalRefs.Add(modalRef); + return InvokeAsync(StateHasChanged); + } + + private Task OnCloseModal(BitModalReference modalRef) + { + _modalRefs.Remove(modalRef); + return InvokeAsync(StateHasChanged); + } + + + + public void Dispose() + { + _modalService.OnAddModal -= OnModalAdd; + _modalService.OnCloseModal -= OnCloseModal; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalReference.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalReference.cs new file mode 100644 index 0000000000..e017ad10f3 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalReference.cs @@ -0,0 +1,50 @@ +namespace Bit.BlazorUI; + +public class BitModalReference +{ + private readonly BitModalService _modalService; + + + + public string Id { get; init; } + + public bool Persistent { get; private set; } + + public object? Content { get; private set; } + + public RenderFragment? Modal { get; private set; } + + public BitModalParameters? Parameters { get; private set; } + + + + + public BitModalReference(BitModalService modalService, bool persistent) + { + Id = BitShortId.NewId(); + _modalService = modalService; + Persistent = persistent; + } + + + + public void SetContent(object content) + { + Content = content; + } + + public void SetModal(RenderFragment modal) + { + Modal = modal; + } + + public void SetParameters(BitModalParameters? parameters) + { + Parameters = parameters; + } + + public void Close() + { + _modalService.Close(this); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalService.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalService.cs new file mode 100644 index 0000000000..d019966a8e --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalService.cs @@ -0,0 +1,150 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; + +namespace Bit.BlazorUI; + +public class BitModalService +{ + private BitModalContainer? _container; + private readonly ConcurrentQueue _persistentModalsQueue = new(); + + + + /// + /// The event for when a new modal gets added through calling the Show method. + /// + public event Func? OnAddModal; + + /// + /// The event for when a modal gets removed through calling the Close method. + /// + public event Func? OnCloseModal; + + + + /// + /// Initializes the current modal container that is responsible for rendering the modals. + /// + public void InitContainer(BitModalContainer container) + { + _container = container; + _container.InjectPersistentModals(_persistentModalsQueue); + } + + /// + /// Closes an already opened modal using its reference. + /// + public async Task Close(BitModalReference modalRef) + { + var modalClose = OnCloseModal; + if (modalClose is not null) + { + await modalClose(modalRef); + } + } + + /// + /// Shows a new persistent BitModal that will persist through the lifecycle of the application until it gets shown. + /// + public Task Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool persistent = false) + { + return Show(null, null, persistent); + } + + /// + /// Shows a new BitModal with a custom component with parameters as its content. + /// + public Task Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + Dictionary? parameters, bool persistent = false) + { + return Show(parameters, null, persistent); + } + + /// + /// Shows a new BitModal with a custom component with parameters as its content. + /// + public Task Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + Dictionary parameters) + { + return Show(parameters, null, false); + } + + /// + /// Shows a new BitModal with a custom component as its content with custom parameters for the modal. + /// + public Task Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + BitModalParameters modalParameters) + { + return Show(null, modalParameters, false); + } + + /// + /// Shows a new BitModal with a custom component as its content with custom parameters for the modal. + /// + public Task Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + BitModalParameters? modalParameters, bool persistent = false) + { + return Show(null, modalParameters, persistent); + } + + /// + /// Shows a new BitModal with a custom component as its content with custom parameters for the custom component and the modal. + /// + public async Task Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + Dictionary? parameters, + BitModalParameters? modalParameters, + bool persistent = false) + { + var componentType = typeof(T); + + if (typeof(IComponent).IsAssignableFrom(componentType) is false) + { + throw new ArgumentException($"Type {componentType.Name} must be a Blazor component"); + } + + var modalReference = new BitModalReference(this, persistent); + modalReference.SetParameters(modalParameters); + + var content = new RenderFragment(builder => + { + var i = 0; + builder.OpenComponent(i++, componentType); + + if (parameters is not null) + { + foreach (var parameter in parameters) + { + builder.AddAttribute(i++, parameter.Key, parameter.Value); + } + } + + builder.AddComponentReferenceCapture(i, c => { modalReference.SetContent((T)c); }); + builder.CloseComponent(); + }); + + var modal = new RenderFragment(builder => + { + builder.OpenComponent(0); + builder.SetKey(modalReference.Id); + builder.AddComponentParameter(1, nameof(BitModal.IsOpen), true); + builder.AddComponentParameter(2, nameof(BitModal.OnOverlayClick), EventCallback.Factory.Create(modalReference, () => modalReference.Close())); + builder.AddComponentParameter(3, nameof(BitModal.ChildContent), content); + builder.CloseComponent(); + }); + modalReference.SetModal(modal); + + var modalAdd = OnAddModal; + if (modalAdd is not null) + { + await modalAdd(modalReference); + } + + if (persistent && _container is null) + { + _persistentModalsQueue.Enqueue(modalReference); + } + + return modalReference; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/IServiceCollectionExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/IServiceCollectionExtensions.cs new file mode 100644 index 0000000000..2a476d7020 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/IServiceCollectionExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Bit.BlazorUI; + +public static class IServiceCollectionExtensions +{ + public static IServiceCollection AddBitBlazorUIExtrasServices(this IServiceCollection services, bool singleton = false) + { + if (singleton) + { + services.TryAddSingleton(); + } + else + { + services.TryAddScoped(); + } + + return services; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Surfaces/Modal/BitModalTests.cs b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Surfaces/Modal/BitModalTests.cs index 9de4770dbf..eb3b8ed478 100644 --- a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Surfaces/Modal/BitModalTests.cs +++ b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Surfaces/Modal/BitModalTests.cs @@ -61,7 +61,7 @@ public void BitModalIsModelessTest(bool isModeless) }); var element = com.Find(".bit-mdl"); - Assert.AreEqual(element.Attributes["aria-modal"].Value, (isModeless is false).ToString()); + Assert.AreEqual(element.Attributes["aria-modal"].Value, (isModeless is false).ToString().ToLower()); var elementOverlay = com.FindAll(".bit-mdl-ovl"); Assert.AreEqual(isModeless ? 0 : 1, elementOverlay.Count); @@ -151,7 +151,7 @@ public void BitModalContentTest() var elementContent = com.Find(".bit-mdl-ctn"); - elementContent.MarkupMatches("
    Test Content
    "); + elementContent.MarkupMatches("
    Test Content
    "); } [TestMethod] @@ -193,18 +193,18 @@ public void BitModalOnDismissShouldWorkCorrect() } [DataTestMethod, - DataRow(BitModalPosition.Center), - DataRow(BitModalPosition.TopLeft), - DataRow(BitModalPosition.TopCenter), - DataRow(BitModalPosition.TopRight), - DataRow(BitModalPosition.CenterLeft), - DataRow(BitModalPosition.CenterRight), - DataRow(BitModalPosition.BottomLeft), - DataRow(BitModalPosition.BottomCenter), - DataRow(BitModalPosition.BottomRight), + DataRow(BitPosition.Center), + DataRow(BitPosition.TopLeft), + DataRow(BitPosition.TopCenter), + DataRow(BitPosition.TopRight), + DataRow(BitPosition.CenterLeft), + DataRow(BitPosition.CenterRight), + DataRow(BitPosition.BottomLeft), + DataRow(BitPosition.BottomCenter), + DataRow(BitPosition.BottomRight), DataRow(null) ] - public void BitModalPositionTest(BitModalPosition? position) + public void BitPositionTest(BitPosition? position) { var com = RenderComponent(parameters => { @@ -214,15 +214,15 @@ public void BitModalPositionTest(BitModalPosition? position) var positionClass = position switch { - BitModalPosition.Center => "bit-mdl-ctr", - BitModalPosition.TopLeft => "bit-mdl-tl", - BitModalPosition.TopCenter => "bit-mdl-tc", - BitModalPosition.TopRight => "bit-mdl-tr", - BitModalPosition.CenterLeft => "bit-mdl-cl", - BitModalPosition.CenterRight => "bit-mdl-cr", - BitModalPosition.BottomLeft => "bit-mdl-bl", - BitModalPosition.BottomCenter => "bit-mdl-bc", - BitModalPosition.BottomRight => "bit-mdl-br", + BitPosition.Center => "bit-mdl-ctr", + BitPosition.TopLeft => "bit-mdl-tl", + BitPosition.TopCenter => "bit-mdl-tc", + BitPosition.TopRight => "bit-mdl-tr", + BitPosition.CenterLeft => "bit-mdl-cl", + BitPosition.CenterRight => "bit-mdl-cr", + BitPosition.BottomLeft => "bit-mdl-bl", + BitPosition.BottomCenter => "bit-mdl-bc", + BitPosition.BottomRight => "bit-mdl-br", _ => "bit-mdl-ctr", }; diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModal.razor b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModal.razor index cda47ae2ef..e9638db9a2 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModal.razor +++ b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModal.razor @@ -3,25 +3,25 @@ @if (IsOpen) { -
    - @if (Modeless is false) + dir="@ModalParameters.Dir?.ToString().ToLower()" + aria-labelledby="@ModalParameters.TitleAriaId" + aria-describedby="@ModalParameters.SubtitleAriaId" + aria-modal="@((ModalParameters.Modeless is false).ToString().ToLower())" + role="@GetRole()"> + @if (ModalParameters.Modeless is false) { - diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModal.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModal.razor.cs index a63b8b8516..c19d8f49bb 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModal.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModal.razor.cs @@ -7,10 +7,13 @@ public partial class BitModal : BitComponentBase, IAsyncDisposable private bool _internalIsOpen; private string _containerId = default!; + [Inject] private IJSRuntime _js { get; set; } = default!; - [Inject] private IJSRuntime _js { get; set; } = default!; + [CascadingParameter] + private BitModalParameters ModalParameters { get => modalParameters; set { modalParameters = value; modalParameters.SetModal(this); } } + private BitModalParameters modalParameters = new(); /// @@ -79,25 +82,30 @@ public partial class BitModal : BitComponentBase, IAsyncDisposable public bool IsOpen { get; set; } /// - /// Whether the Modal should be modeless (e.g. not dismiss when focusing/clicking outside of the Modal). if true: IsBlocking is ignored, there will be no overlay. + /// Whether the Modal should be modeless (e.g. not dismiss when focusing/clicking outside of the Modal). if true: Blocking is ignored, there will be no overlay. /// [Parameter] public bool Modeless { get; set; } /// - /// A callback function for when the Modal is dismissed light dismiss, before the animation completes. + /// A callback function for when the Modal is dismissed. /// [Parameter] public EventCallback OnDismiss { get; set; } + /// + /// A callback function for when somewhere on the overlay element of the Modal is clicked. + /// + [Parameter] public EventCallback OnOverlayClick { get; set; } + /// /// Position of the Modal on the screen. /// [Parameter, ResetClassBuilder] - public BitModalPosition? Position { get; set; } + public BitPosition? Position { get; set; } /// /// Set the element selector for which the Modal disables its scroll if applicable. /// - [Parameter] public string ScrollerSelector { get; set; } = "body"; + [Parameter] public string? ScrollerSelector { get; set; } /// /// Custom CSS styles for different parts of the BitModal component. @@ -121,39 +129,42 @@ public partial class BitModal : BitComponentBase, IAsyncDisposable protected override void RegisterCssClasses() { ClassBuilder.Register(() => Classes?.Root); + ClassBuilder.Register(() => ModalParameters.Classes?.Root); - ClassBuilder.Register(() => AbsolutePosition ? "bit-mdl-abs" : string.Empty); + ClassBuilder.Register(() => ModalParameters.AbsolutePosition ? "bit-mdl-abs" : string.Empty); - ClassBuilder.Register(() => FullSize || FullHeight ? "bit-mdl-fhe" : string.Empty); - ClassBuilder.Register(() => FullSize || FullWidth ? "bit-mdl-fwi" : string.Empty); + ClassBuilder.Register(() => ModalParameters.FullSize || ModalParameters.FullHeight ? "bit-mdl-fhe" : string.Empty); + ClassBuilder.Register(() => ModalParameters.FullSize || ModalParameters.FullWidth ? "bit-mdl-fwi" : string.Empty); - ClassBuilder.Register(() => Position switch + ClassBuilder.Register(() => ModalParameters.Position switch { - BitModalPosition.Center => "bit-mdl-ctr", - BitModalPosition.TopLeft => "bit-mdl-tl", - BitModalPosition.TopCenter => "bit-mdl-tc", - BitModalPosition.TopRight => "bit-mdl-tr", - BitModalPosition.CenterLeft => "bit-mdl-cl", - BitModalPosition.CenterRight => "bit-mdl-cr", - BitModalPosition.BottomLeft => "bit-mdl-bl", - BitModalPosition.BottomCenter => "bit-mdl-bc", - BitModalPosition.BottomRight => "bit-mdl-br", + BitPosition.Center => "bit-mdl-ctr", + BitPosition.TopLeft => "bit-mdl-tl", + BitPosition.TopCenter => "bit-mdl-tc", + BitPosition.TopRight => "bit-mdl-tr", + BitPosition.CenterLeft => "bit-mdl-cl", + BitPosition.CenterRight => "bit-mdl-cr", + BitPosition.BottomLeft => "bit-mdl-bl", + BitPosition.BottomCenter => "bit-mdl-bc", + BitPosition.BottomRight => "bit-mdl-br", _ => "bit-mdl-ctr" }); } protected override void RegisterCssStyles() { - StyleBuilder.Register(() => Styles?.Root); + StyleBuilder.Register(() => ModalParameters.Styles?.Root); StyleBuilder.Register(() => _offsetTop > 0 ? FormattableString.Invariant($"top:{_offsetTop}px") : string.Empty); } - protected override Task OnInitializedAsync() + protected override void OnInitialized() { _containerId = $"BitModal-{UniqueId}-container"; - return base.OnInitializedAsync(); + ModalParameters.SetModal(this); + + base.OnInitialized(); } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -166,7 +177,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (IsOpen) { - if (Draggable) + if (ModalParameters.Draggable) { _ = _js.BitModalSetupDragDrop(_containerId, GetDragElementSelector()); } @@ -182,11 +193,11 @@ protected override async Task OnAfterRenderAsync(bool firstRender) _offsetTop = 0; - if (AutoToggleScroll is false) return; - - _offsetTop = await _js.ToggleOverflow(ScrollerSelector, IsOpen); + //if (ModalParameters.AutoToggleScroll is false) return; + //_offsetTop = await _js.ToggleOverflow(ModalParameters.ScrollerSelector ?? "body", IsOpen); + await ToggleScroll(IsOpen); - if (AbsolutePosition is false) return; + if (ModalParameters.AbsolutePosition is false) return; StyleBuilder.Reset(); StateHasChanged(); @@ -194,17 +205,35 @@ protected override async Task OnAfterRenderAsync(bool firstRender) - private async Task CloseModal(MouseEventArgs e) + private async Task HandleOnOverlayClick(MouseEventArgs e) { - if (IsEnabled is false) return; - if (Blocking is not false) return; + if (ModalParameters.IsEnabled is false) return; + + if (ModalParameters.Blocking) return; + + await ModalParameters.OnOverlayClick.InvokeAsync(e); if (await AssignIsOpen(false) is false) return; - await OnDismiss.InvokeAsync(e); + await ModalParameters.OnDismiss.InvokeAsync(e); } - private string GetDragElementSelector() => DragElementSelector ?? $"#{_containerId}"; + private string GetDragElementSelector() + { + return ModalParameters.DragElementSelector ?? $"#{_containerId}"; + } + + private string GetRole() + { + return (ModalParameters.IsAlert ?? (ModalParameters.Blocking && ModalParameters.Modeless is false)) ? "alertdialog" : "dialog"; + } + + private async Task ToggleScroll(bool isOpen) + { + if (ModalParameters.AutoToggleScroll is false) return; + + _offsetTop = await _js.ToggleOverflow(ModalParameters.ScrollerSelector ?? "body", isOpen); + } @@ -220,6 +249,7 @@ protected virtual async ValueTask DisposeAsync(bool disposing) try { + await ToggleScroll(false); await _js.BitModalRemoveDragDrop(_containerId, GetDragElementSelector()); } catch (JSDisconnectedException) { } // we can ignore this exception here diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModal.ts b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModal.ts index aae6ccfada..c07eaf45c3 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModal.ts +++ b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModal.ts @@ -24,6 +24,9 @@ x = e.clientX; y = e.clientY; + const { width } = element.getBoundingClientRect(); + element.style.width = `${width}px`; + document.addEventListener('pointermove', handlePointerMove); listeners['pointermove'] = handlePointerMove; diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModalClassStyles.cs b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModalClassStyles.cs index e01158b637..c44a1458f4 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModalClassStyles.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModalClassStyles.cs @@ -16,4 +16,15 @@ public class BitModalClassStyles /// Custom CSS classes/styles for the modal content of the BitModal. /// public string? Content { get; set; } + + + public static BitModalClassStyles Merge(BitModalClassStyles? classStyles1, BitModalClassStyles? classStyles2) + { + return new BitModalClassStyles + { + Root = classStyles1?.Root ?? classStyles2?.Root, + Overlay = classStyles1?.Overlay ?? classStyles2?.Overlay, + Content = classStyles1?.Content ?? classStyles2?.Content + }; + } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModalParameters.cs b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModalParameters.cs new file mode 100644 index 0000000000..e9cd0da410 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModalParameters.cs @@ -0,0 +1,119 @@ +namespace Bit.BlazorUI; + +public class BitModalParameters +{ + public bool IsEnabled { get { return _modal?.IsEnabled ?? field; } set; } = true; + + public Dictionary HtmlAttributes { get { return _modal?.HtmlAttributes ?? field; } set; } = []; + + public BitDir? Dir { get { return _modal?.Dir ?? field; } set; } + + + public bool AutoToggleScroll { get { return _modal?.AutoToggleScroll ?? field; } set; } + + public bool AbsolutePosition { get { return _modal?.AbsolutePosition ?? field; } set; } + + public bool Blocking { get { return _modal?.Blocking ?? field; } set; } + + public BitModalClassStyles? Classes { get; set; } + + public string? DragElementSelector { get { return _modal?.DragElementSelector ?? field; } set; } + + public bool Draggable { get { return _modal?.Draggable ?? field; } set; } + + public bool FullHeight { get { return _modal?.FullHeight ?? field; } set; } + + public bool FullSize { get { return _modal?.FullSize ?? field; } set; } + + public bool FullWidth { get { return _modal?.FullWidth ?? field; } set; } + + public bool? IsAlert { get { return _modal?.IsAlert ?? field; } set; } + + public bool Modeless { get { return _modal?.Modeless ?? field; } set; } + + public EventCallback OnDismiss + { + get + { + return EventCallback.Factory.Create(new object(), async () => + { + if (_modal is not null) await _modal.OnDismiss.InvokeAsync(); + await field.InvokeAsync(); + }); + } + set; + } + + public EventCallback OnOverlayClick + { + get + { + return EventCallback.Factory.Create(new object(), async () => + { + if (_modal is not null) await _modal.OnOverlayClick.InvokeAsync(); + await field.InvokeAsync(); + }); + } + set; + } + + public BitPosition? Position { get { return _modal?.Position ?? field; } set; } + + public string? ScrollerSelector { get { return _modal?.ScrollerSelector ?? field; } set; } + + public BitModalClassStyles? Styles { get; set; } + + public string? SubtitleAriaId { get { return _modal?.SubtitleAriaId ?? field; } set; } + + public string? TitleAriaId { get { return _modal?.TitleAriaId ?? field; } set; } + + + private BitModal? _modal; + public void SetModal(BitModal modal) + { + _modal = modal; + } + + + public static BitModalParameters? Merge(BitModalParameters? params1, BitModalParameters? params2) + { + if (params1 is null && params2 is null) return null; + + if (params2 is null) return params1; + if (params1 is null) return params2; + + + return new BitModalParameters + { + IsEnabled = (params1.IsEnabled is false || params2.IsEnabled is false) is false, + HtmlAttributes = params1.HtmlAttributes.Concat(params2.HtmlAttributes).ToDictionary(kv => kv.Key, kv => kv.Value), + Dir = params1.Dir ?? params2.Dir, + AutoToggleScroll = params1.AutoToggleScroll || params2.AutoToggleScroll, + AbsolutePosition = params1.AbsolutePosition || params2.AbsolutePosition, + Blocking = params1.Blocking || params2.Blocking, + Classes = BitModalClassStyles.Merge(params1.Classes, params2.Classes), + DragElementSelector = params1.DragElementSelector ?? params2.DragElementSelector, + Draggable = params1.Draggable || params2.Draggable, + FullHeight = params1.FullHeight || params2.FullHeight, + FullSize = params1.FullSize || params2.FullSize, + FullWidth = params1.FullWidth || params2.FullWidth, + IsAlert = params1.IsAlert ?? params2.IsAlert, + Modeless = params1.Modeless || params2.Modeless, + OnDismiss = EventCallback.Factory.Create(new object(), async () => + { + await params1.OnDismiss.InvokeAsync(); + await params2.OnDismiss.InvokeAsync(); + }), + OnOverlayClick = EventCallback.Factory.Create(new object(), async () => + { + await params1.OnOverlayClick.InvokeAsync(); + await params2.OnOverlayClick.InvokeAsync(); + }), + Position = params1.Position ?? params2.Position, + ScrollerSelector = params1.ScrollerSelector ?? params2.ScrollerSelector, + Styles = BitModalClassStyles.Merge(params1.Styles, params2.Styles), + SubtitleAriaId = params1.SubtitleAriaId ?? params2.SubtitleAriaId, + TitleAriaId = params1.TitleAriaId ?? params2.TitleAriaId, + }; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModalPosition.cs b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModalPosition.cs deleted file mode 100644 index cb63061b41..0000000000 --- a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Modal/BitModalPosition.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Bit.BlazorUI; -public enum BitModalPosition -{ - Center, - TopLeft, - TopCenter, - TopRight, - CenterLeft, - CenterRight, - BottomLeft, - BottomCenter, - BottomRight -} diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/BitShortId.cs b/src/BlazorUI/Bit.BlazorUI/Utils/BitShortId.cs index fdf0e983a7..ee69a9bc6a 100644 --- a/src/BlazorUI/Bit.BlazorUI/Utils/BitShortId.cs +++ b/src/BlazorUI/Bit.BlazorUI/Utils/BitShortId.cs @@ -1,6 +1,6 @@ namespace Bit.BlazorUI; -internal class BitShortId +public class BitShortId { private static readonly string _chars = "abcdefghijklmnopqrstuvwxyz0123456789"; private static readonly int _length = _chars.Length; diff --git a/src/BlazorUI/Bit.BlazorUI/Utils/IServiceCollectionExtensions.cs b/src/BlazorUI/Bit.BlazorUI/Utils/IServiceCollectionExtensions.cs index 76440671a0..02ddf71eb9 100644 --- a/src/BlazorUI/Bit.BlazorUI/Utils/IServiceCollectionExtensions.cs +++ b/src/BlazorUI/Bit.BlazorUI/Utils/IServiceCollectionExtensions.cs @@ -1,12 +1,20 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Bit.BlazorUI; public static class IServiceCollectionExtensions { - public static IServiceCollection AddBitBlazorUIServices(this IServiceCollection services) + public static IServiceCollection AddBitBlazorUIServices(this IServiceCollection services, bool singleton = false) { - services.AddScoped(); + if (singleton) + { + services.TryAddSingleton(); + } + else + { + services.TryAddScoped(); + } return services; } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/ComponentDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/ComponentDemo.razor index 020e7c3ec3..ffef99e155 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/ComponentDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/ComponentDemo.razor @@ -8,6 +8,7 @@ @ComponentDescription + @if (Notes.HasValue()) {
    @@ -15,10 +16,12 @@ @Notes
    } +
    Usage @ChildContent
    +
    API @@ -52,7 +55,10 @@ - + @if (_extraComponents.Contains(ComponentName) is false) + { + + }
    diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/ComponentDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/ComponentDemo.razor.cs index a2ab158895..dc63a3b9c1 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/ComponentDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Components/ComponentDemo.razor.cs @@ -281,5 +281,5 @@ public partial class ComponentDemo - private readonly List _extraComponents = ["DataGrid", "Chart"]; + private readonly List _extraComponents = ["DataGrid", "Chart", "PdfReader", "ModalService"]; } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Extensions/IServiceCollectionExtensions.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Extensions/IServiceCollectionExtensions.cs index d2be000908..2f3c89c0ec 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Extensions/IServiceCollectionExtensions.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Extensions/IServiceCollectionExtensions.cs @@ -23,7 +23,8 @@ public static IServiceCollection AddClientSharedServices(this IServiceCollection services.TryAddTransient(); services.TryAddScoped(); - services.AddBitBlazorUIServices(); + services.AddBitBlazorUIServices(singleton: AppRenderMode.IsBlazorHybrid); + services.AddBitBlazorUIExtrasServices(singleton: AppRenderMode.IsBlazorHybrid); services.AddSharedServices(); return services; diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/BitModalServiceDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/BitModalServiceDemo.razor new file mode 100644 index 0000000000..dd4b124125 --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/BitModalServiceDemo.razor @@ -0,0 +1,20 @@ +@page "/components/modalservice" + +@inherits AppComponentBase + + + + + + + Show + + + + + \ No newline at end of file diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/BitModalServiceDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/BitModalServiceDemo.razor.cs new file mode 100644 index 0000000000..32057f1d0a --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/BitModalServiceDemo.razor.cs @@ -0,0 +1,71 @@ +namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.ModalService; + +public partial class BitModalServiceDemo +{ + private readonly List componentParameters = + [ + new() + { + Name = "OnAddModal", + Type = "event Func?", + DefaultValue = "", + Description = "The event for when a new modal gets added through calling the Show method.", + }, + new() + { + Name = "OnCloseModal", + Type = "event Func?", + DefaultValue = "", + Description = "The event for when a modal gets removed through calling the Close method.", + }, + new() + { + Name = "Close", + Type = "void (BitModalReference modal)", + DefaultValue = "", + Description = "Closes an already opened modal using its reference.", + }, + new() + { + Name = "Show", + Type = "Task (Dictionary? parameters)", + DefaultValue = "", + Description = "Shows a new BitModal with a custom component with parameters as its content.", + }, + new() + { + Name = "Show", + Type = "Task (BitModalParameters? modalParameters)", + DefaultValue = "", + Description = "Shows a new BitModal with a custom component as its content with custom parameters for the modal.", + }, + new() + { + Name = "Show", + Type = "Task (Dictionary? parameters, BitModalParameters? modalParameters)", + DefaultValue = "", + Description = "Shows a new BitModal with a custom component as its content with custom parameters for the custom component and the modal.", + }, + ]; + + + [AutoInject] private BitModalService modalService = default!; + + private async Task ShowModal() + { + await modalService.Show(new BitModalParameters() { Modeless = true }); + } + + + private readonly string example1RazorCode = @" +Show + +"; + private readonly string example1CsharpCode = @" +[AutoInject] private BitModalService modalService = default!; + +private async Task ShowModal() +{ + await modalService.Show(new BitModalParameters() { Modeless = true }); +}"; +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/BitModalServiceDemo.razor.scss b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/BitModalServiceDemo.razor.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/ModalContent.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/ModalContent.razor new file mode 100644 index 0000000000..69bddf28ea --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/ModalService/ModalContent.razor @@ -0,0 +1,18 @@ + + This is the title! + + This is the body. + + + Ok + Cancel + + + +@code { + [CascadingParameter] private BitModalReference modalReference { get; set; } = default!; + private void Close() + { + modalReference.Close(); + } +} \ No newline at end of file diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/PdfReader/BitPdfReaderDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/PdfReader/BitPdfReaderDemo.razor.cs index 25109b1894..9c94f0f049 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/PdfReader/BitPdfReaderDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/PdfReader/BitPdfReaderDemo.razor.cs @@ -1,5 +1,4 @@ - -namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.PdfReader; +namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Extras.PdfReader; public partial class BitPdfReaderDemo { diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Modal/BitModalDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Modal/BitModalDemo.razor index aca6587b20..683e56b935 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Modal/BitModalDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Modal/BitModalDemo.razor @@ -239,19 +239,19 @@
    To set the modal position on the page you can use the Position parameter.

    - Top Left - Top Center - Top Right + Top Left + Top Center + Top Right
    - Center Left - Center - Center Right + Center Left + Center + Center Right
    - Bottom Left - Bottom Center - Bottom Right + Bottom Left + Bottom Center + Bottom Right
    diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Modal/BitModalDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Modal/BitModalDemo.razor.cs index a71094ba97..44520654da 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Modal/BitModalDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Modal/BitModalDemo.razor.cs @@ -104,13 +104,19 @@ public partial class BitModalDemo Description = "A callback function for when the Modal is dismissed.", }, new() + { + Name = "OnOverlayClick", + Type = "EventCallback", + Description = "A callback function for when somewhere on the overlay element of the Modal is clicked.", + }, + new() { Name = "Position", - Type = "BitModalPosition?", + Type = "BitPosition?", DefaultValue = "null", Description = "Position of the Modal on the screen.", LinkType = LinkType.Link, - Href = "#modal-position-enum", + Href = "#position-enum", }, new() { @@ -181,8 +187,8 @@ public partial class BitModalDemo [ new() { - Id = "modal-position-enum", - Name = "BitModalPosition", + Id = "position-enum", + Name = "BitPosition", Description = "", Items = [ @@ -213,8 +219,8 @@ public partial class BitModalDemo private bool isOpenScrollerSelector; private bool isOpenPosition; - private BitModalPosition position; - private void OpenModalInPosition(BitModalPosition positionValue) + private BitPosition position; + private void OpenModalInPosition(BitPosition positionValue) { isOpenPosition = true; position = positionValue; @@ -562,10 +568,10 @@ Quisque ultricies mi nec leo ultricies mollis. Vivamus egestas volutpat lacinia. - OpenModalInPosition(BitModalPosition.TopLeft)"">Top Left - OpenModalInPosition(BitModalPosition.TopRight)"">Top Right - OpenModalInPosition(BitModalPosition.BottomLeft)"">Bottom Left - OpenModalInPosition(BitModalPosition.BottomRight)"">Bottom Right + OpenModalInPosition(BitPosition.TopLeft)"">Top Left + OpenModalInPosition(BitPosition.TopRight)"">Top Right + OpenModalInPosition(BitPosition.BottomLeft)"">Bottom Left + OpenModalInPosition(BitPosition.BottomRight)"">Bottom Right
    @@ -578,9 +584,9 @@ Quisque ultricies mi nec leo ultricies mollis. Vivamus egestas volutpat lacinia. "; private readonly string example5CsharpCode = @" private bool isOpenPosition; -private BitModalPosition position; +private BitPosition position; -private void OpenModalInPosition(BitModalPosition positionValue) +private void OpenModalInPosition(BitPosition positionValue) { isOpenPosition = true; position = positionValue; diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor index 619dd830b3..e1a7a2d4d2 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor @@ -250,6 +250,9 @@ PdfReader + + ModalService +
    \ No newline at end of file diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs index e16622235b..174f1e812d 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs @@ -155,6 +155,14 @@ public partial class NavMenu : IDisposable new() { Text = "DataGrid", Url = "/components/datagrid", AdditionalUrls = ["/components/data-grid"] }, new() { Text = "Chart", Url = "/components/chart" }, new() { Text = "PdfReader", Url = "/components/pdfreader" }, + new() + { + Text = "Services", + ChildItems = + [ + new() { Text = "ModalService", Url = "/components/modalservice" }, + ] + }, ] }, new() { Text = "Iconography", Url = "/iconography" }, From fe0fa81d9853e39e71ccd7b5d92638ecb56fad15 Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Sun, 1 Dec 2024 20:49:38 +0100 Subject: [PATCH 29/87] feat(templates): add support for azure SignalR scale out in Boilerplate #9365 (#9366) --- .../IClientCoreServiceCollectionExtensions.cs | 3 +- .../Services/SignalRInfinitiesRetryPolicy.cs | 13 +------ .../src/Directory.Packages.props | 1 + .../src/Directory.Packages8.props | 1 + .../Boilerplate.Server.Api.csproj | 1 + .../Categories/CategoryController.cs | 5 ++- .../Diagnostics/DiagnosticsController.cs | 15 ++++++-- .../IdentityController.ResetPassword.cs | 1 - .../Identity/IdentityController.cs | 2 - .../Controllers/Identity/UserController.cs | 17 +++++---- .../Controllers/Products/ProductController.cs | 5 ++- ...241201163409_InitialMigration.Designer.cs} | 5 ++- ....cs => 20241201163409_InitialMigration.cs} | 3 +- .../Migrations/AppDbContextModelSnapshot.cs | 3 ++ .../Models/Identity/UserSession.cs | 4 ++ .../Program.Services.cs | 15 ++++---- .../Boilerplate.Server.Api/SignalR/AppHub.cs | 31 +++++++++++++-- .../SignalR/AppHubConnectionHandler.cs | 38 ------------------- .../Boilerplate.Server.Api/appsettings.json | 7 ++++ .../Program.Middlewares.cs | 14 +++++++ .../Boilerplate.Server.Web/appsettings.json | 7 ++++ 21 files changed, 110 insertions(+), 81 deletions(-) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/{20241126123435_InitialMigration.Designer.cs => 20241201163409_InitialMigration.Designer.cs} (99%) rename src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/{20241126123435_InitialMigration.cs => 20241201163409_InitialMigration.cs} (99%) delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/SignalR/AppHubConnectionHandler.cs 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 7756a019eb..2ad8579192 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 @@ -147,10 +147,11 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle .WithAutomaticReconnect(sp.GetRequiredService()) .WithUrl(new Uri(absoluteServerAddressProvider.GetAddress(), "app-hub"), options => { - options.SkipNegotiation = true; + options.SkipNegotiation = false; // Required for Azure SignalR. options.Transports = HttpTransportType.WebSockets; // Avoid enabling long polling or Server-Sent Events. Focus on resolving the issue with WebSockets instead. // WebSockets should be enabled on services like IIS or Cloudflare CDN, offering significantly better performance. + options.HttpMessageHandlerFactory = httpClientHandler => sp.GetRequiredService().Invoke(httpClientHandler); options.AccessTokenProvider = async () => { var accessToken = await authTokenProvider.GetAccessToken(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignalRInfinitiesRetryPolicy.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignalRInfinitiesRetryPolicy.cs index 3e758969cd..36a88e7246 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignalRInfinitiesRetryPolicy.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignalRInfinitiesRetryPolicy.cs @@ -4,19 +4,8 @@ namespace Boilerplate.Client.Core.Services; public class SignalRInfinitiesRetryPolicy : IRetryPolicy { - private static TimeSpan[] delays = new double[] { 1, 3, 5, 10, 15, 20, 30 } - .Select(TimeSpan.FromSeconds) - .ToArray(); - public TimeSpan? NextRetryDelay(RetryContext retryContext) { - var index = retryContext.PreviousRetryCount; - - if (index < delays.Length) - { - return delays[index]; - } - - return TimeSpan.FromSeconds(30); + return TimeSpan.FromSeconds(1); // It's already handled by HttpMessageHandlers/RetryDelegatingHandler during negotiate http request. } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props index c87d83cf25..0aef804e6f 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props @@ -47,6 +47,7 @@ + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props index 55c094a8d0..97253b7f86 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props @@ -47,6 +47,7 @@ + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj index 4557a568f0..7e9651eedf 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj @@ -35,6 +35,7 @@ + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs index 2f8443550a..2b07229ad1 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs @@ -98,8 +98,9 @@ public async Task Delete(Guid id, string concurrencyStamp, CancellationToken can //#if (signalR == true) private async Task PublishDashboardDataChanged(CancellationToken cancellationToken) { - // Checkout AppHubConnectionHandler's comments for more info. - await appHubContext.Clients.GroupExcept("AuthenticatedClients", excludedConnectionIds: [User.GetSessionId().ToString()]).SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.DASHBOARD_DATA_CHANGED, cancellationToken); + // Checkout AppHub's comments for more info. + // In order to exclude current user session, gets its signalR connection id from database and use GroupExcept instead. + await appHubContext.Clients.Group("AuthenticatedClients").SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.DASHBOARD_DATA_CHANGED, cancellationToken); } //#endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs index afa1cf384e..3e2004d847 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs @@ -5,6 +5,7 @@ using Boilerplate.Server.Api.SignalR; //#endif using Boilerplate.Server.Api.Services; +using Boilerplate.Server.Api.Models.Identity; using Boilerplate.Shared.Controllers.Diagnostics; namespace Boilerplate.Server.Api.Controllers.Diagnostics; @@ -34,7 +35,15 @@ public async Task DoDiagnostics(CancellationToken cancellationToken) result.AppendLine($"Trace => {Request.HttpContext.TraceIdentifier}"); var isAuthenticated = User.IsAuthenticated(); - Guid? userSessionId = isAuthenticated ? User.GetSessionId() : null; + Guid? userSessionId = null; + UserSession? userSession = null; + + if (isAuthenticated) + { + userSessionId = User.GetSessionId(); + userSession = await DbContext + .UserSessions.SingleAsync(us => us.Id == userSessionId, cancellationToken); + } result.AppendLine($"IsAuthenticated: {isAuthenticated.ToString().ToLowerInvariant()}"); @@ -51,9 +60,9 @@ public async Task DoDiagnostics(CancellationToken cancellationToken) //#endif //#if (signalR == true) - if (isAuthenticated) + if (isAuthenticated && userSession!.SignalRConnectionId is not null) { - await appHubContext.Clients.Client(userSessionId.ToString()!).SendAsync(SignalREvents.SHOW_MESSAGE, DateTimeOffset.Now.ToString("HH:mm:ss"), cancellationToken); + await appHubContext.Clients.Client(userSession.SignalRConnectionId).SendAsync(SignalREvents.SHOW_MESSAGE, DateTimeOffset.Now.ToString("HH:mm:ss"), cancellationToken); } //#endif diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.ResetPassword.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.ResetPassword.cs index 15b16b7bb9..5a295f4a32 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.ResetPassword.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.ResetPassword.cs @@ -53,7 +53,6 @@ public async Task SendResetPasswordToken(SendResetPasswordTokenRequestDto reques } //#if (signalR == true) - // Checkout AppHubConnectionHandler's comments for more info. sendMessagesTasks.Add(appHubContext.Clients.User(user.Id.ToString()).SendAsync(SignalREvents.SHOW_MESSAGE, message, cancellationToken)); //#endif diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs index 100195e018..256ece5cf9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/IdentityController.cs @@ -313,7 +313,6 @@ public async Task SendOtp(IdentityRequestDto request, string? returnUrl = null, //#endif //#if (signalR == true) - // Checkout AppHubConnectionHandler's comments for more info. sendMessagesTasks.Add(appHubContext.Clients.User(user.Id.ToString()).SendAsync(SignalREvents.SHOW_MESSAGE, pushMessage, cancellationToken)); //#endif @@ -371,7 +370,6 @@ public async Task SendTwoFactorToken(SignInRequestDto request, CancellationToken //#if (signalR == true) if (firstStepAuthenticationMethod != "SignalR") { - // Checkout AppHubConnectionHandler's comments for more info. sendMessagesTasks.Add(appHubContext.Clients.User(user.Id.ToString()).SendAsync(SignalREvents.SHOW_MESSAGE, message, cancellationToken)); } //#endif diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs index 8a751dce69..bde3081140 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Identity/UserController.cs @@ -85,8 +85,11 @@ public async Task RevokeSession(Guid id, CancellationToken cancellationToken) await DbContext.SaveChangesAsync(cancellationToken); //#if (signalR == true) - // Checkout AppHubConnectionHandler's comments for more info. - await appHubContext.Clients.Client(userSession.Id.ToString()).SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.SESSION_REVOKED, cancellationToken); + // Checkout AppHub's comments for more info. + if (userSession.SignalRConnectionId is not null) + { + await appHubContext.Clients.Client(userSession.SignalRConnectionId).SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.SESSION_REVOKED, cancellationToken); + } //#endif } @@ -357,7 +360,7 @@ public async Task SendElevatedAccessToken(CancellationToken cancellationToken) // Elevated access token claim gets added to access token upon refresh token request call, so their lifetime would be the same if (resendDelay < TimeSpan.Zero) - throw new TooManyRequestsExceptions(Localizer[nameof(AppStrings.WaitForElevatedAccessTokenRequestResendDelay) , resendDelay.Value.Humanize(culture: CultureInfo.CurrentUICulture)]); + throw new TooManyRequestsExceptions(Localizer[nameof(AppStrings.WaitForElevatedAccessTokenRequestResendDelay), resendDelay.Value.Humanize(culture: CultureInfo.CurrentUICulture)]); user.ElevatedAccessTokenRequestedOn = DateTimeOffset.Now; var result = await userManager.UpdateAsync(user); @@ -386,12 +389,12 @@ public async Task SendElevatedAccessToken(CancellationToken cancellationToken) } //#if (signalR == true) - // Checkout AppHubConnectionHandler's comments for more info. + // Checkout AppHub's comments for more info. var userSessionIdsExceptCurrentUserSessionId = await DbContext.UserSessions - .Where(us => us.UserId == user.Id && us.Id != currentUserSessionId) - .Select(us => us.Id) + .Where(us => us.UserId == user.Id && us.Id != currentUserSessionId && us.SignalRConnectionId != null) + .Select(us => us.SignalRConnectionId!) .ToArrayAsync(cancellationToken); - sendMessagesTasks.Add(appHubContext.Clients.Clients(userSessionIdsExceptCurrentUserSessionId.Select(us => us.ToString()).ToArray()).SendAsync(SignalREvents.SHOW_MESSAGE, messageText, cancellationToken)); + sendMessagesTasks.Add(appHubContext.Clients.Clients(userSessionIdsExceptCurrentUserSessionId).SendAsync(SignalREvents.SHOW_MESSAGE, messageText, cancellationToken)); //#endif //#if (notification == true) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs index 807fed1c29..40673c8145 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs @@ -93,8 +93,9 @@ public async Task Delete(Guid id, string concurrencyStamp, CancellationToken can //#if (signalR == true) private async Task PublishDashboardDataChanged(CancellationToken cancellationToken) { - // Checkout AppHubConnectionHandler's comments for more info. - await appHubContext.Clients.GroupExcept("AuthenticatedClients", excludedConnectionIds: [User.GetSessionId().ToString()]).SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.DASHBOARD_DATA_CHANGED, cancellationToken); + // Checkout AppHub's comments for more info. + // In order to exclude current user session, gets its signalR connection id from database and use GroupExcept instead. + await appHubContext.Clients.Group("AuthenticatedClients").SendAsync(SignalREvents.PUBLISH_MESSAGE, SharedPubSubMessages.DASHBOARD_DATA_CHANGED, cancellationToken); } //#endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241126123435_InitialMigration.Designer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241201163409_InitialMigration.Designer.cs similarity index 99% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241126123435_InitialMigration.Designer.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241201163409_InitialMigration.Designer.cs index 12d477247d..83dd14b4fc 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241126123435_InitialMigration.Designer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241201163409_InitialMigration.Designer.cs @@ -6,7 +6,7 @@ namespace Boilerplate.Server.Api.Data.Migrations; [DbContext(typeof(AppDbContext))] -[Migration("20241126123435_InitialMigration")] +[Migration("20241201163409_InitialMigration")] partial class InitialMigration { /// @@ -249,6 +249,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("RenewedOn") .HasColumnType("INTEGER"); + b.Property("SignalRConnectionId") + .HasColumnType("TEXT"); + b.Property("StartedOn") .HasColumnType("INTEGER"); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241126123435_InitialMigration.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241201163409_InitialMigration.cs similarity index 99% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241126123435_InitialMigration.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241201163409_InitialMigration.cs index 5d57be872e..99d357eb01 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241126123435_InitialMigration.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/20241201163409_InitialMigration.cs @@ -228,7 +228,8 @@ protected override void Up(MigrationBuilder migrationBuilder) Privileged = table.Column(type: "INTEGER", nullable: false), StartedOn = table.Column(type: "INTEGER", nullable: false), RenewedOn = table.Column(type: "INTEGER", nullable: true), - UserId = table.Column(type: "TEXT", nullable: false) + UserId = table.Column(type: "TEXT", nullable: false), + SignalRConnectionId = table.Column(type: "TEXT", nullable: true) }, constraints: table => { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs index 37fafbc8a6..b73ad591d7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Data/Migrations/AppDbContextModelSnapshot.cs @@ -247,6 +247,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RenewedOn") .HasColumnType("INTEGER"); + b.Property("SignalRConnectionId") + .HasColumnType("TEXT"); + b.Property("StartedOn") .HasColumnType("INTEGER"); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/UserSession.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/UserSession.cs index d8dddb11d0..5e59e08122 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/UserSession.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Models/Identity/UserSession.cs @@ -36,4 +36,8 @@ public partial class UserSession //#if (notification == true) public PushNotificationSubscription? PushNotificationSubscription { get; set; } //#endif + + //#if (signalR == true) + public string? SignalRConnectionId { get; set; } + //#endif } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs index 39c2d33004..930e2b56e4 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs @@ -23,11 +23,6 @@ using Boilerplate.Server.Api.Controllers; using Boilerplate.Server.Api.Models.Identity; using Boilerplate.Server.Api.Services.Identity; -//#if (signalR == true) -using Microsoft.AspNetCore.SignalR; -using Boilerplate.Server.Api.Signalr; -using Boilerplate.Server.Api.SignalR; -//#endif namespace Boilerplate.Server.Api; @@ -160,11 +155,17 @@ public static void AddServerApiProjectServices(this WebApplicationBuilder builde }); //#if (signalR == true) - services.AddSingleton, AppHubConnectionHandler>(); - services.AddSignalR(options => + var signalRBuilder = services.AddSignalR(options => { options.EnableDetailedErrors = env.IsDevelopment(); }); + if (string.IsNullOrEmpty(configuration["Azure:SignalR:ConnectionString"]) is false) + { + signalRBuilder.AddAzureSignalR(options => + { + configuration.GetRequiredSection("Azure:SignalR").Bind(options); + }); + } //#endif services.AddPooledDbContextFactory(AddDbContext); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/SignalR/AppHub.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/SignalR/AppHub.cs index 903894402c..d8ba84e0e4 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/SignalR/AppHub.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/SignalR/AppHub.cs @@ -2,25 +2,41 @@ namespace Boilerplate.Server.Api.SignalR; +/// +/// SignalR supports basic scenarios like sending messages to all connected clients using `Clients.All()`, +/// which broadcasts to all SignalR connections, whether authenticated or not. Similarly, `Clients.User(userId)` +/// sends messages to all open browser tabs or applications associated with a specific user. +/// +/// In addition to these, the following enhanced scenarios are supported: +/// 1. `Clients.Group("AuthenticatedClients")`: Sends a message to all browser tabs and apps that are signed in. +/// 2. Each user session knows its own SignalR connection Id. For instance, the application +/// already uses this approach in the `UserController's RevokeSession` method by sending a SignalR message to +/// `Clients.Client(userSession.SignalRConnectionId)`. This ensures that the corresponding browser tab or app clears +/// its access/refresh tokens from storage and navigates to the sign-in page if necessary. +/// [AllowAnonymous] public partial class AppHub : Hub { + [AutoInject] private RootServiceScopeProvider rootScopeProvider = default!; + public override async Task OnConnectedAsync() { if (Context.User.IsAuthenticated() is false) { - if (Context.GetHttpContext()?.Request?.Query?.TryGetValue("access_token", out var _) is true) + if (Context.GetHttpContext()?.Request.Headers.Authorization.Any() is true) { // AppHub allows anonymous connections. However, if an Authorization is included // and the user is not authenticated, it indicates the client has sent an invalid or expired access token. // In this scenario, we should refresh the access token and attempt to reconnect. - throw new HubException(nameof(AppStrings.UnauthorizedException)); } } else { - // Checkout AppHubConnectionHandler's comments for more info. + await using var scope = rootScopeProvider.Invoke(); + await using var dbContext = scope.ServiceProvider.GetRequiredService(); + await dbContext.UserSessions.Where(us => us.Id == Context.User!.GetSessionId()).ExecuteUpdateAsync(us => us.SetProperty(x => x.SignalRConnectionId, Context.ConnectionId)); + await Groups.AddToGroupAsync(Context.ConnectionId, "AuthenticatedClients"); } @@ -29,7 +45,14 @@ public override async Task OnConnectedAsync() public override async Task OnDisconnectedAsync(Exception? exception) { - await Groups.RemoveFromGroupAsync(Context.ConnectionId, "AuthenticatedClients"); + if (Context.User.IsAuthenticated()) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, "AuthenticatedClients"); + + await using var scope = rootScopeProvider.Invoke(); + await using var dbContext = scope.ServiceProvider.GetRequiredService(); + await dbContext.UserSessions.Where(us => us.Id == Context.User!.GetSessionId()).ExecuteUpdateAsync(us => us.SetProperty(x => x.SignalRConnectionId, (string?)null)); + } await base.OnDisconnectedAsync(exception); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/SignalR/AppHubConnectionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/SignalR/AppHubConnectionHandler.cs deleted file mode 100644 index efb814bec9..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/SignalR/AppHubConnectionHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.AspNetCore.SignalR; -using Boilerplate.Server.Api.SignalR; -using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Http.Connections; - -namespace Boilerplate.Server.Api.Signalr; - -/// -/// SignalR supports basic scenarios like sending messages to all connected clients using `Clients.All()`, -/// which broadcasts to all SignalR connections, whether authenticated or not. Similarly, `Clients.User(userId)` -/// sends messages to all open browser tabs or applications associated with a specific user. -/// -/// In addition to these, the following enhanced scenarios are supported: -/// 1. `Clients.Group("AuthenticatedClients")`: Sends a message to all browser tabs and apps that are signed in. -/// 2. User session IDs can function as an equivalent to SignalR connection IDs. For instance, the application -/// already uses this approach in the `UserController's RevokeSession` method by sending a SignalR message to -/// `Clients.Client(userSession.Id.ToString())`. This ensures that the corresponding browser tab or app clears -/// its access/refresh tokens from storage and navigates to the sign-in page if necessary. -/// -public class AppHubConnectionHandler : HubConnectionHandler -{ - public AppHubConnectionHandler(HubLifetimeManager lifetimeManager, IHubProtocolResolver protocolResolver, IOptions globalHubOptions, IOptions> hubOptions, ILoggerFactory loggerFactory, IUserIdProvider userIdProvider, IServiceScopeFactory serviceScopeFactory) - : base(lifetimeManager, protocolResolver, globalHubOptions, hubOptions, loggerFactory, userIdProvider, serviceScopeFactory) - { - } - - public override async Task OnConnectedAsync(ConnectionContext connection) - { - var user = connection.GetHttpContext()?.User; - if (user?.IsAuthenticated() is true) - { - connection.ConnectionId = user!.GetSessionId().ToString(); - } - - await base.OnConnectedAsync(connection); - } -} - diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json index 0c7ca54c08..f5dbc8657b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json @@ -15,6 +15,13 @@ "AzureBlobStorageSasUrl_Comment": "More info about blob storage sas url at https://learn.microsoft.com/en-us/azure/ai-services/translator/document-translation/how-to-guides/create-sas-tokens?tabs=blobs#create-sas-tokens-in-the-azure-portal" //#endif }, + //#if (signalR == true) + "Azure": { + "SignalR": { + "ConnectionString": null + } + }, + //#endif "DataProtectionCertificatePassword": "P@ssw0rdP@ssw0rd", "DataProtectionCertificatePassword_Comment": "It can also be configured using: dotnet user-secrets set 'DataProtectionCertificatePassword' '@nyPassw0rd'", "Identity": { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs index 79041e59da..a4c8b20982 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs @@ -125,6 +125,20 @@ public static void ConfigureMiddlewares(this WebApplication app) }).WithTags("Test"); //#if (signalR == true) + if (string.IsNullOrEmpty(configuration["Azure:SignalR:ConnectionString"]) is false + && settings.WebAppRender.BlazorMode is not Client.Web.BlazorWebAppMode.BlazorWebAssembly) + { + // Azure SignalR is going to send blazor server / auto messages to the Azure Cloud which is useless in this case, + // because scale out lots of messages that are related to the current opened tab of browser only is not necessary and will cost you lots of money. + // https://github.com/Azure/azure-signalr/issues/1738 + // Solutions: + // - Switch to Blazor WebAssembly in production. Hint: To leverage Blazor server's enhanced development experience in local dev environment, you can disable Azure SignalR by setting "Azure:SignalR:ConnectionString" to null in appsettings.json or appsettings.Development.json. + // OR + // - Use Standalone API mode: + // Publish and run the Server.Api project independently to serve restful APIs and SignalR services like AppHub (Just like https://adminpanel-api.bitplatform.dev/swagger deployment) + // and use the Server.Web project solely as a Blazor Server or pre-rendering service provider. + throw new InvalidOperationException("Azure SignalR is not supported with Blazor Server and Auto"); + } app.MapHub("/app-hub"); //#endif diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json index 4d1dea9cd8..3f8b82770c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json @@ -95,6 +95,13 @@ } }, //#endif + //#if (signalR == true) + "Azure": { + "SignalR": { + "ConnectionString": null + } + }, + //#endif "AllowedHosts": "*", "ForwardedHeaders": { "ForwardedHeaders_Comment": "These values apply only if your backend is hosted behind a CDN (such as Cloudflare).", From 53db3444f3fdd15e3df7e82d0fa8183f86e3d340 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Sun, 1 Dec 2024 23:27:13 +0330 Subject: [PATCH 30/87] feat(prerelease): v-9.1.0-pre-05 #9363 (#9368) --- src/Besql/Bit.Besql/wwwroot/bit-besql.js | 2 +- src/Bit.Build.props | 4 ++-- src/BlazorUI/Bit.BlazorUI/Scripts/general.ts | 2 +- .../Bit.BlazorUI.Demo.Server.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Shared.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Client.Core.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Client.Maui.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Client.Web.csproj | 6 +++--- .../wwwroot/service-worker.published.js | 2 +- .../Bit.BlazorUI.Demo.Client.Windows.csproj | 4 ++-- src/BlazorUI/Demo/Directory.Build.props | 2 +- .../Bit.Bswup.Demo/wwwroot/service-worker.js | 2 +- .../wwwroot/service-worker.published.js | 2 +- .../wwwroot/service-worker.js | 2 +- .../wwwroot/service-worker.published.js | 2 +- .../Bit.Bswup/Scripts/bit-bswup.progress.ts | 2 +- src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts | 2 +- src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts | 2 +- .../FullDemo/Client/wwwroot/service-worker.js | 2 +- .../Client/wwwroot/service-worker.published.js | 2 +- src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts | 2 +- src/Bup/Bit.Bup/Scripts/bit-bup.ts | 2 +- src/Butil/Bit.Butil/Scripts/butil.ts | 2 +- .../BlazorEmpty.Client.csproj | 8 ++++---- .../BlazorEmpty/BlazorEmpty.csproj | 8 ++++---- .../wwwroot/service-worker.published.js | 2 +- .../Bit.Boilerplate/src/Directory.Build.props | 2 +- .../src/Directory.Packages.props | 18 +++++++++--------- .../src/Directory.Packages8.props | 18 +++++++++--------- .../Bit.Websites.Careers.Client.csproj | 10 +++++----- .../Bit.Websites.Careers.Server.csproj | 4 ++-- .../Bit.Websites.Careers.Shared.csproj | 4 ++-- src/Websites/Careers/src/Directory.Build.props | 2 +- .../Bit.Websites.Platform.Client.csproj | 12 ++++++------ .../Templates03GettingStartedPage.razor | 4 ++-- .../Templates03GettingStartedPage.razor.cs | 2 +- .../Bit.Websites.Platform.Server.csproj | 4 ++-- .../Bit.Websites.Platform.Shared.csproj | 4 ++-- .../Platform/src/Directory.Build.props | 2 +- .../Bit.Websites.Sales.Client.csproj | 10 +++++----- .../Bit.Websites.Sales.Server.csproj | 4 ++-- .../Bit.Websites.Sales.Shared.csproj | 4 ++-- src/Websites/Sales/src/Directory.Build.props | 2 +- 43 files changed, 93 insertions(+), 93 deletions(-) diff --git a/src/Besql/Bit.Besql/wwwroot/bit-besql.js b/src/Besql/Bit.Besql/wwwroot/bit-besql.js index c335510454..717a4718f1 100644 --- a/src/Besql/Bit.Besql/wwwroot/bit-besql.js +++ b/src/Besql/Bit.Besql/wwwroot/bit-besql.js @@ -1,5 +1,5 @@ var BitBesql = BitBesql || {}; -BitBesql.version = window['bit-besql version'] = '9.1.0-pre-04'; +BitBesql.version = window['bit-besql version'] = '9.1.0-pre-05'; async function synchronizeDbWithCache(file) { diff --git a/src/Bit.Build.props b/src/Bit.Build.props index 6dbff0c1b8..127c408a97 100644 --- a/src/Bit.Build.props +++ b/src/Bit.Build.props @@ -27,8 +27,8 @@ 9.1.0 - https://github.com/bitfoundation/bitplatform/releases/tag/v-$(ReleaseVersion)-pre-04 - $(ReleaseVersion)-pre-04 + https://github.com/bitfoundation/bitplatform/releases/tag/v-$(ReleaseVersion)-pre-05 + $(ReleaseVersion)-pre-05 $(ReleaseVersion).$([System.DateTime]::Now.ToString(HHmm)) diff --git a/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts b/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts index 49d7673bd6..119a4b3ca3 100644 --- a/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts +++ b/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts @@ -1,4 +1,4 @@ -(BitBlazorUI as any).version = (window as any)['bit-blazorui version'] = '9.1.0-pre-04'; +(BitBlazorUI as any).version = (window as any)['bit-blazorui version'] = '9.1.0-pre-05'; interface DotNetObject { invokeMethod(methodIdentifier: string, ...args: any[]): T; diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj index fc5606e7c2..f666454539 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj @@ -5,11 +5,11 @@
    - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj index 47d044058c..fa8c64687b 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj @@ -5,11 +5,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj index 77807f52fb..1417eaa0d6 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj @@ -16,11 +16,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj index 97cd9f8713..05835443a9 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj @@ -85,12 +85,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj index 01a014b1eb..23e28040bc 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj @@ -24,13 +24,13 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js index 1de622cabe..4808be6c12 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-04 +// bit version: 9.1.0-pre-05 // https://github.com/bitfoundation/bitplatform/tree/develop/src/Bswup self.assetsInclude = []; diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj index a677d72859..066003bd05 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj @@ -29,11 +29,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Directory.Build.props b/src/BlazorUI/Demo/Directory.Build.props index 1e28858485..93833841f3 100644 --- a/src/BlazorUI/Demo/Directory.Build.props +++ b/src/BlazorUI/Demo/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js index 3d17920d94..4b433d9d0c 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-04 +// bit version: 9.1.0-pre-05 self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; self.caseInsensitiveUrl = true; diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js index 606af822e6..7f51476b73 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-04 +// bit version: 9.1.0-pre-05 self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; self.caseInsensitiveUrl = true; diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js index f53be0a51e..9a66578c20 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js +++ b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-04 +// bit version: 9.1.0-pre-05 // In development, always fetch from the network and do not enable offline support. // This is because caching would make development more difficult (changes would not diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js index 852d206ba6..9707b334f9 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js +++ b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-04 +// bit version: 9.1.0-pre-05 self.assetsInclude = []; self.assetsExclude = [ diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts index f709822b3c..073f4a850a 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts @@ -1,4 +1,4 @@ -window['bit-bswup.progress version'] = '9.1.0-pre-04'; +window['bit-bswup.progress version'] = '9.1.0-pre-05'; ; (function () { (window as any).startBswupProgress = (autoReload: boolean, diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts index 267aa70693..08e3d1ead6 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts @@ -1,4 +1,4 @@ -self['bit-bswup.sw version'] = '9.1.0-pre-04'; +self['bit-bswup.sw version'] = '9.1.0-pre-05'; interface Window { clients: any diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts index d618ee2279..aee87d7ea2 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts @@ -1,5 +1,5 @@ const BitBswup = {} as any; -BitBswup.version = window['bit-bswup version'] = '9.1.0-pre-04'; +BitBswup.version = window['bit-bswup version'] = '9.1.0-pre-05'; declare const Blazor: any; diff --git a/src/Bswup/FullDemo/Client/wwwroot/service-worker.js b/src/Bswup/FullDemo/Client/wwwroot/service-worker.js index 746afaaf53..c77aad1566 100644 --- a/src/Bswup/FullDemo/Client/wwwroot/service-worker.js +++ b/src/Bswup/FullDemo/Client/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-04 +// bit version: 9.1.0-pre-05 // In development, always fetch from the network and do not enable offline support. // This is because caching would make development more difficult (changes would not diff --git a/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js b/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js index 4e9a196507..9ba44393c8 100644 --- a/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js +++ b/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-04 +// bit version: 9.1.0-pre-05 self.assetsInclude = []; self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; diff --git a/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts b/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts index 81f67a2615..c55cefd81f 100644 --- a/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts +++ b/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts @@ -1,4 +1,4 @@ -window['bit-bup.progress version'] = '9.1.0-pre-04'; +window['bit-bup.progress version'] = '9.1.0-pre-05'; ; (function () { (window as any).startBupProgress = (showLogs: boolean, showAssets: boolean, appContainerSelector: string, hideApp: boolean, autoHide: boolean) => { diff --git a/src/Bup/Bit.Bup/Scripts/bit-bup.ts b/src/Bup/Bit.Bup/Scripts/bit-bup.ts index 94b621475a..80c986b56d 100644 --- a/src/Bup/Bit.Bup/Scripts/bit-bup.ts +++ b/src/Bup/Bit.Bup/Scripts/bit-bup.ts @@ -1,5 +1,5 @@ var BitBup = BitBup || {}; -BitBup.version = window['bit-bup version'] = '9.1.0-pre-04'; +BitBup.version = window['bit-bup version'] = '9.1.0-pre-05'; declare const Blazor: any; diff --git a/src/Butil/Bit.Butil/Scripts/butil.ts b/src/Butil/Bit.Butil/Scripts/butil.ts index 4149eaac32..5dd869ce67 100644 --- a/src/Butil/Bit.Butil/Scripts/butil.ts +++ b/src/Butil/Bit.Butil/Scripts/butil.ts @@ -1,2 +1,2 @@ var BitButil = BitButil || {}; -BitButil.version = window['bit-butil version'] = '9.1.0-pre-04'; \ No newline at end of file +BitButil.version = window['bit-butil version'] = '9.1.0-pre-05'; \ No newline at end of file diff --git a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj index 36c6b40480..c398c5623b 100644 --- a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj +++ b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj @@ -1,4 +1,4 @@ - + @@ -17,14 +17,14 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj index 55351eae52..521205e5a3 100644 --- a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj +++ b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj @@ -1,4 +1,4 @@ - + @@ -19,14 +19,14 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js index 80443e737d..4a5d2adf37 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js @@ -1,5 +1,5 @@ //+:cnd:noEmit -// bit version: 9.1.0-pre-04 +// bit version: 9.1.0-pre-05 // https://github.com/bitfoundation/bitplatform/tree/develop/src/Bswup //#if (notification == true) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props index 60d15f9530..595614bbac 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props @@ -1,4 +1,4 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props index 0aef804e6f..2390360259 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props @@ -1,14 +1,14 @@  - - - - - - - - + + + + + + + + @@ -45,7 +45,7 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props index 97253b7f86..9049c35858 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props @@ -1,14 +1,14 @@  - - - - - - - - + + + + + + + + @@ -45,7 +45,7 @@ - + diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj index 59a84dc572..696e134b87 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj @@ -22,15 +22,15 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj index 2c44ab8911..8899a123f6 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj index ea7b0f0d47..0e15f9fe59 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Directory.Build.props b/src/Websites/Careers/src/Directory.Build.props index d5bea38995..58ed932a2a 100644 --- a/src/Websites/Careers/src/Directory.Build.props +++ b/src/Websites/Careers/src/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj index 70fbbb4c08..63d809f32e 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj @@ -22,16 +22,16 @@ - - - + + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor index f3eaa7d3e9..4ef2ddd4a7 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor @@ -167,8 +167,8 @@ rm $HOME/dotnet.tar.gz }
  • -
    Install Bit Boilerplate project template
    - dotnet new install Bit.Boilerplate::9.1.0-pre-04 +
    Install Bit Boilerplate project template
    + dotnet new install Bit.Boilerplate::9.1.0-pre-05
  • @if (showCrossPlatform && devOS is "Windows") { diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs index f74dec8b6b..1fd137f526 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs @@ -38,7 +38,7 @@ public partial class Templates03GettingStartedPage command:"dotnet nuget add source \"https://api.nuget.org/v3/index.json\" --name \"nuget.org\"; dotnet workload install wasm-tools;"), (text:@"echo 'Install the Bit.Boilerplate project template https://www.nuget.org/packages/Boilerplate.Templates';", - command:"dotnet new install Bit.Boilerplate::9.1.0-pre-04;") + command:"dotnet new install Bit.Boilerplate::9.1.0-pre-05;") ]; if (enableVirtualization) diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj index 1c6469761c..2db44271cd 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj index ea7b0f0d47..0e15f9fe59 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj @@ -6,11 +6,11 @@
    - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Directory.Build.props b/src/Websites/Platform/src/Directory.Build.props index 115b00a13e..63452c549b 100644 --- a/src/Websites/Platform/src/Directory.Build.props +++ b/src/Websites/Platform/src/Directory.Build.props @@ -1,4 +1,4 @@ - + preview diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj index b74f3b0462..02e89fb5b0 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj @@ -22,15 +22,15 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj index 769a26898e..4d0cbbf850 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj index ea7b0f0d47..0e15f9fe59 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Directory.Build.props b/src/Websites/Sales/src/Directory.Build.props index 6b4f7a7660..601644aa85 100644 --- a/src/Websites/Sales/src/Directory.Build.props +++ b/src/Websites/Sales/src/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 From 830d9e1eb49dcf611665dd44b578d6f090980ff1 Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Mon, 2 Dec 2024 09:26:23 +0100 Subject: [PATCH 31/87] fix(templates): resolve windows app update issues in Boilerplate #9361 (#9369) --- .../Pages/Identity/SignIn/TfaPanel.razor | 5 +- .../IClientCoreServiceCollectionExtensions.cs | 2 +- .../Services/WindowsStorageService.cs | 73 ++++++++++++++++--- .../Diagnostics/DiagnosticsController.cs | 1 + .../Program.Services.cs | 4 +- .../src/Shared/Dtos/AppJsonContext.cs | 1 + 6 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/TfaPanel.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/TfaPanel.razor index a3bc9fbd65..35bd88b0aa 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/TfaPanel.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/TfaPanel.razor @@ -1,4 +1,4 @@ -@inherits AppComponentBase +@inherits AppComponentBase
    @@ -6,7 +6,8 @@ @Localizer[nameof(AppStrings.TfaPanelSubtitle)] - 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 2ad8579192..101fa0683f 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 @@ -114,7 +114,7 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle .UseSqlite($"Data Source={dbPath}"); //#if (framework == 'net9.0') - if (AppEnvironment.IsProd()) + if (AppEnvironment.IsDev() is false) { optionsBuilder.UseModel(OfflineDbContextModel.Instance); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsStorageService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsStorageService.cs index 55dccac820..184509e1b7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsStorageService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsStorageService.cs @@ -1,40 +1,89 @@ -namespace Boilerplate.Client.Windows.Services; +using System.IO.IsolatedStorage; + +namespace Boilerplate.Client.Windows.Services; public partial class WindowsStorageService : IStorageService { + private Dictionary? persistentStorage; private readonly Dictionary tempStorage = []; + public async ValueTask IsPersistent(string key) + { + return string.IsNullOrEmpty(await GetItem(key)) is false; + } + public async ValueTask GetItem(string key) { if (tempStorage.TryGetValue(key, out string? value)) return value; - return Application.UserAppDataRegistry.GetValue(key) as string; - } + persistentStorage ??= await Restore(); - public async ValueTask IsPersistent(string key) - { - return string.IsNullOrEmpty(await GetItem(key)) is false; + return persistentStorage.GetValueOrDefault(key, null); } public async ValueTask RemoveItem(string key) { - Application.UserAppDataRegistry.DeleteValue(key, throwOnMissingValue: false); tempStorage.Remove(key); + + persistentStorage ??= await Restore(); + + if (persistentStorage.Remove(key)) + { + await Save(persistentStorage); + } } public async ValueTask SetItem(string key, string? value, bool persistent = true) { if (persistent) { - Application.UserAppDataRegistry.SetValue(key, value ?? string.Empty); + persistentStorage ??= await Restore(); + persistentStorage[key] = value; + await Save(persistentStorage); } else { - if (tempStorage.TryAdd(key, value) is false) - { - tempStorage[key] = value; - } + tempStorage[key] = value; + } + } + + const string WindowsStorageFilename = "Boilerplate.Client.Windows.storage.json"; + private static readonly SemaphoreSlim ioLock = new(1, 1); + // Restore application-scope property from isolated storage + private static async Task> Restore() + { + try + { + await ioLock.WaitAsync(); + using IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForDomain(); + using IsolatedStorageFileStream stream = new IsolatedStorageFileStream(WindowsStorageFilename, FileMode.Open, storage); + return (await JsonSerializer.DeserializeAsync(stream, AppJsonContext.Default.DictionaryStringString))!; + } + catch (IsolatedStorageException exp) when (exp.InnerException is FileNotFoundException) + { + return []; + } + finally + { + ioLock.Release(); + } + } + + // Persist application-scope property to isolated storage + private static async Task Save(Dictionary data) + { + try + { + await ioLock.WaitAsync(); + using IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForDomain(); + using IsolatedStorageFileStream stream = new IsolatedStorageFileStream(WindowsStorageFilename, FileMode.Create, storage); + using StreamWriter writer = new StreamWriter(stream); + await writer.WriteAsync(JsonSerializer.Serialize(data, AppJsonContext.Default.DictionaryStringString)); + } + finally + { + ioLock.Release(); } } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs index 3e2004d847..dae2fd1f8a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs @@ -21,6 +21,7 @@ public partial class DiagnosticsController : AppControllerBase, IDiagnosticsCont [AutoInject] private IHubContext appHubContext = default!; //#endif + [HttpPost] public async Task DoDiagnostics(CancellationToken cancellationToken) { StringBuilder result = new(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs index 930e2b56e4..3140a8d23e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs @@ -316,8 +316,6 @@ private static void AddIdentity(WebApplicationBuilder builder) }) .AddBearerToken(IdentityConstants.BearerScheme, options => { - configuration.GetRequiredSection("Identity").Bind(options); - var validationParameters = new TokenValidationParameters { ClockSkew = TimeSpan.Zero, @@ -349,6 +347,8 @@ private static void AddIdentity(WebApplicationBuilder builder) context.Token ??= context.Request.Query.ContainsKey("access_token") ? context.Request.Query["access_token"] : context.Request.Cookies["access_token"]; } }; + + configuration.GetRequiredSection("Identity").Bind(options); }); if (string.IsNullOrEmpty(configuration["Authentication:Google:ClientId"]) is false) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs index 3672667e13..9c951f7541 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs @@ -18,6 +18,7 @@ namespace Boilerplate.Shared.Dtos; ///
    [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] [JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(string[]))] [JsonSerializable(typeof(RestErrorInfo))] [JsonSerializable(typeof(GitHubStats))] From 5f0d9a32f04653c10a7d72d47d31aa27f1cafde8 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Mon, 2 Dec 2024 13:08:13 +0330 Subject: [PATCH 32/87] feat(templates): use BitPullToRefresh in Boilerplate #9370 (#9371) --- .../Authorized/Settings/SessionsSection.razor | 55 +++++----- .../Settings/SessionsSection.razor.cs | 15 ++- .../Settings/SessionsSection.razor.scss | 8 +- .../Pages/Authorized/Todo/TodoPage.razor | 101 +++++++++--------- .../Pages/Authorized/Todo/TodoPage.razor.cs | 12 ++- 5 files changed, 105 insertions(+), 86 deletions(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor index f19d2a66dd..aa40f2f5a2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor @@ -3,7 +3,7 @@
    - + @if (isLoading) { @@ -15,7 +15,6 @@ } - @if (currentSession is not null) { @Localizer[nameof(AppStrings.CurrentSession)] @@ -23,6 +22,7 @@ - @Localizer[nameof(AppStrings.OtherSessions)] - - - - - - - - - - - - + + @Localizer[nameof(AppStrings.OtherSessions)] + + + + + + + + + + + + }
    diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor.cs index cd08279bb6..2fb5d51e7e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor.cs @@ -24,16 +24,18 @@ protected override async Task OnInitAsync() } - private async Task LoadSessions() + private async Task LoadSessions(bool showLoading = true) { - isLoading = true; + if (showLoading) + { + isLoading = true; + } try { - List userSessions = []; currentSessionId = await PrerenderStateService.GetValue(async () => (await AuthenticationStateTask).User.GetSessionId()); - userSessions = await userController.GetUserSessions(CurrentCancellationToken); + var userSessions = await userController.GetUserSessions(CurrentCancellationToken); otherSessions = userSessions.Where(s => s.Id != currentSessionId).ToArray(); currentSession = userSessions.Single(s => s.Id == currentSessionId); } @@ -43,7 +45,10 @@ private async Task LoadSessions() } finally { - isLoading = false; + if (showLoading) + { + isLoading = false; + } } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor.scss index f0fb673fa3..95ee964b54 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SessionsSection.razor.scss @@ -6,6 +6,12 @@ section { ::deep { .session-persona { + @include lt-sm { + max-width: 100%; + } + } + + .other-session-persona { @include lt-sm { max-width: calc(100% - 30px); } @@ -13,7 +19,7 @@ section { .sessions-list { width: 100%; - padding: 1px; + padding: 2px; height: 20rem; } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor index 15927794b6..d1df41b853 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor @@ -22,7 +22,7 @@ @Localizer[nameof(AppStrings.Add)] - + @@ -67,59 +67,60 @@ } else { - - - - - @Localizer[nameof(AppStrings.NoTodos)] - - - - - - @if (todo.IsInEditMode is false) - { - - - @todo.Date.ToLocalTime().ToString("F") - + + + + + + @Localizer[nameof(AppStrings.NoTodos)] + + + + + + @if (todo.IsInEditMode is false) + { + + + @todo.Date.ToLocalTime().ToString("F") + - - + + - - - } - else - { - + + + } + else + { + - - @Localizer[nameof(AppStrings.Cancel)] - - - @Localizer[nameof(AppStrings.Save)] - - } + + @Localizer[nameof(AppStrings.Cancel)] + + + @Localizer[nameof(AppStrings.Save)] + + } + + - - - - + + + } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor.cs index a9c3f06851..3459ec3636 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Todo/TodoPage.razor.cs @@ -38,9 +38,12 @@ protected override async Task OnInitAsync() await base.OnInitAsync(); } - private async Task LoadTodoItems() + private async Task LoadTodoItems(bool showLoading = true) { - isLoading = true; + if (showLoading) + { + isLoading = true; + } try { @@ -50,7 +53,10 @@ private async Task LoadTodoItems() } finally { - isLoading = false; + if (showLoading) + { + isLoading = false; + } } } From 8ffe8f1d5e08cb8b63df83a8b424de6de9ab88f9 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Mon, 2 Dec 2024 13:08:37 +0330 Subject: [PATCH 33/87] feat(templates): use BitModalService in Boilerplate #9372 (#9373) --- .../Layout/DiagnosticModal.razor.cs | 10 +-- .../Components/Layout/MessageBox.razor | 39 ++++++++--- .../Components/Layout/MessageBox.razor.cs | 11 ++- .../Components/Layout/MessageBox.razor.scss | 20 +++++- .../Components/Layout/Modal.razor | 27 -------- .../Components/Layout/Modal.razor.cs | 63 ----------------- .../Components/Layout/Modal.razor.scss | 26 ------- .../Components/Layout/Prompt.razor | 69 ++++++++++++------- .../Components/Layout/Prompt.razor.cs | 13 +++- .../Components/Layout/Prompt.razor.scss | 20 +++++- .../Components/Layout/RootLayout.razor | 4 +- .../Components/Layout/RootLayout.razor.cs | 1 + .../IClientCoreServiceCollectionExtensions.cs | 4 +- .../Services/MessageBoxService.cs | 17 ++--- .../Services/ModalService.cs | 49 ------------- .../Services/PromptService.cs | 25 ++++--- .../Boilerplate.Client.Core/Styles/app.scss | 9 +++ .../Diagnostics/DiagnosticsController.cs | 2 +- .../Diagnostics/IDiagnosticsController.cs | 2 +- 19 files changed, 171 insertions(+), 240 deletions(-) delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.cs delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.scss delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ModalService.cs 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 index 12f2db9e1f..6c1488e097 100644 --- 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 @@ -134,15 +134,15 @@ private async Task ThrowTestException() showKnownException = !showKnownException; - if (showKnownException) - throw new InvalidOperationException("Something critical happened."); - else - throw new DomainLogicException("Something bad happened."); + throw showKnownException + ? new InvalidOperationException("Something critical happened.") + : new DomainLogicException("Something bad happened."); } private async Task CallDiagnosticsApi() { - await messageBoxService.Show(await diagnosticsController.DoDiagnostics(CurrentCancellationToken), "Diagnostics Result"); + var result = await diagnosticsController.PerformDiagnostics(CurrentCancellationToken); + messageBoxService.Show(result, "Diagnostics Result"); } private void ResetLogs() diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor index 7e10ff122e..c74fe265a1 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor @@ -1,13 +1,30 @@ @inherits AppComponentBase -
    - - @Body - -
    - - - - @Localizer[nameof(AppStrings.Ok)] - - +
    + + + + @Title + + + + + + + +
    + + @Body + +
    + + + + @Localizer[nameof(AppStrings.Ok)] + + +
    +
    diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor.cs index 8ef16e20c7..6ee29e88ed 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/MessageBox.razor.cs @@ -2,12 +2,19 @@ public partial class MessageBox { + [CascadingParameter] private BitModalReference? modalReference { get; set; } + + [Parameter] public string? Title { get; set; } [Parameter] public string? Body { get; set; } - [Parameter] public Action? OnOk { get; set; } + private void CloseModal() + { + modalReference?.Close(); + } + private void OnOkClick() { - OnOk?.Invoke(); + modalReference?.Close(); } } 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 ac94c637e7..0e7cb6ba05 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 @@ -1,7 +1,25 @@ -.body { +@import '../../Styles/abstracts/_media-queries.scss'; + +section { + padding: 1rem; + min-width: 20rem; + max-height: var(--app-height); + + @include lt-md { + min-width: unset; + } +} + +.body { width: 100%; flex-grow: 1; display: flex; overflow: auto; white-space: pre; } + +::deep { + .stack { + max-height: calc(var(--app-height) - 3rem); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor deleted file mode 100644 index ae47327841..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor +++ /dev/null @@ -1,27 +0,0 @@ -@inherits AppComponentBase - -
    - -
    - - - - @title - - - - - - - - @if (componentType is not null) - { - - } - -
    -
    -
    \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.cs deleted file mode 100644 index a9f8f6c42b..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace Boilerplate.Client.Core.Components.Layout; - -public partial class Modal -{ - private bool isOpen; - private string? title; - private Type? componentType; - private bool disposed = false; - private Action? unsubscribeShow; - private Action? unsubscribeClose; - private IDictionary? componentParameters; - - private TaskCompletionSource? tcs; - - protected override Task OnInitAsync() - { - unsubscribeShow = PubSubService.Subscribe(ClientPubSubMessages.SHOW_MODAL, async args => - { - var data = (ModalData)args!; - - tcs = data.TaskCompletionSource; - - await InvokeAsync(() => - { - isOpen = true; - title = data.Title; - componentType = data.ComponentType; - componentParameters = data.Parameters; - - StateHasChanged(); - }); - }); - - unsubscribeClose = PubSubService.Subscribe(ClientPubSubMessages.CLOSE_MODAL, async _ => - { - await CloseModal(); - await InvokeAsync(StateHasChanged); - }); - - return base.OnInitAsync(); - } - - private async Task CloseModal() - { - isOpen = false; - tcs?.SetResult(); - } - - protected override async ValueTask DisposeAsync(bool disposing) - { - await base.DisposeAsync(disposing); - - if (disposed || disposing is false) return; - - tcs?.TrySetResult(); - tcs = null; - - unsubscribeShow?.Invoke(); - unsubscribeClose?.Invoke(); - - disposed = true; - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.scss deleted file mode 100644 index d412824f7a..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Modal.razor.scss +++ /dev/null @@ -1,26 +0,0 @@ -@import '../../Styles/abstracts/_media-queries.scss'; - -section { - padding: 1rem; - min-width: 20rem; - max-height: var(--app-height); - - @include lt-md { - min-width: unset; - } -} - -::deep { - .root { - width: var(--app-width); - height: var(--app-height); - top: var(--app-inset-top); - left: var(--app-inset-left); - right: var(--app-inset-right); - bottom: var(--app-inset-bottom); - } - - .stack { - max-height: calc(var(--app-height) - 3rem); - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor index 575837fd03..09a164bc67 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor @@ -1,29 +1,46 @@ @inherits AppComponentBase -
    - - - @Body - - @if (OtpInput) - { - - } - else - { - - } - -
    +
    + + + + @Title + + + + + + - - - @Localizer[nameof(AppStrings.Ok)] - - +
    + + + @Body + + @if (OtpInput) + { + + } + else + { + + } + +
    + + + + @Localizer[nameof(AppStrings.Ok)] + + +
    +
    diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.cs index 28241ca407..ef0d5d94ad 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.cs @@ -2,14 +2,23 @@ public partial class Prompt { - private string? value; - + [Parameter] public string? Title { get; set; } [Parameter] public bool OtpInput { get; set; } [Parameter] public string? Body { get; set; } [Parameter] public Action? OnOk { get; set; } + [Parameter] public Action? OnCancel { get; set; } + + private string? value; + + private void CloseModal() + { + OnCancel?.Invoke(); + } private void OnOkClick() { + if (string.IsNullOrEmpty(value)) return; + OnOk?.Invoke(value); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.scss index ac94c637e7..0e7cb6ba05 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/Prompt.razor.scss @@ -1,7 +1,25 @@ -.body { +@import '../../Styles/abstracts/_media-queries.scss'; + +section { + padding: 1rem; + min-width: 20rem; + max-height: var(--app-height); + + @include lt-md { + min-width: unset; + } +} + +.body { width: 100%; flex-grow: 1; display: flex; overflow: auto; white-space: pre; } + +::deep { + .stack { + max-height: calc(var(--app-height) - 3rem); + } +} 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 0577dae99b..7016fe3615 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 @@ -44,8 +44,8 @@ - + - \ 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 c845c5ec78..8db7a84ec6 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 @@ -7,6 +7,7 @@ public partial class RootLayout : IDisposable { private BitDir? currentDir; private string? currentUrl; + private readonly BitModalParameters modalParameters = new() { Classes = new() { Root = "modal" } }; /// /// 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 101fa0683f..186f523b99 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 @@ -39,7 +39,6 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle // Defining them as singletons would result in them being shared across all users in Blazor Server and during pre-rendering. // To address this, we use the AddSessioned extension method. // AddSessioned applies AddSingleton in BlazorHybrid and AddScoped in Blazor WebAssembly and Blazor Server, ensuring correct service lifetimes for each environment. - services.AddSessioned(); services.AddSessioned(); services.AddSessioned(); services.AddSessioned(); @@ -67,7 +66,8 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle .ValidateOnStart(); services.AddBitButilServices(); - services.AddBitBlazorUIServices(); + services.AddBitBlazorUIServices(singleton: AppPlatform.IsBlazorHybrid); + services.AddBitBlazorUIExtrasServices(singleton: AppPlatform.IsBlazorHybrid); // This code constructs a chain of HTTP message handlers. By default, it uses `HttpClientHandler` // to send requests to the server. However, you can replace `HttpClientHandler` with other HTTP message diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxService.cs index d071764907..2279e1e970 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/MessageBoxService.cs @@ -2,21 +2,16 @@ public partial class MessageBoxService { - [AutoInject] private ModalService modalService = default!; + [AutoInject] private BitModalService modalService = default!; - public Task Show(string message, string title = "") + public void Show(string message, string title = "") { - TaskCompletionSource tcs = new(); Dictionary parameters = new() { - { nameof(MessageBox.Body), message }, - { nameof(MessageBox.OnOk), () => { tcs.SetResult(true); modalService.Close(); } } + { nameof(MessageBox.Title), title }, + { nameof(MessageBox.Body), message } }; - modalService.Show(parameters, title).ContinueWith(async task => - { - await task; - tcs.TrySetResult(false); - }); - return tcs.Task; + + _ = modalService.Show(parameters); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ModalService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ModalService.cs deleted file mode 100644 index 695eae7950..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ModalService.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace Boilerplate.Client.Core.Services; - -public partial class ModalService -{ - private bool isRunning = false; - private readonly ConcurrentQueue queue = new(); - - - [AutoInject] private readonly PubSubService pubSubService = default!; - - - public void Close() - { - pubSubService.Publish(ClientPubSubMessages.CLOSE_MODAL); - } - - public Task Show(IDictionary? parameters = null, string title = "") - { - TaskCompletionSource tcs = new(); - - queue.Enqueue(new(typeof(T), parameters, title, tcs)); - - if (isRunning is false) - { - isRunning = true; - _ = ProcessQueue(); - } - - return tcs.Task; - } - - private async Task ProcessQueue() - { - if (queue.IsEmpty) - { - isRunning = false; - return; - } - - if (queue.TryDequeue(out var data)) - { - pubSubService.Publish(ClientPubSubMessages.SHOW_MODAL, data, persistent: true); - - await data.TaskCompletionSource.Task; - } - - _ = ProcessQueue(); - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PromptService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PromptService.cs index 518ac61ba0..201b885e6d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PromptService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PromptService.cs @@ -1,23 +1,28 @@ -namespace Boilerplate.Client.Core.Services; +using Microsoft.AspNetCore.Components.Web; + +namespace Boilerplate.Client.Core.Services; public partial class PromptService { - [AutoInject] private ModalService modalService = default!; + [AutoInject] private BitModalService modalService = default!; - public Task Show(string message, string title = "", bool otpInput = false) + public async Task Show(string message, string title = "", bool otpInput = false) { TaskCompletionSource tcs = new(); - Dictionary parameters = new() + BitModalReference? modalReference = null; + Dictionary promptParameters = new() { + { nameof(Prompt.Title), title }, { nameof(Prompt.Body), message }, { nameof(Prompt.OtpInput), otpInput }, - { nameof(Prompt.OnOk), (string value) => { tcs.SetResult(value); modalService.Close(); } } + { nameof(Prompt.OnCancel), () => { tcs.SetResult(null); modalReference?.Close(); } }, + { nameof(Prompt.OnOk), (string value) => { tcs.SetResult(value); modalReference?.Close(); } } }; - modalService.Show(parameters, title).ContinueWith(async task => + var modalParameters = new BitModalParameters() { - await task; - tcs.TrySetResult(null); - }); - return tcs.Task; + OnOverlayClick = EventCallback.Factory.Create(this, () => tcs.SetResult(null)) + }; + modalReference = await modalService.Show(promptParameters, modalParameters); + return await tcs.Task; } } 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 8d709324f8..cda4bec71d 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 @@ -71,6 +71,15 @@ p { width: min(25rem, 100%); } +.modal { + width: var(--app-width); + height: var(--app-height); + top: var(--app-inset-top); + left: var(--app-inset-left); + right: var(--app-inset-right); + bottom: var(--app-inset-bottom); +} + div[dir=rtl] { .bitdatagrid-paginator { .go-next, .go-last { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs index dae2fd1f8a..da75c04b7d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Diagnostics/DiagnosticsController.cs @@ -22,7 +22,7 @@ public partial class DiagnosticsController : AppControllerBase, IDiagnosticsCont //#endif [HttpPost] - public async Task DoDiagnostics(CancellationToken cancellationToken) + public async Task PerformDiagnostics(CancellationToken cancellationToken) { StringBuilder result = new(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Diagnostics/IDiagnosticsController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Diagnostics/IDiagnosticsController.cs index c82185e5e7..9cd0bf8bee 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Diagnostics/IDiagnosticsController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Diagnostics/IDiagnosticsController.cs @@ -4,5 +4,5 @@ public interface IDiagnosticsController : IAppController { [HttpPost] - Task DoDiagnostics(CancellationToken cancellationToken); + Task PerformDiagnostics(CancellationToken cancellationToken); } From 020f04a5e9472f8d6d5209af703c4283b40832e6 Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Mon, 2 Dec 2024 12:57:51 +0100 Subject: [PATCH 34/87] fix(infra): resolve CDN issues with brotli comressed assets #9367 (#9376) --- .../Startup/Middlewares.cs | 5 +-- .../Program.Middlewares.cs | 5 +-- .../Components/App.razor | 29 ++++++++--------- .../Startup/Middlewares.cs | 5 +-- .../Templates03GettingStartedPage.razor | 11 +++++-- .../Components/App.razor | 31 +++++++++---------- .../Startup/Middlewares.cs | 5 +-- .../Startup/Middlewares.cs | 5 +-- 8 files changed, 51 insertions(+), 45 deletions(-) diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Startup/Middlewares.cs b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Startup/Middlewares.cs index bc579d3bdd..e0d3a9d423 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Startup/Middlewares.cs +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Startup/Middlewares.cs @@ -39,8 +39,9 @@ public static void Use(WebApplication app, IWebHostEnvironment env, IConfigurati { context.Response.GetTypedHeaders().CacheControl = new() { - MaxAge = TimeSpan.FromDays(7), - Public = true + Public = true, + NoTransform = true, + MaxAge = TimeSpan.FromDays(7) }; }); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs index a4c8b20982..253540dc08 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs @@ -81,8 +81,9 @@ public static void ConfigureMiddlewares(this WebApplication app) { context.Response.GetTypedHeaders().CacheControl = new() { - MaxAge = TimeSpan.FromDays(7), - Public = true + Public = true, + NoTransform = true, + MaxAge = TimeSpan.FromDays(7) }; }); } diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Components/App.razor b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Components/App.razor index 69e45f603c..7610b52f68 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Components/App.razor +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Components/App.razor @@ -21,23 +21,20 @@ - @if (HttpContext.Request.IsCrawlerClient() is false) - { - - + - - - } + } + }); + + + diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Startup/Middlewares.cs b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Startup/Middlewares.cs index b8d762807b..c8254b483b 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Startup/Middlewares.cs +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Startup/Middlewares.cs @@ -38,8 +38,9 @@ public static void Use(WebApplication app, IWebHostEnvironment env, IConfigurati { context.Response.GetTypedHeaders().CacheControl = new() { - MaxAge = TimeSpan.FromDays(7), - Public = true + Public = true, + NoTransform = true, + MaxAge = TimeSpan.FromDays(7) }; }); } diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor index 4ef2ddd4a7..7721828b8e 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor @@ -108,8 +108,7 @@ @if (devOS is "Linux") {
    - -
    To install .NET SDK on Linux(Ubuntu) run the following commands:
    + @if (dotnetVersion is "net9.0") { wget https://download.visualstudio.microsoft.com/download/pr/308f16a9-2ecf-4a42-b8bb-c1233de985fd/be6e87045ab21935bd8bb98ce69026c4/dotnet-sdk-9.0.100-linux-x64.tar.gz -O $HOME/dotnet.tar.gz @@ -131,6 +130,14 @@ rm $HOME/dotnet.tar.gz } + @if (devOS is "Linux") + { +
  • + Install python +
    Ensure the `python --version` command works. While Ubuntu comes with Python 3 pre-installed, you need to create a symbolic link to use the `python` command. Use the following command:
    + sudo apt update sudo apt install python-is-python3 +
  • + }
  • Install Node.js

  • diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Components/App.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Components/App.razor index 136bf33140..877a7267b4 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Components/App.razor +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Components/App.razor @@ -42,24 +42,21 @@ - @if (HttpContext.Request.IsCrawlerClient() is false) - { - - + - - - - } + } + }); + + + + diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Startup/Middlewares.cs b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Startup/Middlewares.cs index 3fb3814349..219413febd 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Startup/Middlewares.cs +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Startup/Middlewares.cs @@ -38,8 +38,9 @@ public static void Use(WebApplication app, IWebHostEnvironment env, IConfigurati { context.Response.GetTypedHeaders().CacheControl = new() { - MaxAge = TimeSpan.FromDays(7), - Public = true + Public = true, + NoTransform = true, + MaxAge = TimeSpan.FromDays(7) }; }); } diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Server/Startup/Middlewares.cs b/src/Websites/Sales/src/Bit.Websites.Sales.Server/Startup/Middlewares.cs index 923ecdb312..faa6cba38a 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Server/Startup/Middlewares.cs +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Server/Startup/Middlewares.cs @@ -38,8 +38,9 @@ public static void Use(WebApplication app, IWebHostEnvironment env, IConfigurati { context.Response.GetTypedHeaders().CacheControl = new() { - MaxAge = TimeSpan.FromDays(7), - Public = true + Public = true, + NoTransform = true, + MaxAge = TimeSpan.FromDays(7) }; }); } From b98ca2233fa4e8226142fb864a6b9be265a58fc3 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Mon, 2 Dec 2024 16:50:58 +0330 Subject: [PATCH 35/87] feat(templates): remove singleton service registration for BlazorUI in Boilerplate #9377 (#9378) --- .../Extensions/IClientCoreServiceCollectionExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 186f523b99..5aed5f5975 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 @@ -66,7 +66,7 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle .ValidateOnStart(); services.AddBitButilServices(); - services.AddBitBlazorUIServices(singleton: AppPlatform.IsBlazorHybrid); + services.AddBitBlazorUIServices(); services.AddBitBlazorUIExtrasServices(singleton: AppPlatform.IsBlazorHybrid); // This code constructs a chain of HTTP message handlers. By default, it uses `HttpClientHandler` From f677c5cb27323436a5ea29509602eb39caf5a34f Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Mon, 2 Dec 2024 20:16:53 +0330 Subject: [PATCH 36/87] fix(blazorui): resolve issues of FixedColor and IconOnly parameters in BitButton #9382 (#9383) --- .../Buttons/BitButton/BitButton.scss | 21 ++++++++------ .../Components/Buttons/BitButtonDemo.razor | 28 +++++++++++++++---- .../Buttons/BitButtonDemo.razor.samples.cs | 19 +++++++++++-- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss index 906d31496f..8a93d5a46d 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss @@ -89,15 +89,6 @@ font-size: var(--bit-btn-lbl-fontsize); } -.bit-btn-ntx { - padding: var(--bit-btn-pad-ntx); - --bit-btn-icn-margintop: 0; -} - -.bit-btn-fxc { - color: var(--bit-btn-clr-txt); -} - .bit-btn-flw { width: 100%; z-index: $zindex-base; @@ -410,3 +401,15 @@ transform: translateX(-50%); bottom: var(--bit-btn-float-offset); } + + +// important: these need to be here at the end! + +.bit-btn-fxc { + color: var(--bit-btn-clr-txt); +} + +.bit-btn-ntx { + padding: var(--bit-btn-pad-ntx); + --bit-btn-icn-margintop: 0; +} \ No newline at end of file diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor index 00d72a7287..9911e939d8 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor @@ -372,13 +372,29 @@
    Setting the FullWidth makes the button occupy 100% of its container's width.

    -
    - Full Width Button -
    + Full Width Button +
    + + + + +
    FixedColor flag parameter makes the foreground color to be fixed regardless of its hovering state.
    +
    + + +
    - +
    Offering a range of specialized color variants with Primary being the default, providing visual cues for specific actions or states within your application.

    @@ -492,7 +508,7 @@
    - +
    Empower customization by overriding default styles and classes, allowing tailored design modifications to suit specific UI requirements.


    @@ -535,7 +551,7 @@
    - +
    Use BitButton in right-to-left (RTL).

    diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.samples.cs index 63d0e9880f..053421653b 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.samples.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Buttons/BitButtonDemo.razor.samples.cs @@ -311,6 +311,19 @@ private async Task LoadingTemplateClick() Full Width Button"; private readonly string example15RazorCode = @" + + +"; + + private readonly string example16RazorCode = @" Primary Primary Primary @@ -382,7 +395,7 @@ private async Task LoadingTemplateClick() TertiaryBorder TertiaryBorder"; - private readonly string example16RazorCode = @" + private readonly string example17RazorCode = @" - IsStyledPanelOpen = true"">Open styled panel - IsClassedPanelOpen = true"">Open classed panel - IsPanelStylesOpen = true"">Open panel styles - IsPanelClassesOpen = true"">Open panel classes + isStyledPanelOpen = true"">Open Styled panel + isClassedPanelOpen = true"">Open Classed panel + + isPanelStylesOpen = true"">Open panel Styles + isPanelClassesOpen = true"">Open panel Classes - Content goes here. + + BitPanel with custom style. + + BitPanel with custom class:
    Item 1
    Item 2
    Item 3
    - + - Content goes here. - - - - Content goes here. - + + BitPanel with Styles to customize its elements. + - Content goes here. - -"; + BitPanel with Classes to customize its elements. +"; private readonly string example6CsharpCode = @" -private bool IsStyledPanelOpen = false; -private bool IsClassedPanelOpen = false; -private bool IsPanelStylesOpen = false; -private bool IsPanelClassesOpen = false;"; +private bool isStyledPanelOpen; +private bool isClassedPanelOpen; +private bool isPanelStylesOpen; +private bool isPanelClassesOpen;"; private readonly string example7RazorCode = @" - IsRtlPanelOpen = true""> - باز کردن پنل - + isRtlPanelOpenStart = true"">آغاز + isRtlPanelOpenEnd = true"">پایان + + +

    + لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. + چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد. + کتابهای زیادی در شصت و سه درصد گذشته، حال و آینده شناخت فراوان جامعه و متخصصان را می طلبد تا با نرم افزارها شناخت بیشتری را برای طراحان رایانه ای علی الخصوص طراحان خلاقی و فرهنگ پیشرو در زبان فارسی ایجاد کرد. + در این صورت می توان امید داشت که تمام و دشواری موجود در ارائه راهکارها و شرایط سخت تایپ به پایان رسد وزمان مورد نیاز شامل حروفچینی دستاوردهای اصلی و جوابگوی سوالات پیوسته اهل دنیای موجود طراحی اساسا مورد استفاده قرار گیرد. +

    +
    - +

    لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی می باشد. @@ -500,5 +518,6 @@ در این صورت می توان امید داشت که تمام و دشوار

    "; private readonly string example7CsharpCode = @" -private bool IsRtlPanelOpen = false;"; +private bool isRtlPanelOpenStart; +private bool isRtlPanelOpenEnd;"; } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Panel/BitPanelDemo.razor.scss b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Panel/BitPanelDemo.razor.scss index d8d1a8b930..a54db15c4f 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Panel/BitPanelDemo.razor.scss +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Panel/BitPanelDemo.razor.scss @@ -19,7 +19,8 @@ } ::deep { - .header-margin, .custom-header { + .header-margin, + .custom-header { margin-top: var(--bit-status-bar-height); @supports (-webkit-touch-callout: none) { @@ -33,12 +34,14 @@ } } - .custom-class .item { - width: 3rem; - height: 3rem; - margin: 0.5rem; - border-radius: 0.5rem; - background-color: brown; + .custom-class { + .item { + width: 3rem; + height: 3rem; + margin: 0.5rem; + border-radius: 0.5rem; + background-color: brown; + } } .custom-container { From a4863beb4f4fadb7cbacdf856c095fb920cb9141 Mon Sep 17 00:00:00 2001 From: Mohammad Aminsafaei Date: Wed, 4 Dec 2024 11:54:51 +0330 Subject: [PATCH 44/87] feat(blazorui): add InitialSelectedItems parameter to BitDorpdown #9364 (#9381) --- .../Inputs/Dropdown/BitDropdown.razor.cs | 30 +++++++++++++++++-- .../Inputs/Dropdown/BitDropdownDemo.razor.cs | 7 +++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.razor.cs index a60c48ab2a..f21c5e3b8f 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.razor.cs @@ -126,6 +126,11 @@ namespace Bit.BlazorUI; ///
  • [Parameter] public RenderFragment? HeaderTemplate { get; set; } + /// + /// The initial items that will be used to set selected items when using an ItemProvider. + /// + [Parameter] public IEnumerable? InitialSelectedItems { get; set; } + /// /// Determines the opening state of the callout. (two-way bound) /// @@ -779,14 +784,32 @@ protected override async Task OnInitializedAsync() if (MultiSelect) { - if (ValuesHasBeenSet is false && DefaultValues is not null) + if (ItemsProvider is not null && (InitialSelectedItems?.Any() ?? false)) + { + _selectedItems.AddRange(InitialSelectedItems); + + if (ValuesHasBeenSet is false) + { + await AssignValues(_selectedItems.Select(s => GetValue(s))); + } + } + else if (ValuesHasBeenSet is false && DefaultValues is not null) { await AssignValues(DefaultValues); } } else { - if (ValueHasBeenSet is false && DefaultValue is not null) + if (ItemsProvider is not null && (InitialSelectedItems?.Any() ?? false)) + { + _selectedItems.Add(InitialSelectedItems.First()); + + if (ValueHasBeenSet is false) + { + Value = GetValue(_selectedItems.First()); + } + } + else if (ValueHasBeenSet is false && DefaultValue is not null) { Value = DefaultValue; } @@ -1143,6 +1166,9 @@ private async ValueTask> InternalItemsProvider(ItemsP _lastShowItems = [.. providerResult.Items]; + UpdateSelectedItemsFromValues(); + await InvokeAsync(StateHasChanged); + return new ItemsProviderResult(providerResult.Items, providerResult.TotalItemCount); } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/BitDropdownDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/BitDropdownDemo.razor.cs index 96cacf31fa..1fcc769595 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/BitDropdownDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/BitDropdownDemo.razor.cs @@ -131,6 +131,13 @@ public partial class BitDropdownDemo Description = "The custom template for rendering the header items of the dropdown.", }, new() + { + Name = "InitialSelectedItems", + Type = "IEnumerable?", + DefaultValue = "null", + Description = "The initial items that will be used to set selected items when using an ItemProvider.", + }, + new() { Name = "IsOpen", Type = "bool", From 2c5ea077a3b9db1fab1b46ebd1be077447fedebb Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Thu, 5 Dec 2024 12:31:35 +0100 Subject: [PATCH 45/87] feat(templates): improve Boilerplate forwarded headers middleware configuration #9393 (#9394) --- .github/workflows/admin-sample.cd.yml | 32 ++++++------------- .../Components/ClientAppCoordinator.cs | 10 ++++-- .../appsettings.Production.json | 4 +-- .../appsettings.Development.json | 5 ++- .../Boilerplate.Server.Api/appsettings.json | 6 ++-- .../appsettings.Development.json | 5 ++- .../Boilerplate.Server.Web/appsettings.json | 26 +++++---------- .../src/Shared/SharedSettings.cs | 2 +- 8 files changed, 40 insertions(+), 50 deletions(-) diff --git a/.github/workflows/admin-sample.cd.yml b/.github/workflows/admin-sample.cd.yml index 8ffa774e73..c750296103 100644 --- a/.github/workflows/admin-sample.cd.yml +++ b/.github/workflows/admin-sample.cd.yml @@ -3,8 +3,7 @@ # Project templates come equipped with CI/CD for both Azure DevOps and GitHub, providing you with a hassle-free way to get started with your new project. It is important to note that you should not depend on the contents of this file. More info at https://bitplatform.dev/templates/dev-ops env: - API_SERVER_ADDRESS: 'https://adminpanel-api.bitplatform.dev' - WEB_SERVER_ADDRESS: 'https://adminpanel.bitplatform.dev' + SERVER_ADDRESS: 'https://adminpanel.bitplatform.dev' APP_SERVICE_NAME: 'bit-adminpanel' on: @@ -36,7 +35,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 --sample Admin --appInsights --sentry --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --api Standalone --notification --captcha reCaptcha --signalR --framework net9.0 + cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --appInsights --sentry --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 @@ -44,7 +43,7 @@ jobs: files: 'AdminPanel/src/Shared/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Core/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Web/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Web/appsettings.Production.json' env: WebAppRender.BlazorMode: BlazorWebAssembly - ServerAddress: ${{ env.API_SERVER_ADDRESS }} + ServerAddress: ${{ env.SERVER_ADDRESS }} Logging.Sentry.Dsn: ${{ secrets.ADMINPANEL_SENTRY_DSN }} GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} AdsPushVapid.PublicKey: ${{ secrets.ADMINPANEL_PUBLIC_VAPIDKEY }} @@ -61,24 +60,13 @@ jobs: run: dotnet build AdminPanel/src/Client/AdminPanel.Client.Core/AdminPanel.Client.Core.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - name: Publish - run: dotnet publish AdminPanel/src/Server/AdminPanel.Server.Api/AdminPanel.Server.Api.csproj -c Release -p:PwaEnabled=true --self-contained -r linux-x64 -o ${{env.DOTNET_ROOT}}/server -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" + run: dotnet publish AdminPanel/src/Server/AdminPanel.Server.Web/AdminPanel.Server.Web.csproj -c Release -p:PwaEnabled=true --self-contained -r linux-x64 -o ${{env.DOTNET_ROOT}}/server -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" - name: Upload server artifact uses: actions/upload-artifact@v4 with: name: server-bundle path: ${{env.DOTNET_ROOT}}/server - - - name: Publish adminpanel blazor wasm standalone - run: | - sed -i 's/adminpanel.bitplatform.dev/adminpanel-api.bitplatform.dev/g' AdminPanel/src/Client/AdminPanel.Client.Web/wwwroot/index.html - dotnet publish AdminPanel/src/Client/AdminPanel.Client.Web/AdminPanel.Client.Web.csproj -c Release -p:PwaEnabled=true -o ${{env.DOTNET_ROOT}}/static -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" - - - name: Upload static artifact - uses: actions/upload-artifact@v4 - with: - name: static-bundle - path: ${{env.DOTNET_ROOT}}/static include-hidden-files: true # Required for wwwroot/.well-known folder deploy_api_blazor: @@ -145,14 +133,14 @@ 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 --sample Admin --windows --appInsights --sentry --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --offlineDb --framework net9.0 + cd ..\..\..\ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --windows --appInsights --sentry --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --offlineDb --framework net9.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 with: files: 'AdminPanel\src\Shared\appsettings.json, AdminPanel\src\Client\AdminPanel.Client.Core\appsettings.json, AdminPanel\src\Client\AdminPanel.Client.Windows\appsettings.json' env: - ServerAddress: ${{ env.API_SERVER_ADDRESS }} + ServerAddress: ${{ env.SERVER_ADDRESS }} Logging.Sentry.Dsn: ${{ secrets.ADMINPANEL_SENTRY_DSN }} GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} WindowsUpdate.FilesUrl: https://windows-adminpanel.bitplatform.dev @@ -193,7 +181,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 --sample Admin --appInsights --sentry --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 + cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --appInsights --sentry --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 - uses: actions/setup-node@v4 with: @@ -218,7 +206,7 @@ jobs: with: files: 'AdminPanel/src/Shared/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Core/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Maui/appsettings.json' env: - ServerAddress: ${{ env.API_SERVER_ADDRESS }} + ServerAddress: ${{ env.SERVER_ADDRESS }} Logging.Sentry.Dsn: ${{ secrets.ADMINPANEL_SENTRY_DSN }} GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} @@ -270,14 +258,14 @@ 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 --sample Admin --appInsights --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 + cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --appInsights --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 with: files: 'AdminPanel/src/Shared/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Core/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Maui/appsettings.json' env: - ServerAddress: ${{ env.API_SERVER_ADDRESS }} + ServerAddress: ${{ env.SERVER_ADDRESS }} Logging.Sentry.Dsn: ${{ secrets.ADMINPANEL_SENTRY_DSN }} GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} 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 eb09997a36..1fa8cb7bd8 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 @@ -83,6 +83,7 @@ private void NavigationManager_LocationChanged(object? sender, LocationChangedEv navigatorLogger.LogInformation("Navigator's location changed to {Location}", e.Location); } + private Guid? lastPropagatedUserId = Guid.Empty; /// /// This code manages the association of a user with sensitive services, such as SignalR, push notifications, App Insights, and others, /// ensuring the user is correctly set or cleared as needed. @@ -91,11 +92,14 @@ public async Task PropagateUserId(bool firstRun, Task task) { try { - Abort(); // Cancels ongoing user id propagation, because the new authentication state is available. - var user = (await task).User; var isAuthenticated = user.IsAuthenticated(); - TelemetryContext.UserId = isAuthenticated ? user.GetUserId() : null; + var userId = isAuthenticated ? user.GetUserId() : (Guid?)null; + if (lastPropagatedUserId == userId) + return; + Abort(); // Cancels ongoing user id propagation, because the new authentication state is available. + lastPropagatedUserId = userId; + TelemetryContext.UserId = userId; TelemetryContext.UserSessionId = isAuthenticated ? user.GetSessionId() : null; // Typically, we use the logger directly without utilizing logger.BeginScope. diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.Production.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.Production.json index a21d17f43b..fcc77c4d43 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.Production.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.Production.json @@ -1,6 +1,6 @@ -{ +{ "WebAppRender": { - "BlazorMode": "BlazorAuto" + "BlazorMode": "BlazorWebAssembly" }, "$schema": "https://json.schemastore.org/appsettings.json" } \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.Development.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.Development.json index 211cab3b63..303d62d4fb 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.Development.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.Development.json @@ -1,4 +1,7 @@ -{ +{ + "ForwardedHeaders": { + "AllowedHosts": [ "*" ] + }, "DetailedErrors": true, "$schema": "https://json.schemastore.org/appsettings.json" } \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json index f5dbc8657b..73f16771bf 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/appsettings.json @@ -114,10 +114,12 @@ }, "AllowedHosts": "*", "ForwardedHeaders": { - "ForwardedHeaders_Comment": "These values apply only if your backend is hosted behind a CDN (such as Cloudflare).", + "ForwardedHeaders": "All", + "ForwardedHeaders_Comment": "These values apply only if your backend is hosted behind a CDN (such as `Cloudflare`).", "ForwardedHostHeaderName": "X-Forwarded-Host", "ForwardedHostHeaderName_Comment": "For Cloudflare, use X-Host instead of X-Forwarded-Host.", - "ForwardedHeaders": "All" + "AllowedHosts": [ "" ], + "AllowedHosts_Comment": "If you're using a CDN like Cloudflare in front of your server, make sure to add your domain name to the `ForwardedHeaders:AllowedHosts` setting." }, "$schema": "https://json.schemastore.org/appsettings.json" } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.Development.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.Development.json index 211cab3b63..303d62d4fb 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.Development.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.Development.json @@ -1,4 +1,7 @@ -{ +{ + "ForwardedHeaders": { + "AllowedHosts": [ "*" ] + }, "DetailedErrors": true, "$schema": "https://json.schemastore.org/appsettings.json" } \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json index 3f8b82770c..5cd6473329 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/appsettings.json @@ -3,19 +3,16 @@ "ConnectionStrings": { "SqlServerConnectionString": "Data Source=(localdb)\\mssqllocaldb; Initial Catalog=BoilerplateDb;Integrated Security=true;Application Name=Boilerplate;TrustServerCertificate=True;", "SqliteConnectionString": "Data Source=App_Data/BoilerplateDb.db;", - "SqliteConnectionString_Comment": "To debug inside docker, change ConnectionStrings__SqliteConnectionString's value in launchSettings.json", "PostgreSQLConnectionString": "User ID=postgres;Password=postgres;Host=localhost;Database=BoilerplateDb;", "MySqlSQLConnectionString": "Server=localhost;Port=3306;Database=BoilerplateDb;Uid=root;Pwd=123456;", - "AzureBlobStorageSasUrl": "emulator", - "AzureBlobStorageSasUrl_Comment": "More info about blob storage sas url at https://learn.microsoft.com/en-us/azure/ai-services/translator/document-translation/how-to-guides/create-sas-tokens?tabs=blobs#create-sas-tokens-in-the-azure-portal" + "AzureBlobStorageSasUrl": "emulator" + }, "DataProtectionCertificatePassword": "P@ssw0rdP@ssw0rd", - "DataProtectionCertificatePassword_Comment": "It can also be configured using: dotnet user-secrets set 'DataProtectionCertificatePassword' 'P@ssw0rdP@ssw0rd'", "Identity": { "Issuer": "Boilerplate", "Audience": "Boilerplate", "BearerTokenExpiration": "0.00:05:00", - "BearerTokenExpiration_Comment": "BearerTokenExpiration used as JWT's expiration claim, access token's expires in and cookie's max age. Format: D.HH:mm:ss", "RefreshTokenExpiration": "14.00:00:00", "EmailTokenLifetime": "0.00:02:00", "PhoneNumberTokenLifetime": "0.00:02:00", @@ -36,7 +33,6 @@ }, "Email": { "Host": "LocalFolder", - "Host_Comment": "Local folder means storing emails as .eml file in App_Data/sent-emails folder (Recommended for testing purposes only) instead of sending them using smtp server.", "Port": "587", "DefaultFromEmail": "info@Boilerplate.com", "UserName": null, @@ -49,26 +45,18 @@ }, "UserProfileImagesDir": "attachments/profiles/", "GoogleRecaptchaSecretKey": "6LdMKr4pAAAAANvngWNam_nlHzEDJ2t6SfV6L_DS", - "AdsPushVapid_Comment": "https://github.com/adessoTurkey-dotNET/AdsPush", "AdsPushVapid": { - "AdsPushVapid_Comment": "Web push's vapid. More info at https://tools.reactpwa.com/vapid/", "Subject": "mailto:test@bitplatform.dev", - "PrivateKey": "dMIR1ICj-lDWYZ-ZYCwXKyC2ShYayYYkEL-oOPnpq9c", - "PublicKey_Comment": "Set public key in Client.Core's appsettings.json" + "PrivateKey": "dMIR1ICj-lDWYZ-ZYCwXKyC2ShYayYYkEL-oOPnpq9c" }, "AdsPushAPNS": { "P8PrivateKey": null, - "P8PrivateKey_Comment": "p8 certificate string without spaces and start/end tags.", "P8PrivateKeyId": null, - "P8PrivateKeyId_Comment": "10-digit p8 certificate id; often part of a downloadable certificate filename", "TeamId": null, - "TeamId_Comment": "10-digit Apple team id shown on the Apple Developer Membership Page", "AppBundleIdentifier": null, - "EnvironmentType": "Development", - "EnvironmentType_Comment": "Apns Env one of Development or Production" + "EnvironmentType": "Development" }, "AdsPushFirebase": { - "AdsPushFirebase_Comment": "Filed names in service_account.json => project_id,private_key_id,private_key,client_email,client_id,client_x509_cert_url", "Type": "service_account", "AuthUri": "https://accounts.google.com/o/oauth2/auth", "TokenUri": "https://oauth2.googleapis.com/token", @@ -104,10 +92,12 @@ //#endif "AllowedHosts": "*", "ForwardedHeaders": { - "ForwardedHeaders_Comment": "These values apply only if your backend is hosted behind a CDN (such as Cloudflare).", + "ForwardedHeaders": "All", + "ForwardedHeaders_Comment": "These values apply only if your backend is hosted behind a CDN (such as `Cloudflare`).", "ForwardedHostHeaderName": "X-Forwarded-Host", "ForwardedHostHeaderName_Comment": "For Cloudflare, use X-Host instead of X-Forwarded-Host.", - "ForwardedHeaders": "All" + "AllowedHosts": [ "" ], + "AllowedHosts_Comment": "If you're using a CDN like Cloudflare in front of your server, make sure to add your domain name to the `ForwardedHeaders:AllowedHosts` setting." }, "$schema": "https://json.schemastore.org/appsettings.json" } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/SharedSettings.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/SharedSettings.cs index 2cc3ddc864..25b5597677 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/SharedSettings.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/SharedSettings.cs @@ -4,7 +4,7 @@ namespace Boilerplate.Shared; public partial class SharedSettings : IValidatableObject { /// - /// If you are hosting the API and web client on different URLs (e.g., adminpanel-api.bitplatform.dev and adminpanel.bitplatform.dev), you must set `WebClientUrl` to your web client's address. + /// If you are hosting the API and web client on different URLs (e.g., api.test.com and test.com), you must set `WebClientUrl` to your web client's address. /// This ensures that the API server redirects to the correct URL after social sign-ins and other similar actions. /// public string? WebClientUrl { get; set; } From 239f78e8190769b99ee465c9a0e170406185c919 Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Fri, 6 Dec 2024 06:52:09 +0100 Subject: [PATCH 46/87] feat(templates): add support for pop-up based social sign-in #9399 (#9402) --- .../Components/ClientAppCoordinator.cs | 8 +++ .../Components/Layout/JsBridge.razor.cs | 6 +++ .../IClientCoreServiceCollectionExtensions.cs | 3 +- .../Boilerplate.Client.Core/Scripts/app.ts | 53 +++++++++++++++---- .../Services/ClientPubSubMessages.cs | 4 ++ .../DefaultExternalNavigationService.cs | 13 ++++- .../Services/PubSubService.cs | 2 + .../Services/SignalRInfinitiesRetryPolicy.cs | 11 ---- .../src/Directory.Packages.props | 1 + .../src/Directory.Packages8.props | 1 + .../Boilerplate.Server.Api.csproj | 1 + .../Program.Middlewares.cs | 7 ++- .../Program.Middlewares.cs | 7 ++- 13 files changed, 90 insertions(+), 27 deletions(-) delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignalRInfinitiesRetryPolicy.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 1fa8cb7bd8..942bedaa81 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 @@ -36,6 +36,8 @@ public partial class ClientAppCoordinator : AppComponentBase [AutoInject] private IPushNotificationService pushNotificationService = default!; //#endif + private Action? unsubscribe; + protected override async Task OnInitAsync() { if (AppPlatform.IsBlazorHybrid) @@ -45,6 +47,10 @@ protected override async Task OnInitAsync() if (InPrerenderSession is false) { + unsubscribe = PubSubService.Subscribe(ClientPubSubMessages.NAVIGATE_TO, async (uri) => + { + NavigationManager.NavigateTo(uri!.ToString()!); + }); TelemetryContext.UserAgent = await navigator.GetUserAgent(); TelemetryContext.TimeZone = await jsRuntime.GetTimeZone(); TelemetryContext.Culture = CultureInfo.CurrentCulture.Name; @@ -246,6 +252,8 @@ await storageService.GetItem("Culture") ?? // 2- User settings private List signalROnDisposables = []; protected override async ValueTask DisposeAsync(bool disposing) { + unsubscribe?.Invoke(); + NavigationManager.LocationChanged -= NavigationManager_LocationChanged; AuthManager.AuthenticationStateChanged -= AuthenticationStateChanged; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/JsBridge.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/JsBridge.razor.cs index 4acf8c6e2e..d38d4351ac 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/JsBridge.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/JsBridge.razor.cs @@ -27,6 +27,12 @@ public async Task ShowDiagnostic() PubSubService.Publish(ClientPubSubMessages.SHOW_DIAGNOSTIC_MODAL); } + [JSInvokable(nameof(PublishMessage))] + public async Task PublishMessage(string message, string? payload) + { + PubSubService.Publish(message, payload); + } + public void Dispose() { dotnetObj?.Dispose(); 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 69530b3e59..b00b65b05a 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 @@ -136,7 +136,6 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle services.AddTypedHttpClients(); //#if (signalR == true) - services.AddSingleton(); services.AddSessioned(sp => { var authManager = sp.GetRequiredService(); @@ -144,7 +143,7 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle var absoluteServerAddressProvider = sp.GetRequiredService(); var hubConnection = new HubConnectionBuilder() - .WithAutomaticReconnect(sp.GetRequiredService()) + .WithStatefulReconnect() .WithUrl(new Uri(absoluteServerAddressProvider.GetAddress(), "app-hub"), options => { options.SkipNegotiation = false; // Required for Azure SignalR. 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 9992da2431..947cee0255 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,9 +1,16 @@ //+:cnd:noEmit + +interface DotNetObject { + invokeMethod(methodIdentifier: string, ...args: any[]): T; + invokeMethodAsync(methodIdentifier: string, ...args: any[]): Promise; + dispose(): void; +} + class App { + // For additional details, see the JsBridge.cs file. private static jsBridgeObj: DotNetObject; public static registerJsBridge(dotnetObj: DotNetObject) { - // For additional details, see the JsBridge.cs file. App.jsBridgeObj = dotnetObj; } @@ -11,6 +18,10 @@ class App { return App.jsBridgeObj?.invokeMethodAsync('ShowDiagnostic'); } + public static publishMessage(message: string, payload: any) { + return App.jsBridgeObj?.invokeMethodAsync('PublishMessage', message, payload); + } + 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])); @@ -48,36 +59,60 @@ class App { if (pushManager == null) return null; let subscription = await pushManager.getSubscription(); + if (subscription == null) { subscription = await pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: vapidPublicKey }); } + const pushChannel = subscription.toJSON(); const p256dh = pushChannel.keys!['p256dh']; const auth = pushChannel.keys!['auth']; - return { deviceId: `${p256dh}-${auth}`, platform: 'browser', p256dh: p256dh, auth: auth, endpoint: pushChannel.endpoint }; + + return { + deviceId: `${p256dh}-${auth}`, + platform: 'browser', + p256dh: p256dh, + auth: auth, + endpoint: pushChannel.endpoint + }; }; //#endif } -declare class BitTheme { static init(options: any): void; }; +window.addEventListener('message', handleMessage); +window.addEventListener('load', handleLoad); +window.addEventListener('resize', setCssWindowSizes); -interface DotNetObject { - invokeMethod(methodIdentifier: string, ...args: any[]): T; - invokeMethodAsync(methodIdentifier: string, ...args: any[]): Promise; - dispose(): void; +function handleMessage(e: MessageEvent) { + // Enable publishing messages from JavaScript's `window.postMessage` to the C# `PubSubService`. + if (e.data.key === 'PUBLISH_MESSAGE') { + App.publishMessage(e.data.message, e.data.payload); + } } -window.addEventListener('load', setCssWindowSizes); -window.addEventListener('resize', setCssWindowSizes); +function handleLoad() { + setCssWindowSizes(); + + if (window.opener != null) { + // The IExternalNavigationService is responsible for opening pages in a new window, + // such as during social sign-in flows. Once the external navigation is complete, + // and the user is redirected back to the newly opened window, + // the following code ensures that the original window is notified of where it should navigate next. + window.opener.postMessage({ key: 'PUBLISH_MESSAGE', message: 'NAVIGATE_TO', payload: window.location.href }); + setTimeout(() => window.close(), 100); + } +} function setCssWindowSizes() { document.documentElement.style.setProperty('--win-width', `${window.innerWidth}px`); document.documentElement.style.setProperty('--win-height', `${window.innerHeight}px`); } +declare class BitTheme { static init(options: any): void; }; + BitTheme.init({ system: true, onChange: (newTheme: string, oldThem: string) => { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientPubSubMessages.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientPubSubMessages.cs index 3a7e05b86b..b0414e6ce4 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientPubSubMessages.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientPubSubMessages.cs @@ -22,5 +22,9 @@ public static partial class ClientPubSubMessages 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); + /// + /// Supposed to be called using JavaScript to navigate between pages without reloading the app. + /// + public const string NAVIGATE_TO = nameof(NAVIGATE_TO); public const string SHOW_DIAGNOSTIC_MODAL = nameof(SHOW_DIAGNOSTIC_MODAL); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DefaultExternalNavigationService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DefaultExternalNavigationService.cs index f82360f428..57c2db6d29 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DefaultExternalNavigationService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/DefaultExternalNavigationService.cs @@ -2,10 +2,21 @@ public partial class DefaultExternalNavigationService : IExternalNavigationService { + [AutoInject] private readonly Window window = default!; [AutoInject] private readonly NavigationManager navigationManager = default!; public async Task NavigateToAsync(string url) { - navigationManager.NavigateTo(url, forceLoad: true, replace: true); + if (AppPlatform.IsBlazorHybrid) + { + navigationManager.NavigateTo(url, forceLoad: true, replace: true); + return; + } + + if (await window.Open(url, "_blank", new WindowFeatures() { Popup = true, Height = 768, Width = 1024 }) is false // Let's try with popup first. + && await window.Open(url, "_blank", new WindowFeatures() { Popup = false }) is false) // Let's try new tab + { + navigationManager.NavigateTo(url, forceLoad: true, replace: true); // If all else fails, let's try to navigate in the same tab. + } } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PubSubService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PubSubService.cs index 62818332d8..9e914e8ae2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PubSubService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PubSubService.cs @@ -2,6 +2,8 @@ /// /// Service for Publish/Subscribe pattern. +/// You could publish messages within blazor components, pages outside blazor components (Like MAUI Xaml pages), JavaScript +/// codes using window.postMessage or even from server side using SignalR ( as example. /// public partial class PubSubService { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignalRInfinitiesRetryPolicy.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignalRInfinitiesRetryPolicy.cs deleted file mode 100644 index 36a88e7246..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/SignalRInfinitiesRetryPolicy.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.SignalR.Client; - -namespace Boilerplate.Client.Core.Services; - -public class SignalRInfinitiesRetryPolicy : IRetryPolicy -{ - public TimeSpan? NextRetryDelay(RetryContext retryContext) - { - return TimeSpan.FromSeconds(1); // It's already handled by HttpMessageHandlers/RetryDelegatingHandler during negotiate http request. - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props index 1c8699815b..1308e45b01 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props @@ -21,6 +21,7 @@ + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props index 5c9a64fbf5..8b6b2f5374 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props @@ -21,6 +21,7 @@ + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj index 7e9651eedf..8d6f6853f9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Boilerplate.Server.Api.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Middlewares.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Middlewares.cs index cd0f59b765..a12e69e9d5 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Middlewares.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Middlewares.cs @@ -45,8 +45,11 @@ private static void ConfigureMiddlewares(this WebApplication app) { app.UseHttpsRedirection(); app.UseResponseCompression(); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + app.UseXContentTypeOptions(); + app.UseXXssProtection(options => options.EnabledWithBlockMode()); + app.UseXfo(options => options.SameOrigin()); } app.UseResponseCaching(); @@ -79,7 +82,7 @@ private static void ConfigureMiddlewares(this WebApplication app) }).WithTags("Test"); //#if (signalR == true) - app.MapHub("/app-hub"); + app.MapHub("/app-hub", options => options.AllowStatefulReconnects = true); //#endif app.MapControllers().RequireAuthorization(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs index 253540dc08..74e353d811 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs @@ -57,8 +57,11 @@ public static void ConfigureMiddlewares(this WebApplication app) { app.UseHttpsRedirection(); app.UseResponseCompression(); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + app.UseXContentTypeOptions(); + app.UseXXssProtection(options => options.EnabledWithBlockMode()); + app.UseXfo(options => options.SameOrigin()); } app.UseResponseCaching(); @@ -140,7 +143,7 @@ public static void ConfigureMiddlewares(this WebApplication app) // and use the Server.Web project solely as a Blazor Server or pre-rendering service provider. throw new InvalidOperationException("Azure SignalR is not supported with Blazor Server and Auto"); } - app.MapHub("/app-hub"); + app.MapHub("/app-hub", options => options.AllowStatefulReconnects = true); //#endif app.MapControllers().RequireAuthorization(); From a372dc309544c17e50d142a531f18b76a9062e3e Mon Sep 17 00:00:00 2001 From: Mohammad Aminsafaei Date: Fri, 6 Dec 2024 10:13:53 +0330 Subject: [PATCH 47/87] feat(blazorui): add InitialSelectedItems's demo in BitDropdown #9396 (#9398) --- .../Dropdown/_BitDropdownCustomDemo.razor | 15 +++++ .../Dropdown/_BitDropdownCustomDemo.razor.cs | 42 +++++++++++++ .../_BitDropdownCustomDemo.razor.samples.cs | 57 ++++++++++++++++++ .../Dropdown/_BitDropdownItemDemo.razor | 16 +++++ .../Dropdown/_BitDropdownItemDemo.razor.cs | 45 ++++++++++++++ .../_BitDropdownItemDemo.razor.samples.cs | 60 +++++++++++++++++++ 6 files changed, 235 insertions(+) diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownCustomDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownCustomDemo.razor index 6d27c53e01..8444bcf5ab 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownCustomDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownCustomDemo.razor @@ -431,6 +431,21 @@ MultiSelect ItemsProvider="LoadItems" Placeholder="Select items" + NameSelectors="nameSelectors" />



    +
    With ItemsProvider and InitialSelectedItems:

    + +
    + diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownCustomDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownCustomDemo.razor.cs index 7f73fa617e..0aee70205d 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownCustomDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownCustomDemo.razor.cs @@ -135,6 +135,48 @@ private List GetRtlCustoms() => private IEnumerable comboBoxValues2 = []; private IEnumerable comboBoxValues3 = []; + private IEnumerable initialSelectedItem = [ + new() + { + Text = "Product 100", + Value = "100", + Payload = new ProductDto { + Id = 100, + Price = 60, + Name = "Product 100" + }, + Label = "Product 100", + Type = BitDropdownItemType.Normal + } + ]; + + private IEnumerable initialSelectedItems = [ + new() + { + Text = "Product 100", + Value = "100", + Payload = new ProductDto { + Id = 100, + Price = 60, + Name = "Product 100" + }, + Label = "Product 100", + Type = BitDropdownItemType.Normal + }, + new() + { + Text = "Product 99", + Value = "99", + Payload = new ProductDto { + Id = 99, + Price = 75, + Name = "Product 99" + }, + Label = "Product 99", + Type = BitDropdownItemType.Normal + } + ]; + protected override void OnInitialized() { virtualizeCustoms1 = Enumerable.Range(1, 10_000) diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownCustomDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownCustomDemo.razor.samples.cs index fa27dd2ae4..25e5578c94 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownCustomDemo.razor.samples.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownCustomDemo.razor.samples.cs @@ -832,6 +832,21 @@ public class Product MultiSelect ItemsProvider=""LoadItems"" Placeholder=""Select items"" + NameSelectors=""nameSelectors"" /> + + + +"; private readonly string example12CsharpCode = @" public class Product @@ -850,6 +865,48 @@ public class Product private ICollection? virtualizeCustoms1; private ICollection? virtualizeCustoms2; +private IEnumerable initialSelectedItem = [ + new() + { + Text = ""Product 100"", + Value = ""100"", + Payload = new ProductDto { + Id = 100, + Price = 60, + Name = ""Product 100"" + }, + Label = ""Product 100"", + Type = BitDropdownItemType.Normal + } +]; + +private IEnumerable initialSelectedItems = [ + new() + { + Text = ""Product 100"", + Value = ""100"", + Payload = new ProductDto { + Id = 100, + Price = 60, + Name = ""Product 100"" + }, + Label = ""Product 100"", + Type = BitDropdownItemType.Normal + }, + new() + { + Text = ""Product 99"", + Value = ""99"", + Payload = new ProductDto { + Id = 99, + Price = 75, + Name = ""Product 99"" + }, + Label = ""Product 99"", + Type = BitDropdownItemType.Normal + } +]; + protected override void OnInitialized() { virtualizeCustoms1 = Enumerable.Range(1, 10_000) diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownItemDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownItemDemo.razor index 708eea8e85..82869e5848 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownItemDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownItemDemo.razor @@ -425,6 +425,22 @@ ItemsProvider="LoadItems" Placeholder="Select items" TItem="BitDropdownItem" TValue="string" /> +



    +
    With ItemsProvider and InitialSelectedItems:

    + +
    + diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownItemDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownItemDemo.razor.cs index 57eff57c9d..5b0a81ab89 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownItemDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownItemDemo.razor.cs @@ -104,6 +104,51 @@ private List> GetStyleClassItems() => private IEnumerable comboBoxValues2 = []; private IEnumerable comboBoxValues3 = []; + private IEnumerable> initialSelectedItem = [ + new() + { + Text = "Product 100", + Value = "100", + Data = new ProductDto { + Id = 100, + Price = 60, + Name = "Product 100" + }, + AriaLabel = "Product 100", + IsEnabled = true, + ItemType = BitDropdownItemType.Normal + } + ]; + + private IEnumerable> initialSelectedItems = [ + new() + { + Text = "Product 100", + Value = "100", + Data = new ProductDto { + Id = 100, + Price = 60, + Name = "Product 100" + }, + AriaLabel = "Product 100", + IsEnabled = true, + ItemType = BitDropdownItemType.Normal + }, + new() + { + Text = "Product 99", + Value = "99", + Data = new ProductDto { + Id = 99, + Price = 75, + Name = "Product 99" + }, + AriaLabel = "Product 99", + IsEnabled = true, + ItemType = BitDropdownItemType.Normal + } + ]; + protected override void OnInitialized() { virtualizeItems1 = Enumerable.Range(1, 10_000) diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownItemDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownItemDemo.razor.samples.cs index 713f58a769..e86ba8df3a 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownItemDemo.razor.samples.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/Dropdown/_BitDropdownItemDemo.razor.samples.cs @@ -552,11 +552,71 @@ public class DropdownItemData MultiSelect ItemsProvider=""LoadItems"" Placeholder=""Select items"" + TItem=""BitDropdownItem"" TValue=""string"" /> + +"" TValue=""string"" /> + +"" TValue=""string"" />"; private readonly string example12CsharpCode = @" private ICollection>? virtualizeItems1; private ICollection>? virtualizeItems2; +private IEnumerable> initialSelectedItem = [ + new() + { + Text = ""Product 100"", + Value = ""100"", + Data = new ProductDto { + Id = 100, + Price = 60, + Name = ""Product 100"" + }, + AriaLabel = ""Product 100"", + IsEnabled = true, + ItemType = BitDropdownItemType.Normal + } +]; + +private IEnumerable> initialSelectedItems = [ + new() + { + Text = ""Product 100"", + Value = ""100"", + Data = new ProductDto { + Id = 100, + Price = 60, + Name = ""Product 100"" + }, + AriaLabel = ""Product 100"", + IsEnabled = true, + ItemType = BitDropdownItemType.Normal + }, + new() + { + Text = ""Product 99"", + Value = ""99"", + Data = new ProductDto { + Id = 99, + Price = 75, + Name = ""Product 99"" + }, + AriaLabel = ""Product 99"", + IsEnabled = true, + ItemType = BitDropdownItemType.Normal + } +]; + protected override void OnInitialized() { virtualizeItems1 = Enumerable.Range(1, 10_000) From 18ead050271622a24fa5a1d24d18ec9e6b159f6b Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Fri, 6 Dec 2024 07:44:24 +0100 Subject: [PATCH 48/87] fix(besql): resolve Bit Besql memory leak issues #9403 (#9404) --- src/Besql/.gitignore | 2 +- src/Besql/Bit.Besql/BesqlDbContextFactory.cs | 131 ------------------ .../Bit.Besql/BesqlDbContextInterceptor.cs | 62 +++++++++ src/Besql/Bit.Besql/BesqlHistoryRepository.cs | 21 +++ .../Bit.Besql/BesqlPooledDbContextFactory.cs | 60 ++++++++ .../Bit.Besql/BrowserCacheBesqlStorage.cs | 9 +- src/Besql/Bit.Besql/IBesqlStorage.cs | 4 +- .../IServiceCollectionBesqlExtentions.cs | 40 +++--- src/Besql/Bit.Besql/NoopBesqlStorage.cs | 14 ++ .../Bit.Besql/NoopMigrationsDatabaseLock.cs | 19 +++ src/Besql/Bit.Besql/wwwroot/bit-besql.js | 79 ++++------- .../OfflineDbContextModelBuilder.cs | 2 +- .../Data/OfflineDbContext.cs | 22 +-- .../Extensions/ServiceCollectionExtensions.cs | 9 +- .../Bit.Besql.Demo.Client/Pages/Weather.razor | 50 +++---- .../Demo/Bit.Besql.Demo.Client/Program.cs | 2 +- .../Demo/Bit.Besql.Demo/Bit.Besql.Demo.csproj | 12 +- 17 files changed, 276 insertions(+), 262 deletions(-) delete mode 100644 src/Besql/Bit.Besql/BesqlDbContextFactory.cs create mode 100644 src/Besql/Bit.Besql/BesqlDbContextInterceptor.cs create mode 100644 src/Besql/Bit.Besql/BesqlHistoryRepository.cs create mode 100644 src/Besql/Bit.Besql/BesqlPooledDbContextFactory.cs create mode 100644 src/Besql/Bit.Besql/NoopBesqlStorage.cs create mode 100644 src/Besql/Bit.Besql/NoopMigrationsDatabaseLock.cs diff --git a/src/Besql/.gitignore b/src/Besql/.gitignore index ad381ba36a..02690e85c9 100644 --- a/src/Besql/.gitignore +++ b/src/Besql/.gitignore @@ -1 +1 @@ -/Demo/Bit.Besql.Demo/Offline-ClientDb.db* \ No newline at end of file +/Demo/Bit.Besql.Demo/Offline-Client.db* \ No newline at end of file diff --git a/src/Besql/Bit.Besql/BesqlDbContextFactory.cs b/src/Besql/Bit.Besql/BesqlDbContextFactory.cs deleted file mode 100644 index 574e56e95f..0000000000 --- a/src/Besql/Bit.Besql/BesqlDbContextFactory.cs +++ /dev/null @@ -1,131 +0,0 @@ -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Internal; - -namespace Bit.Besql; - -public class BesqlDbContextFactory : DbContextFactory - where TContext : DbContext -{ - private static readonly IDictionary FileNames = new Dictionary(); - - private readonly IBesqlStorage cache; - private Task? startupTask = null; - private int lastStatus = -2; - - public BesqlDbContextFactory( - IBesqlStorage cache, - IServiceProvider serviceProvider, - DbContextOptions options, - IDbContextFactorySource factorySource) - : base(serviceProvider, options, factorySource) - { - this.cache = cache; - startupTask = RestoreAsync(); - } - - private static string Filename => FileNames[typeof(TContext)]; - - private static string BackupFile => $"{BesqlDbContextFactory.Filename}_bak"; - - public static void Reset() => FileNames.Clear(); - - public static string? GetFilenameForType() => - FileNames.ContainsKey(typeof(TContext)) ? FileNames[typeof(TContext)] : null; - - public override async Task CreateDbContextAsync(CancellationToken cancellationToken = default) - { - await CheckForStartupTaskAsync(); - - var ctx = await base.CreateDbContextAsync(cancellationToken); - - ctx.SavedChanges += SyncDbToCacheAsync; - - return ctx; - } - - private async Task DoSwap(string source, string target) - { - await using var src = new SqliteConnection($"Data Source={source}"); - await using var tgt = new SqliteConnection($"Data Source={target}"); - - await src.OpenAsync(); - await tgt.OpenAsync(); - - src.BackupDatabase(tgt); - - await tgt.CloseAsync(); - await src.CloseAsync(); - } - - private async Task GetFilename() - { - await using var ctx = await base.CreateDbContextAsync(); - var filename = "filenotfound.db"; - var type = ctx.GetType(); - if (FileNames.TryGetValue(type, out var value)) - { - return value; - } - - var cs = ctx.Database.GetConnectionString(); - - if (cs != null) - { - var file = cs.Split(';').Select(s => s.Split('=')) - .Select(split => new - { - key = split[0].ToLowerInvariant(), - value = split[1], - }) - .Where(kv => kv.key.Contains("data source") || - kv.key.Contains("datasource") || - kv.key.Contains("filename")) - .Select(kv => kv.value) - .FirstOrDefault(); - if (file != null) - { - filename = file; - } - } - - FileNames.Add(type, filename); - return filename; - } - - private async Task CheckForStartupTaskAsync() - { - if (startupTask != null) - { - lastStatus = await startupTask; - startupTask?.Dispose(); - startupTask = null; - } - } - - private async void SyncDbToCacheAsync(object sender, SavedChangesEventArgs e) - { - var ctx = (TContext)sender; - await ctx.Database.CloseConnectionAsync(); - await CheckForStartupTaskAsync(); - if (e.EntitiesSavedCount > 0) - { - // unique to avoid conflicts. Is deleted after caching. - var backupName = $"{BesqlDbContextFactory.BackupFile}-{Guid.NewGuid().ToString().Split('-')[0]}"; - await DoSwap(BesqlDbContextFactory.Filename, backupName); - lastStatus = await cache.SyncDb(backupName); - } - } - - private async Task RestoreAsync() - { - var filename = $"{await GetFilename()}_bak"; - lastStatus = await cache.SyncDb(filename); - if (lastStatus == 0) - { - await DoSwap(filename, FileNames[typeof(TContext)]); - } - - return lastStatus; - } -} diff --git a/src/Besql/Bit.Besql/BesqlDbContextInterceptor.cs b/src/Besql/Bit.Besql/BesqlDbContextInterceptor.cs new file mode 100644 index 0000000000..bc9d65b875 --- /dev/null +++ b/src/Besql/Bit.Besql/BesqlDbContextInterceptor.cs @@ -0,0 +1,62 @@ +using System.Data.Common; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Bit.Besql; + +public class BesqlDbContextInterceptor(IBesqlStorage storage) : IDbCommandInterceptor, ISingletonInterceptor +{ + public async ValueTask ReaderExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + DbDataReader result, + CancellationToken cancellationToken = default) + { + if (IsTargetedCommand(command.CommandText)) + { + _ = Sync(eventData.Context!.Database.GetDbConnection().DataSource).ConfigureAwait(false); + } + + return result; + } + + public async ValueTask NonQueryExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + int result, + CancellationToken cancellationToken) + { + if (IsTargetedCommand(command.CommandText)) + { + _ = Sync(eventData.Context!.Database.GetDbConnection().DataSource).ConfigureAwait(false); + } + + return result; + } + + public async ValueTask ScalarExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + object? result, + CancellationToken cancellationToken = default) + { + if (IsTargetedCommand(command.CommandText)) + { + _ = Sync(eventData.Context!.Database.GetDbConnection().DataSource).ConfigureAwait(false); + } + return result; + } + + private bool IsTargetedCommand(string sql) + { + var keywords = new[] { "INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "DROP" }; + return keywords.Any(k => sql.Contains(k, StringComparison.OrdinalIgnoreCase)); + } + + private async Task Sync(string dataSource) + { + var fileName = dataSource.Trim('/'); + await Task.Yield(); + await storage.Persist(fileName).ConfigureAwait(false); + } +} diff --git a/src/Besql/Bit.Besql/BesqlHistoryRepository.cs b/src/Besql/Bit.Besql/BesqlHistoryRepository.cs new file mode 100644 index 0000000000..bd14a5a289 --- /dev/null +++ b/src/Besql/Bit.Besql/BesqlHistoryRepository.cs @@ -0,0 +1,21 @@ +#if NET9_0_OR_GREATER +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Sqlite.Migrations.Internal; + +namespace Bit.Besql; + +// https://github.com/dotnet/efcore/issues/33731 +public class BesqlHistoryRepository(HistoryRepositoryDependencies dependencies) : SqliteHistoryRepository(dependencies) +{ + public override IMigrationsDatabaseLock AcquireDatabaseLock() + { + return new NoopMigrationsDatabaseLock(this); + } + + public override Task AcquireDatabaseLockAsync( + CancellationToken cancellationToken) + { + return Task.FromResult(new NoopMigrationsDatabaseLock(this)); + } +} +#endif diff --git a/src/Besql/Bit.Besql/BesqlPooledDbContextFactory.cs b/src/Besql/Bit.Besql/BesqlPooledDbContextFactory.cs new file mode 100644 index 0000000000..9d669c4c05 --- /dev/null +++ b/src/Besql/Bit.Besql/BesqlPooledDbContextFactory.cs @@ -0,0 +1,60 @@ +using System.Data.Common; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Bit.Besql; + +public class BesqlPooledDbContextFactory : PooledDbContextFactory + where TDbContext : DbContext +{ + private readonly string _fileName; + private readonly IBesqlStorage _storage; + private readonly string _connectionString; + private readonly TaskCompletionSource _initTcs = new(); + + public BesqlPooledDbContextFactory( + IBesqlStorage storage, + DbContextOptions options) + : base(options) + { + _connectionString = options.Extensions + .OfType() + .First(r => string.IsNullOrEmpty(r.ConnectionString) is false).ConnectionString!; + + _fileName = new DbConnectionStringBuilder() + { + ConnectionString = _connectionString + }["Data Source"].ToString()!.Trim('/'); + + _storage = storage; + _ = InitAsync(); + } + + public override async Task CreateDbContextAsync(CancellationToken cancellationToken = default) + { + await _initTcs.Task.ConfigureAwait(false); + + var ctx = await base.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + + return ctx; + } + + private async Task InitAsync() + { + try + { + await _storage.Init(_fileName).ConfigureAwait(false); + await using var connection = new SqliteConnection(_connectionString); + await connection.OpenAsync().ConfigureAwait(false); + await using var command = connection.CreateCommand(); + command.CommandText = "PRAGMA synchronous = FULL;"; + await command.ExecuteNonQueryAsync().ConfigureAwait(false); + _initTcs.SetResult(); + } + catch (Exception exp) + { + _initTcs.SetException(exp); + } + } +} diff --git a/src/Besql/Bit.Besql/BrowserCacheBesqlStorage.cs b/src/Besql/Bit.Besql/BrowserCacheBesqlStorage.cs index a7df78f415..271f992168 100644 --- a/src/Besql/Bit.Besql/BrowserCacheBesqlStorage.cs +++ b/src/Besql/Bit.Besql/BrowserCacheBesqlStorage.cs @@ -4,8 +4,13 @@ namespace Bit.Besql; public sealed class BrowserCacheBesqlStorage(IJSRuntime jsRuntime) : IBesqlStorage { - public async Task SyncDb(string filename) + public async Task Init(string filename) { - return await jsRuntime.InvokeAsync("synchronizeDbWithCache", filename); + await jsRuntime.InvokeVoidAsync("BitBesql.init", filename).ConfigureAwait(false); + } + + public async Task Persist(string filename) + { + await jsRuntime.InvokeVoidAsync("BitBesql.persist", filename).ConfigureAwait(false); } } diff --git a/src/Besql/Bit.Besql/IBesqlStorage.cs b/src/Besql/Bit.Besql/IBesqlStorage.cs index 12518aa00d..00015bbf19 100644 --- a/src/Besql/Bit.Besql/IBesqlStorage.cs +++ b/src/Besql/Bit.Besql/IBesqlStorage.cs @@ -2,5 +2,7 @@ public interface IBesqlStorage { - Task SyncDb(string filename); + Task Init(string filename); + + Task Persist(string filename); } diff --git a/src/Besql/Bit.Besql/IServiceCollectionBesqlExtentions.cs b/src/Besql/Bit.Besql/IServiceCollectionBesqlExtentions.cs index 4bd158ebf0..4104950b27 100644 --- a/src/Besql/Bit.Besql/IServiceCollectionBesqlExtentions.cs +++ b/src/Besql/Bit.Besql/IServiceCollectionBesqlExtentions.cs @@ -1,43 +1,45 @@ using Bit.Besql; using Microsoft.EntityFrameworkCore; +#if NET9_0_OR_GREATER +using Microsoft.EntityFrameworkCore.Migrations; +#endif using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Extensions.DependencyInjection; public static class IServiceCollectionBesqlExtentions { - public static IServiceCollection AddBesqlDbContextFactory( - this IServiceCollection services, - Action? optionsAction) + public static IServiceCollection AddBesqlDbContextFactory(this IServiceCollection services, Action optionsAction) where TContext : DbContext { if (OperatingSystem.IsBrowser()) { - services.TryAddScoped(); - services.AddDbContextFactory>( - optionsAction ?? ((s, p) => { }), ServiceLifetime.Scoped); + services.AddSingleton(); + services.TryAddSingleton(); + // To make optimized db context work in blazor wasm: https://github.com/dotnet/efcore/issues/31751 + // https://learn.microsoft.com/en-us/ef/core/performance/advanced-performance-topics?tabs=with-di%2Cexpression-api-with-constant#compiled-models + AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue31751", true); + services.AddDbContextFactory>((serviceProvider, options) => + { + options.AddInterceptors(serviceProvider.GetRequiredService()); +#if NET9_0_OR_GREATER + options.ReplaceService(); +#endif + optionsAction.Invoke(serviceProvider, options); + }); } else { - services.AddDbContextFactory( - optionsAction ?? ((s, p) => { }), ServiceLifetime.Scoped); + services.TryAddSingleton(); + services.AddPooledDbContextFactory(optionsAction); } return services; } - public static IServiceCollection AddBesqlDbContextFactory( - this IServiceCollection services, - Action? optionsAction) - where TContext : DbContext - { - return services.AddBesqlDbContextFactory((s, p) => optionsAction?.Invoke(p)); - } - - public static IServiceCollection AddBesqlDbContextFactory( - this IServiceCollection services) + public static IServiceCollection AddBesqlDbContextFactory(this IServiceCollection services, Action? optionsAction) where TContext : DbContext { - return services.AddBesqlDbContextFactory(options => { }); + return services.AddBesqlDbContextFactory((serviceProvider, options) => optionsAction?.Invoke(options)); } } diff --git a/src/Besql/Bit.Besql/NoopBesqlStorage.cs b/src/Besql/Bit.Besql/NoopBesqlStorage.cs new file mode 100644 index 0000000000..d1f77d4897 --- /dev/null +++ b/src/Besql/Bit.Besql/NoopBesqlStorage.cs @@ -0,0 +1,14 @@ +namespace Bit.Besql; + +internal class NoopBesqlStorage : IBesqlStorage +{ + public Task Init(string filename) + { + return Task.CompletedTask; + } + + public Task Persist(string filename) + { + return Task.CompletedTask; + } +} diff --git a/src/Besql/Bit.Besql/NoopMigrationsDatabaseLock.cs b/src/Besql/Bit.Besql/NoopMigrationsDatabaseLock.cs new file mode 100644 index 0000000000..3eba416129 --- /dev/null +++ b/src/Besql/Bit.Besql/NoopMigrationsDatabaseLock.cs @@ -0,0 +1,19 @@ +#if NET9_0_OR_GREATER +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Bit.Besql; + +public class NoopMigrationsDatabaseLock(IHistoryRepository historyRepository) : IMigrationsDatabaseLock +{ + IHistoryRepository IMigrationsDatabaseLock.HistoryRepository => historyRepository; + + public void Dispose() + { + } + + public ValueTask DisposeAsync() + { + return ValueTask.CompletedTask; + } +} +#endif diff --git a/src/Besql/Bit.Besql/wwwroot/bit-besql.js b/src/Besql/Bit.Besql/wwwroot/bit-besql.js index feb7b63741..5e87a31171 100644 --- a/src/Besql/Bit.Besql/wwwroot/bit-besql.js +++ b/src/Besql/Bit.Besql/wwwroot/bit-besql.js @@ -1,66 +1,47 @@ -var BitBesql = BitBesql || {}; +var BitBesql = window.BitBesql || {}; BitBesql.version = window['bit-besql version'] = '9.1.0-pre-08'; -async function synchronizeDbWithCache(file) { +BitBesql.init = async function init(fileName) { + const sqliteFilePath = `/${fileName}`; + const cacheStorageFilePath = `/data/cache/${fileName}`; - window.sqlitedb = window.sqlitedb || { - init: false, - cache: await caches.open('Bit-Besql') - }; + BitBesql.dbCache = await caches.open('Bit-Besql'); - const db = window.sqlitedb; - - const backupPath = `/${file}`; - const cachePath = `/data/cache/${file.substring(0, file.indexOf('_bak'))}`; + const dbCache = BitBesql.dbCache; - if (!db.init) { - - db.init = true; - - const resp = await db.cache.match(cachePath); - - if (resp && resp.ok) { - - const res = await resp.arrayBuffer(); - - if (res) { - console.log(`Restoring ${res.byteLength} bytes.`); - window.Blazor.runtime.Module.FS.writeFile(backupPath, new Uint8Array(res)); - return 0; - } + const resp = await dbCache.match(cacheStorageFilePath); + if (resp && resp.ok) { + const res = await resp.arrayBuffer(); + if (res) { + window.Blazor.runtime.Module.FS.writeFile(sqliteFilePath, new Uint8Array(res)); } - return -1; } +} - if (window.Blazor.runtime.Module.FS.analyzePath(backupPath).exists) { - - const waitFlush = new Promise((done, _) => { - setTimeout(done, 10); - }); +BitBesql.persist = async function persist(fileName) { - await waitFlush; + const dbCache = BitBesql.dbCache; - const data = window.Blazor.runtime.Module.FS.readFile(backupPath); + const sqliteFilePath = `/${fileName}`; + const cacheStorageFilePath = `/data/cache/${fileName}`; - const blob = new Blob([data], { - type: 'application/octet-stream', - ok: true, - status: 200 - }); + if (!window.Blazor.runtime.Module.FS.analyzePath(sqliteFilePath).exists) + throw new Error(`Database ${fileName} not found.`); - const headers = new Headers({ - 'content-length': blob.size - }); + const data = window.Blazor.runtime.Module.FS.readFile(sqliteFilePath); - const response = new Response(blob, { - headers - }); + const blob = new Blob([data], { + type: 'application/octet-stream', + status: 200 + }); - await db.cache.put(cachePath, response); + const headers = new Headers({ + 'content-length': blob.size + }); - window.Blazor.runtime.Module.FS.unlink(backupPath); + const response = new Response(blob, { + headers + }); - return 1; - } - return -1; + await dbCache.put(cacheStorageFilePath, response); } diff --git a/src/Besql/Demo/Bit.Besql.Demo.Client/Data/CompiledModel/OfflineDbContextModelBuilder.cs b/src/Besql/Demo/Bit.Besql.Demo.Client/Data/CompiledModel/OfflineDbContextModelBuilder.cs index 9918c918f3..8c7d4b8682 100644 --- a/src/Besql/Demo/Bit.Besql.Demo.Client/Data/CompiledModel/OfflineDbContextModelBuilder.cs +++ b/src/Besql/Demo/Bit.Besql.Demo.Client/Data/CompiledModel/OfflineDbContextModelBuilder.cs @@ -11,7 +11,7 @@ namespace Bit.Besql.Demo.Client.Data.CompiledModel public partial class OfflineDbContextModel { private OfflineDbContextModel() - : base(skipDetectChanges: false, modelId: new Guid("83924357-cac0-4352-b1b4-39edcbc1a1e3"), entityTypeCount: 1) + : base(skipDetectChanges: false, modelId: new Guid("ac96847b-e3a9-46a3-82cf-7605a37f26af"), entityTypeCount: 1) { } diff --git a/src/Besql/Demo/Bit.Besql.Demo.Client/Data/OfflineDbContext.cs b/src/Besql/Demo/Bit.Besql.Demo.Client/Data/OfflineDbContext.cs index d22ebe6cc8..8008dd9fc0 100644 --- a/src/Besql/Demo/Bit.Besql.Demo.Client/Data/OfflineDbContext.cs +++ b/src/Besql/Demo/Bit.Besql.Demo.Client/Data/OfflineDbContext.cs @@ -1,6 +1,5 @@ using Bit.Besql.Demo.Client.Model; using Microsoft.EntityFrameworkCore; -using Bit.Besql.Demo.Client.Data.CompiledModel; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Bit.Besql.Demo.Client.Data; @@ -8,17 +7,7 @@ namespace Bit.Besql.Demo.Client.Data; public class OfflineDbContext(DbContextOptions options) : DbContext(options) { - static OfflineDbContext() - { - if (OperatingSystem.IsBrowser()) - { - // To make optimized db context work in blazor wasm: https://github.com/dotnet/efcore/issues/31751 - // https://learn.microsoft.com/en-us/ef/core/performance/advanced-performance-topics?tabs=with-di%2Cexpression-api-with-constant#compiled-models - AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue31751", true); - } - } - - public DbSet WeatherForecasts { get; set; } + public DbSet WeatherForecasts { get; set; } = default!; protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -27,15 +16,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder - .UseModel(OfflineDbContextModel.Instance) // use generated compiled model in order to make db context optimized - .UseSqlite("Data Source=Offline-ClientDb.db"); - - base.OnConfiguring(optionsBuilder); - } - protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { // SQLite does not support expressions of type 'DateTimeOffset' in ORDER BY clauses. Convert the values to a supported type: diff --git a/src/Besql/Demo/Bit.Besql.Demo.Client/Extensions/ServiceCollectionExtensions.cs b/src/Besql/Demo/Bit.Besql.Demo.Client/Extensions/ServiceCollectionExtensions.cs index 9251336c58..b9b2af855c 100644 --- a/src/Besql/Demo/Bit.Besql.Demo.Client/Extensions/ServiceCollectionExtensions.cs +++ b/src/Besql/Demo/Bit.Besql.Demo.Client/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ using Bit.Besql.Demo.Client.Data; +using Microsoft.EntityFrameworkCore; +using Bit.Besql.Demo.Client.Data.CompiledModel; namespace Microsoft.Extensions.DependencyInjection; @@ -6,7 +8,12 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddAppServices(this IServiceCollection services) { - services.AddBesqlDbContextFactory(); + services.AddBesqlDbContextFactory((sp, optionsBuilder) => + { + optionsBuilder + .UseModel(OfflineDbContextModel.Instance) // use generated compiled model in order to make db context optimized + .UseSqlite($"Data Source=Offline-Client.db"); + }); return services; } diff --git a/src/Besql/Demo/Bit.Besql.Demo.Client/Pages/Weather.razor b/src/Besql/Demo/Bit.Besql.Demo.Client/Pages/Weather.razor index 87fda42eec..b6408819e3 100644 --- a/src/Besql/Demo/Bit.Besql.Demo.Client/Pages/Weather.razor +++ b/src/Besql/Demo/Bit.Besql.Demo.Client/Pages/Weather.razor @@ -12,58 +12,48 @@

    This component demonstrates showing data.

    -@if (forecasts == null) +@if (forecastsCount == null) {

    Loading...

    } else { - - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
    DateTemp. (C)Temp. (F)Summary
    @forecast.Date.ToLocalTime().ToString("G")@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
    -} + + - +

    @forecastsCount

    +} @code { - private List? forecasts; + private int? forecastsCount; private async Task AddWeatherForecast() { await using var dbContext = await DbContextFactory.CreateDbContextAsync(); - var forecast = await dbContext.WeatherForecasts.AddAsync(new() + await dbContext.WeatherForecasts.AddAsync(new() { Date = new DateTimeOffset(2024, 1, 4, 10, 10, 10, TimeSpan.Zero), - Summary = "Test", - TemperatureC = 17 + Summary = "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y zA B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y zA B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y zA B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y zA B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y zA B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y zA B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y zA B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y zA B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z ", + TemperatureC = Random.Shared.Next(1, 30) }); await dbContext.SaveChangesAsync(); - forecasts!.Add(forecast.Entity); + forecastsCount++; + } + + private async Task DeleteSomeForecasts() + { + await using var dbContext = await DbContextFactory.CreateDbContextAsync(); + var deletedCount = await dbContext.WeatherForecasts + .Where(w => w.TemperatureC % 2 == 0) + .ExecuteDeleteAsync(); + forecastsCount -= deletedCount; } protected override async Task OnInitializedAsync() { await using var dbContext = await DbContextFactory.CreateDbContextAsync(); - forecasts = await dbContext.WeatherForecasts.OrderBy(c => c.Date).ToListAsync(); + forecastsCount = await dbContext.WeatherForecasts.CountAsync(); await base.OnInitializedAsync(); } diff --git a/src/Besql/Demo/Bit.Besql.Demo.Client/Program.cs b/src/Besql/Demo/Bit.Besql.Demo.Client/Program.cs index 9448d3d914..3a5bf82f45 100644 --- a/src/Besql/Demo/Bit.Besql.Demo.Client/Program.cs +++ b/src/Besql/Demo/Bit.Besql.Demo.Client/Program.cs @@ -1,4 +1,4 @@ -using Bit.Besql.Demo.Client.Data; +using Bit.Besql.Demo.Client.Data; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.EntityFrameworkCore; diff --git a/src/Besql/Demo/Bit.Besql.Demo/Bit.Besql.Demo.csproj b/src/Besql/Demo/Bit.Besql.Demo/Bit.Besql.Demo.csproj index 9750703360..309ce0a2d6 100644 --- a/src/Besql/Demo/Bit.Besql.Demo/Bit.Besql.Demo.csproj +++ b/src/Besql/Demo/Bit.Besql.Demo/Bit.Besql.Demo.csproj @@ -13,12 +13,10 @@ + Optimize-DbContext -OutputDir Data\CompiledModel commands. --> all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -27,6 +25,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 8357a2c03cd4b1e5035983ff135d9d3045d802ca Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Fri, 6 Dec 2024 19:11:45 +0330 Subject: [PATCH 49/87] feat(templates): improve home page of Boilerplate #9406 (#9410) --- .../Components/Pages/HomePage.razor | 168 ++++++++---------- .../Components/Pages/HomePage.razor.cs | 39 +++- .../src/Shared/Dtos/Statistics/GitHubStats.cs | 13 +- .../Shared/Dtos/Statistics/NugetStatsDto.cs | 3 - 4 files changed, 107 insertions(+), 116 deletions(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor index bdec845cc6..b49e8792bf 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor @@ -18,97 +18,81 @@
    - - @Localizer[nameof(AppStrings.GitHubRepo)] - - - - - - @if (isLoading) - { - - } - else if (gitHubStats is not null) - { - - - Name: - @gitHubStats.Name + + + Statistics + + + @if (isLoadingGitHub) + { + + - - Description: - @gitHubStats.Description + } + else if (gitHubStats is not null) + { + + + Name: + @gitHubStats.Name + + + Total stars: + @gitHubStats.StargazersCount.ToString("N0") + + + Forks count: + @gitHubStats.ForksCount.ToString("N0") + + + Repo: + + + + - - Homepage: - @gitHubStats.Homepage + } + else + { + GitHub stats could not be loaded. + } + + + @if (isLoadingNuget) + { + + - - Total stars: - @gitHubStats.StargazersCount.ToString("N0") + } + else if (nugetStats is not null) + { + var data = nugetStats.Data[0]; + + + Package id: + @data.Id + + + Version: + @data.Version + + + Total downloads: + @data.TotalDownloads.ToString("N0") + - - Forks count: - @gitHubStats.ForksCount.ToString("N0") - - - Open issues count: - @gitHubStats.OpenIssuesCount.ToString("N0") - - - Default branch: - @gitHubStats.DefaultBranch - - - } - else - { - Stats could not be loaded. - } - - - @if (isLoading) - { - - } - else if (nugetStats is not null) - { - var data = nugetStats.Data[0]; - - - Package id: - @data.Id - - - Version: - @data.Version - - - Title: - @data.Title - - - Project url: - @data.ProjectUrl - - - Total downloads: - @data.TotalDownloads.ToString("N0") - - - } - else - { - Stats could not be loaded. - } - - - + } + else + { + Nuget stats could not be loaded. + } + + + +
    @@ -116,13 +100,13 @@ bit platform @Localizer[nameof(AppStrings.BitPlatformMessage)] - + @Localizer[nameof(AppStrings.LearnMore)] - + @@ -135,10 +119,10 @@ @Localizer[nameof(AppStrings.BitBlazorUIMessage)] - + @Localizer[nameof(AppStrings.WatchVideo)] - + @Localizer[nameof(AppStrings.LearnMore)] @@ -153,10 +137,10 @@ @Localizer[nameof(AppStrings.BitProjectTemplateMessage)] - + @Localizer[nameof(AppStrings.WatchVideo)] - + @Localizer[nameof(AppStrings.LearnMore)] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor.cs index d77a5634ee..b2846c7c99 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor.cs @@ -12,7 +12,8 @@ public partial class HomePage [AutoInject] private IStatisticsController statisticsController = default!; - private bool isLoading = true; + private bool isLoadingGitHub = true; + private bool isLoadingNuget = true; private GitHubStats? gitHubStats; private NugetStatsDto? nugetStats; @@ -20,21 +21,41 @@ protected override async Task OnInitAsync() { await base.OnInitAsync(); + // If required, you should typically manage the authorization header for external APIs in **AuthDelegatingHandler.cs** + // and handle error extraction from failed responses in **ExceptionDelegatingHandler.cs**. + + // These external API calls are provided as sample references for anonymous API usage in pre-rendering anonymous pages, + // and comprehensive exception handling is not intended for these examples. + + // However, the logic in other HTTP message handlers, such as **LoggingDelegatingHandler** and **RetryDelegatingHandler**, + // effectively addresses most scenarios. + + await Task.WhenAll(LoadGitHub(), LoadNuget()); + } + + private async Task LoadGitHub() + { try { - (nugetStats, gitHubStats) = await ( - statisticsController.GetNugetStats(packageId: "Bit.BlazorUI", CurrentCancellationToken), - statisticsController.GetGitHubStats(CurrentCancellationToken)); + gitHubStats = await statisticsController.GetGitHubStats(CurrentCancellationToken); } - catch + finally + { + isLoadingGitHub = false; + await InvokeAsync(StateHasChanged); + } + } + + private async Task LoadNuget() + { + try { - // If required, you should typically manage the authorization header for external APIs in **AuthDelegatingHandler.cs** and handle error extraction from failed responses in **ExceptionDelegatingHandler.cs**. - // These external API calls are provided as sample references for anonymous API usage in pre-rendering anonymous pages, and comprehensive exception handling is not intended for these examples. - // However, the logic in other HTTP message handlers, such as **LoggingDelegatingHandler** and **RetryDelegatingHandler**, effectively addresses most scenarios. + nugetStats = await statisticsController.GetNugetStats(packageId: "Bit.BlazorUI", CurrentCancellationToken); } finally { - isLoading = false; + isLoadingNuget = false; + await InvokeAsync(StateHasChanged); } } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/GitHubStats.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/GitHubStats.cs index 27903db9f0..264e39932b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/GitHubStats.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/GitHubStats.cs @@ -1,18 +1,7 @@ namespace Boilerplate.Shared.Dtos.Statistics; public record GitHubStats( - [property: JsonPropertyName("id")] int Id, [property: JsonPropertyName("name")] string Name, - [property: JsonPropertyName("full_name")] string FullName, - [property: JsonPropertyName("description")] string Description, - [property: JsonPropertyName("homepage")] string Homepage, - [property: JsonPropertyName("size")] int Size, [property: JsonPropertyName("stargazers_count")] int StargazersCount, - [property: JsonPropertyName("watchers_count")] int WatchersCount, - [property: JsonPropertyName("language")] string Language, - [property: JsonPropertyName("forks_count")] int ForksCount, - [property: JsonPropertyName("open_issues_count")] int OpenIssuesCount, - [property: JsonPropertyName("default_branch")] string DefaultBranch, - [property: JsonPropertyName("network_count")] int NetworkCount, - [property: JsonPropertyName("subscribers_count")] int SubscribersCount + [property: JsonPropertyName("forks_count")] int ForksCount ); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/NugetStatsDto.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/NugetStatsDto.cs index 04d74ac268..51b082431b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/NugetStatsDto.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/NugetStatsDto.cs @@ -7,9 +7,6 @@ public record NugetStatsDto( public record Datum( [property: JsonPropertyName("id")] string Id, [property: JsonPropertyName("version")] string Version, - [property: JsonPropertyName("description")] string Description, - [property: JsonPropertyName("title")] string Title, - [property: JsonPropertyName("projectUrl")] string ProjectUrl, [property: JsonPropertyName("totalDownloads")] int TotalDownloads ); From ee028322cd6bf6672e8247a94470f294acbbeb65 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Fri, 6 Dec 2024 19:12:23 +0330 Subject: [PATCH 50/87] feat(blazorui): improve loading components #9408 (#9411) --- .../Progress/Loading/Base/BitLoadingBase.cs | 3 +++ .../Progress/Loading/BitHeartLoading.razor.cs | 1 + .../Progress/Loading/BitHeartLoading.scss | 4 ++-- .../Progress/Loading/BitRippleLoading.razor.cs | 3 ++- .../Progress/Loading/BitRippleLoading.scss | 16 ++++++++-------- .../Progress/Loading/BitRollerLoading.razor.cs | 2 +- .../Progress/Loading/BitRollerLoading.scss | 4 ++-- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/Base/BitLoadingBase.cs b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/Base/BitLoadingBase.cs index 4b537a8604..c9d9247d69 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/Base/BitLoadingBase.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/Base/BitLoadingBase.cs @@ -2,6 +2,9 @@ namespace Bit.BlazorUI; +/// +/// The original loading css came from https://loading.io/css/ +/// public abstract class BitLoadingBase : BitComponentBase { /// diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitHeartLoading.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitHeartLoading.razor.cs index 8b2ea09005..d5fe1e3142 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitHeartLoading.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitHeartLoading.razor.cs @@ -9,6 +9,7 @@ protected override void RegisterCssStyles() base.RegisterCssStyles(); StyleBuilder.Register(() => $"--bit-ldn-hrt-24:{Convert(24)}px"); + StyleBuilder.Register(() => $"--bit-ldn-hrt-28:{Convert(28)}px"); StyleBuilder.Register(() => $"--bit-ldn-hrt-32:{Convert(32)}px"); StyleBuilder.Register(() => $"--bit-ldn-hrt-40:{Convert(40)}px"); } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitHeartLoading.scss b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitHeartLoading.scss index 102012f62f..4771a20635 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitHeartLoading.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitHeartLoading.scss @@ -12,8 +12,8 @@ .bit-ldn-hrt-chl { position: absolute; - top: var(--bit-ldn-hrt-32); - left: var(--bit-ldn-hrt-32); + top: var(--bit-ldn-hrt-28); + left: var(--bit-ldn-hrt-28); width: var(--bit-ldn-hrt-32); height: var(--bit-ldn-hrt-32); background: var(--bit-ldn-color); diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRippleLoading.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRippleLoading.razor.cs index 30a799451e..36fc6c7a69 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRippleLoading.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRippleLoading.razor.cs @@ -9,7 +9,8 @@ protected override void RegisterCssStyles() base.RegisterCssStyles(); StyleBuilder.Register(() => $"--bit-ldn-rpl-4:{Convert(4)}px"); + StyleBuilder.Register(() => $"--bit-ldn-rpl-8:{Convert(8)}px"); StyleBuilder.Register(() => $"--bit-ldn-rpl-36:{Convert(36)}px"); - StyleBuilder.Register(() => $"--bit-ldn-rpl-72:{Convert(72)}px"); + StyleBuilder.Register(() => $"--bit-ldn-rpl-80:{Convert(80)}px"); } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRippleLoading.scss b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRippleLoading.scss index ae9eef883b..1726515222 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRippleLoading.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRippleLoading.scss @@ -22,34 +22,34 @@ @keyframes bit-ldn-rpl-anm { 0% { - width: 0; - height: 0; opacity: 0; top: var(--bit-ldn-rpl-36); left: var(--bit-ldn-rpl-36); + width: var(--bit-ldn-rpl-8); + height: var(--bit-ldn-rpl-8); } 4.9% { - width: 0; - height: 0; opacity: 0; top: var(--bit-ldn-rpl-36); left: var(--bit-ldn-rpl-36); + width: var(--bit-ldn-rpl-8); + height: var(--bit-ldn-rpl-8); } 5% { - width: 0; - height: 0; opacity: 1; top: var(--bit-ldn-rpl-36); left: var(--bit-ldn-rpl-36); + width: var(--bit-ldn-rpl-8); + height: var(--bit-ldn-rpl-8); } 100% { top: 0px; left: 0px; opacity: 0; - width: var(--bit-ldn-rpl-72); - height: var(--bit-ldn-rpl-72); + width: var(--bit-ldn-rpl-80); + height: var(--bit-ldn-rpl-80); } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRollerLoading.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRollerLoading.razor.cs index b9a66ce283..c69cdeb7e9 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRollerLoading.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRollerLoading.razor.cs @@ -9,7 +9,7 @@ protected override void RegisterCssStyles() base.RegisterCssStyles(); StyleBuilder.Register(() => $"--bit-ldn-rol-4:{Convert(4)}px"); - StyleBuilder.Register(() => $"--bit-ldn-rol-7:{Convert(7)}px"); + StyleBuilder.Register(() => $"--bit-ldn-rol-8:{Convert(8)}px"); StyleBuilder.Register(() => $"--bit-ldn-rol-12:{Convert(12)}px"); StyleBuilder.Register(() => $"--bit-ldn-rol-17:{Convert(17)}px"); StyleBuilder.Register(() => $"--bit-ldn-rol-24:{Convert(24)}px"); diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRollerLoading.scss b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRollerLoading.scss index 5faf8c0904..c137a74c76 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRollerLoading.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Progress/Loading/BitRollerLoading.scss @@ -18,8 +18,8 @@ display: block; position: absolute; border-radius: 50%; - width: var(--bit-ldn-rol-7); - height: var(--bit-ldn-rol-7); + width: var(--bit-ldn-rol-8); + height: var(--bit-ldn-rol-8); background: var(--bit-ldn-color); margin: calc(-1 * var(--bit-ldn-rol-4)) 0 0 calc(-1 * var(--bit-ldn-rol-4)); } From 1feb63012d2d3dce2dc5639a19d211f862e5ef8a Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Fri, 6 Dec 2024 19:13:16 +0330 Subject: [PATCH 51/87] feat(blazorui): improve BitPullToRefresh component #9412 (#9414) --- .../PullToRefresh/BitPullToRefresh.razor | 2 +- .../PullToRefresh/BitPullToRefresh.razor.cs | 2 +- .../PullToRefresh/BitPullToRefresh.ts | 20 ++++++------------- .../PullToRefresh/BitPullToRefreshDemo.razor | 2 +- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/PullToRefresh/BitPullToRefresh.razor b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/PullToRefresh/BitPullToRefresh.razor index 09028e60d8..d3d6f7d066 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/PullToRefresh/BitPullToRefresh.razor +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/PullToRefresh/BitPullToRefresh.razor @@ -24,7 +24,7 @@ } else { - + } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/PullToRefresh/BitPullToRefresh.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/PullToRefresh/BitPullToRefresh.razor.cs index 447e746c7a..6ff4fa180b 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/PullToRefresh/BitPullToRefresh.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/PullToRefresh/BitPullToRefresh.razor.cs @@ -37,7 +37,7 @@ public partial class BitPullToRefresh : BitComponentBase, IAsyncDisposable [Parameter] public int Margin { get; set; } = 30; /// - /// The callback for when the threshold of the pull-down happens. + /// The callback for when the trigger condition of the pull-down happens. /// [Parameter] public EventCallback OnRefresh { get; set; } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/PullToRefresh/BitPullToRefresh.ts b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/PullToRefresh/BitPullToRefresh.ts index f5b4817bb7..b60ab0a51e 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/PullToRefresh/BitPullToRefresh.ts +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/PullToRefresh/BitPullToRefresh.ts @@ -15,15 +15,13 @@ dotnetObj: DotNetObject) { const anchorEl = anchor ?? document.body as HTMLElement; const scrollerEl = scrollerElement ?? ((scrollerSelector && document.querySelector(scrollerSelector)) ?? (!!anchor ? anchor.children[0] : anchorEl)) as HTMLElement; - const isTouchDevice = Utils.isTouchDevice(); + + let diff = 0; let startY = -1; let refreshing = false; + const isTouchDevice = Utils.isTouchDevice(); - const getY = (e: TouchEvent | PointerEvent) => { - return (e instanceof TouchEvent) - ? e.touches[0].screenY - : e.screenY; - } + const getY = (e: TouchEvent | PointerEvent) => isTouchDevice ? (e as TouchEvent).touches[0].screenY : (e as PointerEvent).screenY; const onScroll = () => { anchorEl.style.touchAction = scrollerEl.scrollTop === 0 ? 'pan-x pan-down pinch-zoom' : ""; @@ -43,15 +41,13 @@ if (startY === -1 || refreshing) return; if (scrollerEl.scrollTop !== 0) { - //startY = getY(e); startY = -1; return; } - let diff = getY(e) - startY; + diff = getY(e) - startY; if (diff < 0) { - //startY = getY(e); startY = -1; return; } @@ -69,14 +65,10 @@ }; const onEnd = async (e: TouchEvent | PointerEvent): Promise => { if (startY === -1 || refreshing) return; - - let diff = parseInt(loadingEl.style.minHeight); - diff = isNaN(diff) ? 0 : diff; - diff = (diff - margin) / factor; + startY = -1; await dotnetObj.invokeMethodAsync('OnEnd', diff); - startY = -1; if (diff >= trigger) { refreshing = true; await dotnetObj.invokeMethodAsync('Refresh'); diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/PullToRefresh/BitPullToRefreshDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/PullToRefresh/BitPullToRefreshDemo.razor index 46cd68f059..7fd4783c85 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/PullToRefresh/BitPullToRefreshDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/PullToRefresh/BitPullToRefreshDemo.razor @@ -33,7 +33,7 @@ - + From 794f69840086204c54e5840d27ddeef2e0a68abf Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Fri, 6 Dec 2024 19:16:04 +0330 Subject: [PATCH 52/87] feat(blazorui): improve BitPanel component #9413 (#9416) --- .../Surfaces/Panel/BitPanel.razor.cs | 23 +++++- .../Components/Surfaces/Panel/BitPanel.ts | 73 +++++++++++-------- .../Panel/BitPanelJsRuntimeExtensions.cs | 5 ++ .../Surfaces/Panel/BitPanelDemo.razor | 6 +- .../Surfaces/Panel/BitPanelDemo.razor.cs | 2 +- 5 files changed, 72 insertions(+), 37 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanel.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanel.razor.cs index d75ed32df8..2df6ec5cc3 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanel.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanel.razor.cs @@ -1,8 +1,9 @@ namespace Bit.BlazorUI; -public partial class BitPanel : BitComponentBase +public partial class BitPanel : BitComponentBase, IAsyncDisposable { private int _offsetTop; + private bool _disposed; private bool _internalIsOpen; private string _containerId = default!; @@ -262,4 +263,24 @@ private string GetPanelStyle() return string.Join(';', styles); } + + + public async ValueTask DisposeAsync() + { + await DisposeAsync(true); + GC.SuppressFinalize(this); + } + + protected virtual async ValueTask DisposeAsync(bool disposing) + { + if (_disposed || disposing is false) return; + + try + { + await _js.BitPanelDispose(UniqueId); + } + catch (JSDisconnectedException) { } // we can ignore this exception here + + _disposed = true; + } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanel.ts b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanel.ts index b893a584cd..0f55b7ab31 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanel.ts +++ b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanel.ts @@ -19,8 +19,8 @@ let originalTransform: string; const isTouchDevice = Utils.isTouchDevice(); - const getX = (e: TouchEvent | PointerEvent) => (e instanceof TouchEvent) ? e.touches[0].screenX : e.screenX; - const getY = (e: TouchEvent | PointerEvent) => (e instanceof TouchEvent) ? e.touches[0].screenY : e.screenY; + const getX = (e: TouchEvent | PointerEvent) => isTouchDevice ? (e as TouchEvent).touches[0].screenX : (e as PointerEvent).screenX; + const getY = (e: TouchEvent | PointerEvent) => isTouchDevice ? (e as TouchEvent).touches[0].screenY : (e as PointerEvent).screenY; const onStart = async (e: TouchEvent | PointerEvent): Promise => { startX = getX(e); @@ -79,47 +79,48 @@ }; const onEnd = async (e: TouchEvent | PointerEvent): Promise => { - startX = -1; - startY = -1; + startX = startY = -1; element.style.transitionDuration = ''; - - if (((!isRtl && position === BitPanelPosition.Start) || (isRtl && position === BitPanelPosition.End)) && diffX < 0) { - if ((Math.abs(diffX) / bcr.width) > trigger) { - diffX = diffY = 0; - return await dotnetObj.invokeMethodAsync('OnClose'); + try { + if (((!isRtl && position === BitPanelPosition.Start) || (isRtl && position === BitPanelPosition.End)) && diffX < 0) { + if ((Math.abs(diffX) / bcr.width) > trigger) { + return await dotnetObj.invokeMethodAsync('OnClose'); + } } - } - if (((!isRtl && position === BitPanelPosition.End) || (isRtl && position === BitPanelPosition.Start)) && diffX > 0) { - if ((diffX / bcr.width) > trigger) { - diffX = diffY = 0; - return await dotnetObj.invokeMethodAsync('OnClose'); + if (((!isRtl && position === BitPanelPosition.End) || (isRtl && position === BitPanelPosition.Start)) && diffX > 0) { + if ((diffX / bcr.width) > trigger) { + return await dotnetObj.invokeMethodAsync('OnClose'); + } } - } - if (position === BitPanelPosition.Top && diffY < 0) { - if ((Math.abs(diffY) / bcr.height) > trigger) { - diffX = diffY = 0; - return await dotnetObj.invokeMethodAsync('OnClose'); + if (position === BitPanelPosition.Top && diffY < 0) { + if ((Math.abs(diffY) / bcr.height) > trigger) { + return await dotnetObj.invokeMethodAsync('OnClose'); + } } - } - if (position === BitPanelPosition.Bottom && diffY > 0) { - if ((diffY / bcr.height) > trigger) { - diffX = diffY = 0; - return await dotnetObj.invokeMethodAsync('OnClose'); + if (position === BitPanelPosition.Bottom && diffY > 0) { + if ((diffY / bcr.height) > trigger) { + return await dotnetObj.invokeMethodAsync('OnClose'); + } } - } - - element.style.transform = originalTransform; - await dotnetObj.invokeMethodAsync('OnEnd', diffX, diffY); + element.style.transform = originalTransform; + } finally { + await dotnetObj.invokeMethodAsync('OnEnd', diffX, diffY); + diffX = diffY = 0; + } }; const onLeave = (e: PointerEvent) => { - if (startY === -1) return; - startY = -1; + dotnetObj.invokeMethodAsync('OnEnd', diffX, diffY); + + startX = startY = -1; + diffX = diffY = 0; + element.style.transitionDuration = ''; + element.style.transform = originalTransform; } if (isTouchDevice) { @@ -130,7 +131,7 @@ element.addEventListener('pointerdown', onStart); element.addEventListener('pointermove', onMove); element.addEventListener('pointerup', onEnd); - element.addEventListener('pointerleave', onLeave, false); + element.addEventListener('pointerleave', onEnd, false); } const panel = new BitPanel(id, element, trigger, dotnetObj); @@ -143,11 +144,19 @@ element.removeEventListener('pointerdown', onStart); element.removeEventListener('pointermove', onMove); element.removeEventListener('pointerup', onEnd); - element.removeEventListener('pointerleave', onLeave, false); + element.removeEventListener('pointerleave', onEnd, false); } }); Panel._panels.push(panel); } + + public static dispose(id: string) { + const panel = Panel._panels.find(r => r.id === id); + if (!panel) return; + + Panel._panels = Panel._panels.filter(r => r.id !== id); + panel.dispose(); + } } class BitPanel { diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanelJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanelJsRuntimeExtensions.cs index c3b890dfba..a067205ae3 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanelJsRuntimeExtensions.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanelJsRuntimeExtensions.cs @@ -11,4 +11,9 @@ internal static ValueTask BitPanelSetup(this IJSRuntime js, { return js.InvokeVoid("BitBlazorUI.Panel.setup", id, trigger, position, isRtl, dotnetObjectReference); } + + internal static ValueTask BitPanelDispose(this IJSRuntime jsRuntime, string id) + { + return jsRuntime.InvokeVoid("BitBlazorUI.Panel.dispose", id); + } } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Panel/BitPanelDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Panel/BitPanelDemo.razor index 46313ed5c3..e282901360 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Panel/BitPanelDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Panel/BitPanelDemo.razor @@ -70,9 +70,9 @@
    FooterTemplate:

    Open Panel - + -

    +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas lorem nulla, malesuada ut sagittis sit amet, vulputate in leo. Maecenas vulputate congue sapien eu tincidunt. Etiam eu sem turpis. Fusce tempor sagittis nunc, ut interdum ipsum vestibulum non. Proin dolor elit, aliquam eget tincidunt non, vestibulum ut @@ -81,7 +81,7 @@ Vivamus ultrices, turpis sed malesuada gravida, eros ipsum venenatis elit, et volutpat eros dui et ante. Quisque ultricies mi nec leo ultricies mollis. Vivamus egestas volutpat lacinia. Quisque pharetra eleifend efficitur. -

    +
    Save diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Panel/BitPanelDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Panel/BitPanelDemo.razor.cs index 0c338f94b6..f1d64e297e 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Panel/BitPanelDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Surfaces/Panel/BitPanelDemo.razor.cs @@ -332,7 +332,7 @@ Quisque ultricies mi nec leo ultricies mollis. Vivamus egestas volutpat lacinia. private readonly string example3RazorCode = @" isPanelWithFooterOpen = true"">Open Panel - +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas lorem nulla, malesuada ut sagittis sit From 768a5ff6a13f5bd80d190056318bbadec8741ad1 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Fri, 6 Dec 2024 19:16:42 +0330 Subject: [PATCH 53/87] feat(templates): apply max-width to sign-in page of Boilerplate #9407 (#9417) --- .../Components/Pages/Identity/SignIn/SignInPage.razor | 2 +- .../Components/Pages/Identity/SignIn/SignInPage.razor.scss | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor index 63e997e6f3..b2175f43a1 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor @@ -7,7 +7,7 @@

    - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.scss index 265a370b27..88076417eb 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.scss @@ -4,3 +4,9 @@ section { width: 100%; height: 100%; } + +::deep { + form { + width: 304px; + } +} \ No newline at end of file From dfdab14ec69a665ec653a8f0140034f594b869f6 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Fri, 6 Dec 2024 19:32:29 +0330 Subject: [PATCH 54/87] feat(blazorui): add BitSwipeTrap component #9301 (#9397) --- .../Utilities/SwipeTrap/BitSwipeDirection.cs | 27 + .../Utilities/SwipeTrap/BitSwipeTrap.razor | 12 + .../Utilities/SwipeTrap/BitSwipeTrap.razor.cs | 122 ++++ .../Utilities/SwipeTrap/BitSwipeTrap.scss | 3 + .../Utilities/SwipeTrap/BitSwipeTrap.ts | 133 ++++ .../SwipeTrap/BitSwipeTrapEventArgs.cs | 27 + .../BitSwipeTrapJsRuntimeExtensions.cs | 20 + .../SwipeTrap/BitSwipeTrapTriggerArgs.cs | 22 + src/BlazorUI/Bit.BlazorUI/Scripts/Utils.ts | 15 + .../Bit.BlazorUI/Styles/components.scss | 1 + .../SwipeTrap/BitSwipeTrapDemo.razor | 95 +++ .../SwipeTrap/BitSwipeTrapDemo.razor.cs | 570 ++++++++++++++++++ .../SwipeTrap/BitSwipeTrapDemo.razor.scss | 87 +++ .../Pages/Home/ComponentsSection.razor | 3 + .../Shared/NavMenu.razor.cs | 1 + .../compilerconfig.json | 18 + 16 files changed, 1156 insertions(+) create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeDirection.cs create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.razor create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.razor.cs create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.scss create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.ts create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrapEventArgs.cs create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrapJsRuntimeExtensions.cs create mode 100644 src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrapTriggerArgs.cs create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor.cs create mode 100644 src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor.scss diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeDirection.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeDirection.cs new file mode 100644 index 0000000000..26196b2280 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeDirection.cs @@ -0,0 +1,27 @@ +namespace Bit.BlazorUI; + +/// +/// The direction in which the swipe trap triggers. +/// +public enum BitSwipeDirection +{ + /// + /// Swipe to right direction. + /// + Right = 0, + + /// + /// Swipe to left direction. + /// + Left = 1, + + /// + /// Swipe to top direction. + /// + Top = 2, + + /// + /// Swipe to bottom direction. + /// + Bottom = 3, +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.razor b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.razor new file mode 100644 index 0000000000..3b653759fa --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.razor @@ -0,0 +1,12 @@ +@namespace Bit.BlazorUI +@inherits BitComponentBase + +
    + @ChildContent +
    \ No newline at end of file diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.razor.cs new file mode 100644 index 0000000000..4e9212f319 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.razor.cs @@ -0,0 +1,122 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Bit.BlazorUI; + +public partial class BitSwipeTrap : BitComponentBase, IAsyncDisposable +{ + private bool _disposed; + + + + [Inject] private IJSRuntime _js { get; set; } = default!; + + + + /// + /// The content of the swipe trap. + /// + [Parameter] public RenderFragment? ChildContent { get; set; } + + /// + /// The event callback for when the swipe action starts on the container of the swipe trap. + /// + [Parameter] public EventCallback OnStart { get; set; } + + /// + /// The event callback for when the swipe action moves on the container of the swipe trap. + /// + [Parameter] public EventCallback OnMove { get; set; } + + /// + /// The event callback for when the swipe action ends on the container of the swipe trap. + /// + [Parameter] public EventCallback OnEnd { get; set; } + + /// + /// The event callback for when the swipe action triggers based on the Trigger constraint. + /// + [Parameter] public EventCallback OnTrigger { get; set; } + + /// + /// The threshold in pixel for swiping distance that starts the swipe process process which stops the default behavior. + /// + [Parameter] public decimal? Threshold { get; set; } + + /// + /// The throttle time in milliseconds to apply a delay between periodic calls to raise the events (default is 10). + /// + [Parameter] public int? Throttle { get; set; } + + /// + /// The swiping point (fraction of element's width or an absolute value) to trigger and call the OnTrigger event (default is 0.25m). + /// + [Parameter] public decimal? Trigger { get; set; } + + + + [JSInvokable("OnStart")] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitSwipeTrapEventArgs))] + public async Task _OnStart(decimal startX, decimal startY) + { + await OnStart.InvokeAsync(new(startX, startY, 0, 0)); + } + + [JSInvokable("OnMove")] + public async Task _OnMove(decimal startX, decimal startY, decimal diffX, decimal diffY) + { + await OnMove.InvokeAsync(new(startX, startY, diffX, diffY)); + } + + [JSInvokable("OnEnd")] + public async Task _OnEnd(decimal startX, decimal startY, decimal diffX, decimal diffY) + { + await OnEnd.InvokeAsync(new(startX, startY, diffX, diffY)); + } + + [JSInvokable("OnTrigger")] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BitSwipeTrapTriggerArgs))] + public async Task _OnTrigger(decimal diffX, decimal diffY) + { + var direction = Math.Abs(diffX) > Math.Abs(diffY) + ? diffX > 0 ? BitSwipeDirection.Right : BitSwipeDirection.Left + : diffY > 0 ? BitSwipeDirection.Bottom : BitSwipeDirection.Top; + + await OnTrigger.InvokeAsync(new(direction, diffX, diffY)); + } + + + + protected override string RootElementClass => "bit-stp"; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var dotnetObj = DotNetObjectReference.Create(this); + await _js.BitSwipeTrapSetup(UniqueId, RootElement, Trigger ?? 0.25m, Threshold ?? 0, Throttle ?? 10, dotnetObj); + } + + await base.OnAfterRenderAsync(firstRender); + } + + + + public async ValueTask DisposeAsync() + { + await DisposeAsync(true); + GC.SuppressFinalize(this); + } + + protected virtual async ValueTask DisposeAsync(bool disposing) + { + if (_disposed || disposing is false) return; + + try + { + await _js.BitSwipeTrapDispose(UniqueId); + } + catch (JSDisconnectedException) { } // we can ignore this exception here + + _disposed = true; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.scss b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.scss new file mode 100644 index 0000000000..aad586fbe6 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.scss @@ -0,0 +1,3 @@ +.bit-stp { + width: fit-content; +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.ts b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.ts new file mode 100644 index 0000000000..08eca3d6fe --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrap.ts @@ -0,0 +1,133 @@ +namespace BitBlazorUI { + export class SwipeTrap { + private static _swipeTraps: BitSwipeTrap[] = []; + + public static setup( + id: string, + element: HTMLElement, + trigger: number, + threshold: number, + throttle: number, + dotnetObj: DotNetObject) { + const bcr = element.getBoundingClientRect(); + + let diffX = 0; + let diffY = 0; + let startX = -1; + let startY = -1; + const isTouchDevice = Utils.isTouchDevice(); + const throttledMove = Utils.throttle((sx: number, sy: number, dx: number, dy: number) => dotnetObj.invokeMethodAsync('OnMove', sx, sy, dx, dy), throttle); + + const getX = (e: TouchEvent | PointerEvent) => isTouchDevice ? (e as TouchEvent).touches[0].screenX : (e as PointerEvent).screenX; + const getY = (e: TouchEvent | PointerEvent) => isTouchDevice ? (e as TouchEvent).touches[0].screenY : (e as PointerEvent).screenY; + + const onStart = async (e: TouchEvent | PointerEvent): Promise => { + startX = getX(e); + startY = getY(e); + + await dotnetObj.invokeMethodAsync('OnStart', startX, startY); + }; + + const onMove = async (e: TouchEvent | PointerEvent): Promise => { + if (startX === -1 && startY === -1) return; + + diffX = getX(e) - startX; + diffY = getY(e) - startY; + + if ((Math.abs(diffX) > threshold || Math.abs(diffY) > threshold) && e.cancelable) { + e.preventDefault(); + e.stopPropagation(); + } + + throttledMove(startX, startY, diffX, diffY); + }; + + const onEnd = async (e: TouchEvent | PointerEvent): Promise => { + if (startX == -1 || startY == -1) return; + const sX = startX; + const sY = startY; + + startX = startY = -1; + + try { + const div = ((Math.abs(trigger) < 1) ? bcr.width : 1); + const compX = Math.abs(diffX) / div; + const compY = Math.abs(diffY) / div; + if (compX > Math.abs(trigger) || compY > Math.abs(trigger)) { + return await dotnetObj.invokeMethodAsync('OnTrigger', diffX, diffY); + } + } finally { + await dotnetObj.invokeMethodAsync('OnEnd', sX, sY, diffX, diffY); + diffX = diffY = 0; + } + }; + + const onLeave = (e: PointerEvent) => { + if (startX == -1 || startY == -1) return; + dotnetObj.invokeMethodAsync('OnEnd', startX, startY, diffX, diffY); + startX = startY = -1; + diffX = diffY = 0; + } + + if (isTouchDevice) { + element.addEventListener('touchstart', onStart); + element.addEventListener('touchmove', onMove); + element.addEventListener('touchend', onEnd); + element.addEventListener('touchcancel', onEnd); + } else { + element.addEventListener('pointerdown', onStart); + element.addEventListener('pointermove', onMove); + element.addEventListener('pointerup', onEnd); + element.addEventListener('pointerleave', onEnd, false); + } + + const swipeTrap = new BitSwipeTrap(id, element, trigger, dotnetObj); + + swipeTrap.setRemoveHandlersFn(() => { + if (isTouchDevice) { + element.removeEventListener('touchstart', onStart); + element.removeEventListener('touchmove', onMove); + element.removeEventListener('touchend', onEnd); + element.removeEventListener('touchcancel', onEnd); + } else { + element.removeEventListener('pointerdown', onStart); + element.removeEventListener('pointermove', onMove); + element.removeEventListener('pointerup', onEnd); + element.removeEventListener('pointerleave', onEnd, false); + } + }); + SwipeTrap._swipeTraps.push(swipeTrap); + } + + public static dispose(id: string) { + const swipeTrap = SwipeTrap._swipeTraps.find(r => r.id === id); + if (!swipeTrap) return; + + SwipeTrap._swipeTraps = SwipeTrap._swipeTraps.filter(r => r.id !== id); + swipeTrap.dispose(); + } + } + + class BitSwipeTrap { + id: string; + element: HTMLElement; + trigger: number; + dotnetObj: DotNetObject; + removeHandlers: () => void = () => { }; + + constructor(id: string, element: HTMLElement, trigger: number, dotnetObj: DotNetObject) { + this.id = id; + this.element = element; + this.trigger = trigger; + this.dotnetObj = dotnetObj; + } + public setRemoveHandlersFn(removeHandlersFn: () => void) { + this.removeHandlers = removeHandlersFn; + } + + public dispose() { + this.removeHandlers(); + this.dotnetObj.dispose(); + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrapEventArgs.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrapEventArgs.cs new file mode 100644 index 0000000000..94b721b902 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrapEventArgs.cs @@ -0,0 +1,27 @@ +namespace Bit.BlazorUI; + +/// +/// The event arguments of the SwipeTrap events. +/// +public class BitSwipeTrapEventArgs(decimal startX, decimal startY, decimal diffX, decimal diffY) +{ + /// + /// The horizontal start point of the swipe action in pixels. + /// + public decimal StartX { get; set; } = startX; + + /// + /// The vertical start point of the swipe action in pixels. + /// + public decimal StartY { get; set; } = startY; + + /// + /// The horizontal difference of swipe action in pixels. + /// + public decimal DiffX { get; set; } = diffX; + + /// + /// The vertical difference of swipe action in pixels. + /// + public decimal DiffY { get; set; } = diffY; +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrapJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrapJsRuntimeExtensions.cs new file mode 100644 index 0000000000..270c1b512d --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrapJsRuntimeExtensions.cs @@ -0,0 +1,20 @@ +namespace Bit.BlazorUI; + +internal static class BitSwipeTrapJsRuntimeExtensions +{ + internal static ValueTask BitSwipeTrapSetup(this IJSRuntime js, + string id, + ElementReference element, + decimal trigger, + decimal threshold, + int throttle, + DotNetObjectReference? dotnetObjectReference) + { + return js.InvokeVoid("BitBlazorUI.SwipeTrap.setup", id, element, trigger, threshold, throttle, dotnetObjectReference); + } + + internal static ValueTask BitSwipeTrapDispose(this IJSRuntime jsRuntime, string id) + { + return jsRuntime.InvokeVoid("BitBlazorUI.SwipeTrap.dispose", id); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrapTriggerArgs.cs b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrapTriggerArgs.cs new file mode 100644 index 0000000000..a746e8cf47 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI/Components/Utilities/SwipeTrap/BitSwipeTrapTriggerArgs.cs @@ -0,0 +1,22 @@ +namespace Bit.BlazorUI; + +/// +/// The event arguments of the SwipeTrap trigger event. +/// +public class BitSwipeTrapTriggerArgs(BitSwipeDirection direction, decimal diffX, decimal diffY) +{ + /// + /// The swipe direction in which the action triggered. + /// + public BitSwipeDirection Direction { get; set; } = direction; + + /// + /// The horizontal difference of swipe action in pixels. + /// + public decimal DiffX { get; set; } = diffX; + + /// + /// The vertical difference of swipe action in pixels. + /// + public decimal DiffY { get; set; } = diffY; +} diff --git a/src/BlazorUI/Bit.BlazorUI/Scripts/Utils.ts b/src/BlazorUI/Bit.BlazorUI/Scripts/Utils.ts index 61aee070dd..4ecc3b1f3f 100644 --- a/src/BlazorUI/Bit.BlazorUI/Scripts/Utils.ts +++ b/src/BlazorUI/Bit.BlazorUI/Scripts/Utils.ts @@ -3,6 +3,21 @@ public static MIN_MOBILE_WIDTH = 320; public static MAX_MOBILE_WIDTH = 600; + public static throttle(mainFunction: Function, delay: number) { + let timeoutItd: number | null = null; + + return (...args: any[]) => { + if (timeoutItd === null) { + mainFunction(...args); + if (delay > 0) { + timeoutItd = setTimeout(() => { + timeoutItd = null; + }, delay); + } + } + }; + } + public static isTouchDevice() { const matchMedia = window.matchMedia("(pointer: coarse)").matches; const maxTouchPoints = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); diff --git a/src/BlazorUI/Bit.BlazorUI/Styles/components.scss b/src/BlazorUI/Bit.BlazorUI/Styles/components.scss index 240b18b8a0..ca7003052f 100644 --- a/src/BlazorUI/Bit.BlazorUI/Styles/components.scss +++ b/src/BlazorUI/Bit.BlazorUI/Styles/components.scss @@ -83,4 +83,5 @@ @import "../Components/Utilities/PullToRefresh/BitPullToRefresh.scss"; @import "../Components/Utilities/Separator/BitSeparator.scss"; @import "../Components/Utilities/Sticky/BitSticky.scss"; +@import "../Components/Utilities/SwipeTrap/BitSwipeTrap.scss"; @import "../Components/Utilities/Text/BitText.scss"; diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor new file mode 100644 index 0000000000..b6d8ad507d --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor @@ -0,0 +1,95 @@ +@page "/components/swipetrap" + + + +
    + + + +
    Try swipe or drag on the container:

    + +
    +
    StartX: @swipeTrapEventArgsBasic?.StartX
    +
    StartY: @swipeTrapEventArgsBasic?.StartY
    +
    DiffX: @swipeTrapEventArgsBasic?.DiffX
    +
    DiffY: @swipeTrapEventArgsBasic?.DiffY
    +
    ---
    +
    Triggered? @isTriggeredBasic
    +
    Trigger direction: @swipeTrapTriggerArgsBasic?.Direction
    +
    Trigger diffX: @swipeTrapTriggerArgsBasic?.DiffX
    +
    Trigger diffY: @swipeTrapTriggerArgsBasic?.DiffY
    +
    +
    +
    +
    + + + +
    Open the panel and try to close it by swiping it to the left:

    +
    + +
    + + +
    +

    Title

    +
    Item1
    +
    Item2
    +
    Item3
    +
    +
    +
    +
    +
    +
    + + + +
    Swipe each row to the right to trigger the delete action:

    +
    + @foreach (int idx in itemsList) + { + var i = idx; +
    +
    Delete
    + +
    +
    Item@(i + 1)
    +
    +
    +
    + } +
    +
    + Reset + +
    +
    +
    +
    \ No newline at end of file diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor.cs new file mode 100644 index 0000000000..800b85ba46 --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor.cs @@ -0,0 +1,570 @@ +namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Utilities.SwipeTrap; + +public partial class BitSwipeTrapDemo +{ + private readonly List componentParameters = + [ + new() + { + Name = "ChildContent", + Type = "RenderFragment?", + DefaultValue = "null", + Description = "The content of the swipe trap." + }, + new() + { + Name = "OnStart", + Type = "EventCallback", + DefaultValue = "", + Description = "The event callback for when the swipe action starts on the container of the swipe trap.", + LinkType = LinkType.Link, + Href = "#swipetrap-event-args", + }, + new() + { + Name = "OnMove", + Type = "EventCallback", + DefaultValue = "", + Description = "The event callback for when the swipe action moves on the container of the swipe trap.", + LinkType = LinkType.Link, + Href = "#swipetrap-event-args", + }, + new() + { + Name = "OnEnd", + Type = "EventCallback", + DefaultValue = "", + Description = "The event callback for when the swipe action ends on the container of the swipe trap.", + LinkType = LinkType.Link, + Href = "#swipetrap-event-args", + }, + new() + { + Name = "OnTrigger", + Type = "EventCallback", + DefaultValue = "", + Description = "The event callback for when the swipe action triggers based on the Trigger constraint.", + LinkType = LinkType.Link, + Href = "#swipetrap-trigger-args", + }, + new() + { + Name = "Threshold", + Type = "decimal?", + DefaultValue = "null", + Description = "The threshold in pixel for swiping distance that starts the swipe process process which stops the default behavior." + }, + new() + { + Name = "Throttle", + Type = "int?", + DefaultValue = "null", + Description = "The throttle time in milliseconds to apply a delay between periodic calls to raise the events (default is 10)." + }, + new() + { + Name = "Trigger", + Type = "decimal?", + DefaultValue = "null", + Description = "The swiping point (fraction of element's width or an absolute value) to trigger and call the OnTrigger event (default is 0.25m)." + }, + ]; + + private readonly List componentSubClasses = + [ + new() + { + Id = "swipetrap-event-args", + Title = "BitSwipeTrapEventArgs", + Description = "The event arguments of the SwipeTrap events.", + Parameters = + [ + new() + { + Name = "StartX", + Type = "decimal", + DefaultValue = "0", + Description = "The horizontal start point of the swipe action in pixels." + }, + new() + { + Name = "StartY", + Type = "decimal", + DefaultValue = "0", + Description = "The vertical start point of the swipe action in pixels." + }, + new() + { + Name = "DiffX", + Type = "decimal", + DefaultValue = "0", + Description = "The horizontal difference of swipe action in pixels." + }, + new() + { + Name = "DiffY", + Type = "decimal", + DefaultValue = "0", + Description = "The vertical difference of swipe action in pixels." + }, + ] + }, + new() + { + Id = "swipetrap-trigger-args", + Title = "BitSwipeTrapTriggerArgs", + Description = "The event arguments of the SwipeTrap trigger event.", + Parameters = + [ + new() + { + Name = "Direction", + Type = "BitSwipeDirection", + DefaultValue = "", + Description = "The swipe direction in which the action triggered.", + LinkType = LinkType.Link, + Href = "#swipe-direction-enum" + + }, + new() + { + Name = "DiffX", + Type = "decimal", + DefaultValue = "0", + Description = "The horizontal difference of swipe action in pixels." + }, + new() + { + Name = "DiffY", + Type = "decimal", + DefaultValue = "0", + Description = "The vertical difference of swipe action in pixels." + }, + ] + } + ]; + + private readonly List componentSubEnums = + [ + new() + { + Id = "swipe-direction-enum", + Name = "BitSwipeDirection", + Description = "The direction in which the swipe trap triggers.", + Items = + [ + new() + { + Name = "Right", + Value = "0", + Description = "Swipe to right direction." + }, + new() + { + Name = "Left", + Value = "1", + Description = "Swipe to left direction." + }, + new() + { + Name = "Top", + Value = "2", + Description = "Swipe to top direction." + }, + new() + { + Name = "Bottom", + Value = "3", + Description = "Swipe to bottom direction." + }, + ] + } + ]; + + + + private bool isTriggeredBasic; + BitSwipeTrapEventArgs? swipeTrapEventArgsBasic; + BitSwipeTrapTriggerArgs? swipeTrapTriggerArgsBasic; + private void HandleOnStartBasic(BitSwipeTrapEventArgs args) + { + swipeTrapEventArgsBasic = args; + } + private void HandleOnMoveBasic(BitSwipeTrapEventArgs args) + { + swipeTrapEventArgsBasic = args; + } + private void HandleOnEndBasic(BitSwipeTrapEventArgs args) + { + swipeTrapEventArgsBasic = args; + } + private void HandleOnTriggerBasic(BitSwipeTrapTriggerArgs args) + { + isTriggeredBasic = true; + swipeTrapTriggerArgsBasic = args; + _ = Task.Delay(3000).ContinueWith(_ => + { + isTriggeredBasic = false; + swipeTrapEventArgsBasic = null; + swipeTrapTriggerArgsBasic = null; + StateHasChanged(); + }); + } + + + private decimal diffXPanel; + private bool isPanelOpen; + private void OpenPanel() + { + isPanelOpen = true; + } + private void ClosePanel() + { + isPanelOpen = false; + } + private void HandleOnMovePanel(BitSwipeTrapEventArgs args) + { + diffXPanel = args.DiffX; + } + private void HandleOnEndPanel(BitSwipeTrapEventArgs args) + { + diffXPanel = 0; + } + private void HandleOnTriggerPanel(BitSwipeTrapTriggerArgs args) + { + if (args.Direction == BitSwipeDirection.Left) + { + diffXPanel = 0; + ClosePanel(); + } + } + private string GetPanelStyle() + { + return diffXPanel < 0 ? $"transform: translateX({diffXPanel}px)" : ""; + } + + + private int deletingIndex = -1; + private bool isListDialogOpen; + private TaskCompletionSource listTcs; + private List itemsList = Enumerable.Range(0, 10).ToList(); + private decimal[] diffXList = Enumerable.Repeat(0m, 10).ToArray(); + private void HandleOnMoveList(BitSwipeTrapEventArgs args, int index) + { + diffXList[index] = args.DiffX; + } + private void HandleOnEndList(BitSwipeTrapEventArgs args, int index) + { + if (diffXList[index] < 60) + { + diffXList[index] = 0; + } + } + private async Task HandleOnTriggerList(BitSwipeTrapTriggerArgs args, int index) + { + if (args.Direction == BitSwipeDirection.Right) + { + deletingIndex = index; + listTcs = new(); + isListDialogOpen = true; + await listTcs.Task; + isListDialogOpen = false; + diffXList[index] = 0; + deletingIndex = -1; + } + } + private string GetRowStyle(int index) + { + var x = Math.Min(diffXList[index], 60); + return x > 0 ? $"transform: translateX({x}px)" : ""; + } + private void HandleOnOkList() + { + if (deletingIndex != -1) + { + itemsList.Remove(deletingIndex); + } + listTcs.SetResult(); + } + private void HandleOnCancelList() + { + listTcs.SetResult(); + } + private void ResetList() + { + itemsList = Enumerable.Range(0, 10).ToList(); + } + + + + private readonly string example1RazorCode = @" + + + +
    +
    StartX: @swipeTrapEventArgs?.StartX
    +
    StartY: @swipeTrapEventArgs?.StartY
    +
    DiffX: @swipeTrapEventArgs?.DiffX
    +
    DiffY: @swipeTrapEventArgs?.DiffY
    +
    ---
    +
    Triggered? @isTriggered
    +
    Trigger direction: @swipeTrapTriggerArgs?.Direction
    +
    Trigger diffX: @swipeTrapTriggerArgs?.DiffX
    +
    Trigger diffY: @swipeTrapTriggerArgs?.DiffY
    +
    +
    "; + private readonly string example1CsharpCode = @" +private bool isTriggeredBasic; +BitSwipeTrapEventArgs? swipeTrapEventArgsBasic; +BitSwipeTrapTriggerArgs? swipeTrapTriggerArgsBasic; +private void HandleOnStartBasic(BitSwipeTrapEventArgs args) +{ + swipeTrapEventArgsBasic = args; +} +private void HandleOnMoveBasic(BitSwipeTrapEventArgs args) +{ + swipeTrapEventArgsBasic = args; +} +private void HandleOnEndBasic(BitSwipeTrapEventArgs args) +{ + swipeTrapEventArgsBasic = args; +} +private void HandleOnTriggerBasic(BitSwipeTrapTriggerArgs args) +{ + isTriggeredBasic = true; + swipeTrapTriggerArgsBasic = args; + _ = Task.Delay(2000).ContinueWith(_ => + { + isTriggeredBasic = false; + swipeTrapEventArgsBasic = null; + swipeTrapTriggerArgsBasic = null; + InvokeAsync(StateHasChanged); + }); +}"; + + private readonly string example2RazorCode = @" + + +
    + +
    + + +
    +

    Title

    +
    Item1
    +
    Item2
    +
    Item3
    +
    +
    +
    +
    "; + private readonly string example2CsharpCode = @" +private decimal diffXPanel; +private bool isPanelOpen; +private void OpenPanel() +{ + isPanelOpen = true; +} +private void ClosePanel() +{ + isPanelOpen = false; +} +private void HandleOnMovePanel(BitSwipeTrapEventArgs args) +{ + diffXPanel = args.DiffX; +} +private void HandleOnEndPanel(BitSwipeTrapEventArgs args) +{ + diffXPanel = 0; +} +private void HandleOnTriggerPanel(BitSwipeTrapTriggerArgs args) +{ + if (args.Direction == BitSwipeDirection.Left) + { + diffXPanel = 0; + ClosePanel(); + } +} +private string GetPanelStyle() +{ + return diffXPanel < 0 ? $""transform: translateX({diffXPanel}px)"" : """"; +}"; + + private readonly string example3RazorCode = @" + + +
    + @foreach (int idx in itemsList) + { + var i = idx; +
    +
    Delete
    + HandleOnMoveList(args, i)"" + OnEnd=""args => HandleOnEndList(args, i)"" + OnTrigger=""args => HandleOnTriggerList(args, i)""> +
    +
    Item@(i + 1)
    +
    +
    +
    + } +
    +Reset +"; + private readonly string example3CsharpCode = @" +private int deletingIndex = -1; +private bool isListDialogOpen; +private TaskCompletionSource listTcs; +private List itemsList = Enumerable.Range(0, 10).ToList(); +private decimal[] diffXList = Enumerable.Repeat(0m, 10).ToArray(); +private void HandleOnMoveList(BitSwipeTrapEventArgs args, int index) +{ + diffXList[index] = args.DiffX; +} +private void HandleOnEndList(BitSwipeTrapEventArgs args, int index) +{ + if (diffXList[index] < 60) + { + diffXList[index] = 0; + } +} +private async Task HandleOnTriggerList(BitSwipeTrapTriggerArgs args, int index) +{ + if (args.Direction == BitSwipeDirection.Right) + { + deletingIndex = index; + listTcs = new(); + isListDialogOpen = true; + await listTcs.Task; + isListDialogOpen = false; + diffXList[index] = 0; + deletingIndex = -1; + } +} +private string GetRowStyle(int index) +{ + var x = Math.Min(diffXList[index], 60); + return x > 0 ? $""transform: translateX({x}px)"" : """"; +} +private void HandleOnOkList() +{ + if (deletingIndex != -1) + { + itemsList.Remove(deletingIndex); + } + listTcs.SetResult(); +} +private void HandleOnCancelList() +{ + listTcs.SetResult(); +} +private void ResetList() +{ + itemsList = Enumerable.Range(0, 10).ToList(); +}"; +} + diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor.scss b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor.scss new file mode 100644 index 0000000000..5b3a05aedb --- /dev/null +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor.scss @@ -0,0 +1,87 @@ +.basic-container { + width: 100%; + cursor: grab; + height: 500px; + display: flex; + user-select: none; + align-items: center; + flex-direction: column; + justify-content: center; + border: 1px solid lightgray; +} + +.panel-container { + width: 100%; + height: 300px; + overflow: hidden; + user-select: none; + position: relative; + border: 1px solid lightgray; + + button { + padding: 0.5rem; + } + + .panel { + left: 0; + color: black; + width: 200px; + cursor: grab; + inset-block: 0; + position: absolute; + background-color: lightgray; + transform: translateX(-100%); + + &.open { + transform: translateX(0); + } + + .panel-trap { + gap: 1rem; + height: 100%; + display: flex; + flex-direction: column; + background-color: gray; + } + } +} + +.list-container { + gap: 4px; + width: 100%; + color: black; + height: 300px; + display: flex; + overflow-y: auto; + user-select: none; + overflow-x: hidden; + position: relative; + flex-direction: column; + border: 1px solid lightgray; + + .row { + min-height: 40px; + position: relative; + } + + .delete { + width: 60px; + color: white; + height: 100%; + padding: 4px; + position: absolute; + background-color: red; + } + + .row-trap { + width: 100%; + height: 100%; + cursor: grab; + padding: 4px; + position: absolute; + background-color: gray; + } +} + +::deep { +} diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor index e1a7a2d4d2..bec34a5508 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Home/ComponentsSection.razor @@ -235,6 +235,9 @@ Sticky + + SwipeTrap + Text diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs index 174f1e812d..d1277d3a9f 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Shared/NavMenu.razor.cs @@ -144,6 +144,7 @@ public partial class NavMenu : IDisposable new() { Text = "PullToRefresh", Url = "/components/pulltorefresh" }, new() { Text = "Separator", Url = "/components/separator" }, new() { Text = "Sticky", Url = "/components/sticky" }, + new() { Text = "SwipeTrap", Url = "/components/swipetrap" }, new() { Text = "Text", Url = "/components/text" }, ], }, diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json index b0b71bb475..1ea4c0c110 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/compilerconfig.json @@ -59,6 +59,18 @@ "minify": { "enabled": false }, "options": { "sourceMap": false } }, + { + "outputFile": "Pages/Components/Extras/ModalService/BitModalServiceDemo.razor.css", + "inputFile": "Pages/Components/Extras/ModalService/BitModalServiceDemo.razor.scss", + "minify": { "enabled": false }, + "options": { "sourceMap": false } + }, + { + "outputFile": "Pages/Components/Extras/PdfReader/BitPdfReaderDemo.razor.css", + "inputFile": "Pages/Components/Extras/PdfReader/BitPdfReaderDemo.razor.scss", + "minify": { "enabled": false }, + "options": { "sourceMap": false } + }, { "outputFile": "Pages/Components/Inputs/Calendar/BitCalendarDemo.razor.css", "inputFile": "Pages/Components/Inputs/Calendar/BitCalendarDemo.razor.scss", @@ -407,6 +419,12 @@ "minify": { "enabled": false }, "options": { "sourceMap": false } }, + { + "outputFile": "Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor.css", + "inputFile": "Pages/Components/Utilities/SwipeTrap/BitSwipeTrapDemo.razor.scss", + "minify": { "enabled": false }, + "options": { "sourceMap": false } + }, { "outputFile": "Pages/Components/Utilities/Text/BitTextDemo.razor.css", "inputFile": "Pages/Components/Utilities/Text/BitTextDemo.razor.scss", From 962be407dc3264f437d71f57b4f5f9402cb090b0 Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Fri, 6 Dec 2024 19:03:43 +0100 Subject: [PATCH 55/87] feat(deps): update project dependencies #9409 (#9415) --- .devcontainer/devcontainer.json | 2 +- docs/how-to-build.md | 2 +- .../Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj | 6 +++--- .../Bit.BlazorUI.Demo.Server.csproj | 6 +++--- .../Bit.BlazorUI.Demo.Shared.csproj | 2 +- .../Bit.BlazorUI.Demo.Client.Maui.csproj | 4 ++-- .../Bit.BlazorUI.Demo.Client.Windows.csproj | 2 +- .../Bit.Butil.Demo.Maui/Bit.Butil.Demo.Maui.csproj | 4 ++-- .../Bit.Boilerplate/src/Directory.Packages.props | 12 ++++++------ .../Bit.Boilerplate/src/Directory.Packages8.props | 6 +++--- .../Boilerplate.Server.Web.csproj | 1 + .../Bit.Websites.Careers.Server.csproj | 4 ++-- .../Bit.Websites.Careers.Shared.csproj | 2 +- .../Templates/Templates03GettingStartedPage.razor | 2 +- .../Bit.Websites.Platform.Server.csproj | 4 ++-- .../Bit.Websites.Platform.Shared.csproj | 2 +- .../Bit.Websites.Sales.Server.csproj | 4 ++-- .../Bit.Websites.Sales.Shared.csproj | 2 +- src/global.json | 2 +- 19 files changed, 35 insertions(+), 34 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 36eb001212..4dc61256ca 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ "hostRequirements": { "cpus": 4 }, - "onCreateCommand": "wget https://download.visualstudio.microsoft.com/download/pr/308f16a9-2ecf-4a42-b8bb-c1233de985fd/be6e87045ab21935bd8bb98ce69026c4/dotnet-sdk-9.0.100-linux-x64.tar.gz -O $HOME/dotnet.tar.gz && export DOTNET_ROOT=$HOME/.dotnet && mkdir -p \"$DOTNET_ROOT\" && tar zxf $HOME/dotnet.tar.gz -C \"$DOTNET_ROOT\" && export PATH=$DOTNET_ROOT:$DOTNET_ROOT/tools:$PATH && find . -type f -name '*.csproj' -exec sed -i 's/Microsoft.NET.Sdk.BlazorWebAssembly/Microsoft.NET.Sdk.Web/g' {} \\;", + "onCreateCommand": "wget https://download.visualstudio.microsoft.com/download/pr/d74fd2dd-3384-4952-924b-f5d492326e35/e91d8295d4cbe82ba3501e411d78c9b8/dotnet-sdk-9.0.101-linux-x64.tar.gz -O $HOME/dotnet.tar.gz && export DOTNET_ROOT=$HOME/.dotnet && mkdir -p \"$DOTNET_ROOT\" && tar zxf $HOME/dotnet.tar.gz -C \"$DOTNET_ROOT\" && export PATH=$DOTNET_ROOT:$DOTNET_ROOT/tools:$PATH && find . -type f -name '*.csproj' -exec sed -i 's/Microsoft.NET.Sdk.BlazorWebAssembly/Microsoft.NET.Sdk.Web/g' {} \\;", "waitFor": "onCreateCommand", "customizations": { "codespaces": { diff --git a/docs/how-to-build.md b/docs/how-to-build.md index d850c5aef6..753e2bd855 100644 --- a/docs/how-to-build.md +++ b/docs/how-to-build.md @@ -22,7 +22,7 @@ building each one of them requires some specific steps that are explained below. Building each of the bit platform projects needs the following basic requirements other than the specific requirements that are explained later: -- [.NET 9 SDK (9.0.100)](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) +- [.NET 9 SDK (9.0.101)](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) - [Node.js](https://nodejs.org)
    diff --git a/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj b/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj index f877b1856b..79d9f65d85 100644 --- a/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj +++ b/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj @@ -14,9 +14,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj index a227dacaa9..c929a04369 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj @@ -13,14 +13,14 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - + diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj index 356f34e5fa..4fed2bc39b 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj @@ -17,7 +17,7 @@ - + compile; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj index 84c10b8473..6b09ef33cf 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj @@ -103,8 +103,8 @@ - - + + $(ReleaseVersion).$([System.DateTime]::Now.ToString(HHmm)) diff --git a/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts b/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts index 9143435a91..e4695718fc 100644 --- a/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts +++ b/src/BlazorUI/Bit.BlazorUI/Scripts/general.ts @@ -1,4 +1,4 @@ -(BitBlazorUI as any).version = (window as any)['bit-blazorui version'] = '9.1.0-pre-08'; +(BitBlazorUI as any).version = (window as any)['bit-blazorui version'] = '9.1.0-pre-09'; interface DotNetObject { invokeMethod(methodIdentifier: string, ...args: any[]): T; diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj index c929a04369..bca7f8bf86 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Server/Bit.BlazorUI.Demo.Server.csproj @@ -5,11 +5,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj index 4fed2bc39b..562d04e9d9 100644 --- a/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj +++ b/src/BlazorUI/Demo/Bit.BlazorUI.Demo.Shared/Bit.BlazorUI.Demo.Shared.csproj @@ -5,11 +5,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj index d0c5771a2e..fbe3772716 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj @@ -16,11 +16,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj index 6b09ef33cf..e9b776eab7 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj @@ -85,12 +85,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj index e8a35da3cd..39468c30a6 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/Bit.BlazorUI.Demo.Client.Web.csproj @@ -24,13 +24,13 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js index f930392226..e96bf5245e 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Web/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-08 +// bit version: 9.1.0-pre-09 // https://github.com/bitfoundation/bitplatform/tree/develop/src/Bswup self.assetsInclude = []; diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj index efce539743..11a97d6c97 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Windows/Bit.BlazorUI.Demo.Client.Windows.csproj @@ -29,11 +29,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/Demo/Directory.Build.props b/src/BlazorUI/Demo/Directory.Build.props index 093f790e02..4ba566564f 100644 --- a/src/BlazorUI/Demo/Directory.Build.props +++ b/src/BlazorUI/Demo/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js index ff9637c526..d9a2636d22 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-08 +// bit version: 9.1.0-pre-09 self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; self.caseInsensitiveUrl = true; diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js index 5d45a37abb..e2c4c89438 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-08 +// bit version: 9.1.0-pre-09 self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; self.caseInsensitiveUrl = true; diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js index 37659e38fa..bcebf7d68b 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js +++ b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-08 +// bit version: 9.1.0-pre-09 // In development, always fetch from the network and do not enable offline support. // This is because caching would make development more difficult (changes would not diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js index dbcc75585e..ed5f9b054e 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js +++ b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-08 +// bit version: 9.1.0-pre-09 self.assetsInclude = []; self.assetsExclude = [ diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts index 9c121f880c..c8505fc380 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.progress.ts @@ -1,4 +1,4 @@ -window['bit-bswup.progress version'] = '9.1.0-pre-08'; +window['bit-bswup.progress version'] = '9.1.0-pre-09'; ; (function () { (window as any).startBswupProgress = (autoReload: boolean, diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts index 247b5a306b..fe151bcdd2 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.sw.ts @@ -1,4 +1,4 @@ -self['bit-bswup.sw version'] = '9.1.0-pre-08'; +self['bit-bswup.sw version'] = '9.1.0-pre-09'; interface Window { clients: any diff --git a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts index e6495b2d0a..be866db824 100644 --- a/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts +++ b/src/Bswup/Bit.Bswup/Scripts/bit-bswup.ts @@ -1,5 +1,5 @@ const BitBswup = {} as any; -BitBswup.version = window['bit-bswup version'] = '9.1.0-pre-08'; +BitBswup.version = window['bit-bswup version'] = '9.1.0-pre-09'; declare const Blazor: any; diff --git a/src/Bswup/FullDemo/Client/wwwroot/service-worker.js b/src/Bswup/FullDemo/Client/wwwroot/service-worker.js index b96abc064f..c6b68bd84a 100644 --- a/src/Bswup/FullDemo/Client/wwwroot/service-worker.js +++ b/src/Bswup/FullDemo/Client/wwwroot/service-worker.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-08 +// bit version: 9.1.0-pre-09 // In development, always fetch from the network and do not enable offline support. // This is because caching would make development more difficult (changes would not diff --git a/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js b/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js index b9f985f5d4..061b147981 100644 --- a/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js +++ b/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js @@ -1,4 +1,4 @@ -// bit version: 9.1.0-pre-08 +// bit version: 9.1.0-pre-09 self.assetsInclude = []; self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; diff --git a/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts b/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts index 13df1b6dee..344e62fe49 100644 --- a/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts +++ b/src/Bup/Bit.Bup/Scripts/bit-bup.progress.ts @@ -1,4 +1,4 @@ -window['bit-bup.progress version'] = '9.1.0-pre-08'; +window['bit-bup.progress version'] = '9.1.0-pre-09'; ; (function () { (window as any).startBupProgress = (showLogs: boolean, showAssets: boolean, appContainerSelector: string, hideApp: boolean, autoHide: boolean) => { diff --git a/src/Bup/Bit.Bup/Scripts/bit-bup.ts b/src/Bup/Bit.Bup/Scripts/bit-bup.ts index 756babf900..3ea9403ac3 100644 --- a/src/Bup/Bit.Bup/Scripts/bit-bup.ts +++ b/src/Bup/Bit.Bup/Scripts/bit-bup.ts @@ -1,5 +1,5 @@ var BitBup = BitBup || {}; -BitBup.version = window['bit-bup version'] = '9.1.0-pre-08'; +BitBup.version = window['bit-bup version'] = '9.1.0-pre-09'; declare const Blazor: any; diff --git a/src/Butil/Bit.Butil/Scripts/butil.ts b/src/Butil/Bit.Butil/Scripts/butil.ts index 52b3562c01..3581914b70 100644 --- a/src/Butil/Bit.Butil/Scripts/butil.ts +++ b/src/Butil/Bit.Butil/Scripts/butil.ts @@ -1,2 +1,2 @@ var BitButil = BitButil || {}; -BitButil.version = window['bit-butil version'] = '9.1.0-pre-08'; \ No newline at end of file +BitButil.version = window['bit-butil version'] = '9.1.0-pre-09'; \ No newline at end of file diff --git a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj index 3dd872cb17..c7fbe7f497 100644 --- a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj +++ b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty.Client/BlazorEmpty.Client.csproj @@ -1,4 +1,4 @@ - + @@ -17,14 +17,14 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj index cbd628f73b..413b8ddd21 100644 --- a/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj +++ b/src/Templates/BlazorEmpty/Bit.BlazorEmpty/BlazorEmpty/BlazorEmpty.csproj @@ -1,4 +1,4 @@ - + @@ -19,14 +19,14 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js index 057da98c48..16d3d2e9cb 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js @@ -1,5 +1,5 @@ //+:cnd:noEmit -// bit version: 9.1.0-pre-08 +// bit version: 9.1.0-pre-09 // https://github.com/bitfoundation/bitplatform/tree/develop/src/Bswup //#if (notification == true) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props index 10273bfe4b..9c260a80f0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Build.props @@ -1,4 +1,4 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props index 4534d38869..e1db282570 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages.props @@ -1,14 +1,14 @@  - - - - - - - - + + + + + + + + @@ -46,7 +46,7 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props index af4fbb1a38..b3e16f6036 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props @@ -1,14 +1,14 @@  - - - - - - - - + + + + + + + + @@ -46,7 +46,7 @@ - + diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj index c8dd5920e5..9608ecb317 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Client/Bit.Websites.Careers.Client.csproj @@ -22,15 +22,15 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj index e9d64a6671..a189b06495 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Server/Bit.Websites.Careers.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj b/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj index e02bb64074..eb71d1a457 100644 --- a/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj +++ b/src/Websites/Careers/src/Bit.Websites.Careers.Shared/Bit.Websites.Careers.Shared.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Careers/src/Directory.Build.props b/src/Websites/Careers/src/Directory.Build.props index c8b355846f..91c2a55468 100644 --- a/src/Websites/Careers/src/Directory.Build.props +++ b/src/Websites/Careers/src/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj index 969cfbb9e5..941d51b755 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Bit.Websites.Platform.Client.csproj @@ -22,16 +22,16 @@ - - - + + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor index 1a0c9481ff..7fae4fdb68 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor @@ -174,8 +174,8 @@ rm $HOME/dotnet.tar.gz }
  • -
    Install Bit Boilerplate project template
    - dotnet new install Bit.Boilerplate::9.1.0-pre-08 +
    Install Bit Boilerplate project template
    + dotnet new install Bit.Boilerplate::9.1.0-pre-09
  • @if (showCrossPlatform && devOS is "Windows") { diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs index 18df6d253e..28ecb7f8a7 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs @@ -38,7 +38,7 @@ public partial class Templates03GettingStartedPage command:"dotnet nuget add source \"https://api.nuget.org/v3/index.json\" --name \"nuget.org\"; dotnet workload install wasm-tools;"), (text:@"echo 'Install the Bit.Boilerplate project template https://www.nuget.org/packages/Boilerplate.Templates';", - command:"dotnet new install Bit.Boilerplate::9.1.0-pre-08;") + command:"dotnet new install Bit.Boilerplate::9.1.0-pre-09;") ]; if (enableVirtualization) diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj index e320e6d508..ed4d7028d1 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Server/Bit.Websites.Platform.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj b/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj index e02bb64074..eb71d1a457 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Shared/Bit.Websites.Platform.Shared.csproj @@ -6,11 +6,11 @@
    - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Platform/src/Directory.Build.props b/src/Websites/Platform/src/Directory.Build.props index 41cf6ed29e..64f7a840f2 100644 --- a/src/Websites/Platform/src/Directory.Build.props +++ b/src/Websites/Platform/src/Directory.Build.props @@ -1,4 +1,4 @@ - + preview diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj index e13a2244bb..28dcf4f7a0 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Client/Bit.Websites.Sales.Client.csproj @@ -22,15 +22,15 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj index 5ea32a1adf..ef2c5309e1 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Server/Bit.Websites.Sales.Server.csproj @@ -10,11 +10,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj b/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj index e02bb64074..eb71d1a457 100644 --- a/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj +++ b/src/Websites/Sales/src/Bit.Websites.Sales.Shared/Bit.Websites.Sales.Shared.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Websites/Sales/src/Directory.Build.props b/src/Websites/Sales/src/Directory.Build.props index 38eee63667..599f11de65 100644 --- a/src/Websites/Sales/src/Directory.Build.props +++ b/src/Websites/Sales/src/Directory.Build.props @@ -1,4 +1,4 @@ - + 13.0 From 357f0e207d675f1ea76c4a8feeaa40dda8763dbb Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Fri, 6 Dec 2024 21:30:28 +0100 Subject: [PATCH 58/87] feat(templates): update adminpanel demo CD pipeline based on recent globa.json change #9421 (#9422) --- .github/workflows/admin-sample.cd.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/admin-sample.cd.yml b/.github/workflows/admin-sample.cd.yml index c750296103..53ba25dce3 100644 --- a/.github/workflows/admin-sample.cd.yml +++ b/.github/workflows/admin-sample.cd.yml @@ -28,7 +28,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - global-json-file: src/Templates/Boilerplate/Bit.Boilerplate/global.json + global-json-file: src/global.json - name: Create project from Boilerplate run: | @@ -122,7 +122,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - global-json-file: src\Templates\Boilerplate\Bit.Boilerplate\global.json + global-json-file: src\global.json - uses: actions/setup-node@v4 with: @@ -174,7 +174,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - global-json-file: src/Templates/Boilerplate/Bit.Boilerplate/global.json + global-json-file: src/global.json - name: Create project from Boilerplate run: | @@ -243,7 +243,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - global-json-file: src/Templates/Boilerplate/Bit.Boilerplate/global.json + global-json-file: src/global.json - uses: maxim-lobanov/setup-xcode@v1.6.0 with: From 36a4f5d4ca959aa8d3972106c6120d8f7eec27ad Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Sun, 8 Dec 2024 09:29:28 +0330 Subject: [PATCH 59/87] feat(blazorui): correct @key attribute calculation #9425 (#9426) --- .../Buttons/BitMenuButton/BitMenuButton.razor | 4 +- .../BitMenuButton/BitMenuButton.razor.cs | 5 +++ .../Navs/Breadcrumb/BitBreadcrumb.razor | 12 +++--- .../Navs/Breadcrumb/BitBreadcrumb.razor.cs | 5 +++ .../Components/Navs/Nav/BitNav.razor | 5 ++- .../Components/Navs/Nav/BitNav.razor.cs | 40 +++++++++---------- .../Components/Navs/Nav/_BitNavChild.razor | 7 ++-- .../Components/Navs/Nav/_BitNavChild.razor.cs | 4 +- .../Components/Navs/NavBar/BitNavBar.razor | 5 ++- .../Components/Navs/NavBar/BitNavBar.razor.cs | 4 +- 10 files changed, 53 insertions(+), 38 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitMenuButton/BitMenuButton.razor b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitMenuButton/BitMenuButton.razor index 90ff2c90e3..6f4bcb16a5 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitMenuButton/BitMenuButton.razor +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitMenuButton/BitMenuButton.razor @@ -90,13 +90,13 @@ style="@Styles?.Callout" class="bit-mnb-cal @Classes?.Callout">
    [Parameter] public string? Placeholder { get; set; } + /// + /// Enables the responsive mode in small screens + /// + [Parameter] public bool Responsive { get; set; } + /// /// Whether the TimePicker's close button should be shown or not. /// @@ -178,6 +173,11 @@ public partial class BitCircularTimePicker : BitInputBase, IAsyncDisp ///
    [Parameter] public BitTimeFormat TimeFormat { get; set; } + /// + /// Whether or not the Text field of the TimePicker is underlined. + /// + [Parameter] public bool Underlined { get; set; } + /// /// The format of the time in the TimePicker /// @@ -241,7 +241,7 @@ protected override void RegisterCssClasses() ClassBuilder.Register(() => IconLocation is BitIconLocation.Left ? "bit-ctp-lic" : string.Empty); - ClassBuilder.Register(() => IsUnderlined ? "bit-ctp-und" : string.Empty); + ClassBuilder.Register(() => Underlined ? "bit-ctp-und" : string.Empty); ClassBuilder.Register(() => HasBorder ? string.Empty : "bit-ctp-nbd"); @@ -503,15 +503,14 @@ await _js.ToggleCallout(_dotnetObj, _calloutId, null, IsOpen, - IsResponsive ? BitResponsiveMode.Top : BitResponsiveMode.None, + Responsive ? BitResponsiveMode.Top : BitResponsiveMode.None, BitDropDirection.TopAndBottom, Dir is BitDir.Rtl, "", 0, "", "", - false, - RootElementClass); + false); } private async Task UpdateTime(MouseEventArgs e) @@ -611,6 +610,26 @@ private async Task UpdateTime(MouseEventArgs e) return style.HasValue() ? style : null; } + private string GetCalloutCssClasses() + { + List classes = ["bit-ctp-cal"]; + + if (Classes?.Callout is not null) + { + classes.Add(Classes.Callout); + } + + if (Responsive) + { + classes.Add("bit-ctp-res"); + } + + return string.Join(' ', classes).Trim(); + } + + + + /// protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TimeSpan? result, [NotNullWhen(false)] out string? validationErrorMessage) { diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePicker.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePicker.scss index 220d0eb661..43b7eb6535 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePicker.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePicker.scss @@ -1,4 +1,5 @@ @import "../../../../Styles/functions.scss"; +@import "../../../../Styles/media-queries.scss"; .bit-ctp { &.bit-dis { @@ -389,15 +390,28 @@ } } -.bit-ctp-rsp { - .bit-ctp-clk { - margin: spacing(2) auto; - } - - .bit-ctp-cbn { +.bit-ctp-res { + @include lt-sm { + top: 0; + left: 0; + opacity: 0; width: 100%; - min-width: spacing(4); - max-width: spacing(5); - font-size: spacing(2.5); + display: block; + overflow: hidden; + animation-name: unset; + transform: translateY(-100%); + box-shadow: $box-shadow-callout; + transition: transform 200ms ease-out, opacity 100ms linear; + + .bit-ctp-clk { + margin: spacing(2) auto; + } + + .bit-ctp-cbn { + width: 100%; + min-width: spacing(4); + max-width: spacing(5); + font-size: spacing(2.5); + } } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.razor b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.razor index a4efe15cb9..beff08a35b 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.razor +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.razor @@ -87,7 +87,9 @@ readonly="@(AllowTextInput is false)" /> } -
    +
    [Parameter] public string Placeholder { get; set; } = string.Empty; + /// + /// Enables the responsive mode in small screens. + /// + [Parameter] public bool Responsive { get; set; } + /// /// Whether the DatePicker's close button should be shown or not. /// @@ -376,6 +370,12 @@ private int _minuteView ///
    [Parameter] public BitTimeFormat TimeFormat { get; set; } + /// + /// Whether or not the text field of the DatePicker is underlined. + /// + [Parameter, ResetClassBuilder] + public bool Underlined { get; set; } + /// /// The title of the week number (tooltip). /// @@ -462,7 +462,7 @@ protected override void RegisterCssClasses() ClassBuilder.Register(() => IconLocation is BitIconLocation.Left ? "bit-dtp-lic" : string.Empty); - ClassBuilder.Register(() => IsUnderlined ? "bit-dtp-und" : string.Empty); + ClassBuilder.Register(() => Underlined ? "bit-dtp-und" : string.Empty); ClassBuilder.Register(() => HasBorder is false ? "bit-dtp-nbd" : string.Empty); @@ -1456,14 +1456,30 @@ private async Task ToggleCallout() _calloutId, null, IsOpen, - IsResponsive ? BitResponsiveMode.Top : BitResponsiveMode.None, + Responsive ? BitResponsiveMode.Top : BitResponsiveMode.None, BitDropDirection.TopAndBottom, Dir is BitDir.Rtl, "", 0, "", "", - false, - RootElementClass); + false); + } + + private string GetCalloutCssClasses() + { + List classes = ["bit-dtp-cal"]; + + if (Classes?.Callout is not null) + { + classes.Add(Classes.Callout); + } + + if (Responsive) + { + classes.Add("bit-dtp-res"); + } + + return string.Join(' ', classes).Trim(); } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.scss index eac76ee7e7..480e51c3bd 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.scss @@ -1,4 +1,5 @@ @import "../../../../Styles/functions.scss"; +@import "../../../../Styles/media-queries.scss"; .bit-dtp { margin: 0; @@ -186,85 +187,6 @@ animation-timing-function: cubic-bezier(0.1, 0.9, 0.2, 1); } -.bit-dtp-rsp { - .bit-dtp-dwp, - .bit-dtp-mwp, - .bit-dtp-twp { - flex-grow: 1; - } - - .bit-dtp-pkh { - min-height: spacing(5.5); - } - - .bit-dtp-pkc { - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-around; - } - - .bit-dtp-pkr { - height: 100%; - } - - .bit-dtp-nbt, - .bit-dtp-gtb, - .bit-dtp-gtn, - .bit-dtp-wlb, - .bit-dtp-dbt, - .bit-dtp-pkb, - .bit-dtp-wnm { - width: 100%; - height: 100%; - display: flex; - aspect-ratio: 1; - max-width: unset; - align-items: center; - font-size: spacing(2); - justify-content: center; - } - - .bit-dtp-nbt, - .bit-dtp-gtb, - .bit-dtp-gtn { - width: spacing(5.5); - } - - .bit-dtp-wlb { - font-weight: bold; - } - - .bit-dtp-grp { - justify-content: center; - } - - .bit-dtp-tdv { - font-size: spacing(7); - } - - .bit-dtp-tin { - width: 100%; - min-width: spacing(7); - max-width: spacing(12); - font-size: spacing(5); - } - - .bit-dtp-tbt { - width: 100%; - min-width: spacing(7); - max-width: spacing(12); - font-size: spacing(3); - } - - .bit-dtp-bam, - .bit-dtp-bpm { - width: 100%; - font-size: spacing(4); - margin: spacing(0.25); - } -} - .bit-dtp-cac { height: 100%; outline: none; @@ -823,4 +745,97 @@ animation: none; position: relative; } -} \ No newline at end of file +} + + +.bit-dtp-res { + @include lt-sm { + top: 0; + left: 0; + opacity: 0; + width: 100%; + display: block; + overflow: hidden; + animation-name: unset; + transform: translateY(-100%); + box-shadow: $box-shadow-callout; + transition: transform 200ms ease-out, opacity 100ms linear; + + .bit-dtp-dwp, + .bit-dtp-mwp, + .bit-dtp-twp { + flex-grow: 1; + } + + .bit-dtp-pkh { + min-height: spacing(5.5); + } + + .bit-dtp-pkc { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-around; + } + + .bit-dtp-pkr { + height: 100%; + } + + .bit-dtp-nbt, + .bit-dtp-gtb, + .bit-dtp-gtn, + .bit-dtp-wlb, + .bit-dtp-dbt, + .bit-dtp-pkb, + .bit-dtp-wnm { + width: 100%; + height: 100%; + display: flex; + aspect-ratio: 1; + max-width: unset; + align-items: center; + font-size: spacing(2); + justify-content: center; + } + + .bit-dtp-nbt, + .bit-dtp-gtb, + .bit-dtp-gtn { + width: spacing(5.5); + } + + .bit-dtp-wlb { + font-weight: bold; + } + + .bit-dtp-grp { + justify-content: center; + } + + .bit-dtp-tdv { + font-size: spacing(7); + } + + .bit-dtp-tin { + width: 100%; + min-width: spacing(7); + max-width: spacing(12); + font-size: spacing(5); + } + + .bit-dtp-tbt { + width: 100%; + min-width: spacing(7); + max-width: spacing(12); + font-size: spacing(3); + } + + .bit-dtp-bam, + .bit-dtp-bpm { + width: 100%; + font-size: spacing(4); + margin: spacing(0.25); + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DateRangePicker/BitDateRangePicker.razor b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DateRangePicker/BitDateRangePicker.razor index 80d4233f48..8ca8a6d6b0 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DateRangePicker/BitDateRangePicker.razor +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DateRangePicker/BitDateRangePicker.razor @@ -86,7 +86,9 @@ readonly="@(AllowTextInput is false)" /> } -
    +