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 4f26fb8605..0e20c83b5a 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,25 +28,26 @@ 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 --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 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.API_SERVER_ADDRESS }} - GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} WebAppRender.BlazorMode: BlazorWebAssembly - ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} + 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 }} - uses: actions/setup-node@v4 with: @@ -60,24 +60,13 @@ jobs: run: dotnet build AdminPanel/src/Client/AdminPanel.Client.Core/AdminPanel.Client.Core.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - name: Publish - run: dotnet publish AdminPanel/src/Server/AdminPanel.Server.Api/AdminPanel.Server.Api.csproj -c Release -p:PwaEnabled=true --self-contained -r linux-x64 -o ${{env.DOTNET_ROOT}}/server -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" + run: dotnet publish AdminPanel/src/Server/AdminPanel.Server.Web/AdminPanel.Server.Web.csproj -c Release -p:PwaEnabled=true --self-contained -r linux-x64 -o ${{env.DOTNET_ROOT}}/server -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" - name: Upload server artifact uses: actions/upload-artifact@v4 with: name: server-bundle path: ${{env.DOTNET_ROOT}}/server - - - name: Publish adminpanel blazor wasm standalone - run: | - sed -i 's/adminpanel.bitplatform.dev/adminpanel-api.bitplatform.dev/g' AdminPanel/src/Client/AdminPanel.Client.Web/wwwroot/index.html - dotnet publish AdminPanel/src/Client/AdminPanel.Client.Web/AdminPanel.Client.Web.csproj -c Release -p:PwaEnabled=true -o ${{env.DOTNET_ROOT}}/static -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" - - - name: Upload static artifact - uses: actions/upload-artifact@v4 - with: - name: static-bundle - path: ${{env.DOTNET_ROOT}}/static include-hidden-files: true # Required for wwwroot/.well-known folder deploy_api_blazor: @@ -106,6 +95,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 @@ -133,7 +125,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: @@ -144,27 +136,28 @@ jobs: cd src\Templates\Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ..\..\..\ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --windows --appInsights --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --offlineDb --framework net9.0 + cd ..\..\..\ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --windows --appInsights --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 ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - + - name: Generate CSS/JS files run: dotnet build AdminPanel\src\Client\AdminPanel.Client.Core\AdminPanel.Client.Core.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - name: Publish run: | cd AdminPanel\src\Client\AdminPanel.Client.Windows\ - dotnet publish AdminPanel.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:CompressionEnabled=false + dotnet publish AdminPanel.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --self-contained dotnet tool restore - dotnet vpk pack -u AdminPanel.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e AdminPanel.Client.Windows.exe -r win-x86 --framework net9.0-x86-desktop,webview2 --icon .\wwwroot\favicon.ico --packTitle 'AdminPanel' + dotnet vpk pack -u AdminPanel.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e AdminPanel.Client.Windows.exe -r win-x86 --framework webview2 --icon .\wwwroot\favicon.ico --packTitle 'AdminPanel' - name: Upload artifact uses: actions/upload-artifact@v4 @@ -184,14 +177,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 --appCenter --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 + cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --appInsights --sentry --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 - uses: actions/setup-node@v4 with: @@ -216,14 +209,11 @@ 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 }} - - name: Set app center secret - run: | - sed -i 's/appCenterSecret = null;/appCenterSecret = "ea9b98ea-93a0-48c7-982a-0a72f4ad6d04";/g' AdminPanel/src/Client/AdminPanel.Client.Maui/MauiProgram.cs - - name: Install maui run: cd src && dotnet workload install maui-android @@ -235,8 +225,8 @@ jobs: dotnet build AdminPanel/src/Client/AdminPanel.Client.Core/AdminPanel.Client.Core.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release dotnet build AdminPanel/src/Client/AdminPanel.Client.Maui/AdminPanel.Client.Maui.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - - name: Build aab - run: dotnet publish AdminPanel/src/Client/AdminPanel.Client.Maui/AdminPanel.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="AdminPanel.keystore" -p:AndroidSigningKeyAlias=bitplatform -p:AndroidSigningKeyPass="${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}" -p:AndroidSigningStorePass="${{ secrets.ANDROID_RELEASE_SIGNING_PASSWORD }}" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="AdminPanel" -p:ApplicationId="com.bitplatform.AdminPanel.Template" -p:CompressionEnabled=false -f net9.0-android + - name: Publish aab + run: dotnet publish AdminPanel/src/Client/AdminPanel.Client.Maui/AdminPanel.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="AdminPanel.keystore" -p:AndroidSigningKeyAlias=bitplatform -p:AndroidSigningKeyPass="${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}" -p:AndroidSigningStorePass="${{ secrets.ANDROID_RELEASE_SIGNING_PASSWORD }}" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="AdminPanel" -p:ApplicationId="com.bitplatform.AdminPanel.Template" -f net9.0-android - name: Upload artifact uses: actions/upload-artifact@v4 @@ -256,11 +246,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.0' + xcode-version: '16.2' - uses: actions/setup-node@v4 with: @@ -271,21 +261,18 @@ jobs: cd src/Templates/Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --appInsights --appCenter --serverUrl ${{ env.WEB_SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net9.0 + cd ../../../ && dotnet new bit-bp --name AdminPanel --database PostgreSQL --sample Admin --appInsights --sentry --serverUrl ${{ env.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 }} - - name: Set app center secret - run: | - brew install gnu-sed && gsed -i 's/appCenterSecret = null;/appCenterSecret = "0bc0d910-dc84-4887-a3a0-eee6b1b55797";/g' AdminPanel/src/Client/AdminPanel.Client.Maui/MauiProgram.cs - - name: Install maui run: cd src && dotnet workload install maui @@ -309,7 +296,7 @@ jobs: dotnet build AdminPanel/src/Client/AdminPanel.Client.Maui/AdminPanel.Client.Maui.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - name: Build ipa - run: dotnet publish AdminPanel/src/Client/AdminPanel.Client.Maui/AdminPanel.Client.Maui.csproj -p:RuntimeIdentifier=ios-arm64 -c Release -p:ArchiveOnBuild=true -p:CodesignKey="iPhone Distribution" -p:CodesignProvision="AdminPanel" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="AdminPanel" -p:ApplicationId="com.bitplatform.AdminPanel.Template" -p:CompressionEnabled=false -f net9.0-ios + run: dotnet publish AdminPanel/src/Client/AdminPanel.Client.Maui/AdminPanel.Client.Maui.csproj -p:RuntimeIdentifier=ios-arm64 -c Release -p:ArchiveOnBuild=true -p:CodesignKey="iPhone Distribution" -p:CodesignProvision="AdminPanel" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="AdminPanel" -p:ApplicationId="com.bitplatform.AdminPanel.Template" -f net9.0-ios - name: Upload artifact uses: actions/upload-artifact@v4 diff --git a/.github/workflows/bit.full.ci.yml b/.github/workflows/bit.full.ci.yml index f6799fe9ad..aa1c90c845 100644 --- a/.github/workflows/bit.full.ci.yml +++ b/.github/workflows/bit.full.ci.yml @@ -1,4 +1,4 @@ -name: bit platform full CI +name: bit platform full CI on: workflow_dispatch: @@ -62,7 +62,7 @@ jobs: dotnet new bit-bp --name SimpleTest --database Sqlite --framework net8.0 cd SimpleTest/src/Server/SimpleTest.Server.Api/ dotnet tool restore - dotnet ef migrations add InitialMigration + dotnet ef migrations add InitialMigration --verbose dotnet ef database update cd ../../Tests dotnet build @@ -83,7 +83,7 @@ jobs: dotnet new bit-bp --name TestSqlite --database Sqlite --advancedTests --framework net9.0 cd TestSqlite/src/Server/TestSqlite.Server.Api/ dotnet tool restore - dotnet ef migrations add InitialMigration + dotnet ef migrations add InitialMigration --verbose dotnet ef database update cd ../../Tests dotnet build @@ -105,7 +105,7 @@ jobs: dotnet new bit-bp --name TestSqlServer --database SqlServer --advancedTests --framework net8.0 cd TestSqlServer/src/Server/TestSqlServer.Server.Api/ dotnet tool restore - dotnet ef migrations add InitialMigration + dotnet ef migrations add InitialMigration --verbose dotnet ef database update cd ../../Tests dotnet test --logger GitHubActions --filter "${{ env.BLAZOR_SERVER_TEST_FILTER }}" @@ -125,7 +125,7 @@ jobs: dotnet new bit-bp --name MultilingualDisabled --database Sqlite --advancedTests --framework net8.0 cd MultilingualDisabled/src/Server/MultilingualDisabled.Server.Api/ dotnet tool restore - dotnet ef migrations add InitialMigration + dotnet ef migrations add InitialMigration --verbose dotnet ef database update cd ../../Tests dotnet test -p:MultilingualEnabled=false --logger GitHubActions --filter "${{ env.MULTILINGUAL_DISABLED_TEST_FILTER }}" -- MSTest.Parallelize.Workers=1 @@ -138,16 +138,12 @@ jobs: path: ./MultilingualDisabled/src/Tests/TestResults retention-days: 14 - - name: Test PostgreSQL, Cosmos, MySql, Other database options + - name: Test PostgreSQL, MySql, Other database options run: | dotnet new bit-bp --name TestPostgreSQL --database PostgreSQL --framework net8.0 cd TestPostgreSQL/src/Server/TestPostgreSQL.Server.Api/ dotnet build cd ../../../../ - dotnet new bit-bp --name TestCosmos --database Cosmos --framework net9.0 - cd TestCosmos/src/Server/TestCosmos.Server.Api/ - dotnet build - cd ../../../../ dotnet new bit-bp --name TestMySql --database MySql --framework net8.0 cd TestMySql/src/Server/TestMySql.Server.Api/ dotnet build @@ -181,12 +177,12 @@ jobs: - name: Test sample configuration 1 run: | - dotnet new bit-bp --name TestProject --database Cosmos --filesStorage AzureBlobStorage --api Integrated --captcha reCaptcha --pipeline Azure --sample Admin --offlineDb --windows --appInsights --appCenter --signalR --notification --framework net9.0 + dotnet new bit-bp --name TestProject --database SqlServer --filesStorage AzureBlobStorage --api Integrated --captcha reCaptcha --pipeline Azure --sample Admin --offlineDb --windows --appInsights --sentry --signalR --notification --framework net9.0 dotnet build TestProject/TestProject.sln -p:MultilingualEnabled=true -p:PwaEnabled=true -p:Environment=Staging - name: Test sample configuration 2 run: | - dotnet new bit-bp --name TestProject2 --database Other --filesStorage Other --api Standalone --captcha None --pipeline None --sample None --offlineDb false --windows false --appInsights false --appCenter false --signalR false --notification false --framework net8.0 + dotnet new bit-bp --name TestProject2 --database Other --filesStorage Other --api Standalone --captcha None --pipeline None --sample None --offlineDb false --windows false --appInsights false --sentry false --signalR false --notification false --framework net8.0 dotnet build TestProject2/TestProject2.sln -p:MultilingualEnabled=false -p:PwaEnabled=false -p:Environment=Development - name: Run BeforeBuildTasks diff --git a/.github/workflows/blazorui.demo.cd.yml b/.github/workflows/blazorui.demo.cd.yml index 4babd7c6d2..40aba44568 100644 --- a/.github/workflows/blazorui.demo.cd.yml +++ b/.github/workflows/blazorui.demo.cd.yml @@ -120,9 +120,9 @@ jobs: - name: Publish run: | cd src\BlazorUI\Demo\Client\Bit.BlazorUI.Demo.Client.Windows\ - dotnet publish Bit.BlazorUI.Demo.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:CompressionEnabled=false + dotnet publish Bit.BlazorUI.Demo.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:CompressionEnabled=false --self-contained dotnet tool restore - dotnet vpk pack -u Bit.BlazorUI.Demo.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e Bit.BlazorUI.Demo.Client.Windows.exe -r win-x86 --framework net9.0-x86-desktop,webview2 --icon .\wwwroot\favicon.ico --packTitle 'Bit Blazor UI' + dotnet vpk pack -u Bit.BlazorUI.Demo.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e Bit.BlazorUI.Demo.Client.Windows.exe -r win-x86 --framework webview2 --icon .\wwwroot\favicon.ico --packTitle 'Bit Blazor UI' - name: Upload artifact uses: actions/upload-artifact@v4 @@ -171,7 +171,7 @@ jobs: - name: Generate CSS/JS files run: dotnet build src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Bit.BlazorUI.Demo.Client.Core.csproj -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -c Release - - name: Build aab + - name: Publish aab run: dotnet publish src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Maui/Bit.BlazorUI.Demo.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="BitBlazorUIDemo.keystore" -p:AndroidSigningKeyAlias=bitplatform -p:AndroidSigningKeyPass="${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}" -p:AndroidSigningStorePass="${{ secrets.ANDROID_RELEASE_SIGNING_PASSWORD }}" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:CompressionEnabled=false -f net9.0-android - name: Upload artifact @@ -196,7 +196,7 @@ jobs: - uses: maxim-lobanov/setup-xcode@v1.6.0 with: - xcode-version: '16.0' + 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 5b7cc8156a..388adcf25c 100644 --- a/.github/workflows/todo-sample.cd.yml +++ b/.github/workflows/todo-sample.cd.yml @@ -42,19 +42,20 @@ jobs: cd src/Templates/Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample Todo --appInsights --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net8.0 + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample Todo --appInsights --sentry --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net8.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 with: files: 'TodoSample/src/Shared/appsettings.json, TodoSample/src/Client/TodoSample.Client.Core/appsettings.json, TodoSample/src/Client/TodoSample.Client.Web/appsettings.json, TodoSample/src/Client/TodoSample.Client.Web/appsettings.Production.json' env: + WebAppRender.PrerenderEnabled: true ServerAddress: ${{ env.SERVER_ADDRESS }} - GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} WebAppRender.BlazorMode: BlazorWebAssembly - WebAppRender.PrerenderEnabled: true - ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} + Logging.Sentry.Dsn: ${{ secrets.TODO_SENTRY_DSN }} AdsPushVapid.PublicKey: ${{ secrets.TODO_PUBLIC_VAPIDKEY }} + GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} + ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - name: Install wasm run: cd src && dotnet workload install wasm-tools @@ -103,6 +104,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 @@ -145,7 +149,7 @@ jobs: cd src\Templates\Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ..\..\..\ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample Todo --windows --appInsights --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --framework net8.0 + cd ..\..\..\ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample Todo --windows --appInsights --sentry --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --captcha reCaptcha --signalR --framework net8.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 @@ -153,29 +157,20 @@ jobs: files: 'TodoSample\src\Shared\appsettings.json, TodoSample\src\Client\TodoSample.Client.Core\appsettings.json, TodoSample\src\Client\TodoSample.Client.Windows\appsettings.json' env: ServerAddress: ${{ env.SERVER_ADDRESS }} - GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} + Logging.Sentry.Dsn: ${{ secrets.TODO_SENTRY_DSN }} WindowsUpdate.FilesUrl: https://windows-todo.bitplatform.dev + GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - - name: Delete App Splash Screen - run: rm TodoSample/src/Client/TodoSample.Client.Windows/Resources/SplashScreen.png - - - name: Extract App Splash Screen from env - uses: timheuer/base64-to-file@v1.2 - with: - fileDir: './TodoSample/src/Client/TodoSample.Client.Windows/Resources/' - fileName: 'SplashScreen.png' - encodedString: ${{ vars.TODO_WPF_SPLASH_SCREEN }} - - name: Generate CSS/JS files run: dotnet build TodoSample\src\Client\TodoSample.Client.Core\TodoSample.Client.Core.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - name: Publish run: | cd TodoSample\src\Client\TodoSample.Client.Windows\ - dotnet publish TodoSample.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:CompressionEnabled=false + dotnet publish TodoSample.Client.Windows.csproj -c Release -o .\publish-result -r win-x86 -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" dotnet tool restore - dotnet vpk pack -u TodoSample.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e TodoSample.Client.Windows.exe -r win-x86 --framework net8.0-x86-desktop,webview2 --icon .\wwwroot\favicon.ico --packTitle TodoSample + dotnet vpk pack -u TodoSample.Client.Windows -v "${{ vars.APPLICATION_DISPLAY_VERSION }}" -p .\publish-result -e TodoSample.Client.Windows.exe -r win-x86 --framework webview2 --icon .\wwwroot\favicon.ico --packTitle TodoSample - name: Upload artifact uses: actions/upload-artifact@v4 @@ -210,7 +205,7 @@ jobs: cd src/Templates/Boilerplate && dotnet build -c Release dotnet pack -c Release -o . -p:ReleaseVersion=0.0.0 -p:PackageVersion=0.0.0 dotnet new install Bit.Boilerplate.0.0.0.nupkg - cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample Todo --appInsights --appCenter --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net8.0 + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample Todo --appInsights --sentry --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net8.0 - name: Extract Android signing key from env uses: timheuer/base64-to-file@v1.2 @@ -232,13 +227,10 @@ jobs: files: 'TodoSample/src/Shared/appsettings.json, TodoSample/src/Client/TodoSample.Client.Core/appsettings.json, TodoSample/src/Client/TodoSample.Client.Maui/appsettings.json' env: ServerAddress: ${{ env.SERVER_ADDRESS }} + Logging.Sentry.Dsn: ${{ secrets.TODO_SENTRY_DSN }} GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - - name: Set app center secret - run: | - sed -i 's/appCenterSecret = null;/appCenterSecret = "de0219a6-fdcd-44f7-8c28-c108331ed27c";/g' TodoSample/src/Client/TodoSample.Client.Maui/MauiProgram.cs - - name: Install maui run: cd src && dotnet workload install maui-android @@ -270,8 +262,8 @@ jobs: dotnet build TodoSample/src/Client/TodoSample.Client.Core/TodoSample.Client.Core.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release dotnet build TodoSample/src/Client/TodoSample.Client.Maui/TodoSample.Client.Maui.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - - name: Build aab - run: dotnet publish TodoSample/src/Client/TodoSample.Client.Maui/TodoSample.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="TodoSample.keystore" -p:AndroidSigningKeyAlias=bitplatform -p:AndroidSigningKeyPass="${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}" -p:AndroidSigningStorePass="${{ secrets.ANDROID_RELEASE_SIGNING_PASSWORD }}" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="TodoSample" -p:ApplicationId="com.bitplatform.Todo.Template" -p:CompressionEnabled=false -f net8.0-android + - name: Publish aab + run: dotnet publish TodoSample/src/Client/TodoSample.Client.Maui/TodoSample.Client.Maui.csproj -c Release -p:AndroidPackageFormat=aab -p:AndroidKeyStore=true -p:AndroidSigningKeyStore="TodoSample.keystore" -p:AndroidSigningKeyAlias=bitplatform -p:AndroidSigningKeyPass="${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }}" -p:AndroidSigningStorePass="${{ secrets.ANDROID_RELEASE_SIGNING_PASSWORD }}" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="TodoSample" -p:ApplicationId="com.bitplatform.Todo.Template" -f net8.0-android - name: Upload artifact uses: actions/upload-artifact@v4 @@ -303,14 +295,14 @@ jobs: - uses: maxim-lobanov/setup-xcode@v1.6.0 with: - xcode-version: '16.0' + 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 --appCenter --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --framework net8.0 + cd ../../../ && dotnet new bit-bp --name TodoSample --database PostgreSQL --sample Todo --appInsights --sentry --serverUrl ${{ env.SERVER_ADDRESS }} --filesStorage AzureBlobStorage --notification --captcha reCaptcha --signalR --framework net8.0 - name: Update core appsettings.json uses: devops-actions/variable-substitution@v1.2 @@ -318,13 +310,10 @@ jobs: files: 'TodoSample/src/Shared/appsettings.json, TodoSample/src/Client/TodoSample.Client.Core/appsettings.json, TodoSample/src/Client/TodoSample.Client.Maui/appsettings.json' env: ServerAddress: ${{ env.SERVER_ADDRESS }} + Logging.Sentry.Dsn: ${{ secrets.TODO_SENTRY_DSN }} GoogleRecaptchaSiteKey: ${{ secrets.GOOGLE_RECAPTCHA_SITE_KEY }} ApplicationInsights.ConnectionString: ${{ secrets.APPLICATION_INSIGHTS_CONNECTION_STRING }} - - name: Set app center secret - run: | - brew install gnu-sed && gsed -i 's/appCenterSecret = null;/appCenterSecret = "f72e6774-1c83-404c-bca8-6e5198fb8e0e";/g' TodoSample/src/Client/TodoSample.Client.Maui/MauiProgram.cs - - name: Install maui run: cd src && dotnet workload install maui @@ -368,7 +357,7 @@ jobs: dotnet build TodoSample/src/Client/TodoSample.Client.Maui/TodoSample.Client.Maui.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release - name: Build ipa - run: dotnet publish TodoSample/src/Client/TodoSample.Client.Maui/TodoSample.Client.Maui.csproj -p:RuntimeIdentifier=ios-arm64 -c Release -p:ArchiveOnBuild=true -p:CodesignKey="iPhone Distribution" -p:CodesignProvision="TodoTemplate" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="Todo" -p:ApplicationId="com.bitplatform.Todo.Template" -p:CompressionEnabled=false -f net8.0-ios + run: dotnet publish TodoSample/src/Client/TodoSample.Client.Maui/TodoSample.Client.Maui.csproj -p:RuntimeIdentifier=ios-arm64 -c Release -p:ArchiveOnBuild=true -p:CodesignKey="iPhone Distribution" -p:CodesignProvision="TodoTemplate" -p:ApplicationDisplayVersion="${{ vars.APPLICATION_DISPLAY_VERSION }}" -p:ApplicationVersion="${{ vars.APPLICATION_VERSION }}" -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" -p:ApplicationTitle="Todo" -p:ApplicationId="com.bitplatform.Todo.Template" -f net8.0-ios - name: Upload artifact uses: actions/upload-artifact@v4 diff --git a/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 80eea970b8..b52a0bd3ea 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.0.1'; +var BitBesql = window.BitBesql || {}; +BitBesql.version = window['bit-besql version'] = '9.1.0'; -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 d027cff25e..c79b843b27 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.0.1 - - https://github.com/bitfoundation/bitplatform/releases/tag/v-$(ReleaseVersion) - $(ReleaseVersion) - $(ReleaseVersion).$([System.DateTime]::Now.ToString(HHmm)) + 9.1.0 + $(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 be0b23b41b..b6ed4c0749 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Bit.BlazorUI.Extras.csproj @@ -4,16 +4,12 @@ net9.0;net8.0 - enable - Bit.BlazorUI true - 0 + enable BeforeBuildTasks; $(ResolveStaticWebAssetsInputsDependsOn) - True - ..\..\AssemblyOriginatorKeyFile.snk @@ -35,6 +31,7 @@ + @@ -57,9 +54,9 @@ - - - + + + @@ -76,14 +73,6 @@ - - True - \ - - - True - \ - diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor new file mode 100644 index 0000000000..18595aad3b --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor @@ -0,0 +1,10 @@ +@namespace Bit.BlazorUI + +@foreach (var modalReference in _modalRefs) +{ + + + @modalReference.Modal + + +} \ No newline at end of file diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor.cs new file mode 100644 index 0000000000..49d89b3568 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalContainer.razor.cs @@ -0,0 +1,63 @@ +using System.Collections.Concurrent; +using System.Threading.Tasks; + +namespace Bit.BlazorUI; + +public partial class BitModalContainer : IDisposable +{ + private readonly List _modalRefs = []; + + + + [Parameter] public BitModalParameters ModalParameters { get; set; } = new(); + + + + [Inject] private BitModalService _modalService { get; set; } = default!; + + + + internal void InjectPersistentModals(ConcurrentQueue queue) + { + while (queue.TryDequeue(out var modalRef)) + { + _modalRefs.Add(modalRef); + } + } + + + + protected override void OnInitialized() + { + base.OnInitialized(); + + _modalService.InitContainer(this); + + _modalService.OnAddModal += OnModalAdd; + _modalService.OnCloseModal += OnCloseModal; + } + + + + private Task OnModalAdd(BitModalReference modalRef) + { + if (_modalRefs.Contains(modalRef)) return Task.CompletedTask; + + _modalRefs.Add(modalRef); + return InvokeAsync(StateHasChanged); + } + + private Task OnCloseModal(BitModalReference modalRef) + { + _modalRefs.Remove(modalRef); + return InvokeAsync(StateHasChanged); + } + + + + public void Dispose() + { + _modalService.OnAddModal -= OnModalAdd; + _modalService.OnCloseModal -= OnCloseModal; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalReference.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalReference.cs new file mode 100644 index 0000000000..e017ad10f3 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalReference.cs @@ -0,0 +1,50 @@ +namespace Bit.BlazorUI; + +public class BitModalReference +{ + private readonly BitModalService _modalService; + + + + public string Id { get; init; } + + public bool Persistent { get; private set; } + + public object? Content { get; private set; } + + public RenderFragment? Modal { get; private set; } + + public BitModalParameters? Parameters { get; private set; } + + + + + public BitModalReference(BitModalService modalService, bool persistent) + { + Id = BitShortId.NewId(); + _modalService = modalService; + Persistent = persistent; + } + + + + public void SetContent(object content) + { + Content = content; + } + + public void SetModal(RenderFragment modal) + { + Modal = modal; + } + + public void SetParameters(BitModalParameters? parameters) + { + Parameters = parameters; + } + + public void Close() + { + _modalService.Close(this); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalService.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalService.cs new file mode 100644 index 0000000000..d019966a8e --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/ModalService/BitModalService.cs @@ -0,0 +1,150 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; + +namespace Bit.BlazorUI; + +public class BitModalService +{ + private BitModalContainer? _container; + private readonly ConcurrentQueue _persistentModalsQueue = new(); + + + + /// + /// The event for when a new modal gets added through calling the Show method. + /// + public event Func? OnAddModal; + + /// + /// The event for when a modal gets removed through calling the Close method. + /// + public event Func? OnCloseModal; + + + + /// + /// Initializes the current modal container that is responsible for rendering the modals. + /// + public void InitContainer(BitModalContainer container) + { + _container = container; + _container.InjectPersistentModals(_persistentModalsQueue); + } + + /// + /// Closes an already opened modal using its reference. + /// + public async Task Close(BitModalReference modalRef) + { + var modalClose = OnCloseModal; + if (modalClose is not null) + { + await modalClose(modalRef); + } + } + + /// + /// Shows a new persistent BitModal that will persist through the lifecycle of the application until it gets shown. + /// + public Task Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + bool persistent = false) + { + return Show(null, null, persistent); + } + + /// + /// Shows a new BitModal with a custom component with parameters as its content. + /// + public Task Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + Dictionary? parameters, bool persistent = false) + { + return Show(parameters, null, persistent); + } + + /// + /// Shows a new BitModal with a custom component with parameters as its content. + /// + public Task Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + Dictionary parameters) + { + return Show(parameters, null, false); + } + + /// + /// Shows a new BitModal with a custom component as its content with custom parameters for the modal. + /// + public Task Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + BitModalParameters modalParameters) + { + return Show(null, modalParameters, false); + } + + /// + /// Shows a new BitModal with a custom component as its content with custom parameters for the modal. + /// + public Task Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + BitModalParameters? modalParameters, bool persistent = false) + { + return Show(null, modalParameters, persistent); + } + + /// + /// Shows a new BitModal with a custom component as its content with custom parameters for the custom component and the modal. + /// + public async Task Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>( + Dictionary? parameters, + BitModalParameters? modalParameters, + bool persistent = false) + { + var componentType = typeof(T); + + if (typeof(IComponent).IsAssignableFrom(componentType) is false) + { + throw new ArgumentException($"Type {componentType.Name} must be a Blazor component"); + } + + var modalReference = new BitModalReference(this, persistent); + modalReference.SetParameters(modalParameters); + + var content = new RenderFragment(builder => + { + var i = 0; + builder.OpenComponent(i++, componentType); + + if (parameters is not null) + { + foreach (var parameter in parameters) + { + builder.AddAttribute(i++, parameter.Key, parameter.Value); + } + } + + builder.AddComponentReferenceCapture(i, c => { modalReference.SetContent((T)c); }); + builder.CloseComponent(); + }); + + var modal = new RenderFragment(builder => + { + builder.OpenComponent(0); + builder.SetKey(modalReference.Id); + builder.AddComponentParameter(1, nameof(BitModal.IsOpen), true); + builder.AddComponentParameter(2, nameof(BitModal.OnOverlayClick), EventCallback.Factory.Create(modalReference, () => modalReference.Close())); + builder.AddComponentParameter(3, nameof(BitModal.ChildContent), content); + builder.CloseComponent(); + }); + modalReference.SetModal(modal); + + var modalAdd = OnAddModal; + if (modalAdd is not null) + { + await modalAdd(modalReference); + } + + if (persistent && _container is null) + { + _persistentModalsQueue.Enqueue(modalReference); + } + + return modalReference; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/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/IServiceCollectionExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/IServiceCollectionExtensions.cs new file mode 100644 index 0000000000..0ae549d8eb --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/IServiceCollectionExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Bit.BlazorUI; + +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 that can be singleton (e.g. the services that do not use IJSRuntime). + /// + public static IServiceCollection AddBitBlazorUIExtrasServices(this IServiceCollection services, bool trySingleton = false) + { + if (trySingleton) + { + services.TryAddSingleton(); + } + else + { + services.TryAddScoped(); + } + + return services; + } +} 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..79d9f65d85 100644 --- a/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj +++ b/src/BlazorUI/Bit.BlazorUI.Tests/Bit.BlazorUI.Tests.csproj @@ -14,9 +14,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/BlazorUI/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/Modal/BitModalTests.cs b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Surfaces/Modal/BitModalTests.cs index 9de4770dbf..eb3b8ed478 100644 --- a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Surfaces/Modal/BitModalTests.cs +++ b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Surfaces/Modal/BitModalTests.cs @@ -61,7 +61,7 @@ public void BitModalIsModelessTest(bool isModeless) }); var element = com.Find(".bit-mdl"); - Assert.AreEqual(element.Attributes["aria-modal"].Value, (isModeless is false).ToString()); + Assert.AreEqual(element.Attributes["aria-modal"].Value, (isModeless is false).ToString().ToLower()); var elementOverlay = com.FindAll(".bit-mdl-ovl"); Assert.AreEqual(isModeless ? 0 : 1, elementOverlay.Count); @@ -151,7 +151,7 @@ public void BitModalContentTest() var elementContent = com.Find(".bit-mdl-ctn"); - elementContent.MarkupMatches("
Test Content
"); + elementContent.MarkupMatches("
Test Content
"); } [TestMethod] @@ -193,18 +193,18 @@ public void BitModalOnDismissShouldWorkCorrect() } [DataTestMethod, - DataRow(BitModalPosition.Center), - DataRow(BitModalPosition.TopLeft), - DataRow(BitModalPosition.TopCenter), - DataRow(BitModalPosition.TopRight), - DataRow(BitModalPosition.CenterLeft), - DataRow(BitModalPosition.CenterRight), - DataRow(BitModalPosition.BottomLeft), - DataRow(BitModalPosition.BottomCenter), - DataRow(BitModalPosition.BottomRight), + DataRow(BitPosition.Center), + DataRow(BitPosition.TopLeft), + DataRow(BitPosition.TopCenter), + DataRow(BitPosition.TopRight), + DataRow(BitPosition.CenterLeft), + DataRow(BitPosition.CenterRight), + DataRow(BitPosition.BottomLeft), + DataRow(BitPosition.BottomCenter), + DataRow(BitPosition.BottomRight), DataRow(null) ] - public void BitModalPositionTest(BitModalPosition? position) + public void BitPositionTest(BitPosition? position) { var com = RenderComponent(parameters => { @@ -214,15 +214,15 @@ public void BitModalPositionTest(BitModalPosition? position) var positionClass = position switch { - BitModalPosition.Center => "bit-mdl-ctr", - BitModalPosition.TopLeft => "bit-mdl-tl", - BitModalPosition.TopCenter => "bit-mdl-tc", - BitModalPosition.TopRight => "bit-mdl-tr", - BitModalPosition.CenterLeft => "bit-mdl-cl", - BitModalPosition.CenterRight => "bit-mdl-cr", - BitModalPosition.BottomLeft => "bit-mdl-bl", - BitModalPosition.BottomCenter => "bit-mdl-bc", - BitModalPosition.BottomRight => "bit-mdl-br", + BitPosition.Center => "bit-mdl-ctr", + BitPosition.TopLeft => "bit-mdl-tl", + BitPosition.TopCenter => "bit-mdl-tc", + BitPosition.TopRight => "bit-mdl-tr", + BitPosition.CenterLeft => "bit-mdl-cl", + BitPosition.CenterRight => "bit-mdl-cr", + BitPosition.BottomLeft => "bit-mdl-bl", + BitPosition.BottomCenter => "bit-mdl-bc", + BitPosition.BottomRight => "bit-mdl-br", _ => "bit-mdl-ctr", }; diff --git a/src/BlazorUI/Bit.BlazorUI.Tests/Components/Surfaces/Panel/BitPanelTests.cs b/src/BlazorUI/Bit.BlazorUI.Tests/Components/Surfaces/Panel/BitPanelTests.cs index c2ef4de625..e7a3a7fbc8 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,49 @@ 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()); + Assert.AreEqual(element.Attributes["aria-modal"].Value, (modeless 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,8 +69,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); + var container = com.Find(".bit-pnl-cnt"); + Assert.AreEqual(isOpen, container.GetStyle().CssText.Contains("opacity: 1")); } [DataTestMethod, @@ -178,14 +186,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 +216,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 +237,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..489b257821 100644 --- a/src/BlazorUI/Bit.BlazorUI/Bit.BlazorUI.csproj +++ b/src/BlazorUI/Bit.BlazorUI/Bit.BlazorUI.csproj @@ -10,8 +10,6 @@ BeforeBuildTasks; $(ResolveStaticWebAssetsInputsDependsOn) - True - ..\..\AssemblyOriginatorKeyFile.snk @@ -71,14 +69,6 @@ - - True - \ - - - True - \ - diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadgePosition.cs b/src/BlazorUI/Bit.BlazorUI/Components/BitPosition.cs similarity index 85% rename from src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadgePosition.cs rename to src/BlazorUI/Bit.BlazorUI/Components/BitPosition.cs index e137d21dd6..b0547bfe6b 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Notifications/Badge/BitBadgePosition.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/BitPosition.cs @@ -1,6 +1,6 @@ namespace Bit.BlazorUI; -public enum BitBadgePosition +public enum BitPosition { TopLeft, TopCenter, diff --git a/src/BlazorUI/Bit.BlazorUI/Components/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 0cc8381d65..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; } @@ -65,6 +65,30 @@ public partial class BitButton : BitComponentBase [Parameter, ResetClassBuilder] public bool FixedColor { get; set; } + /// + /// Enables floating behavior for the button, allowing it to be positioned relative to the viewport. + /// + [Parameter, ResetClassBuilder] + public bool Float { get; set; } + + /// + /// Enables floating behavior for the button, allowing it to be positioned relative to its container. + /// + [Parameter, ResetClassBuilder] + public bool FloatAbsolute { get; set; } + + /// + /// Specifies the offset of the floating button. + /// + [Parameter, ResetStyleBuilder] + public string? FloatOffset { get; set; } + + /// + /// Specifies the position of the floating button. + /// + [Parameter, ResetClassBuilder] + public BitPosition? FloatPosition { get; set; } + /// /// Expand the button width to 100% of the available width. /// @@ -110,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). @@ -120,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. /// @@ -226,14 +255,33 @@ protected override void RegisterCssClasses() ClassBuilder.Register(() => ReversedIcon ? "bit-btn-rvi" : string.Empty); - ClassBuilder.Register(() => FixedColor ? "bit-btn-ftc" : string.Empty); + ClassBuilder.Register(() => FixedColor ? "bit-btn-fxc" : string.Empty); ClassBuilder.Register(() => FullWidth ? "bit-btn-flw" : string.Empty); + + ClassBuilder.Register(() => FloatAbsolute ? "bit-btn-fab" + : Float ? "bit-btn-ffx" : string.Empty); + + ClassBuilder.Register(() => (Float || FloatAbsolute) ? FloatPosition switch + { + BitPosition.TopRight => "bit-btn-trg", + BitPosition.TopCenter => "bit-btn-tcr", + BitPosition.TopLeft => "bit-btn-tlf", + BitPosition.CenterLeft => "bit-btn-clf", + BitPosition.BottomLeft => "bit-btn-blf", + BitPosition.BottomCenter => "bit-btn-bcr", + BitPosition.BottomRight => "bit-btn-brg", + BitPosition.CenterRight => "bit-btn-crg", + BitPosition.Center => "bit-btn-ctr", + _ => "bit-btn-brg" + } : string.Empty); } protected override void RegisterCssStyles() { StyleBuilder.Register(() => Styles?.Root); + + StyleBuilder.Register(() => FloatOffset.HasValue() ? $"--bit-btn-float-offset:{FloatOffset}" : string.Empty); } protected override void OnParametersSet() @@ -263,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/BitButton/BitButton.scss b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss index abbf42f882..8a93d5a46d 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButton/BitButton.scss @@ -19,6 +19,7 @@ border-radius: $shp-border-radius; --bit-btn-clr-bg-dis: #{$clr-bg-dis}; --bit-btn-clr-brd-dis: #{$clr-brd-dis}; + --bit-btn-float-offset: #{spacing(2)}; &.bit-dis { cursor: default; @@ -88,6 +89,21 @@ font-size: var(--bit-btn-lbl-fontsize); } +.bit-btn-flw { + width: 100%; + z-index: $zindex-base; +} + +.bit-btn-fab { + position: absolute; + z-index: $zindex-base; +} + +.bit-btn-ffx { + position: fixed; + z-index: $zindex-base; +} + .bit-btn-fil { border-color: var(--bit-btn-clr); @@ -151,13 +167,6 @@ } } -.bit-btn-ftc { - color: var(--bit-btn-clr-txt); -} - -.bit-btn-flw { - width: 100%; -} .bit-btn-pri { --bit-btn-clr: #{$clr-pri}; @@ -343,7 +352,64 @@ } +.bit-btn-trg { + top: var(--bit-btn-float-offset); + right: var(--bit-btn-float-offset); +} + +.bit-btn-tlf { + top: var(--bit-btn-float-offset); + left: var(--bit-btn-float-offset); +} + +.bit-btn-tcr { + left: 50%; + transform: translateX(-50%); + top: var(--bit-btn-float-offset); +} + +.bit-btn-clf { + top: 50%; + transform: translateY(-50%); + left: var(--bit-btn-float-offset); +} + +.bit-btn-crg { + top: 50%; + transform: translateY(-50%); + right: var(--bit-btn-float-offset); +} + +.bit-btn-ctr { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.bit-btn-blf { + left: var(--bit-btn-float-offset); + bottom: var(--bit-btn-float-offset); +} + +.bit-btn-brg { + right: var(--bit-btn-float-offset); + bottom: var(--bit-btn-float-offset); +} + +.bit-btn-bcr { + left: 50%; + transform: translateX(-50%); + bottom: var(--bit-btn-float-offset); +} + + +// important: these need to be here at the end! + +.bit-btn-fxc { + color: var(--bit-btn-clr-txt); +} + .bit-btn-ntx { padding: var(--bit-btn-pad-ntx); --bit-btn-icn-margintop: 0; -} +} \ No newline at end of file diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.razor.cs index 150b45e63e..9c3229eb85 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitButtonGroup/BitButtonGroup.razor.cs @@ -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. @@ -32,9 +30,7 @@ public partial class BitButtonGroup : BitComponentBase where TItem : clas /// /// 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. @@ -135,7 +131,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.ToList(); } @@ -169,15 +177,6 @@ private async Task HandleOnItemClick(TItem item) } } - private void OnSetChildContentAndItems() - { - if (ChildContent is not null) return; - if (Items.Any() is false || Items == _oldItems) return; - - _oldItems = Items; - _items = Items.ToList(); - } - private string? GetClass(TItem? item) { if (item is null) return null; diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitMenuButton/BitMenuButton.razor b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitMenuButton/BitMenuButton.razor index 90ff2c90e3..6f4bcb16a5 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitMenuButton/BitMenuButton.razor +++ b/src/BlazorUI/Bit.BlazorUI/Components/Buttons/BitMenuButton/BitMenuButton.razor @@ -90,13 +90,13 @@ style="@Styles?.Callout" class="bit-mnb-cal @Classes?.Callout"> [Parameter] public string? Placeholder { get; set; } + /// + /// Enables the responsive mode in small screens + /// + [Parameter] public bool Responsive { get; set; } + /// /// Whether the TimePicker's close button should be shown or not. /// @@ -178,6 +173,11 @@ public partial class BitCircularTimePicker : BitInputBase, IAsyncDisp /// [Parameter] public BitTimeFormat TimeFormat { get; set; } + /// + /// Whether or not the Text field of the TimePicker is underlined. + /// + [Parameter] public bool Underlined { get; set; } + /// /// The format of the time in the TimePicker /// @@ -190,7 +190,7 @@ public partial class BitCircularTimePicker : BitInputBase, IAsyncDisp [JSInvokable("CloseCallout")] - public async Task CloseCalloutBeforeAnotherCalloutIsOpened() + public async Task _CloseCalloutBeforeAnotherCalloutIsOpened() { if (Standalone) return; if (IsEnabled is false) return; @@ -200,8 +200,33 @@ public async Task CloseCalloutBeforeAnotherCalloutIsOpened() StateHasChanged(); } - [JSInvokable(nameof(HandlePointerUp))] - public async Task HandlePointerUp(MouseEventArgs e) + [JSInvokable("OnStart")] + public async Task _OnStart(decimal startX, decimal startY) + { + + } + + [JSInvokable("OnMove")] + public async Task _OnMove(decimal diffX, decimal diffY) + { + + } + + [JSInvokable("OnEnd")] + public async Task _OnEnd(decimal diffX, decimal diffY) + { + + } + + [JSInvokable("OnClose")] + public async Task _OnClose() + { + await CloseCallout(); + await InvokeAsync(StateHasChanged); + } + + [JSInvokable(nameof(_HandlePointerUp))] + public async Task _HandlePointerUp(MouseEventArgs e) { if (IsEnabled is false) return; if (_isPointerDown is false) return; @@ -221,14 +246,16 @@ public async Task HandlePointerUp(MouseEventArgs e) StateHasChanged(); } - [JSInvokable(nameof(HandlePointerMove))] - public async Task HandlePointerMove(MouseEventArgs e) + [JSInvokable(nameof(_HandlePointerMove))] + public async Task _HandlePointerMove(MouseEventArgs e) { if (_isPointerDown is false) return; await UpdateTime(e); } + + public Task OpenCallout() => HandleOnClick(); @@ -241,7 +268,7 @@ protected override void RegisterCssClasses() ClassBuilder.Register(() => IconLocation is BitIconLocation.Left ? "bit-ctp-lic" : string.Empty); - ClassBuilder.Register(() => IsUnderlined ? "bit-ctp-und" : string.Empty); + ClassBuilder.Register(() => Underlined ? "bit-ctp-und" : string.Empty); ClassBuilder.Register(() => HasBorder ? string.Empty : "bit-ctp-nbd"); @@ -259,6 +286,8 @@ protected override void RegisterCssStyles() protected override void OnInitialized() { + _dotnetObj = DotNetObjectReference.Create(this); + _circularTimePickerId = $"BitCircularTimePicker-{UniqueId}"; _labelId = $"{_circularTimePickerId}-label"; _inputId = $"{_circularTimePickerId}-input"; @@ -267,8 +296,6 @@ protected override void OnInitialized() _hour = CurrentValue?.Hours; _minute = CurrentValue?.Minutes; - _dotnetObj = DotNetObjectReference.Create(this); - OnValueChanged += HandleOnValueChanged; base.OnInitialized(); @@ -280,8 +307,9 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (firstRender is false) return; - _pointerUpAbortControllerId = await _js.BitCircularTimePickerRegisterPointerUp(_dotnetObj, nameof(HandlePointerUp)); - _pointerMoveAbortControllerId = await _js.BitCircularTimePickerRegisterPointerMove(_dotnetObj, nameof(HandlePointerMove)); + await _js.SwipesSetup(_calloutId, 0.25m, SwipesPosition.Top, Dir is BitDir.Rtl, _dotnetObj); + _pointerUpAbortControllerId = await _js.BitCircularTimePickerRegisterPointerUp(_dotnetObj, nameof(_HandlePointerUp)); + _pointerMoveAbortControllerId = await _js.BitCircularTimePickerRegisterPointerMove(_dotnetObj, nameof(_HandlePointerMove)); } @@ -324,8 +352,6 @@ private async Task CloseCallout() if (await AssignIsOpen(false) is false) return; await ToggleCallout(); - - StateHasChanged(); } private async Task HandleOnChange(ChangeEventArgs e) @@ -503,15 +529,14 @@ await _js.ToggleCallout(_dotnetObj, _calloutId, null, IsOpen, - IsResponsive ? BitResponsiveMode.Top : BitResponsiveMode.None, + Responsive ? BitResponsiveMode.Top : BitResponsiveMode.None, BitDropDirection.TopAndBottom, Dir is BitDir.Rtl, "", 0, "", "", - false, - RootElementClass); + false); } private async Task UpdateTime(MouseEventArgs e) @@ -611,6 +636,26 @@ private async Task UpdateTime(MouseEventArgs e) return style.HasValue() ? style : null; } + private string GetCalloutCssClasses() + { + List classes = ["bit-ctp-cal"]; + + if (Classes?.Callout is not null) + { + classes.Add(Classes.Callout); + } + + if (Responsive) + { + classes.Add("bit-ctp-res"); + } + + return string.Join(' ', classes).Trim(); + } + + + + /// protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TimeSpan? result, [NotNullWhen(false)] out string? validationErrorMessage) { @@ -660,16 +705,14 @@ protected virtual async ValueTask DisposeAsync(bool disposing) OnValueChanged -= HandleOnValueChanged; - if (_dotnetObj is not null) + try { - // _dotnetObj.Dispose(); // it is getting disposed in the following js call: - try - { - await _js.BitCircularTimePickerAbort(_pointerUpAbortControllerId, true); - await _js.BitCircularTimePickerAbort(_pointerMoveAbortControllerId); - } - catch (JSDisconnectedException) { } // we can ignore this exception here + await _js.ClearCallout(_calloutId); + await _js.SwipesDispose(_calloutId); + await _js.BitCircularTimePickerAbort(_pointerUpAbortControllerId); + await _js.BitCircularTimePickerAbort(_pointerMoveAbortControllerId); } + catch (JSDisconnectedException) { } // we can ignore this exception here _disposed = true; } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePicker.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePicker.scss index 220d0eb661..43b7eb6535 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePicker.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePicker.scss @@ -1,4 +1,5 @@ @import "../../../../Styles/functions.scss"; +@import "../../../../Styles/media-queries.scss"; .bit-ctp { &.bit-dis { @@ -389,15 +390,28 @@ } } -.bit-ctp-rsp { - .bit-ctp-clk { - margin: spacing(2) auto; - } - - .bit-ctp-cbn { +.bit-ctp-res { + @include lt-sm { + top: 0; + left: 0; + opacity: 0; width: 100%; - min-width: spacing(4); - max-width: spacing(5); - font-size: spacing(2.5); + display: block; + overflow: hidden; + animation-name: unset; + transform: translateY(-100%); + box-shadow: $box-shadow-callout; + transition: transform 200ms ease-out, opacity 100ms linear; + + .bit-ctp-clk { + margin: spacing(2) auto; + } + + .bit-ctp-cbn { + width: 100%; + min-width: spacing(4); + max-width: spacing(5); + font-size: spacing(2.5); + } } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePicker.ts b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePicker.ts index 5db0009a56..57603e101d 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePicker.ts +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePicker.ts @@ -1,10 +1,4 @@ namespace BitBlazorUI { - class BitController { - id: string = BitBlazorUI.Utils.uuidv4(); - controller = new AbortController(); - dotnetObj: DotNetObject | undefined; - } - export class CircularTimePicker { private static _bitControllers: BitController[] = []; @@ -23,14 +17,10 @@ return bitController.id; } - public static abort(id: string, dispose: boolean): void { + public static abort(id: string): void { const bitController = CircularTimePicker._bitControllers.find(bc => bc.id == id); bitController?.controller.abort(); - if (dispose) { - bitController?.dotnetObj?.dispose(); - } - CircularTimePicker._bitControllers = CircularTimePicker._bitControllers.filter(bc => bc.id != id); } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePickerJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePickerJsRuntimeExtensions.cs index 38150ac314..5e99b6fcfd 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePickerJsRuntimeExtensions.cs +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/CircularTimePicker/BitCircularTimePickerJsRuntimeExtensions.cs @@ -12,8 +12,8 @@ internal static ValueTask BitCircularTimePickerRegisterPointerMove(this return js.Invoke("BitBlazorUI.CircularTimePicker.registerEvent", "pointermove", obj, methodName); } - internal static ValueTask BitCircularTimePickerAbort(this IJSRuntime jSRuntime, string? abortControllerId, bool dispose = false) + internal static ValueTask BitCircularTimePickerAbort(this IJSRuntime jSRuntime, string? abortControllerId) { - return jSRuntime.InvokeVoid("BitBlazorUI.CircularTimePicker.abort", abortControllerId, dispose); + return jSRuntime.InvokeVoid("BitBlazorUI.CircularTimePicker.abort", abortControllerId); } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/ColorPicker/BitColorPicker.ts b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/ColorPicker/BitColorPicker.ts index f645398a77..aace7ac86d 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/ColorPicker/BitColorPicker.ts +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/ColorPicker/BitColorPicker.ts @@ -1,10 +1,4 @@ namespace BitBlazorUI { - class BitController { - id: string = BitBlazorUI.Utils.uuidv4(); - controller = new AbortController(); - dotnetObj: DotNetObject | undefined; - } - export class ColorPicker { private static _bitControllers: BitController[] = []; diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.razor b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.razor index a4efe15cb9..beff08a35b 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.razor +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.razor @@ -87,7 +87,9 @@ readonly="@(AllowTextInput is false)" /> } -
+
[Parameter] public string Placeholder { get; set; } = string.Empty; + /// + /// Enables the responsive mode in small screens. + /// + [Parameter] public bool Responsive { get; set; } + /// /// Whether the DatePicker's close button should be shown or not. /// @@ -376,6 +372,12 @@ private int _minuteView /// [Parameter] public BitTimeFormat TimeFormat { get; set; } + /// + /// Whether or not the text field of the DatePicker is underlined. + /// + [Parameter, ResetClassBuilder] + public bool Underlined { get; set; } + /// /// The title of the week number (tooltip). /// @@ -434,13 +436,8 @@ private int _minuteView - public Task OpenCallout() - { - return HandleOnClick(); - } - [JSInvokable("CloseCallout")] - public async Task CloseCalloutBeforeAnotherCalloutIsOpened() + public async Task _CloseCalloutBeforeAnotherCalloutIsOpened() { if (Standalone) return; if (IsEnabled is false) return; @@ -450,6 +447,38 @@ public async Task CloseCalloutBeforeAnotherCalloutIsOpened() StateHasChanged(); } + [JSInvokable("OnStart")] + public async Task _OnStart(decimal startX, decimal startY) + { + + } + + [JSInvokable("OnMove")] + public async Task _OnMove(decimal diffX, decimal diffY) + { + + } + + [JSInvokable("OnEnd")] + public async Task _OnEnd(decimal diffX, decimal diffY) + { + + } + + [JSInvokable("OnClose")] + public async Task _OnClose() + { + await CloseCallout(); + await InvokeAsync(StateHasChanged); + } + + + + public Task OpenCallout() + { + return HandleOnClick(); + } + protected override string RootElementClass { get; } = "bit-dtp"; @@ -462,7 +491,7 @@ protected override void RegisterCssClasses() ClassBuilder.Register(() => IconLocation is BitIconLocation.Left ? "bit-dtp-lic" : string.Empty); - ClassBuilder.Register(() => IsUnderlined ? "bit-dtp-und" : string.Empty); + ClassBuilder.Register(() => Underlined ? "bit-dtp-und" : string.Empty); ClassBuilder.Register(() => HasBorder is false ? "bit-dtp-nbd" : string.Empty); @@ -494,6 +523,16 @@ protected override void OnInitialized() base.OnInitialized(); } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (firstRender is false) return; + if (Responsive is false) return; + + await _js.SwipesSetup(_calloutId, 0.25m, SwipesPosition.Top, Dir is BitDir.Rtl, _dotnetObj); + } + protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out DateTimeOffset? result, [NotNullWhen(false)] out string? validationErrorMessage) { if (value.HasNoValue()) @@ -522,18 +561,6 @@ protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(fa : null; } - protected override void Dispose(bool disposing) - { - if (disposing) - { - _dotnetObj?.Dispose(); - _cancellationTokenSource?.Dispose(); - OnValueChanged -= HandleOnValueChanged; - } - - base.Dispose(disposing); - } - private async Task HandleOnClick() @@ -543,11 +570,14 @@ private async Task HandleOnClick() if (await AssignIsOpen(true) is false) return; - var result = await ToggleCallout(); + ResetPickersState(); + + var bodyWidth = await _js.GetBodyWidth(); + var notEnoughWidthAvailable = bodyWidth < MAX_WIDTH; if (_showMonthPickerAsOverlayInternal is false) { - _showMonthPickerAsOverlayInternal = result; + _showMonthPickerAsOverlayInternal = notEnoughWidthAvailable; } if (_showMonthPickerAsOverlayInternal) @@ -557,7 +587,7 @@ private async Task HandleOnClick() if (_showTimePickerAsOverlayInternal is false) { - _showTimePickerAsOverlayInternal = result; + _showTimePickerAsOverlayInternal = notEnoughWidthAvailable; } if (_showTimePickerAsOverlayInternal) @@ -570,6 +600,8 @@ private async Task HandleOnClick() CheckCurrentCalendarMatchesCurrentValue(); } + await ToggleCallout(); + await OnClick.InvokeAsync(); } @@ -668,10 +700,7 @@ private void OnSetParameters() if (Standalone) { - _isMonthPickerOverlayOnTop = false; - _showMonthPickerAsOverlayInternal = ShowMonthPickerAsOverlay; - _isTimePickerOverlayOnTop = false; - _showTimePickerAsOverlayInternal = ShowTimePickerAsOverlay; + ResetPickersState(); if (_showMonthPickerAsOverlayInternal) { @@ -1440,15 +1469,19 @@ private bool ShowMonthPicker() } } - private async Task ToggleCallout() + private void ResetPickersState() { - if (Standalone) return false; - if (IsEnabled is false) return false; - + _showMonthPicker = true; _isMonthPickerOverlayOnTop = false; _showMonthPickerAsOverlayInternal = ShowMonthPickerAsOverlay; _isTimePickerOverlayOnTop = false; _showTimePickerAsOverlayInternal = ShowTimePickerAsOverlay; + } + + private async Task ToggleCallout() + { + if (Standalone) return false; + if (IsEnabled is false) return false; return await _js.ToggleCallout(_dotnetObj, _datePickerId, @@ -1456,14 +1489,56 @@ private async Task ToggleCallout() _calloutId, null, IsOpen, - IsResponsive ? BitResponsiveMode.Top : BitResponsiveMode.None, + Responsive ? BitResponsiveMode.Top : BitResponsiveMode.None, BitDropDirection.TopAndBottom, Dir is BitDir.Rtl, "", 0, "", "", - false, - RootElementClass); + false, + MAX_WIDTH); + } + + private string GetCalloutCssClasses() + { + List classes = ["bit-dtp-cal"]; + + if (Classes?.Callout is not null) + { + classes.Add(Classes.Callout); + } + + if (Responsive) + { + classes.Add("bit-dtp-res"); + } + + return string.Join(' ', classes).Trim(); + } + + + + public async ValueTask DisposeAsync() + { + await DisposeAsync(true); + GC.SuppressFinalize(this); + } + + protected virtual async ValueTask DisposeAsync(bool disposing) + { + if (_disposed || disposing is false) return; + + _cancellationTokenSource?.Dispose(); + OnValueChanged -= HandleOnValueChanged; + + try + { + await _js.ClearCallout(_calloutId); + await _js.SwipesDispose(_calloutId); + } + catch (JSDisconnectedException) { } // we can ignore this exception here + + _disposed = true; } } diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.scss b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.scss index eac76ee7e7..8f3a67c06b 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.scss +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DatePicker/BitDatePicker.scss @@ -1,4 +1,5 @@ @import "../../../../Styles/functions.scss"; +@import "../../../../Styles/media-queries.scss"; .bit-dtp { margin: 0; @@ -186,85 +187,6 @@ animation-timing-function: cubic-bezier(0.1, 0.9, 0.2, 1); } -.bit-dtp-rsp { - .bit-dtp-dwp, - .bit-dtp-mwp, - .bit-dtp-twp { - flex-grow: 1; - } - - .bit-dtp-pkh { - min-height: spacing(5.5); - } - - .bit-dtp-pkc { - height: 100%; - display: flex; - flex-direction: column; - justify-content: space-around; - } - - .bit-dtp-pkr { - height: 100%; - } - - .bit-dtp-nbt, - .bit-dtp-gtb, - .bit-dtp-gtn, - .bit-dtp-wlb, - .bit-dtp-dbt, - .bit-dtp-pkb, - .bit-dtp-wnm { - width: 100%; - height: 100%; - display: flex; - aspect-ratio: 1; - max-width: unset; - align-items: center; - font-size: spacing(2); - justify-content: center; - } - - .bit-dtp-nbt, - .bit-dtp-gtb, - .bit-dtp-gtn { - width: spacing(5.5); - } - - .bit-dtp-wlb { - font-weight: bold; - } - - .bit-dtp-grp { - justify-content: center; - } - - .bit-dtp-tdv { - font-size: spacing(7); - } - - .bit-dtp-tin { - width: 100%; - min-width: spacing(7); - max-width: spacing(12); - font-size: spacing(5); - } - - .bit-dtp-tbt { - width: 100%; - min-width: spacing(7); - max-width: spacing(12); - font-size: spacing(3); - } - - .bit-dtp-bam, - .bit-dtp-bpm { - width: 100%; - font-size: spacing(4); - margin: spacing(0.25); - } -} - .bit-dtp-cac { height: 100%; outline: none; @@ -823,4 +745,97 @@ animation: none; position: relative; } -} \ No newline at end of file +} + + +.bit-dtp-res { + @include lt-sm { + top: 0; + left: 0; + opacity: 0; + width: 100%; + display: block; + overflow: hidden; + animation-name: unset; + transform: translateY(-100%); + box-shadow: $box-shadow-callout; + transition: transform 200ms ease-out, opacity 100ms linear; + + .bit-dtp-dwp, + .bit-dtp-mwp, + .bit-dtp-twp { + flex-grow: 1; + } + + .bit-dtp-pkh { + min-height: spacing(5.5); + } + + .bit-dtp-pkc { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-around; + } + + .bit-dtp-pkr { + height: 100%; + } + + .bit-dtp-nbt, + .bit-dtp-gtb, + .bit-dtp-gtn, + .bit-dtp-wlb, + .bit-dtp-dbt, + .bit-dtp-pkb, + .bit-dtp-wnm { + width: 100%; + height: 100%; + display: flex; + aspect-ratio: 1; + max-width: unset; + align-items: center; + justify-content: center; + font-size: spacing(1.75); + } + + .bit-dtp-nbt, + .bit-dtp-gtb, + .bit-dtp-gtn { + width: spacing(5.5); + } + + .bit-dtp-wlb { + //font-weight: bold; + } + + .bit-dtp-grp { + justify-content: center; + } + + .bit-dtp-tdv { + //font-size: spacing(7); + } + + .bit-dtp-tin { + width: 100%; + min-width: spacing(7); + max-width: spacing(12); + //font-size: spacing(5); + } + + .bit-dtp-tbt { + width: 100%; + min-width: spacing(7); + max-width: spacing(12); + //font-size: spacing(3); + } + + .bit-dtp-bam, + .bit-dtp-bpm { + width: 100%; + //font-size: spacing(4); + margin: spacing(0.25); + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DateRangePicker/BitDateRangePicker.razor b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DateRangePicker/BitDateRangePicker.razor index 80d4233f48..8ca8a6d6b0 100644 --- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DateRangePicker/BitDateRangePicker.razor +++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/_Pickers/DateRangePicker/BitDateRangePicker.razor @@ -86,7 +86,9 @@ readonly="@(AllowTextInput is false)" /> } -
+