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/.github/workflows/admin-sample.cd.yml b/.github/workflows/admin-sample.cd.yml index 8ffa774e73..20c90a0b1e 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: @@ -29,14 +28,14 @@ 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: | 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,25 +60,64 @@ 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 + include-hidden-files: true # Required for wwwroot/.well-known folder + + deploy_blazor_wasm_standalone: + name: build blazor wasm standalone + runs-on: ubuntu-24.04 - - name: Publish adminpanel blazor wasm standalone + steps: + + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: src/global.json + + - name: Create project from Boilerplate 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}}" + 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.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.Web/appsettings.json, AdminPanel/src/Client/AdminPanel.Client.Web/appsettings.Production.json' + env: + ServerAddress: ${{ env.SERVER_ADDRESS }} + Logging.Sentry.Dsn: ${{ secrets.ADMINPANEL_SENTRY_DSN }} + GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} + AdsPushVapid.PublicKey: ${{ secrets.ADMINPANEL_PUBLIC_VAPIDKEY }} + ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - - name: Upload static artifact - uses: actions/upload-artifact@v4 + - uses: actions/setup-node@v4 with: - name: static-bundle - path: ${{env.DOTNET_ROOT}}/static - include-hidden-files: true # Required for wwwroot/.well-known folder + node-version: 22 + + - name: Install wasm + run: cd src && dotnet workload install wasm-tools + + - 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: dotnet publish AdminPanel/src/Client/AdminPanel.Client.Web/AdminPanel.Client.Web.csproj -c Release -p:PwaEnabled=true -o ${{env.DOTNET_ROOT}}/client -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" + + - name: Upload to asw + run: | + npm install -g @azure/static-web-apps-cli + swa deploy --deployment-token ${{ secrets.ADMINPANEL_ASW_TOKEN }} --env production --app-location ${{env.DOTNET_ROOT}}/client/wwwroot deploy_api_blazor: name: deploy api + blazor @@ -107,6 +145,9 @@ jobs: fileName: 'DataProtectionCertificate.pfx' encodedString: ${{ secrets.API_DATA_PROTECTION_CERTIFICATE_FILE_BASE64 }} + - name: Retrieve AppleAuthKey.p8 + run: echo "${{ secrets.APPSTORE_API_KEY_PRIVATE_KEY_ADMIN }}" > AppleAuthKey.p8 + - name: Deploy to Azure Web App id: deploy-to-webapp uses: azure/webapps-deploy@v3 @@ -134,7 +175,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: @@ -145,14 +186,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 @@ -186,14 +227,14 @@ 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: | 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 +259,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 }} @@ -255,11 +296,11 @@ 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: - xcode-version: '16.1' + xcode-version: '16.2' - uses: actions/setup-node@v4 with: @@ -270,14 +311,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 --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 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/.github/workflows/bit.full.ci.yml b/.github/workflows/bit.full.ci.yml index aa1c90c845..c08e74098e 100644 --- a/.github/workflows/bit.full.ci.yml +++ b/.github/workflows/bit.full.ci.yml @@ -140,25 +140,25 @@ jobs: - name: Test PostgreSQL, MySql, Other database options run: | - dotnet new bit-bp --name TestPostgreSQL --database PostgreSQL --framework net8.0 + dotnet new bit-bp --name TestPostgreSQL --database PostgreSQL --framework net8.0 --signalR cd TestPostgreSQL/src/Server/TestPostgreSQL.Server.Api/ dotnet build cd ../../../../ - dotnet new bit-bp --name TestMySql --database MySql --framework net8.0 + dotnet new bit-bp --name TestMySql --database MySql --framework net8.0 --sample Admin --offlineDb cd TestMySql/src/Server/TestMySql.Server.Api/ dotnet build cd ../../../../ - dotnet new bit-bp --name TestOther --database Other --framework net9.0 + dotnet new bit-bp --name TestOther --database Other --framework net9.0 --sample Todo --sentry cd TestOther/src/Server/TestOther.Server.Api/ dotnet build - name: Test file storage options run: | - dotnet new bit-bp --name TestLocal --filesStorage Local --framework net8.0 + dotnet new bit-bp --name TestLocal --filesStorage Local --framework net8.0 --appInsights cd TestLocal/src/Server/TestLocal.Server.Api/ dotnet build cd ../../../../ - dotnet new bit-bp --name TestAzureBlobStorage --filesStorage AzureBlobStorage --framework net9.0 + dotnet new bit-bp --name TestAzureBlobStorage --filesStorage AzureBlobStorage --framework net9.0 --captcha reCaptcha --notification cd TestAzureBlobStorage/src/Server/TestAzureBlobStorage.Server.Api/ dotnet build diff --git a/.github/workflows/blazorui.demo.cd.yml b/.github/workflows/blazorui.demo.cd.yml index 0a366510b7..40aba44568 100644 --- a/.github/workflows/blazorui.demo.cd.yml +++ b/.github/workflows/blazorui.demo.cd.yml @@ -196,7 +196,7 @@ jobs: - uses: maxim-lobanov/setup-xcode@v1.6.0 with: - xcode-version: '16.1' + xcode-version: '16.2' - uses: actions/setup-node@v4 with: diff --git a/.github/workflows/todo-sample.cd.yml b/.github/workflows/todo-sample.cd.yml index 4a0cc17bb1..b668a00726 100644 --- a/.github/workflows/todo-sample.cd.yml +++ b/.github/workflows/todo-sample.cd.yml @@ -62,7 +62,6 @@ jobs: - name: Configure bswup run: | - sed -i 's/\/\/ self.noPrerenderQuery/self.noPrerenderQuery/g' TodoSample/src/Client/TodoSample.Client.Web/wwwroot/service-worker.published.js sed -i 's/\/\/ self.disablePassiveFirstBoot/self.disablePassiveFirstBoot/g' TodoSample/src/Client/TodoSample.Client.Web/wwwroot/service-worker.published.js - name: Generate CSS/JS files @@ -104,6 +103,9 @@ jobs: fileName: 'DataProtectionCertificate.pfx' encodedString: ${{ secrets.API_DATA_PROTECTION_CERTIFICATE_FILE_BASE64 }} + - name: Retrieve AppleAuthKey.p8 + run: echo "${{ secrets.APPSTORE_API_KEY_PRIVATE_KEY_TODO }}" > AppleAuthKey.p8 + - name: Deploy to Azure Web App id: deploy-to-webapp uses: azure/webapps-deploy@v3 @@ -119,6 +121,53 @@ jobs: CLOUDFLARE_ZONE: ${{ secrets.BITPLATFORM_DEV_CLOUDFLARE_ZONE }} CLOUDFLARE_TOKEN: ${{ secrets.CLOUDFLARE_TOKEN }} + deploy_blazor_wasm_standalone: + name: build blazor wasm standalone + runs-on: ubuntu-24.04 + + steps: + + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: src/global.json + + - name: Create project from Boilerplate + run: | + 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 --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net9.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: + ServerAddress: ${{ env.SERVER_ADDRESS }} + GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install wasm + run: cd src && dotnet workload install wasm-tools + + - 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: dotnet publish TodoSample/src/Client/TodoSample.Client.Web/TodoSample.Client.Web.csproj -c Release -p:PwaEnabled=true -o ${{env.DOTNET_ROOT}}/client -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:RunAOTCompilation=true -p:MultilingualEnabled=false + + - name: Upload to asw + run: | + npm install -g @azure/static-web-apps-cli + swa deploy --deployment-token ${{ secrets.TODO_ASW_TOKEN }} --env production --app-location ${{env.DOTNET_ROOT}}/client/wwwroot + build_blazor_hybrid_windows: name: build blazor hybrid (windows) runs-on: windows-2022 @@ -292,14 +341,14 @@ jobs: - uses: maxim-lobanov/setup-xcode@v1.6.0 with: - xcode-version: '16.1' + xcode-version: '16.2' - name: Create project from Boilerplate run: | 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 --signalR --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 --signalR --framework net8.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 diff --git a/README.md b/README.md index d189ce9e8f..d0ffb560b2 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,9 @@ The following apps are our open-source projects powered by the bit platform show 1. [bitplatform.dev](https://bitplatform.dev): .NET 9 Pre-rendered SPA with Blazor WebAssembly 2. [blazorui.bitplatform.dev](https://blazorui.bitplatform.dev): .NET 9 Pre-rendered PWA with Blazor WebAssembly 3. [todo.bitplatform.dev](https://todo.bitplatform.dev): .NET 8 Pre-rendered PWA with Blazor WebAssembly -5. [adminpanel.bitplatform.dev](https://adminpanel.bitplatform.dev): .NET 9 PWA with Blazor WebAssembly Standalone (Hosted on Cloudflare Pages) +5. [adminpanel.bitplatform.dev](https://adminpanel.bitplatform.dev): .NET 9 PWA with Blazor WebAssembly +6. [adminpanel.bitplatform.cc](https://adminpanel.bitplatform.cc): .NET 9 PWA with Blazor WebAssembly Standalone (Free Azure static web app) +7. [todo.bitplatform.cc](https://todo.bitplatform.cc): AOT Compiled .NET 9 PWA with Blazor WebAssembly Standalone (Free Azure static web app) [Todo](https://todo.bitplatform.dev) & [Adminpanel](https://adminpanel.bitplatform.dev) web apps will launch their respective Android and iOS applications if you have already installed them, mirroring the behavior of apps like YouTube and Instagram. 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/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..cfa5257531 --- /dev/null +++ b/src/Besql/Bit.Besql/BesqlDbContextInterceptor.cs @@ -0,0 +1,71 @@ +using System.Data.Common; +using System.Collections.Concurrent; +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) + { + if (IsTargetedCommand(command.CommandText)) + { + _ = ThrottledSync(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)) + { + _ = ThrottledSync(eventData.Context!.Database.GetDbConnection().DataSource).ConfigureAwait(false); + } + + return result; + } + + public async ValueTask ScalarExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + object? result, + CancellationToken cancellationToken) + { + if (IsTargetedCommand(command.CommandText)) + { + _ = ThrottledSync(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 readonly ConcurrentDictionary filesSyncIds = []; + private async Task ThrottledSync(string dataSource) + { + var fileName = dataSource.Trim('/'); + + var localLastSyncId = filesSyncIds[fileName] = Guid.NewGuid(); + + await Task.Delay(50).ConfigureAwait(false); + + if (localLastSyncId != filesSyncIds[fileName]) + return; + + 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/Bit.Besql.csproj b/src/Besql/Bit.Besql/Bit.Besql.csproj index 5910c31fad..b134ea5a7f 100644 --- a/src/Besql/Bit.Besql/Bit.Besql.csproj +++ b/src/Besql/Bit.Besql/Bit.Besql.csproj @@ -6,8 +6,6 @@ net9.0;net8.0 enable enable - True - ..\..\AssemblyOriginatorKeyFile.snk @@ -22,15 +20,4 @@ - - - True - \ - - - True - \ - - - 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..aa548d55e5 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 || {}; -BitBesql.version = window['bit-besql version'] = '9.1.0-pre-08'; +var BitBesql = window.BitBesql || {}; +BitBesql.version = window['bit-besql version'] = '9.1.1'; -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..b3844a9b09 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,57 @@

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 = "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.WeatherForecasts.AddAsync(new() { Date = new DateTimeOffset(2024, 1, 4, 10, 10, 10, TimeSpan.Zero), Summary = "Test", - TemperatureC = 17 + TemperatureC = Random.Shared.Next(1, 30) }); await dbContext.SaveChangesAsync(); - forecasts!.Add(forecast.Entity); + forecastsCount += 2; + } + + private async Task DeleteSomeForecasts() + { + await using var dbContext = await DbContextFactory.CreateDbContextAsync(); + var deletedCount = await dbContext.WeatherForecasts + .Where(w => w.TemperatureC % 2 == 0) + .ExecuteDeleteAsync(); + deletedCount += await dbContext.WeatherForecasts + .Where(w => w.TemperatureC % 3 == 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 + diff --git a/src/Bit.Build.props b/src/Bit.Build.props index b723a0fc39..c7c8af3021 100644 --- a/src/Bit.Build.props +++ b/src/Bit.Build.props @@ -22,16 +22,16 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb $(MSBuildProjectName) MIT + PackageIcon.png + README.md https://github.com/bitfoundation/bitplatform - https://avatars.githubusercontent.com/u/22663390 - - 9.1.0 - - https://github.com/bitfoundation/bitplatform/releases/tag/v-$(ReleaseVersion)-pre-08 - $(ReleaseVersion)-pre-08 - $(ReleaseVersion).$([System.DateTime]::Now.ToString(HHmm)) + 9.1.1 + $(ReleaseVersion) + https://github.com/bitfoundation/bitplatform/releases/tag/v-$(ReleaseVersion) + $([System.String]::Copy($(ReleaseVersion)).Replace('-pre-', '.')) + true en @@ -42,4 +42,25 @@ bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + + + True + $([System.IO.Path]::GetFullPath($([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', 'AssemblyOriginatorKeyFile.snk'))\AssemblyOriginatorKeyFile.snk)) + + + + + True + \ + + + True + \ + + + True + \ + + + diff --git a/src/BlazorUI/Bit.BlazorUI.Assets/Bit.BlazorUI.Assets.csproj b/src/BlazorUI/Bit.BlazorUI.Assets/Bit.BlazorUI.Assets.csproj index ddbbc6edd9..3eded7bb03 100644 --- a/src/BlazorUI/Bit.BlazorUI.Assets/Bit.BlazorUI.Assets.csproj +++ b/src/BlazorUI/Bit.BlazorUI.Assets/Bit.BlazorUI.Assets.csproj @@ -10,8 +10,6 @@ BeforeBuildTasks; $(ResolveStaticWebAssetsInputsDependsOn) - True - ..\..\AssemblyOriginatorKeyFile.snk @@ -37,14 +35,6 @@ - - True - \ - - - True - \ - diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj b/src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj index f6cc045e49..b6ed4c0749 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj @@ -10,8 +10,6 @@ BeforeBuildTasks; $(ResolveStaticWebAssetsInputsDependsOn) - True - ..\..\AssemblyOriginatorKeyFile.snk @@ -75,14 +73,6 @@ - - True - \ - - - True - \ - diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/PdfReader/BitPdfReader.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/PdfReader/BitPdfReader.ts index 4e27a2b717..6735aeec8d 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/PdfReader/BitPdfReader.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/PdfReader/BitPdfReader.ts @@ -95,6 +95,8 @@ namespace BitBlazorUI { canvas = document.getElementById(`${config.id}-${pageNumber}`) as HTMLCanvasElement; } + if (!canvas) return; + const context = canvas.getContext('2d')!; canvas.width = viewport.width; canvas.height = viewport.height; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProPanel/BitProPanel.razor b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProPanel/BitProPanel.razor new file mode 100644 index 0000000000..b461e34683 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProPanel/BitProPanel.razor @@ -0,0 +1,70 @@ +@namespace Bit.BlazorUI +@inherits BitComponentBase + + + @if (Header is not null || HeaderText is not null || ShowCloseButton) + { +
+ @if (Header is not null) + { +
+ @Header +
+ } + else if (HeaderText is not null) + { +
+ @HeaderText +
+ } + + @if (ShowCloseButton) + { + + } +
+ } + +
+ @(Body ?? ChildContent) +
+ + @if (Footer is not null) + { +
+ @Footer +
+ } + else if (FooterText is not null) + { +
+ @FooterText +
+ } +
diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProPanel/BitProPanel.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProPanel/BitProPanel.razor.cs new file mode 100644 index 0000000000..cc7b077732 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProPanel/BitProPanel.razor.cs @@ -0,0 +1,151 @@ +namespace Bit.BlazorUI; + +public partial class BitProPanel : BitComponentBase +{ + /// + /// Enables the auto scrollbar toggle behavior of the panel. + /// + [Parameter] public bool AutoToggleScroll { get; set; } + + /// + /// The alias of the ChildContent. + /// + [Parameter] public RenderFragment? Body { get; set; } + + /// + /// Whether the panel can be dismissed by clicking outside of it on the overlay. + /// + [Parameter] public bool Blocking { get; set; } + + /// + /// The content of the panel. + /// + [Parameter] public RenderFragment? ChildContent { get; set; } + + /// + /// Custom CSS classes for different parts of the panel. + /// + [Parameter] public BitProPanelClassStyles? Classes { get; set; } + + /// + /// The template used to render the footer section of the panel. + /// + [Parameter] public RenderFragment? Footer { get; set; } + + /// + /// The text of the footer section of the panel. + /// + [Parameter] public string? FooterText { get; set; } + + /// + /// The template used to render the header section of the panel. + /// + [Parameter] public RenderFragment? Header { get; set; } + + /// + /// The text of the header section of the panel. + /// + [Parameter] public string? HeaderText { get; set; } + + /// + /// Determines the openness of the panel. + /// + [Parameter, TwoWayBound] + public bool IsOpen { get; set; } + + /// + /// Renders the overlay in full mode that gives it an opaque background. + /// + [Parameter] public bool ModeFull { get; set; } + + /// + /// Removes the overlay element of the panel. + /// + [Parameter] public bool Modeless { get; set; } + + /// + /// A callback function for when the Panel is dismissed. + /// + [Parameter] public EventCallback OnDismiss { get; set; } + + /// + /// The event callback for when the swipe action starts on the container of the panel. + /// + [Parameter] public EventCallback OnSwipeStart { get; set; } + + /// + /// The event callback for when the swipe action moves on the container of the panel. + /// + [Parameter] public EventCallback OnSwipeMove { get; set; } + + /// + /// The event callback for when the swipe action ends on the container of the panel. + /// + [Parameter] public EventCallback OnSwipeEnd { get; set; } + + /// + /// The position of the panel to show on the screen. + /// + [Parameter] public BitPanelPosition? Position { get; set; } + + /// + /// The value of the height or width (based on the position) of the Panel. + /// + [Parameter] public double? Size { get; set; } + + /// + /// Specifies the element selector for which the Panel disables its scroll if applicable. + /// + [Parameter] public string? ScrollerSelector { get; set; } + + /// + /// Shows the close button of the Panel. + /// + [Parameter] public bool ShowCloseButton { get; set; } + + /// + /// Custom CSS styles for different parts of the panel component. + /// + [Parameter] public BitProPanelClassStyles? Styles { get; set; } + + /// + /// The swiping point (difference percentage) based on the width of the panel container to trigger the close action (default is 0.25m). + /// + [Parameter] public decimal? SwipeTrigger { get; set; } + + + + public async Task Open() + { + if (await AssignIsOpen(true) is false) return; + + StateHasChanged(); + } + + public async Task Close() + { + if (await AssignIsOpen(false) is false) return; + + StateHasChanged(); + } + + + + protected override string RootElementClass => "bit-ppl"; + + protected override void RegisterCssClasses() + { + ClassBuilder.Register(() => ModeFull ? "bit-ppl-mfl" : string.Empty); + } + + + + private async Task ClosePanel(MouseEventArgs e) + { + if (IsEnabled is false) return; + + if (await AssignIsOpen(false) is false) return; + + await OnDismiss.InvokeAsync(e); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProPanel/BitProPanel.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProPanel/BitProPanel.scss new file mode 100644 index 0000000000..db747ffc91 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProPanel/BitProPanel.scss @@ -0,0 +1,71 @@ +@import '../../../Bit.BlazorUI/Styles/functions.scss'; + +.bit-ppl { + .bit-pnl-cnt { + display: flex; + flex-direction: column; + } +} + +.bit-ppl-mfl { + .bit-pnl-ovl { + background-color: $clr-bg-overlay; + } +} + +.bit-ppl-hcn { + top: 0; + z-index: 1; + display: flex; + position: sticky; + font-weight: 600; + color: $clr-fg-pri; + overflow-wrap: anywhere; + background-color: $clr-bg-pri; + padding: spacing(2) spacing(2) 0; +} + +.bit-ppl-hdr { + flex-grow: 1; + display: flex; + align-items: center; + font-size: spacing(2.5); +} + +.bit-ppl-cls { + display: flex; + cursor: pointer; + width: spacing(5); + height: spacing(5); + align-items: center; + justify-content: center; + font-size: spacing(1.75); + border-radius: spacing(0.25); + background-color: transparent; + + @media (hover: hover) { + &:hover { + color: $clr-fg-pri-hover; + background-color: $clr-bg-pri-hover; + } + } + + &:active { + color: $clr-fg-pri-active; + background-color: $clr-bg-pri-active; + } +} + +.bit-ppl-bdy { + flex-grow: 1; + overflow-y: auto; + padding: spacing(2); +} + +.bit-ppl-fcn { + bottom: 0; + z-index: 1; + position: sticky; + background-color: $clr-bg-pri; + padding: 0 spacing(2) spacing(2) spacing(2); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProPanel/BitProPanelClassStyles.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProPanel/BitProPanelClassStyles.cs new file mode 100644 index 0000000000..0ce332226a --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ProPanel/BitProPanelClassStyles.cs @@ -0,0 +1,34 @@ +namespace Bit.BlazorUI; + +public class BitProPanelClassStyles : BitPanelClassStyles +{ + /// + /// Custom CSS classes/styles for the header container of the BitProPanel. + /// + public string? HeaderContainer { get; set; } + + /// + /// Custom CSS classes/styles for the header of the BitProPanel. + /// + public string? Header { get; set; } + + /// + /// Custom CSS classes/styles for the close button of the BitProPanel. + /// + public string? CloseButton { get; set; } + + /// + /// Custom CSS classes/styles for the close button of the BitProPanel. + /// + public string? CloseIcon { get; set; } + + /// + /// Custom CSS classes/styles for the body of the BitProPanel. + /// + public string? Body { get; set; } + + /// + /// Custom CSS classes/styles for the footer of the BitProPanel. + /// + public string? Footer { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/IServiceCollectionExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/IServiceCollectionExtensions.cs index 108d64b637..0ae549d8eb 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/IServiceCollectionExtensions.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/IServiceCollectionExtensions.cs @@ -9,7 +9,7 @@ public static class IServiceCollectionExtensions /// Registers required services of the Extras package of the BitBlazorUI components. /// /// - /// Tries to register the services as singleton, but only for the services can be singleton (e.g. the services that do not use IJSRuntime). + /// Tries to register the services as singleton, but only for the services that can be singleton (e.g. the services that do not use IJSRuntime). /// public static IServiceCollection AddBitBlazorUIExtrasServices(this IServiceCollection services, bool trySingleton = false) { diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Styles/bit.blazorui.extras.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Styles/bit.blazorui.extras.scss index 66554e8edc..4d58f1782c 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Styles/bit.blazorui.extras.scss +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Styles/bit.blazorui.extras.scss @@ -1,2 +1,2 @@ -@import "fabric.mdl2.bit.blazoui.extras.scss"; -@import "components.scss"; +@import "components.scss"; +@import "fabric.mdl2.bit.blazoui.extras.scss"; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Styles/components.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Styles/components.scss index 0e9f9d3e91..ca46bd9aef 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Styles/components.scss +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Styles/components.scss @@ -1,4 +1,4 @@ @import "../Components/DataGrid/BitDataGrid.scss"; @import "../Components/DataGrid/Pagination/BitDataGridPaginator.scss"; - @import "../Components/PdfReader/BitPdfReader.scss"; +@import "../Components/ProPanel/BitProPanel.scss"; diff --git a/src/BlazorUI/Bit.BlazorUI.Icons/Bit.BlazorUI.Icons.csproj b/src/BlazorUI/Bit.BlazorUI.Icons/Bit.BlazorUI.Icons.csproj index d6d4fa27b7..b033e530c0 100644 --- a/src/BlazorUI/Bit.BlazorUI.Icons/Bit.BlazorUI.Icons.csproj +++ b/src/BlazorUI/Bit.BlazorUI.Icons/Bit.BlazorUI.Icons.csproj @@ -10,8 +10,6 @@ BeforeBuildTasks; $(ResolveStaticWebAssetsInputsDependsOn) - True - ..\..\AssemblyOriginatorKeyFile.snk @@ -40,14 +38,6 @@ - - True - \ - - - True - \ - diff --git a/src/BlazorUI/Bit.BlazorUI.SourceGenerators/Bit.BlazorUI.SourceGenerators.csproj b/src/BlazorUI/Bit.BlazorUI.SourceGenerators/Bit.BlazorUI.SourceGenerators.csproj index e357d464b3..6154f779a0 100644 --- a/src/BlazorUI/Bit.BlazorUI.SourceGenerators/Bit.BlazorUI.SourceGenerators.csproj +++ b/src/BlazorUI/Bit.BlazorUI.SourceGenerators/Bit.BlazorUI.SourceGenerators.csproj @@ -4,6 +4,7 @@ 13.0 enable true + False diff --git a/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj b/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj index f877b1856b..cb16dd5cbb 100644 --- a/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj +++ b/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj @@ -13,10 +13,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -24,7 +24,8 @@ - + + diff --git a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Extras/ProPanel/BitProPanelTests.cs b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Extras/ProPanel/BitProPanelTests.cs new file mode 100644 index 0000000000..be0b2ba6e8 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Extras/ProPanel/BitProPanelTests.cs @@ -0,0 +1,54 @@ +using Bunit; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bit.BlazorUI.Tests.Components.Extras.ProPanel; + +[TestClass] +public class BitProPanelTests : BunitTestContext +{ + [TestMethod] + public void BitProPanelContentTest() + { + var com = RenderComponent(parameters => + { + parameters.Add(p => p.IsOpen, true); + parameters.AddChildContent("
Test Content
"); + }); + + var elementContent = com.Find(".bit-ppl-bdy"); + + elementContent.MarkupMatches("
Test Content
"); + } + + [TestMethod] + public void BitProPanelFooterContentTest() + { + var footerContent = "
Test Footer Content
"; + + var com = RenderComponent(parameters => + { + parameters.Add(p => p.IsOpen, true); + parameters.Add(p => p.Footer, footerContent); + }); + + var elementContent = com.Find(".bit-ppl-fcn :first-child"); + + elementContent.MarkupMatches(footerContent); + } + + [TestMethod] + public void BitProPanelHeaderContentTest() + { + const string headerContent = "
Test Header Content
"; + + var com = RenderComponent(parameters => + { + parameters.Add(p => p.IsOpen, true); + parameters.Add(p => p.Header, headerContent); + }); + + var elementContent = com.Find(".bit-ppl-hcn :first-child :first-child"); + + elementContent.MarkupMatches(headerContent); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Inputs/Dropdown/BitDropdownTests.cs b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Inputs/Dropdown/BitDropdownTests.cs index 96a5108ba0..e198328c40 100644 --- a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Inputs/Dropdown/BitDropdownTests.cs +++ b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Inputs/Dropdown/BitDropdownTests.cs @@ -57,7 +57,7 @@ public void ResponsiveDropdownShouldTakeCorrectClassNameAndRenderElements(bool i //if (isResponsiveModeEnabled) //{ - // Assert.IsTrue(callout.ClassList.Contains("bit-drp-rsp")); + // Assert.IsTrue(callout.ClassList.Contains("bit-drp-res")); // var lblContainer = component.Find(".bit-drp-rlc"); // Assert.IsNotNull(lblContainer); diff --git a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Surfaces/Panel/BitPanelTests.cs b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Surfaces/Panel/BitPanelTests.cs index c2ef4de625..5477856812 100644 --- a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Surfaces/Panel/BitPanelTests.cs +++ b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Surfaces/Panel/BitPanelTests.cs @@ -1,5 +1,6 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using AngleSharp.Css.Dom; using Bunit; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Bit.BlazorUI.Tests.Components.Surfaces.Panel; @@ -12,42 +13,46 @@ public class BitPanelTests : BunitTestContext DataRow(false), DataRow(true) ] - public void BitPanelIsBlockingTest(bool isBlocking) + public void BitPanelBlockingTest(bool blocking) { var com = RenderComponent(parameters => { - parameters.Add(p => p.IsBlocking, isBlocking); + parameters.Add(p => p.Blocking, blocking); parameters.Add(p => p.IsOpen, isPanelOpen); parameters.Add(p => p.IsOpenChanged, HandleIsOpenChanged); }); - var bitPanel = com.FindAll(".bit-pnl"); - Assert.AreEqual(1, bitPanel.Count); + var container = com.Find(".bit-pnl-cnt"); + Assert.IsTrue(container.GetStyle().CssText.Contains("opacity: 1")); var overlayElement = com.Find(".bit-pnl-ovl"); overlayElement.Click(); - bitPanel = com.FindAll(".bit-pnl"); - Assert.AreEqual(isBlocking ? 1 : 0, bitPanel.Count); + container = com.Find(".bit-pnl-cnt"); + if (blocking) + { + Assert.IsTrue(container.GetStyle().CssText.Contains("opacity: 1")); + } + else + { + Assert.AreEqual("", container.GetStyle().CssText); + } } [DataTestMethod, DataRow(false), DataRow(true) ] - public void BitPanelIsModelessTest(bool isModeless) + public void BitPanelModelessTest(bool modeless) { var com = RenderComponent(parameters => { - parameters.Add(p => p.IsModeless, isModeless); + parameters.Add(p => p.Modeless, modeless); parameters.Add(p => p.IsOpen, true); }); - var element = com.Find(".bit-pnl"); - Assert.AreEqual(element.Attributes["aria-modal"].Value, (isModeless is false).ToString()); - var elementOverlay = com.FindAll(".bit-pnl-ovl"); - Assert.AreEqual(isModeless ? 0 : 1, elementOverlay.Count); + Assert.AreEqual(modeless ? 0 : 1, elementOverlay.Count); } [DataTestMethod, @@ -61,66 +66,8 @@ public void BitPanelIsOpenTest(bool isOpen) parameters.Add(p => p.IsOpen, isOpen); }); - var bitModel = com.FindAll(".bit-pnl"); - Assert.AreEqual(isOpen ? 1 : 0, bitModel.Count); - } - - [DataTestMethod, - DataRow(null), - DataRow(""), - DataRow("Test-S-A-Id") - ] - public void BitPanelSubtitleAriaIdTest(string subtitleAriaId) - { - var com = RenderComponent(parameters => - { - parameters.Add(p => p.SubtitleAriaId, subtitleAriaId); - parameters.Add(p => p.IsOpen, true); - }); - - var element = com.Find(".bit-pnl"); - - if (subtitleAriaId == null) - { - Assert.IsFalse(element.HasAttribute("aria-describedby")); - } - else if (subtitleAriaId == string.Empty) - { - Assert.AreEqual(element.Attributes["aria-describedby"].Value, string.Empty); - } - else - { - Assert.AreEqual(element.Attributes["aria-describedby"].Value, subtitleAriaId); - } - } - - [DataTestMethod, - DataRow(null), - DataRow(""), - DataRow("Test-T-A-Id") - ] - public void BitPanelTitleAriaIdTest(string titleAriaId) - { - var com = RenderComponent(parameters => - { - parameters.Add(p => p.TitleAriaId, titleAriaId); - parameters.Add(p => p.IsOpen, true); - }); - - var element = com.Find(".bit-pnl"); - - if (titleAriaId == null) - { - Assert.IsFalse(element.HasAttribute("aria-labelledby")); - } - else if (titleAriaId == string.Empty) - { - Assert.AreEqual(element.Attributes["aria-labelledby"].Value, string.Empty); - } - else - { - Assert.AreEqual(element.Attributes["aria-labelledby"].Value, titleAriaId); - } + var container = com.Find(".bit-pnl-cnt"); + Assert.AreEqual(isOpen, container.GetStyle().CssText.Contains("opacity: 1")); } [TestMethod] @@ -132,41 +79,9 @@ public void BitPanelContentTest() parameters.AddChildContent("
Test Content
"); }); - var elementContent = com.Find(".bit-pnl-bdy"); - - elementContent.MarkupMatches("
Test Content
"); - } - - [TestMethod] - public void BitPanelFooterContentTest() - { - var footerContent = "
Test Footer Content
"; - - var com = RenderComponent(parameters => - { - parameters.Add(p => p.IsOpen, true); - parameters.Add(p => p.FooterTemplate, footerContent); - }); - - var elementContent = com.Find(".bit-pnl-fcn :first-child"); - - elementContent.MarkupMatches(footerContent); - } - - [TestMethod] - public void BitPanelHeaderContentTest() - { - const string headerContent = "
Test Header Content
"; - - var com = RenderComponent(parameters => - { - parameters.Add(p => p.IsOpen, true); - parameters.Add(p => p.HeaderTemplate, headerContent); - }); - - var elementContent = com.Find(".bit-pnl-hcn :first-child"); + var elementContent = com.Find(".bit-pnl-cnt"); - elementContent.MarkupMatches(headerContent); + elementContent.MarkupMatches("
Test Content
"); } [TestMethod] @@ -178,14 +93,14 @@ public void BitPanelCloseWhenClickOutOfPanelTest() parameters.Add(p => p.IsOpenChanged, HandleIsOpenChanged); }); - var bitPanel = com.FindAll(".bit-pnl"); - Assert.AreEqual(1, bitPanel.Count); + var container = com.Find(".bit-pnl-cnt"); + Assert.IsTrue(container.GetStyle().CssText.Contains("opacity: 1")); var overlayElement = com.Find(".bit-pnl-ovl"); overlayElement.Click(); - bitPanel = com.FindAll(".bit-pnl"); - Assert.AreEqual(0, bitPanel.Count); + container = com.Find(".bit-pnl-cnt"); + Assert.AreEqual("", container.GetStyle().CssText); } [TestMethod] @@ -208,8 +123,8 @@ public void BitPanelOnDismissShouldWorkCorrect() } [DataTestMethod, - DataRow(BitPanelPosition.Right), - DataRow(BitPanelPosition.Left), + DataRow(BitPanelPosition.End), + DataRow(BitPanelPosition.Start), DataRow(BitPanelPosition.Top), DataRow(BitPanelPosition.Bottom), DataRow(null) @@ -229,12 +144,11 @@ public void BitPanelPositionTest(BitPanelPosition? position) var positionClass = position switch { - BitPanelPosition.Right => "bit-pnl-right", - BitPanelPosition.Left => "bit-pnl-left", + BitPanelPosition.End => "bit-pnl-end", + BitPanelPosition.Start => "bit-pnl-start", BitPanelPosition.Top => "bit-pnl-top", BitPanelPosition.Bottom => "bit-pnl-bottom", - - _ => "bit-pnl-right", + _ => "bit-pnl-end", }; Assert.IsTrue(PanelElement.ClassList.Contains(positionClass)); diff --git a/src/BlazorUI/Bit.BlazorUI/Bit.BlazorUI.csproj b/src/BlazorUI/Bit.BlazorUI/Bit.BlazorUI.csproj index 9438d817fd..b362585f92 100644 --- a/src/BlazorUI/Bit.BlazorUI/Bit.BlazorUI.csproj +++ b/src/BlazorUI/Bit.BlazorUI/Bit.BlazorUI.csproj @@ -10,14 +10,18 @@ BeforeBuildTasks; $(ResolveStaticWebAssetsInputsDependsOn) - True - ..\..\AssemblyOriginatorKeyFile.snk + + + + + + @@ -71,14 +75,6 @@ - - True - \ - - - True - \ - diff --git a/src/BlazorUI/Bit.BlazorUI/Components/BitComponentBase.cs b/src/BlazorUI/Bit.BlazorUI/Components/BitComponentBase.cs index c30cbcd9ff..746f61ef90 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/BitComponentBase.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/BitComponentBase.cs @@ -177,9 +177,9 @@ protected override void OnAfterRender(bool firstRender) protected abstract string RootElementClass { get; } - internal ElementClassBuilder ClassBuilder { get; private set; } = new ElementClassBuilder(); + protected ElementClassBuilder ClassBuilder { get; private set; } = new ElementClassBuilder(); - internal ElementStyleBuilder StyleBuilder { get; private set; } = new ElementStyleBuilder(); + protected ElementStyleBuilder StyleBuilder { get; private set; } = new ElementStyleBuilder(); protected virtual void RegisterCssStyles() { } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanelPosition.cs b/src/BlazorUI/Bit.BlazorUI/Components/BitPanelPosition.cs similarity index 79% rename from src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanelPosition.cs rename to src/BlazorUI/Bit.BlazorUI/Components/BitPanelPosition.cs index aad1844ab2..ee39658b77 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Surfaces/Panel/BitPanelPosition.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/BitPanelPosition.cs @@ -1,8 +1,9 @@ namespace Bit.BlazorUI; + public enum BitPanelPosition { - Right, - Left, + Start, + End, Top, Bottom, } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/BitResponsiveMode.cs b/src/BlazorUI/Bit.BlazorUI/Components/BitResponsiveMode.cs index e1b67a3c45..ea9c127a35 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/BitResponsiveMode.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/BitResponsiveMode.cs @@ -15,5 +15,10 @@ public enum BitResponsiveMode /// /// Enables the top responsive mode. /// - Top + Top, + + /// + /// Enables the bottom responsive mode. + /// + Bottom } 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 5cf6327f0a..b0fe05e744 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.razor.cs @@ -33,7 +33,7 @@ public partial class BitButton : BitComponentBase [Parameter] public bool AriaHidden { get; set; } /// - /// If true, shows the loading state while the OnClick event is in progress. + /// If true, enters the loading state automatically while awaiting the OnClick event and prevents subsequent clicks by default. /// [Parameter] public bool AutoLoading { get; set; } @@ -134,9 +134,9 @@ public partial class BitButton : BitComponentBase [Parameter] public RenderFragment? LoadingTemplate { get; set; } /// - /// The callback for the click event of the button. + /// The callback for the click event of the button with a bool argument passing the current loading state. /// - [Parameter] public EventCallback OnClick { get; set; } + [Parameter] public EventCallback OnClick { get; set; } /// /// The content of the primary section of the button (alias of the ChildContent). @@ -144,6 +144,11 @@ public partial class BitButton : BitComponentBase [Parameter, ResetClassBuilder] public RenderFragment? PrimaryTemplate { get; set; } + /// + /// Enables re-clicking in loading state when AutoLoading is enabled. + /// + [Parameter] public bool Reclickable { get; set; } + /// /// Reverses the positions of the icon and the main content of the button. /// @@ -306,15 +311,21 @@ private string GetLabelPositionClass() private async Task HandleOnClick(MouseEventArgs e) { if (IsEnabled is false) return; + if (AutoLoading && IsLoading && Reclickable is false) return; + + var isLoading = IsLoading; if (AutoLoading) { if (await AssignIsLoading(true) is false) return; } - await OnClick.InvokeAsync(e); + await OnClick.InvokeAsync(isLoading); - await AssignIsLoading(false); + if (AutoLoading) + { + await AssignIsLoading(false); + } } private void OnSetHrefAndRel() diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.razor b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.razor index 036addc7c6..34a0df0ad6 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.razor +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.razor @@ -21,8 +21,9 @@ tabindex="@(isEnabled ? 0 : -1)" disabled="@(isEnabled is false)" aria-disabled="@(isEnabled is false)" + title="@GetItemTitle(item)" style="@GetStyle(item)" - class="bit-btg-itm @GetClass(item)"> + class="@GetItemClass(item)"> @if (template is not null) { @template(item) @@ -33,12 +34,17 @@ } else { - var iconName = GetIconName(item); + var iconName = GetItemIconName(item); @if (iconName.HasValue()) { } - @GetText(item) + + var text = GetItemText(item); + if (text.HasValue()) + { + @text + } } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.razor.cs index 150b45e63e..73b9ff5894 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.razor.cs @@ -4,11 +4,11 @@ namespace Bit.BlazorUI; public partial class BitButtonGroup : BitComponentBase where TItem : class { + private TItem? _toggleItem; private List _items = []; private IEnumerable _oldItems = default!; - /// /// The EditContext, which is set if the button is inside an /// @@ -19,9 +19,7 @@ public partial class BitButtonGroup : BitComponentBase where TItem : clas /// /// The content of the BitButtonGroup, that are BitButtonGroupOption components. /// - [Parameter] - [CallOnSet(nameof(OnSetChildContentAndItems))] - public RenderFragment? ChildContent { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } /// /// Defines the general colors available in the bit BlazorUI. @@ -29,12 +27,15 @@ public partial class BitButtonGroup : BitComponentBase where TItem : clas [Parameter, ResetClassBuilder] public BitColor? Color { get; set; } + /// + /// Determines that only the icon should be rendered. + /// + [Parameter] public bool IconOnly { get; set; } + /// /// List of Item, each of which can be a button with different action in the ButtonGroup. /// - [Parameter] - [CallOnSet(nameof(OnSetChildContentAndItems))] - public IEnumerable Items { get; set; } = []; + [Parameter] public IEnumerable Items { get; set; } = []; /// /// The content inside the item can be customized. @@ -62,6 +63,11 @@ public partial class BitButtonGroup : BitComponentBase where TItem : clas [Parameter, ResetClassBuilder] public BitSize? Size { get; set; } + /// + /// Display ButtonGroup with toggle mode enabled for each button. + /// + [Parameter] public bool Toggled { get; set; } + /// /// The visual variant of the button group. /// @@ -135,7 +141,19 @@ protected override void RegisterCssClasses() _ => "bit-btg-md" }); - ClassBuilder.Register(() => Vertical ? "bit-btg-vrt" : ""); + ClassBuilder.Register(() => Vertical ? "bit-btg-vrt" : string.Empty); + } + + protected override void OnParametersSet() + { + base.OnParametersSet(); + + if (ChildContent is not null || Items is null || Items.Any() is false) return; + + if (_oldItems is not null && Items.SequenceEqual(_oldItems)) return; + + _oldItems = Items; + _items = [.. Items]; } @@ -167,15 +185,118 @@ private async Task HandleOnItemClick(TItem item) item.GetValueFromProperty?>(NameSelectors.OnClick.Name)?.Invoke(item); } } + + if (Toggled) + { + if (_toggleItem == item) + { + _toggleItem = null; + } + else + { + _toggleItem = item; + } + } } - private void OnSetChildContentAndItems() + private string? GetItemClass(TItem? item) { - if (ChildContent is not null) return; - if (Items.Any() is false || Items == _oldItems) return; + List classes = ["bit-btg-itm"]; - _oldItems = Items; - _items = Items.ToList(); + if (GetReversedIcon(item)) + { + classes.Add("bit-btg-rvi"); + } + + if (_toggleItem == item) + { + classes.Add("bit-btg-chk"); + } + + var classItem = GetClass(item); + if (classItem.HasValue()) + { + classes.Add(classItem!); + } + + return string.Join(' ', classes); + } + + private string? GetItemText(TItem? item) + { + if (IconOnly) return null; + + if (Toggled) + { + if (_toggleItem == item) + { + var onText = GetOnText(item); + if (onText.HasValue()) + { + return onText; + } + } + else + { + var offText = GetOffText(item); + if (offText.HasValue()) + { + return offText; + } + } + } + + return GetText(item); + } + + private string? GetItemTitle(TItem? item) + { + if (Toggled) + { + if (_toggleItem == item) + { + var onTitle = GetOnTitle(item); + if (onTitle.HasValue()) + { + return onTitle; + } + } + else + { + var offTitle = GetOffTitle(item); + if (offTitle.HasValue()) + { + return offTitle; + } + } + } + + return GetTitle(item); + } + + private string? GetItemIconName(TItem? item) + { + if (Toggled) + { + if (_toggleItem == item) + { + var onIconName = GetOnIconName(item); + if (onIconName.HasValue()) + { + return onIconName; + } + } + else + { + var offIconName = GetOffIconName(item); + if (offIconName.HasValue()) + { + return offIconName; + } + } + } + + return GetIconName(item); } private string? GetClass(TItem? item) @@ -226,6 +347,54 @@ private void OnSetChildContentAndItems() return item.GetValueFromProperty(NameSelectors.IconName.Name); } + private string? GetOnIconName(TItem? item) + { + if (item is null) return null; + + if (item is BitButtonGroupItem buttonGroupItem) + { + return buttonGroupItem.OnIconName; + } + + if (item is BitButtonGroupOption buttonGroupOption) + { + return buttonGroupOption.OnIconName; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.OnIconName.Selector is not null) + { + return NameSelectors.OnIconName.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.OnIconName.Name); + } + + private string? GetOffIconName(TItem? item) + { + if (item is null) return null; + + if (item is BitButtonGroupItem buttonGroupItem) + { + return buttonGroupItem.OffIconName; + } + + if (item is BitButtonGroupOption buttonGroupOption) + { + return buttonGroupOption.OffIconName; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.OffIconName.Selector is not null) + { + return NameSelectors.OffIconName.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.OffIconName.Name); + } + private bool GetIsEnabled(TItem? item) { if (item is null) return false; @@ -321,4 +490,148 @@ private bool GetIsEnabled(TItem? item) return item.GetValueFromProperty(NameSelectors.Text.Name); } + + private string? GetOnText(TItem? item) + { + if (item is null) return null; + + if (item is BitButtonGroupItem buttonGroupItem) + { + return buttonGroupItem.OnText; + } + + if (item is BitButtonGroupOption buttonGroupOption) + { + return buttonGroupOption.OnText; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.OnText.Selector is not null) + { + return NameSelectors.OnText.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.OnText.Name); + } + + private string? GetOffText(TItem? item) + { + if (item is null) return null; + + if (item is BitButtonGroupItem buttonGroupItem) + { + return buttonGroupItem.OffText; + } + + if (item is BitButtonGroupOption buttonGroupOption) + { + return buttonGroupOption.OffText; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.OffText.Selector is not null) + { + return NameSelectors.OffText.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.OffText.Name); + } + + private string? GetTitle(TItem? item) + { + if (item is null) return null; + + if (item is BitButtonGroupItem buttonGroupItem) + { + return buttonGroupItem.Title; + } + + if (item is BitButtonGroupOption buttonGroupOption) + { + return buttonGroupOption.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? GetOnTitle(TItem? item) + { + if (item is null) return null; + + if (item is BitButtonGroupItem buttonGroupItem) + { + return buttonGroupItem.OnTitle; + } + + if (item is BitButtonGroupOption buttonGroupOption) + { + return buttonGroupOption.OnTitle; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.OnTitle.Selector is not null) + { + return NameSelectors.OnTitle.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.OnTitle.Name); + } + + private string? GetOffTitle(TItem? item) + { + if (item is null) return null; + + if (item is BitButtonGroupItem buttonGroupItem) + { + return buttonGroupItem.OffTitle; + } + + if (item is BitButtonGroupOption buttonGroupOption) + { + return buttonGroupOption.OffTitle; + } + + if (NameSelectors is null) return null; + + if (NameSelectors.OffTitle.Selector is not null) + { + return NameSelectors.OffTitle.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.OffTitle.Name); + } + + private bool GetReversedIcon(TItem? item) + { + if (item is null) return false; + + if (item is BitButtonGroupItem buttonGroupItem) + { + return buttonGroupItem.ReversedIcon; + } + + if (item is BitButtonGroupOption buttonGroupOption) + { + return buttonGroupOption.ReversedIcon; + } + + if (NameSelectors is null) return false; + + if (NameSelectors.ReversedIcon.Selector is not null) + { + return NameSelectors.ReversedIcon.Selector!(item); + } + + return item.GetValueFromProperty(NameSelectors.ReversedIcon.Name, false); + } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.scss b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.scss index a630e467f4..9a55b86981 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.scss @@ -34,6 +34,7 @@ line-height: inherit; text-decoration: none; box-sizing: border-box; + justify-content: center; font-family: $tg-font-family; font-weight: $tg-font-weight; border-style: $shp-border-style; @@ -69,6 +70,10 @@ } } +.bit-btg-rvi { + flex-direction: row-reverse; +} + .bit-btg-btx { white-space: nowrap; text-overflow: ellipsis; @@ -131,12 +136,33 @@ } } +.bit-btg-chk { + color: var(--bit-btg-clr-txt); + border-color: var(--bit-btg-clr-dark); + background-color: var(--bit-btg-clr-dark); + + @media (hover: hover) { + &:hover { + border-color: var(--bit-btg-clr-dark-hover); + background-color: var(--bit-btg-clr-dark-hover); + } + } + + &:active { + border-color: var(--bit-btg-clr-dark-active); + background-color: var(--bit-btg-clr-dark-active); + } +} + .bit-btg-pri { --bit-btg-clr: #{$clr-pri}; --bit-btg-clr-txt: #{$clr-pri-text}; --bit-btg-clr-brd: #{$clr-pri-dark}; --bit-btg-clr-hover: #{$clr-pri-hover}; --bit-btg-clr-active: #{$clr-pri-active}; + --bit-btg-clr-dark: #{$clr-pri-dark}; + --bit-btg-clr-dark-hover: #{$clr-pri-dark-hover}; + --bit-btg-clr-dark-active: #{$clr-pri-dark-active}; } .bit-btg-sec { @@ -145,6 +171,9 @@ --bit-btg-clr-brd: #{$clr-sec-dark}; --bit-btg-clr-hover: #{$clr-sec-hover}; --bit-btg-clr-active: #{$clr-sec-active}; + --bit-btg-clr-dark: #{$clr-sec-dark}; + --bit-btg-clr-dark-hover: #{$clr-sec-dark-hover}; + --bit-btg-clr-dark-active: #{$clr-sec-dark-active}; } .bit-btg-ter { @@ -153,6 +182,9 @@ --bit-btg-clr-brd: #{$clr-ter-dark}; --bit-btg-clr-hover: #{$clr-ter-hover}; --bit-btg-clr-active: #{$clr-ter-active}; + --bit-btg-clr-dark: #{$clr-ter-dark}; + --bit-btg-clr-dark-hover: #{$clr-ter-dark-hover}; + --bit-btg-clr-dark-active: #{$clr-ter-dark-active}; } .bit-btg-inf { @@ -161,6 +193,9 @@ --bit-btg-clr-brd: #{$clr-inf-dark}; --bit-btg-clr-hover: #{$clr-inf-hover}; --bit-btg-clr-active: #{$clr-inf-active}; + --bit-btg-clr-dark: #{$clr-inf-dark}; + --bit-btg-clr-dark-hover: #{$clr-inf-dark-hover}; + --bit-btg-clr-dark-active: #{$clr-inf-dark-active}; } .bit-btg-suc { @@ -169,6 +204,9 @@ --bit-btg-clr-brd: #{$clr-suc-dark}; --bit-btg-clr-hover: #{$clr-suc-hover}; --bit-btg-clr-active: #{$clr-suc-active}; + --bit-btg-clr-dark: #{$clr-suc-dark}; + --bit-btg-clr-dark-hover: #{$clr-suc-dark-hover}; + --bit-btg-clr-dark-active: #{$clr-suc-dark-active}; } .bit-btg-wrn { @@ -177,6 +215,9 @@ --bit-btg-clr-brd: #{$clr-wrn-dark}; --bit-btg-clr-hover: #{$clr-wrn-hover}; --bit-btg-clr-active: #{$clr-wrn-active}; + --bit-btg-clr-dark: #{$clr-wrn-dark}; + --bit-btg-clr-dark-hover: #{$clr-wrn-dark-hover}; + --bit-btg-clr-dark-active: #{$clr-wrn-dark-active}; } .bit-btg-swr { @@ -185,6 +226,9 @@ --bit-btg-clr-brd: #{$clr-swr-dark}; --bit-btg-clr-hover: #{$clr-swr-hover}; --bit-btg-clr-active: #{$clr-swr-active}; + --bit-btg-clr-dark: #{$clr-swr-dark}; + --bit-btg-clr-dark-hover: #{$clr-swr-dark-hover}; + --bit-btg-clr-dark-active: #{$clr-swr-dark-active}; } .bit-btg-err { @@ -193,6 +237,9 @@ --bit-btg-clr-brd: #{$clr-err-dark}; --bit-btg-clr-hover: #{$clr-err-hover}; --bit-btg-clr-active: #{$clr-err-active}; + --bit-btg-clr-dark: #{$clr-err-dark}; + --bit-btg-clr-dark-hover: #{$clr-err-dark-hover}; + --bit-btg-clr-dark-active: #{$clr-err-dark-active}; } @@ -202,6 +249,9 @@ --bit-btg-clr-brd: #{$clr-bg-pri}; --bit-btg-clr-hover: #{$clr-bg-pri-hover}; --bit-btg-clr-active: #{$clr-bg-pri-active}; + --bit-btg-clr-dark: #{$clr-bg-pri}; + --bit-btg-clr-dark-hover: #{$clr-bg-pri-hover}; + --bit-btg-clr-dark-active: #{$clr-bg-pri-active}; } .bit-btg-sbg { @@ -210,6 +260,9 @@ --bit-btg-clr-brd: #{$clr-bg-sec}; --bit-btg-clr-hover: #{$clr-bg-sec-hover}; --bit-btg-clr-active: #{$clr-bg-sec-active}; + --bit-btg-clr-dark: #{$clr-bg-sec}; + --bit-btg-clr-dark-hover: #{$clr-bg-sec-hover}; + --bit-btg-clr-dark-active: #{$clr-bg-sec-active}; } .bit-btg-tbg { @@ -218,6 +271,9 @@ --bit-btg-clr-brd: #{$clr-bg-ter}; --bit-btg-clr-hover: #{$clr-bg-ter-hover}; --bit-btg-clr-active: #{$clr-bg-ter-active}; + --bit-btg-clr-dark: #{$clr-bg-ter}; + --bit-btg-clr-dark-hover: #{$clr-bg-ter-hover}; + --bit-btg-clr-dark-active: #{$clr-bg-ter-active}; } .bit-btg-pfg { @@ -226,6 +282,9 @@ --bit-btg-clr-brd: #{$clr-fg-pri}; --bit-btg-clr-hover: #{$clr-fg-pri-hover}; --bit-btg-clr-active: #{$clr-fg-pri-active}; + --bit-btg-clr-dark: #{$clr-fg-pri}; + --bit-btg-clr-dark-hover: #{$clr-fg-pri-hover}; + --bit-btg-clr-dark-active: #{$clr-fg-pri-active}; } .bit-btg-sfg { @@ -234,6 +293,9 @@ --bit-btg-clr-brd: #{$clr-fg-sec}; --bit-btg-clr-hover: #{$clr-fg-sec-hover}; --bit-btg-clr-active: #{$clr-fg-sec-active}; + --bit-btg-clr-dark: #{$clr-fg-sec}; + --bit-btg-clr-dark-hover: #{$clr-fg-sec-hover}; + --bit-btg-clr-dark-active: #{$clr-fg-sec-active}; } .bit-btg-tfg { @@ -242,6 +304,9 @@ --bit-btg-clr-brd: #{$clr-fg-ter}; --bit-btg-clr-hover: #{$clr-fg-ter-hover}; --bit-btg-clr-active: #{$clr-fg-ter-active}; + --bit-btg-clr-dark: #{$clr-fg-ter}; + --bit-btg-clr-dark-hover: #{$clr-fg-ter-hover}; + --bit-btg-clr-dark-active: #{$clr-fg-ter-active}; } .bit-btg-pbr { @@ -250,6 +315,9 @@ --bit-btg-clr-brd: #{$clr-brd-pri}; --bit-btg-clr-hover: #{$clr-brd-pri-hover}; --bit-btg-clr-active: #{$clr-brd-pri-active}; + --bit-btg-clr-dark: #{$clr-brd-pri}; + --bit-btg-clr-dark-hover: #{$clr-brd-pri-hover}; + --bit-btg-clr-dark-active: #{$clr-brd-pri-active}; } .bit-btg-sbr { @@ -258,6 +326,9 @@ --bit-btg-clr-brd: #{$clr-brd-sec}; --bit-btg-clr-hover: #{$clr-brd-sec-hover}; --bit-btg-clr-active: #{$clr-brd-sec-active}; + --bit-btg-clr-dark: #{$clr-brd-sec}; + --bit-btg-clr-dark-hover: #{$clr-brd-sec-hover}; + --bit-btg-clr-dark-active: #{$clr-brd-sec-active}; } .bit-btg-tbr { @@ -266,6 +337,9 @@ --bit-btg-clr-brd: #{$clr-brd-ter}; --bit-btg-clr-hover: #{$clr-brd-ter-hover}; --bit-btg-clr-active: #{$clr-brd-ter-active}; + --bit-btg-clr-dark: #{$clr-brd-ter}; + --bit-btg-clr-dark-hover: #{$clr-brd-ter-hover}; + --bit-btg-clr-dark-active: #{$clr-brd-ter-active}; } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroupItem.cs b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroupItem.cs index afa3ee18bd..0311d50980 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroupItem.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroupItem.cs @@ -23,11 +23,46 @@ public class BitButtonGroupItem /// public string? Key { get; set; } + /// + /// The icon of the item when it is not checked in toggle mode. + /// + [Parameter] public string? OffIconName { get; set; } + + /// + /// The text of the item when it is not checked in toggle mode. + /// + [Parameter] public string? OffText { get; set; } + + /// + /// The title of the item when it is not checked in toggle mode. + /// + [Parameter] public string? OffTitle { get; set; } + + /// + /// The icon of the item when it is checked in toggle mode. + /// + [Parameter] public string? OnIconName { get; set; } + + /// + /// The text of the item when it is checked in toggle mode. + /// + [Parameter] public string? OnText { get; set; } + + /// + /// The title of the item when it is checked in toggle mode. + /// + [Parameter] public string? OnTitle { get; set; } + /// /// Click event handler of the item. /// public Action? OnClick { get; set; } + /// + /// Reverses the positions of the icon and the main content of the item. + /// + [Parameter] public bool ReversedIcon { get; set; } + /// /// The custom value for the style attribute of the item. /// @@ -42,4 +77,9 @@ public class BitButtonGroupItem /// Text to render in the item. /// public string? Text { get; set; } + + /// + /// Title to render in the item. + /// + public string? Title { get; set; } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroupNameSelectors.cs b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroupNameSelectors.cs index fe1caf82d6..75cd27bc93 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroupNameSelectors.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroupNameSelectors.cs @@ -22,11 +22,46 @@ public class BitButtonGroupNameSelectors /// public BitNameSelectorPair Key { get; set; } = new(nameof(BitButtonGroupItem.Key)); + /// + /// OffIconName field name and selector of the custom input class. + /// + public BitNameSelectorPair OffIconName { get; set; } = new(nameof(BitButtonGroupItem.OffIconName)); + + /// + /// OffText field name and selector of the custom input class. + /// + public BitNameSelectorPair OffText { get; set; } = new(nameof(BitButtonGroupItem.OffText)); + + /// + /// OffTitle field name and selector of the custom input class. + /// + public BitNameSelectorPair OffTitle { get; set; } = new(nameof(BitButtonGroupItem.OffTitle)); + + /// + /// OnIconName field name and selector of the custom input class. + /// + public BitNameSelectorPair OnIconName { get; set; } = new(nameof(BitButtonGroupItem.OnIconName)); + + /// + /// OnText field name and selector of the custom input class. + /// + public BitNameSelectorPair OnText { get; set; } = new(nameof(BitButtonGroupItem.OnText)); + + /// + /// OnTitle field name and selector of the custom input class. + /// + public BitNameSelectorPair OnTitle { get; set; } = new(nameof(BitButtonGroupItem.OnTitle)); + /// /// OnClick field name and selector of the custom input class. /// public BitNameSelectorPair?> OnClick { get; set; } = new(nameof(BitButtonGroupItem.OnClick)); + /// + /// ReversedIcon field name and selector of the custom input class. + /// + public BitNameSelectorPair ReversedIcon { get; set; } = new(nameof(BitButtonGroupItem.ReversedIcon)); + /// /// The CSS Style field name and selector of the custom input class. /// @@ -41,4 +76,9 @@ public class BitButtonGroupNameSelectors /// Text field name and selector of the custom input class. /// public BitNameSelectorPair Text { get; set; } = new(nameof(BitButtonGroupItem.Text)); + + /// + /// Title field name and selector of the custom input class. + /// + public BitNameSelectorPair Title { get; set; } = new(nameof(BitButtonGroupItem.Title)); } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroupOption.cs b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroupOption.cs index ce48fed62f..2380942365 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroupOption.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroupOption.cs @@ -28,11 +28,46 @@ public partial class BitButtonGroupOption : ComponentBase, IDisposable /// [Parameter] public string? Key { get; set; } + /// + /// The icon of the option when it is not checked in toggle mode. + /// + [Parameter] public string? OffIconName { get; set; } + + /// + /// The text of the option when it is not checked in toggle mode. + /// + [Parameter] public string? OffText { get; set; } + + /// + /// The title of the option when it is not checked in toggle mode. + /// + [Parameter] public string? OffTitle { get; set; } + + /// + /// The icon of the option when it is checked in toggle mode. + /// + [Parameter] public string? OnIconName { get; set; } + + /// + /// The text of the option when it is checked in toggle mode. + /// + [Parameter] public string? OnText { get; set; } + + /// + /// The title of the option when it is checked in toggle mode. + /// + [Parameter] public string? OnTitle { get; set; } + /// /// Click event handler of the option. /// [Parameter] public EventCallback OnClick { get; set; } + /// + /// Reverses the positions of the icon and the main content of the option. + /// + [Parameter] public bool ReversedIcon { get; set; } + /// /// The custom value for the style attribute of the option. /// @@ -48,6 +83,11 @@ public partial class BitButtonGroupOption : ComponentBase, IDisposable /// [Parameter] public string? Text { get; set; } + /// + /// Title to render in the option + /// + [Parameter] public string? Title { get; set; } + protected override async Task OnInitializedAsync() 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">