diff --git a/.github/workflows/Publish Flux.yml b/.github/workflows/Publish Flux.yml index e39887b..78e3386 100644 --- a/.github/workflows/Publish Flux.yml +++ b/.github/workflows/Publish Flux.yml @@ -38,3 +38,5 @@ jobs: dotnet nuget push BitzArt.Flux.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_APIKEY} dotnet nuget push BitzArt.Flux.REST.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_APIKEY} dotnet nuget push BitzArt.Flux.Json.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_APIKEY} + dotnet nuget push BitzArt.Flux.MudBlazor.${VERSION}.nupkg --source https://api.nuget.org/v3/index.json --api-key ${NUGET_APIKEY} + diff --git a/BitzArt.Flux.sln b/BitzArt.Flux.sln index a3624f9..6304afd 100644 --- a/BitzArt.Flux.sln +++ b/BitzArt.Flux.sln @@ -30,9 +30,21 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{83E6C7 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BitzArt.Flux.SampleApp", "sample\BitzArt.Flux.SampleApp\BitzArt.Flux.SampleApp.csproj", "{2D85CE63-7FB6-4D97-81EF-44351A94A883}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitzArt.Flux.Json", "src\BitzArt.Flux.Json\BitzArt.Flux.Json.csproj", "{72FE978D-615D-49AF-B69A-4D676EA7254F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BitzArt.Flux.Json", "src\BitzArt.Flux.Json\BitzArt.Flux.Json.csproj", "{72FE978D-615D-49AF-B69A-4D676EA7254F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitzArt.Flux.Json.Tests", "tests\BitzArt.Flux.Json.Tests\BitzArt.Flux.Json.Tests.csproj", "{2ED3365D-9DC2-4177-866F-46D1A15CBF61}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BitzArt.Flux.Json.Tests", "tests\BitzArt.Flux.Json.Tests\BitzArt.Flux.Json.Tests.csproj", "{2ED3365D-9DC2-4177-866F-46D1A15CBF61}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MudBlazor", "MudBlazor", "{FB04F142-5BDC-4B3A-9EFF-AFE14F9A7014}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BitzArt.Flux.MudBlazor", "src\MudBlazor\BitzArt.Flux.MudBlazor\BitzArt.Flux.MudBlazor.csproj", "{AB6F442A-B032-41A0-955D-467F66CECD53}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MudBlazor", "MudBlazor", "{0BA6ECD8-3FF5-4E88-8C65-F169F184FE33}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MudBlazor.SampleApp.Shared", "sample\MudBlazor\MudBlazor.SampleApp.Shared\MudBlazor.SampleApp.Shared.csproj", "{F2A4D102-0354-4D03-8D59-8953C8A6056A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MudBlazor.SampleApp", "sample\MudBlazor\MudBlazor.SampleApp\MudBlazor.SampleApp.csproj", "{EFCA3026-BF48-46F3-BAF4-9B6A5DC39A0F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MudBlazor.SampleApp.Client", "sample\MudBlazor\MudBlazor.SampleApp.Client\MudBlazor.SampleApp.Client.csproj", "{1D0CBDFE-7989-40B5-AFED-92D1BA64C36B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -68,6 +80,22 @@ Global {2ED3365D-9DC2-4177-866F-46D1A15CBF61}.Debug|Any CPU.Build.0 = Debug|Any CPU {2ED3365D-9DC2-4177-866F-46D1A15CBF61}.Release|Any CPU.ActiveCfg = Release|Any CPU {2ED3365D-9DC2-4177-866F-46D1A15CBF61}.Release|Any CPU.Build.0 = Release|Any CPU + {AB6F442A-B032-41A0-955D-467F66CECD53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB6F442A-B032-41A0-955D-467F66CECD53}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB6F442A-B032-41A0-955D-467F66CECD53}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB6F442A-B032-41A0-955D-467F66CECD53}.Release|Any CPU.Build.0 = Release|Any CPU + {F2A4D102-0354-4D03-8D59-8953C8A6056A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2A4D102-0354-4D03-8D59-8953C8A6056A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2A4D102-0354-4D03-8D59-8953C8A6056A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2A4D102-0354-4D03-8D59-8953C8A6056A}.Release|Any CPU.Build.0 = Release|Any CPU + {EFCA3026-BF48-46F3-BAF4-9B6A5DC39A0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFCA3026-BF48-46F3-BAF4-9B6A5DC39A0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFCA3026-BF48-46F3-BAF4-9B6A5DC39A0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFCA3026-BF48-46F3-BAF4-9B6A5DC39A0F}.Release|Any CPU.Build.0 = Release|Any CPU + {1D0CBDFE-7989-40B5-AFED-92D1BA64C36B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D0CBDFE-7989-40B5-AFED-92D1BA64C36B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D0CBDFE-7989-40B5-AFED-92D1BA64C36B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D0CBDFE-7989-40B5-AFED-92D1BA64C36B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -81,6 +109,12 @@ Global {2D85CE63-7FB6-4D97-81EF-44351A94A883} = {83E6C753-C4C4-4C4E-8627-E9077F557159} {72FE978D-615D-49AF-B69A-4D676EA7254F} = {A0FAE66D-5574-463E-8723-E28ECEE565EE} {2ED3365D-9DC2-4177-866F-46D1A15CBF61} = {A3976FBB-F37A-4468-955D-8B2FCC79ACD0} + {FB04F142-5BDC-4B3A-9EFF-AFE14F9A7014} = {A0FAE66D-5574-463E-8723-E28ECEE565EE} + {AB6F442A-B032-41A0-955D-467F66CECD53} = {FB04F142-5BDC-4B3A-9EFF-AFE14F9A7014} + {0BA6ECD8-3FF5-4E88-8C65-F169F184FE33} = {83E6C753-C4C4-4C4E-8627-E9077F557159} + {F2A4D102-0354-4D03-8D59-8953C8A6056A} = {0BA6ECD8-3FF5-4E88-8C65-F169F184FE33} + {EFCA3026-BF48-46F3-BAF4-9B6A5DC39A0F} = {0BA6ECD8-3FF5-4E88-8C65-F169F184FE33} + {1D0CBDFE-7989-40B5-AFED-92D1BA64C36B} = {0BA6ECD8-3FF5-4E88-8C65-F169F184FE33} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {251D5A13-6C1B-4B0D-A6F5-3B95FF4C0F54} diff --git a/sample/BitzArt.Flux.SampleApp/BitzArt.Flux.SampleApp.csproj b/sample/BitzArt.Flux.SampleApp/BitzArt.Flux.SampleApp.csproj index 47d5b65..4d30c6d 100644 --- a/sample/BitzArt.Flux.SampleApp/BitzArt.Flux.SampleApp.csproj +++ b/sample/BitzArt.Flux.SampleApp/BitzArt.Flux.SampleApp.csproj @@ -9,7 +9,7 @@ - + diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Client/Layout/MainLayout.razor b/sample/MudBlazor/MudBlazor.SampleApp.Client/Layout/MainLayout.razor new file mode 100644 index 0000000..ae6f235 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Client/Layout/MainLayout.razor @@ -0,0 +1,104 @@ +@inherits LayoutComponentBase + + + + + + + + + Application + + + + + + + + + @Body + + + + +
+ An unhandled error has occurred. + Reload + 🗙 +
+ +@code { + private bool _drawerOpen = true; + private bool _isDarkMode = true; + private MudTheme? _theme = null; + + protected override void OnInitialized() + { + base.OnInitialized(); + + _theme = new() + { + PaletteLight = _lightPalette, + PaletteDark = _darkPalette, + LayoutProperties = new LayoutProperties() + }; + } + + + private void DrawerToggle() + { + _drawerOpen = !_drawerOpen; + } + + private void DarkModeToggle() + { + _isDarkMode = !_isDarkMode; + } + + private readonly PaletteLight _lightPalette = new() + { + Black = "#110e2d", + AppbarText = "#424242", + AppbarBackground = "rgba(255,255,255,0.8)", + DrawerBackground = "#ffffff", + GrayLight = "#e8e8e8", + GrayLighter = "#f9f9f9", + }; + + private readonly PaletteDark _darkPalette = new() + { + Primary = "#7e6fff", + Surface = "#1e1e2d", + Background = "#1a1a27", + BackgroundGray = "#151521", + AppbarText = "#92929f", + AppbarBackground = "rgba(26,26,39,0.8)", + DrawerBackground = "#1a1a27", + ActionDefault = "#74718e", + ActionDisabled = "#9999994d", + ActionDisabledBackground = "#605f6d4d", + TextPrimary = "#b2b0bf", + TextSecondary = "#92929f", + TextDisabled = "#ffffff33", + DrawerIcon = "#92929f", + DrawerText = "#92929f", + GrayLight = "#2a2833", + GrayLighter = "#1e1e2d", + Info = "#4a86ff", + Success = "#3dcb6c", + Warning = "#ffb545", + Error = "#ff3f5f", + LinesDefault = "#33323e", + TableLines = "#33323e", + Divider = "#292838", + OverlayLight = "#1e1e2d80", + }; + + public string DarkLightModeButtonIcon => _isDarkMode switch + { + true => Icons.Material.Rounded.AutoMode, + false => Icons.Material.Outlined.DarkMode, + }; +} + + diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Client/Layout/NavMenu.razor b/sample/MudBlazor/MudBlazor.SampleApp.Client/Layout/NavMenu.razor new file mode 100644 index 0000000..d97dea6 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Client/Layout/NavMenu.razor @@ -0,0 +1,8 @@ + + + Home + Books + + + + diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Client/MudBlazor.SampleApp.Client.csproj b/sample/MudBlazor/MudBlazor.SampleApp.Client/MudBlazor.SampleApp.Client.csproj new file mode 100644 index 0000000..5cc75b9 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Client/MudBlazor.SampleApp.Client.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + Default + MudBlazor.SampleApp.Client + + + + + + + + + + + + + diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Client/Pages/BooksPage.razor b/sample/MudBlazor/MudBlazor.SampleApp.Client/Pages/BooksPage.razor new file mode 100644 index 0000000..fb8ee3a --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Client/Pages/BooksPage.razor @@ -0,0 +1,93 @@ +@page "/books" + +@if (_initialized) +{ +
+ + + + + Books + + + + + + Select Author + @foreach (var author in _authors) + { + @author.Name + } + + + + + + + + + + + + + + + ID + + + + + Author + + + + + Title + + + + + Publish Year + + + + + + @context.Id + + + @_authors.FirstOrDefault(x => x.Id == context.AuthorId)?.Name + + + @context.Title + + + @context.PublishYearDisplay + + + + + + +
+} \ No newline at end of file diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Client/Pages/BooksPage.razor.cs b/sample/MudBlazor/MudBlazor.SampleApp.Client/Pages/BooksPage.razor.cs new file mode 100644 index 0000000..482d263 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Client/Pages/BooksPage.razor.cs @@ -0,0 +1,56 @@ +using BitzArt.Flux; +using BitzArt.Flux.MudBlazor; +using Microsoft.AspNetCore.Components; +using System.Web; + +namespace MudBlazor.SampleApp.Client.Pages; + +public partial class BooksPage : ComponentBase +{ + private IEnumerable _authors = null!; + private Author? _selectedAuthor; + + private string? _search; + + [Inject] private IFluxSetContext Authors { get; set; } = null!; + [Inject] private IFluxSetDataProvider BooksDataProvider { get; set; } = null!; + + private bool _initialized = false; + + protected override async Task OnInitializedAsync() + { + BooksDataProvider.GetParameters = GetBooksParameters; + + await base.OnInitializedAsync(); + _authors = await Authors.GetAllAsync(); + _initialized = true; + + await InvokeAsync(StateHasChanged); + } + + private object[] GetBooksParameters(TableState state) + { + var query = HttpUtility.ParseQueryString(string.Empty); + + if (_selectedAuthor is not null) query["authorId"] = _selectedAuthor.Id.ToString(); + if (!string.IsNullOrWhiteSpace(state.SortLabel)) query["order"] = state.SortLabel; + if (state.SortDirection == SortDirection.Descending) query["desc"] = "true"; + if (!string.IsNullOrWhiteSpace(_search)) query["search"] = _search; + + var queryString = query.Count > 0 ? $"?{query}" : string.Empty; + + return [queryString]; + } + + private async Task OnAuthorSelectedAsync(Author author) + { + _selectedAuthor = author; + await BooksDataProvider.ResetAndReloadAsync(); + } + + private async Task OnSearchAsync(string value) + { + _search = value; + await BooksDataProvider.ResetAndReloadAsync(); + } +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Client/Pages/BooksPage.razor.css b/sample/MudBlazor/MudBlazor.SampleApp.Client/Pages/BooksPage.razor.css new file mode 100644 index 0000000..1d30073 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Client/Pages/BooksPage.razor.css @@ -0,0 +1,9 @@ +.books-page ::deep .mud-table .mud-table-toolbar { + height: inherit !important; + padding-inline-start: 1rem !important; + padding-inline-end: 1rem !important; +} + +.books-page ::deep .mud-table .mud-table-smalldevices-sortselect { + padding-top: 1rem !important; +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Client/Pages/HomePage.razor b/sample/MudBlazor/MudBlazor.SampleApp.Client/Pages/HomePage.razor new file mode 100644 index 0000000..4261525 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Client/Pages/HomePage.razor @@ -0,0 +1,10 @@ +@page "/" + +Home + +Hello, world! + + + This sample application shows how you can use Flux with MudBlazor.
+ Navigate to the Books page to see it in action. +
\ No newline at end of file diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Client/Program.cs b/sample/MudBlazor/MudBlazor.SampleApp.Client/Program.cs new file mode 100644 index 0000000..0ba12ab --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Client/Program.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using MudBlazor.Services; + +namespace MudBlazor.SampleApp.Client; + +internal class Program +{ + static async Task Main(string[] args) + { + var builder = WebAssemblyHostBuilder.CreateDefault(args); + + builder.Services.AddMudServices(); + + builder.Services.AddFlux(builder.HostEnvironment.BaseAddress); + + await builder.Build().RunAsync(); + } +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Client/Routes.razor b/sample/MudBlazor/MudBlazor.SampleApp.Client/Routes.razor new file mode 100644 index 0000000..f756e19 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Client/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Client/_Imports.razor b/sample/MudBlazor/MudBlazor.SampleApp.Client/_Imports.razor new file mode 100644 index 0000000..cb62c6f --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Client/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using global::MudBlazor +@using global::MudBlazor.Services \ No newline at end of file diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Client/appsettings.Development.json b/sample/MudBlazor/MudBlazor.SampleApp.Client/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Client/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Client/appsettings.json b/sample/MudBlazor/MudBlazor.SampleApp.Client/appsettings.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Client/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Shared/AddFluxExtension.cs b/sample/MudBlazor/MudBlazor.SampleApp.Shared/AddFluxExtension.cs new file mode 100644 index 0000000..404f336 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Shared/AddFluxExtension.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.DependencyInjection; +using BitzArt.Flux; +using BitzArt.Flux.MudBlazor; + +namespace MudBlazor.SampleApp; + +public static class AddFluxExtension +{ + public static void AddFlux(this IServiceCollection services, string baseUrl) + { + services.AddFlux(x => + { + x.AddService("library-web-api") + .UsingRest(baseUrl.TrimEnd('/') + "/api") + .AddSet() + .WithEndpoint("authors") + .AddSet() + .WithEndpoint("books") + .WithPageEndpoint("books{query}"); + }); + + services.AddFluxSetDataProvider(); + } +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Shared/Models/Author.cs b/sample/MudBlazor/MudBlazor.SampleApp.Shared/Models/Author.cs new file mode 100644 index 0000000..07e26e3 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Shared/Models/Author.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace MudBlazor.SampleApp; + +public class Author +{ + [JsonPropertyName("id")] + public int? Id { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Shared/Models/Book.cs b/sample/MudBlazor/MudBlazor.SampleApp.Shared/Models/Book.cs new file mode 100644 index 0000000..7ad4556 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Shared/Models/Book.cs @@ -0,0 +1,29 @@ +using System.Text.Json.Serialization; + +namespace MudBlazor.SampleApp; + +public class Book +{ + [JsonPropertyName("id")] + public int? Id { get; set; } + + [JsonPropertyName("title")] + public string? Title { get; set; } + + [JsonPropertyName("authorId")] + public int? AuthorId { get; set; } + + [JsonPropertyName("publishYear")] + public int? PublishYear { get; set; } + + public string PublishYearDisplay + { + get + { + if (!PublishYear.HasValue) return string.Empty; + if (PublishYear < 0) return $"~{-PublishYear} B.C."; + if (PublishYear < 1000) return $"{PublishYear} A.D."; + return PublishYear.ToString()!; + } + } +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp.Shared/MudBlazor.SampleApp.Shared.csproj b/sample/MudBlazor/MudBlazor.SampleApp.Shared/MudBlazor.SampleApp.Shared.csproj new file mode 100644 index 0000000..ced3fd2 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp.Shared/MudBlazor.SampleApp.Shared.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + MudBlazor.SampleApp + + + + + + + + + + + + + diff --git a/sample/MudBlazor/MudBlazor.SampleApp/Components/App.razor b/sample/MudBlazor/MudBlazor.SampleApp/Components/App.razor new file mode 100644 index 0000000..582a428 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp/Components/App.razor @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/MudBlazor/MudBlazor.SampleApp/Components/Pages/Error.razor b/sample/MudBlazor/MudBlazor.SampleApp/Components/Pages/Error.razor new file mode 100644 index 0000000..576cc2d --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp/Components/_Imports.razor b/sample/MudBlazor/MudBlazor.SampleApp/Components/_Imports.razor new file mode 100644 index 0000000..86c45a4 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp/Components/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using global::MudBlazor +@using global::MudBlazor.Services +@using MudBlazor.SampleApp +@using MudBlazor.SampleApp.Client +@using MudBlazor.SampleApp.Components diff --git a/sample/MudBlazor/MudBlazor.SampleApp/Data/MapDataEndpointsExtension.cs b/sample/MudBlazor/MudBlazor.SampleApp/Data/MapDataEndpointsExtension.cs new file mode 100644 index 0000000..10d0c8d --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp/Data/MapDataEndpointsExtension.cs @@ -0,0 +1,62 @@ +using BitzArt.Pagination; +using Microsoft.AspNetCore.Mvc; + +namespace MudBlazor.SampleApp; + +internal static class MapDataEndpointsExtension +{ + public static void MapDataEndpoints(this WebApplication app) + { + app.MapGet("/api/authors", () + => Results.Ok(WebApiData.Authors)); + + app.MapGet("/api/authors/{id:int}", ( + [FromRoute] int id) + => Results.Ok(WebApiData.Authors.FirstOrDefault(x => x.Id == id))); + + app.MapGet("/api/authors/{authorId:int}/books", ( + [FromRoute] int authorId, + [FromQuery] int offset = 0, + [FromQuery] int limit = 10) + => Results.Ok(WebApiData.BooksMap[authorId].ToPage(offset, limit))); + + app.MapGet("/api/books", ( + [FromQuery] int offset = 0, + [FromQuery] int limit = 5, + [FromQuery] int? authorId = null, + [FromQuery] string? search = null, + [FromQuery] string? order = null, + [FromQuery] bool desc = false) => + { + if (limit > 5) limit = 5; + + var q = WebApiData.Books.AsQueryable(); + + if (authorId.HasValue) q = q.Where(x => x.AuthorId == authorId); + if (!string.IsNullOrWhiteSpace(search)) q = q.Where(x => x.Title!.Contains(search, StringComparison.OrdinalIgnoreCase)); + if (!string.IsNullOrWhiteSpace(order)) + { + if (order.Equals("id", StringComparison.CurrentCultureIgnoreCase)) q = !desc + ? q.OrderBy(x => x.Id) + : q.OrderByDescending(x => x.Id); + + if (order.Equals("author", StringComparison.CurrentCultureIgnoreCase)) q = !desc + ? q.OrderBy(x => x.AuthorId) + : q.OrderByDescending(x => x.AuthorId); + + if (order.Equals("title", StringComparison.CurrentCultureIgnoreCase)) q = !desc + ? q.OrderBy(x => x.Title) + : q.OrderByDescending(x => x.Title); + + if (order.Equals("publish", StringComparison.CurrentCultureIgnoreCase)) q = !desc + ? q.OrderBy(x => x.PublishYear) + : q.OrderByDescending(x => x.PublishYear); + } + + return Results.Ok(q.ToPage(offset, limit)); + }); + + app.MapGet("/api/books/{bookId:int}", (int bookId) + => Results.Ok(WebApiData.Books.FirstOrDefault(x => x.Id == bookId))); + } +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp/Data/WebApiData.cs b/sample/MudBlazor/MudBlazor.SampleApp/Data/WebApiData.cs new file mode 100644 index 0000000..08c35d1 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp/Data/WebApiData.cs @@ -0,0 +1,92 @@ +namespace MudBlazor.SampleApp; + +internal static class WebApiData +{ + public static readonly Author[] Authors = + [ + new() { Id = 1, Name = "George R. R. Martin" }, + new() { Id = 2, Name = "J. R. R. Tolkien" }, + new() { Id = 3, Name = "Stephen King" }, + new() { Id = 4, Name = "J. K. Rowling" }, + new() { Id = 5, Name = "Dante Alighieri" }, + new() { Id = 6, Name = "Homer" }, + new() { Id = 7, Name = "Virgil" } + ]; + + public static readonly Book[] Books = + [ + new() { Id = 101, Title = "A Game of Thrones", AuthorId = 1, PublishYear = 1996 }, + new() { Id = 102, Title = "A Clash of Kings", AuthorId = 1, PublishYear = 1998 }, + new() { Id = 103, Title = "A Storm of Swords", AuthorId = 1, PublishYear = 2000 }, + new() { Id = 104, Title = "A Feast for Crows", AuthorId = 1, PublishYear = 2005 }, + new() { Id = 105, Title = "A Dance with Dragons", AuthorId = 1, PublishYear = 2011 }, + new() { Id = 106, Title = "The Winds of Winter", AuthorId = 1, PublishYear = 2023 }, + + new() { Id = 201, Title = "The Hobbit", AuthorId = 2, PublishYear = 1937 }, + new() { Id = 202, Title = "The Lord of the Rings", AuthorId = 2, PublishYear = 1954 }, + new() { Id = 203, Title = "The Silmarillion", AuthorId = 2, PublishYear = 1977 }, + new() { Id = 204, Title = "Unfinished Tales", AuthorId = 2, PublishYear = 1980 }, + + new() { Id = 301, Title = "Carrie", AuthorId = 3, PublishYear = 1974 }, + new() { Id = 302, Title = "The Shining", AuthorId = 3, PublishYear = 1977 }, + new() { Id = 303, Title = "It", AuthorId = 3, PublishYear = 1986 }, + new() { Id = 304, Title = "The Stand", AuthorId = 3, PublishYear = 1978 }, + new() { Id = 305, Title = "Misery", AuthorId = 3, PublishYear = 1987 }, + new() { Id = 306, Title = "The Dark Tower", AuthorId = 3, PublishYear = 1982 }, + + new() { Id = 401, Title = "Harry Potter and the Philosopher's Stone", AuthorId = 4, PublishYear = 1997 }, + new() { Id = 402, Title = "Harry Potter and the Chamber of Secrets", AuthorId = 4, PublishYear = 1998 }, + new() { Id = 403, Title = "Harry Potter and the Prisoner of Azkaban", AuthorId = 4, PublishYear = 1999 }, + new() { Id = 404, Title = "Harry Potter and the Goblet of Fire", AuthorId = 4, PublishYear = 2000 }, + new() { Id = 405, Title = "Harry Potter and the Order of the Phoenix", AuthorId = 4, PublishYear = 2003 }, + new() { Id = 406, Title = "Harry Potter and the Half-Blood Prince", AuthorId = 4, PublishYear = 2005 }, + new() { Id = 407, Title = "Harry Potter and the Deathly Hallows", AuthorId = 4, PublishYear = 2007 }, + + new() { Id = 501, Title = "Inferno", AuthorId = 5, PublishYear = 1320 }, + new() { Id = 502, Title = "Purgatorio", AuthorId = 5, PublishYear = 1321 }, + new() { Id = 503, Title = "Paradiso", AuthorId = 5, PublishYear = 1321 }, + new() { Id = 504, Title = "La Vita Nuova", AuthorId = 5, PublishYear = 1295 }, + new() { Id = 505, Title = "De Monarchia", AuthorId = 5, PublishYear = 1313 }, + new() { Id = 506, Title = "Convivio", AuthorId = 5, PublishYear = 1307 }, + new() { Id = 507, Title = "Rime", AuthorId = 5, PublishYear = 1292 }, + new() { Id = 508, Title = "Vita Nuova", AuthorId = 5, PublishYear = 1295 }, + new() { Id = 509, Title = "De Vulgari Eloquentia", AuthorId = 5, PublishYear = 1302 }, + new() { Id = 510, Title = "Eclogues", AuthorId = 5, PublishYear = 1308 }, + new() { Id = 511, Title = "Quaestio de Aqua et Terra", AuthorId = 5, PublishYear = 1320 }, + new() { Id = 512, Title = "Epistle to Cangrande", AuthorId = 5, PublishYear = 1314 }, + new() { Id = 513, Title = "Epistle to the Florentines", AuthorId = 5, PublishYear = 1304 }, + new() { Id = 514, Title = "Corpus Christi", AuthorId = 5, PublishYear = 1304 }, + new() { Id = 515, Title = "De Monarchia", AuthorId = 5, PublishYear = 1313 }, + + new() { Id = 601, Title = "The Iliad", AuthorId = 6, PublishYear = -750 }, + new() { Id = 602, Title = "The Odyssey", AuthorId = 6, PublishYear = -720 }, + new() { Id = 603, Title = "The Homeric Hymns", AuthorId = 6, PublishYear = -700 }, + new() { Id = 604, Title = "The Epic Cycle", AuthorId = 6, PublishYear = -750 }, + new() { Id = 605, Title = "The Theban Cycle", AuthorId = 6, PublishYear = -750 }, + new() { Id = 606, Title = "The Trojan Cycle", AuthorId = 6, PublishYear = -750 }, + new() { Id = 607, Title = "The Cypria", AuthorId = 6, PublishYear = -750 }, + new() { Id = 608, Title = "The Aethiopis", AuthorId = 6, PublishYear = -750 }, + new() { Id = 609, Title = "The Little Iliad", AuthorId = 6, PublishYear = -750 }, + new() { Id = 610, Title = "The Sack of Troy", AuthorId = 6, PublishYear = -750 }, + new() { Id = 611, Title = "The Returns", AuthorId = 6, PublishYear = -750 }, + new() { Id = 612, Title = "The Telegony", AuthorId = 6, PublishYear = -750 }, + new() { Id = 613, Title = "The Thebaid", AuthorId = 6, PublishYear = -750 }, + new() { Id = 614, Title = "The Epigoni", AuthorId = 6, PublishYear = -750 }, + new() { Id = 615, Title = "The Cyclic Epics", AuthorId = 6, PublishYear = -750 }, + + new() { Id = 701, Title = "Aeneid", AuthorId = 7, PublishYear = 19 }, + new() { Id = 702, Title = "Eclogues", AuthorId = 7, PublishYear = 37 }, + new() { Id = 703, Title = "Georgics", AuthorId = 7, PublishYear = 29 }, + new() { Id = 704, Title = "Culex", AuthorId = 7, PublishYear = 29 }, + new() { Id = 705, Title = "Ciris", AuthorId = 7, PublishYear = 29 }, + new() { Id = 706, Title = "Aetna", AuthorId = 7, PublishYear = 29 }, + new() { Id = 707, Title = "The Disaster of the City of Rome", AuthorId = 7, PublishYear = 29 }, + new() { Id = 708, Title = "Thebaid", AuthorId = 7, PublishYear = 29 }, + new() { Id = 709, Title = "Punica", AuthorId = 7, PublishYear = 17 }, + new() { Id = 710, Title = "Silvae", AuthorId = 7, PublishYear = 96 } + ]; + + public static readonly IDictionary> BooksMap = Books + .GroupBy(x => x.AuthorId!.Value) + .ToDictionary(x => x.Key, x => x.AsEnumerable()); +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp/MudBlazor.SampleApp.csproj b/sample/MudBlazor/MudBlazor.SampleApp/MudBlazor.SampleApp.csproj new file mode 100644 index 0000000..0c1b016 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp/MudBlazor.SampleApp.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + MudBlazor.SampleApp + + + + + + + + + diff --git a/sample/MudBlazor/MudBlazor.SampleApp/Program.cs b/sample/MudBlazor/MudBlazor.SampleApp/Program.cs new file mode 100644 index 0000000..670dbf5 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp/Program.cs @@ -0,0 +1,33 @@ +using MudBlazor.SampleApp.Components; +using MudBlazor.Services; + +namespace MudBlazor.SampleApp +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddMudServices(); + builder.Services.AddRazorComponents() + .AddInteractiveWebAssemblyComponents(); + + builder.Services.AddFlux("http://localhost"); + + var app = builder.Build(); + + app.UseWebAssemblyDebugging(); + + app.UseStaticFiles(); + app.UseAntiforgery(); + + app.MapDataEndpoints(); + app.MapRazorComponents() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(Client._Imports).Assembly); + + app.Run(); + } + } +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp/Properties/launchSettings.json b/sample/MudBlazor/MudBlazor.SampleApp/Properties/launchSettings.json new file mode 100644 index 0000000..1cd8080 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:25069", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5213", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/sample/MudBlazor/MudBlazor.SampleApp/appsettings.Development.json b/sample/MudBlazor/MudBlazor.SampleApp/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp/appsettings.json b/sample/MudBlazor/MudBlazor.SampleApp/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/sample/MudBlazor/MudBlazor.SampleApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/sample/MudBlazor/MudBlazor.SampleApp/wwwroot/favicon.ico b/sample/MudBlazor/MudBlazor.SampleApp/wwwroot/favicon.ico new file mode 100644 index 0000000..1239223 Binary files /dev/null and b/sample/MudBlazor/MudBlazor.SampleApp/wwwroot/favicon.ico differ diff --git a/src/BitzArt.Flux.Json/BitzArt.Flux.Json.csproj b/src/BitzArt.Flux.Json/BitzArt.Flux.Json.csproj index 19ba93e..66fbf24 100644 --- a/src/BitzArt.Flux.Json/BitzArt.Flux.Json.csproj +++ b/src/BitzArt.Flux.Json/BitzArt.Flux.Json.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true BitzArt.Flux BitzArt.Flux.Json @@ -27,7 +28,7 @@
- + diff --git a/src/BitzArt.Flux.REST/BitzArt.Flux.REST.csproj b/src/BitzArt.Flux.REST/BitzArt.Flux.REST.csproj index b7ffbf8..9fce41f 100644 --- a/src/BitzArt.Flux.REST/BitzArt.Flux.REST.csproj +++ b/src/BitzArt.Flux.REST/BitzArt.Flux.REST.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true BitzArt.Flux BitzArt.Flux.Rest @@ -27,7 +28,7 @@ - + diff --git a/src/BitzArt.Flux/BitzArt.Flux.csproj b/src/BitzArt.Flux/BitzArt.Flux.csproj index 0c3316e..8a41e91 100644 --- a/src/BitzArt.Flux/BitzArt.Flux.csproj +++ b/src/BitzArt.Flux/BitzArt.Flux.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + true BitzArt.Flux BitzArt.Flux @@ -24,10 +25,10 @@ - + - - + + diff --git a/src/BitzArt.Flux/Extensions/AddFluxExtension.cs b/src/BitzArt.Flux/Extensions/AddFluxExtension.cs index 840b992..dc1b7bd 100644 --- a/src/BitzArt.Flux/Extensions/AddFluxExtension.cs +++ b/src/BitzArt.Flux/Extensions/AddFluxExtension.cs @@ -29,7 +29,7 @@ public static IServiceCollection AddFlux(this IServiceCollection services, Actio private class FluxAlreadyRegisteredException : Exception { - private const string Msg = "Flux is already registered in this IServiceCollection"; + private const string Msg = "Flux is already registered in this service collection"; public FluxAlreadyRegisteredException() : base(Msg) { } } } diff --git a/src/MudBlazor/BitzArt.Flux.MudBlazor/BitzArt.Flux.MudBlazor.csproj b/src/MudBlazor/BitzArt.Flux.MudBlazor/BitzArt.Flux.MudBlazor.csproj new file mode 100644 index 0000000..c7c9dea --- /dev/null +++ b/src/MudBlazor/BitzArt.Flux.MudBlazor/BitzArt.Flux.MudBlazor.csproj @@ -0,0 +1,35 @@ + + + + net8.0 + enable + enable + true + + BitzArt.Flux.MudBlazor + BitzArt + A universal WebApi client + MIT + git + https://github.com/BitzArt/Flux + https://bitzart.github.io/Flux + README.md + + flux-logo-128.png + + + + + + + + + + + + + + + + + diff --git a/src/MudBlazor/BitzArt.Flux.MudBlazor/Extensions/AddFluxSetDataProviderExtension.cs b/src/MudBlazor/BitzArt.Flux.MudBlazor/Extensions/AddFluxSetDataProviderExtension.cs new file mode 100644 index 0000000..f512fb8 --- /dev/null +++ b/src/MudBlazor/BitzArt.Flux.MudBlazor/Extensions/AddFluxSetDataProviderExtension.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace BitzArt.Flux.MudBlazor; + +/// +/// Extension methods for adding to the service collection. +/// +public static class AddFluxSetDataProviderExtension +{ + /// + /// Adds an to the service collection. + /// + /// + /// The to add the provider to + /// Can be used to provide a non-default + /// to allow chaining + public static IServiceCollection AddFluxSetDataProvider( + this IServiceCollection services, + Func>? setContextImplementationFactory = null) + where TModel : class + { + if (setContextImplementationFactory is null) + { + setContextImplementationFactory = (serviceProvider) + => serviceProvider.GetRequiredService>(); + } + + services.AddScoped>(); + + services.AddScoped>(serviceProvider => + { + var provider = serviceProvider.GetRequiredService>(); + var setContext = setContextImplementationFactory!.Invoke(serviceProvider); + provider.SetContext = setContext; + + return provider; + }); + + return services; + } +} diff --git a/src/MudBlazor/BitzArt.Flux.MudBlazor/Interfaces/IFluxSetDataProvider.cs b/src/MudBlazor/BitzArt.Flux.MudBlazor/Interfaces/IFluxSetDataProvider.cs new file mode 100644 index 0000000..1bebdba --- /dev/null +++ b/src/MudBlazor/BitzArt.Flux.MudBlazor/Interfaces/IFluxSetDataProvider.cs @@ -0,0 +1,57 @@ +using MudBlazor; + +namespace BitzArt.Flux.MudBlazor; + +/// +/// Used to provide data to a MudTable component. +/// +/// +public interface IFluxSetDataProvider + where TModel : class +{ + /// + /// MudTable component that uses this data provider. + /// + public MudTable? Table { get; set; } + + /// + /// Function used to get data from the server. + /// + public Func>> Data { get; } + + /// + /// Can be set to provide parameters for the request. + /// + public Func? GetParameters { get; set; } + + /// + /// Resets current page to 0 on next request. + /// + public void ResetPage(); + + /// + /// Resets current page to 0 and reloads the data. + /// + /// + public Task ResetAndReloadAsync(); + + /// + /// Dynamically determine whether to reset page when processing a request or not. + /// + public Func? ShouldResetPage { get; set; } + + /// + /// Whether to reset page when table ordering changes. + /// + public bool ShouldResetPageOnOrderChanged { get; set; } + + /// + /// Whether to reset page when table ordering direction changes (asc/desc). + /// + public bool ShouldResetPageOnOrderDirectionChanged { get; set; } + + /// + /// Dynamically determine whether to reset page when processing a request based on last and new parameters or not. + /// + public Func? ShouldResetPageOnParameters { get; set; } +} \ No newline at end of file diff --git a/src/MudBlazor/BitzArt.Flux.MudBlazor/Models/FluxSetDataPageQuery.cs b/src/MudBlazor/BitzArt.Flux.MudBlazor/Models/FluxSetDataPageQuery.cs new file mode 100644 index 0000000..dba0160 --- /dev/null +++ b/src/MudBlazor/BitzArt.Flux.MudBlazor/Models/FluxSetDataPageQuery.cs @@ -0,0 +1,13 @@ +using MudBlazor; + +namespace BitzArt.Flux.MudBlazor; + +internal record FluxSetDataPageQuery + where TModel : class +{ + public TableState TableState { get; set; } = null!; + + public object[]? Parameters { get; set; } = null!; + + public TableData Result { get; set; } = null!; +} diff --git a/src/MudBlazor/BitzArt.Flux.MudBlazor/Services/FluxSetDataProvider.cs b/src/MudBlazor/BitzArt.Flux.MudBlazor/Services/FluxSetDataProvider.cs new file mode 100644 index 0000000..da6c1a0 --- /dev/null +++ b/src/MudBlazor/BitzArt.Flux.MudBlazor/Services/FluxSetDataProvider.cs @@ -0,0 +1,224 @@ +using BitzArt.Pagination; +using MudBlazor; +using System.Diagnostics.CodeAnalysis; + +namespace BitzArt.Flux.MudBlazor; + +// TODO: ? Extract reset logic ? +// TODO: ? Extract page state comparison logic ? +// TODO: Cleanup and refactor + +internal class FluxSetDataProvider : IFluxSetDataProvider + where TModel : class +{ + public IFluxSetContext SetContext { get; internal set; } = null!; + + public Func>> Data => GetDataAsync; + + public Func? GetParameters { get; set; } = null; + + public FluxSetDataPageQuery? LastQuery { get; private set; } + + private bool _resetting = false; + + private bool _resetPageOnce = false; + private bool ResetPageOnce + { + get + { + if (_resetPageOnce == true) + { + _resetPageOnce = false; + return true; + } + + return false; + } + set => _resetPageOnce = value; + } + + public void ResetPage() + { + ResetPageOnce = true; + } + + public async Task ResetAndReloadAsync() + { + ResetPage(); + + if (Table is null) throw new InvalidOperationException( + "Table component must be forwarded to the flux data provider for it to be able to trigger a reload."); + + await Table!.ReloadServerData(); + } + + public Func? ShouldResetPage { get; set; } + + public bool ShouldResetPageOnOrderChanged { get; set; } = true; + + public bool ShouldResetPageOnOrderDirectionChanged { get; set; } = true; + + public Func? ShouldResetPageOnParameters { get; set; } = null; + + public MudTable? Table { get; set; } + + [SuppressMessage("Usage", "BL0005:Component parameter should not be set outside of its component.")] + public async Task> GetDataAsync(TableState state, CancellationToken cancellationToken) + { + var parameters = GetParameters is not null ? GetParameters(state) : []; + + if (ShouldReset(state, parameters)) + { + if (Table is null) throw new InvalidOperationException( + "Table component must be forwarded to the flux data provider for it to be able to reset current page."); + + Table.CurrentPage = 0; + _resetting = true; + await Table.ReloadServerData(); + + return LastQuery!.Result; + } + + if (_resetting == true) _resetting = false; + + if (CompareWithLastRequest(state, parameters)) return LastQuery!.Result; + + var currentQuery = new FluxSetDataPageQuery() + { + TableState = state, + Parameters = parameters + }; + var page = await SetContext.GetPageAsync(state.Page * state.PageSize, state.PageSize, parameters: parameters); + var result = BuildTableData(page, currentQuery); + + LastQuery = currentQuery; + + return result; + } + + private bool ShouldReset(TableState state, object[] newParameters) + { + // already resetting, do not loop infinitely + if (_resetting) return false; + + // manual page reset requested + if (ResetPageOnce) return true; + + // reset on order change + if (ShouldResetOrderChanged(state)) return true; + + // reset on order direction change + if (ShouldResetOrderDirectionChanged(state)) return true; + + // dynamic reset + if (ShouldResetDynamic()) return true; + + // dynamic reset based on parameters + if (ShouldResetDynamicOnParameters(newParameters)) return true; + + // reset is not requested + return false; + } + + private bool ShouldResetOrderChanged(TableState newState) => + ShouldResetPageOnOrderChanged + && LastQuery is not null + && HasOrderChanged(LastQuery!.TableState, newState); + + private bool ShouldResetOrderDirectionChanged(TableState newState) => + ShouldResetPageOnOrderDirectionChanged + && LastQuery is not null + && HasOrderDirectionChanged(LastQuery.TableState, newState); + + private bool ShouldResetDynamic() => + ShouldResetPage is not null && ShouldResetPage.Invoke() == true; + + private bool ShouldResetDynamicOnParameters(object[] newParameters) + { + var lastParameters = LastQuery?.Parameters; + + return lastParameters is not null + && ShouldResetPageOnParameters is not null + && ShouldResetPageOnParameters!.Invoke(lastParameters, newParameters) == true; + } + + + private static bool HasOrderChanged(TableState lastState, TableState newState) + { + // sort label (column) has changed + if (lastState.SortLabel != newState.SortLabel) return true; + + // no change detected + return false; + } + + private static bool HasOrderDirectionChanged(TableState lastState, TableState newState) + { + // sort direction has changed + if (lastState.SortDirection != newState.SortDirection) return true; + + // no change detected + return false; + } + + private bool CompareWithLastRequest(TableState newState, object[] newParameters) + { + // no last query, no comparison + if (LastQuery is null) return false; + + // state has changed + var pageStateHasChanged = ComparePageStates(LastQuery!.TableState, newState) == false; + if (pageStateHasChanged) return false; + + // parameters have changed + var parametersHaveChanged = CompareParameters(LastQuery!.Parameters, newParameters) == false; + if (parametersHaveChanged) return false; + + // no change detected + return true; + } + + private static bool ComparePageStates(TableState lastState, TableState newState) + { + // page index has changed + if (lastState.Page != newState.Page) return false; + + // page size has changed + if (lastState.PageSize != newState.PageSize) return false; + + // no change detected + return true; + } + + private static bool CompareParameters(object[]? lastParameters, object[] newParameters) + { + // no last parameters, no comparison + if (lastParameters is null) return false; + + // different number of parameters + if (lastParameters.Length != newParameters.Length) return false; + + // compare each parameter + for (var i = 0; i < lastParameters.Length; i++) + { + if (!lastParameters[i].Equals(newParameters[i])) return false; + } + + // no change detected + return true; + } + + // TODO: Extract as extension method ? + private static TableData BuildTableData(PageResult page, FluxSetDataPageQuery currentQuery) + { + var result = new TableData() + { + Items = page.Data, + TotalItems = page.Total!.Value + }; + + currentQuery.Result = result; + + return result; + } +} diff --git a/tests/BitzArt.Flux.Json.Tests/BitzArt.Flux.Json.Tests.csproj b/tests/BitzArt.Flux.Json.Tests/BitzArt.Flux.Json.Tests.csproj index 347cde1..a713107 100644 --- a/tests/BitzArt.Flux.Json.Tests/BitzArt.Flux.Json.Tests.csproj +++ b/tests/BitzArt.Flux.Json.Tests/BitzArt.Flux.Json.Tests.csproj @@ -10,14 +10,14 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/BitzArt.Flux.REST.Tests/BitzArt.Flux.REST.Tests.csproj b/tests/BitzArt.Flux.REST.Tests/BitzArt.Flux.REST.Tests.csproj index 6591a6a..61f0497 100644 --- a/tests/BitzArt.Flux.REST.Tests/BitzArt.Flux.REST.Tests.csproj +++ b/tests/BitzArt.Flux.REST.Tests/BitzArt.Flux.REST.Tests.csproj @@ -10,15 +10,15 @@ - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/BitzArt.Flux.Tests/BitzArt.Flux.Tests.csproj b/tests/BitzArt.Flux.Tests/BitzArt.Flux.Tests.csproj index 1753f2c..24c8c04 100644 --- a/tests/BitzArt.Flux.Tests/BitzArt.Flux.Tests.csproj +++ b/tests/BitzArt.Flux.Tests/BitzArt.Flux.Tests.csproj @@ -10,14 +10,14 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all