diff --git a/src/MudBlazor.Docs/Pages/Components/DataGrid/DataGridPage.razor b/src/MudBlazor.Docs/Pages/Components/DataGrid/DataGridPage.razor index 8a6866bf208f..39604c05a081 100644 --- a/src/MudBlazor.Docs/Pages/Components/DataGrid/DataGridPage.razor +++ b/src/MudBlazor.Docs/Pages/Components/DataGrid/DataGridPage.razor @@ -96,17 +96,22 @@ - The <MudDataGrid> allows you to group data by column. Setting the Grouping property - to true will turn on grouping which will add a menu item in the column options to toggle grouping of that column. - To disable grouping on a column, set its Groupable property to false. + The <MudDataGrid> allows you to group data by columns. Setting the Grouping property + to true enables grouping, adding a menu item in the column options to toggle grouping for that column. + To disable grouping for a specific column, set its Groupable property to false. + You can group multiple Column elements at the same time. Options for configuring column grouping include the two-way bindable + properties Grouping, GroupExpanded, and GroupByOrder. + + The GroupBy property allows you to define a custom function for grouping a column. - - + + + diff --git a/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingExample.razor b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingExample.razor index d2198635b4ad..7a743cc20421 100644 --- a/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingExample.razor +++ b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingExample.razor @@ -1,9 +1,9 @@ @using System.Net.Http.Json @using MudBlazor.Examples.Data.Models @namespace MudBlazor.Docs.Examples -@inject HttpClient httpClient +@inject HttpClient HttpClient - Periodic Elements @@ -36,24 +36,25 @@
Customize Group Template Customize Group By - Expand All - Collapse All + Expand All + Collapse All
@code { - IEnumerable Elements = new List(); - MudDataGrid dataGrid; - bool _customizeGroupTemplate; - static bool _customizeGroupBy; - static string[] _nonmetals = new string[] { "H", "He","N", "O", "F", "Ne", "Cl", "Ar", "Kr", "Xe", "Rn", "Br", "C", "P", "Se", "Se", "I" }; - Func _groupBy = x => + private IEnumerable _elements = new List(); + private MudDataGrid _dataGrid = null!; + private bool _customizeGroupTemplate; + private static bool _customizeGroupBy; + private static readonly string[] _nonmetals = ["H", "He","N", "O", "F", "Ne", "Cl", "Ar", "Kr", "Xe", "Rn", "Br", "C", "P", "Se", "Se", "I"]; + + private readonly Func _groupBy = x => { if (_customizeGroupBy) return _nonmetals.Contains(x.Sign) ? "Nonmetal": "Metal"; return x.Group; }; - private string GroupClassFunc(GroupDefinition item) + private static string GroupClassFunc(GroupDefinition item) { return item.Grouping.Key?.ToString() == "Nonmetal" || item.Grouping.Key?.ToString() == "Other" ? "mud-theme-warning" @@ -62,22 +63,22 @@ protected override async Task OnInitializedAsync() { - Elements = await httpClient.GetFromJsonAsync>("webapi/periodictable"); + _elements = await HttpClient.GetFromJsonAsync>("webapi/periodictable"); } - void ExpandAllGroups() + private Task ExpandAllGroupsAsync() { - dataGrid?.ExpandAllGroups(); + return _dataGrid.ExpandAllGroupsAsync(); } - void CollapseAllGroups() + private Task CollapseAllGroupsAsync() { - dataGrid?.CollapseAllGroups(); + return _dataGrid.CollapseAllGroupsAsync(); } - void CustomizeByGroupChanged(bool isChecked) + private void CustomizeByGroupChanged(bool isChecked) { _customizeGroupBy = isChecked; - dataGrid.GroupItems(); + _dataGrid.GroupItems(); } } diff --git a/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingMultiLevelExample.razor b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingMultiLevelExample.razor new file mode 100644 index 000000000000..fb75f668d0f1 --- /dev/null +++ b/src/MudBlazor.Docs/Pages/Components/DataGrid/Examples/DataGridGroupingMultiLevelExample.razor @@ -0,0 +1,394 @@ +@using System.Collections.Generic +@using MudBlazor.Utilities +@namespace MudBlazor.Docs.Examples + + + + US States Information2025 + + + + + + + + + + @if (_customizeGroupTemplate) + { + var color = context.Grouping.Key?.ToString() switch + { + "Healthcare" => Color.Primary, + "Tech" => Color.Secondary, + "Tourism" => Color.Info, + _ => Color.Dark + }; +
+ + @context.Grouping.Key + + @context.Grouping.Count() states + +
+ } + else + { + @context.Title: @(context.Grouping.Key) + } +
+
+ + + @if (_customizeGroupTemplate) + { + var (icon, color) = context.Grouping.Key?.ToString() switch + { + "Northeast" or "Southeast" => (Icons.Material.Filled.East, Color.Warning), + "West" or "Pacific" => (Icons.Material.Filled.West, Color.Success), + "Midwest" => (Icons.Material.Filled.LocationOn, Color.Info), + "Southwest" => (Icons.Material.Filled.South, Color.Error), + _ => (Icons.Material.Filled.Public, Color.Default) + }; +
+ + @context.Grouping.Key + + @context.Grouping.Count() states + +
+ } + else + { + @context.Title: @(context.Grouping.Key) + } +
+
+ + + @if (_customizeGroupTemplate) + { + var (icon, color) = context.Grouping.Key?.ToString() switch + { + "Continental" => (Icons.Material.Filled.AcUnit, Color.Primary), + "Humid Subtropical" => (Icons.Material.Filled.WbSunny, Color.Warning), + "Mediterranean" or "Tropical" => (Icons.Material.Filled.WaterDrop, Color.Info), + "Desert" or "Semi-arid" or "Arid" => (Icons.Material.Filled.Landscape, Color.Error), + _ => (Icons.Material.Filled.Cloud, Color.Default) + }; +
+ + @context.Grouping.Key + + @context.Grouping.Count() states + +
+ } + else + { + @context.Title: @(context.Grouping.Key) + } +
+
+ + + @if (_customizeGroupTemplate) + { + Year Inducted: @context.Grouping.Key total @context.Grouping.Count() + } + else + { + @context.Title: @(context.Grouping.Key) + } + + +
+
+ +
+ Customize Group Template + Customize Group By Industry + Customize Group Style + Expand All + Collapse All +
+ +@code { +#nullable enable + public static string __description__ = "Multi Level Grouping within DataGrid"; + // For the Grid + private MudDataGrid _dataGrid = null!; + private string? _searchString = string.Empty; + // Bound Properties @bind- + private bool _customizeGroupTemplate; + private bool _customizeGroupBy; + private bool _customizeGroupStyle; + private bool _primaryIndustryGrouping = true; + private bool _regionGrouping; + private bool _climateGrouping = true; + private int _primaryIndustryOrder ; + private int _climateOrder = 1; + private int _regionOrder = 2; + // Display Options + private bool _primaryIndustryExpanded = true; + private bool _regionExpanded = true; + private bool _climateExpanded = true; + + // Primary grouping by industry type + private Func? _groupBy1; + + protected override void OnInitialized() + { + _groupBy1 = x => + { + if (_customizeGroupBy) + { + return x.PrimaryIndustry switch + { + "Healthcare" => "Health", + "Technology" => "Tech", + "Tourism" => "Vacay", + _ => "Other" + }; + } + + return x.PrimaryIndustry; + }; + } + + protected override void OnAfterRender(bool firstRender) + { + base.OnAfterRender(firstRender); + + if (firstRender) + { + // Check if the dataGrid is grouped and update the state accordingly (for IsGrouped) + if (_dataGrid?.IsGrouped == true) + { + StateHasChanged(); + } + } + } + + private async Task> ServerReload(GridState state) + { + // call API + var data = GetAllStates(_searchString, state.SortDefinitions); + // simulate some wait time + await Task.Delay(150); + + var totalItems = data.Count; + var pagedData = data.Skip(state.Page * state.PageSize).Take(state.PageSize).ToArray(); + return new() + { + TotalItems = totalItems, + Items = pagedData + }; + } + + private static string GroupClassFunc(GroupDefinition item) + { + var key = item.Grouping.Key?.ToString(); + + return item.Title switch + { + "Primary Industry" => key switch + { + "Healthcare" => "mud-theme-primary", + "Tech" => "mud-theme-secondary", + "Tourism" => "mud-theme-info", + _ => "mud-theme-dark" + }, + "Region" => key switch + { + "Northeast" or "Southeast" => "mud-theme-warning", + "West" or "Pacific" => "mud-theme-success", + "Midwest" => "mud-theme-info", + "Southwest" => "mud-theme-error", + _ => string.Empty + }, + _ => key switch + { + "Continental" => "mud-theme-primary", + "Humid Subtropical" => "mud-theme-warning", + "Mediterranean" or "Tropical" => "mud-theme-info", + "Desert" or "Semi-arid" or "Arid" => "mud-theme-error", + _ => string.Empty + } + }; + } + + private static string GroupStyleFunc(GroupDefinition item) + { + var indent = item.Level * 16; // 16px per level + var borderWidth = Math.Max(1, 4 - item.Level); // Decrease border width with depth + + var style = new StyleBuilder() + .AddStyle("padding-left", $"{indent}px") + .AddStyle("border-left", $"{borderWidth}px solid") + .AddStyle("border-color", GetBorderColor(item)) + .AddStyle("opacity", $"{1 - (item.Level - 1) * 0.2}") // Fade out deeper levels slightly + .Build(); + + return style; + } + + private static string GetBorderColor(GroupDefinition item) + { + var key = item.Grouping.Key?.ToString(); + + return item.Title switch + { + // Primary level (Industry) + "Primary Industry" => key switch + { + "Healthcare" => "var(--mud-palette-primary)", + "Tech" => "var(--mud-palette-secondary)", + "Tourism" => "var(--mud-palette-info)", + _ => "var(--mud-palette-dark)" + }, + "Region" => key switch + { + "Northeast" or "Southeast" => "var(--mud-palette-warning)", + "West" or "Pacific" => "var(--mud-palette-success)", + "Midwest" => "var(--mud-palette-info)", + "Southwest" => "var(--mud-palette-error)", + _ => "var(--mud-palette-dark)" + }, + "Climate" => key switch + { + "Continental" => "var(--mud-palette-primary)", + "Humid Subtropical" => "var(--mud-palette-warning)", + "Mediterranean" or "Tropical" => "var(--mud-palette-info)", + "Desert" or "Semi-arid" or "Arid" => "var(--mud-palette-error)", + _ => "var(--mud-palette-dark)" + }, + _ => "var(--mud-palette-dark)" + }; + } + + private Task ExpandAllGroupsAsync() + { + return _dataGrid.ExpandAllGroupsAsync(); + } + + private Task CollapseAllGroupsAsync() + { + return _dataGrid.CollapseAllGroupsAsync(); + } + + private void CustomizeByGroupChanged(bool isChecked) + { + _customizeGroupBy = isChecked; + _dataGrid.GroupItems(); + } + + private static List GetAllStates(string? searchString, ICollection>? sortDefinitions) + { + List data = + [ + new USState(Id: 1, State: "Alabama", Counties: 67, Population: 5024279, PrimaryIndustry: "Manufacturing", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1819), + new USState(Id: 2, State: "Alaska", Counties: 30, Population: 733406, PrimaryIndustry: "Oil and Gas", Region: "Pacific", Climate: "Subarctic", YearInducted: 1959), + new USState(Id: 3, State: "Arizona", Counties: 15, Population: 7151502, PrimaryIndustry: "Healthcare", Region: "Southwest", Climate: "Desert", YearInducted: 1912), + new USState(Id: 4, State: "Arkansas", Counties: 75, Population: 3011524, PrimaryIndustry: "Agriculture", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1836), + new USState(Id: 5, State: "California", Counties: 58, Population: 39538223, PrimaryIndustry: "Technology", Region: "West", Climate: "Mediterranean", YearInducted: 1850), + new USState(Id: 6, State: "Colorado", Counties: 64, Population: 5773714, PrimaryIndustry: "Tourism", Region: "West", Climate: "Semi-arid", YearInducted: 1876), + new USState(Id: 7, State: "Connecticut", Counties: 8, Population: 3605944, PrimaryIndustry: "Finance and Insurance", Region: "Northeast", Climate: "Continental", YearInducted: 1788), + new USState(Id: 8, State: "Delaware", Counties: 3, Population: 989948, PrimaryIndustry: "Finance and Insurance", Region: "Northeast", Climate: "Continental", YearInducted: 1787), + new USState(Id: 9, State: "Florida", Counties: 67, Population: 21538187, PrimaryIndustry: "Tourism", Region: "Southeast", Climate: "Tropical", YearInducted: 1845), + new USState(Id: 10, State: "Georgia", Counties: 159, Population: 10711908, PrimaryIndustry: "Agriculture", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1788), + new USState(Id: 11, State: "Hawaii", Counties: 5, Population: 1455271, PrimaryIndustry: "Tourism", Region: "Pacific", Climate: "Tropical", YearInducted: 1959), + new USState(Id: 12, State: "Idaho", Counties: 44, Population: 1839106, PrimaryIndustry: "Agriculture", Region: "West", Climate: "Continental", YearInducted: 1890), + new USState(Id: 13, State: "Illinois", Counties: 102, Population: 12812508, PrimaryIndustry: "Manufacturing", Region: "Midwest", Climate: "Continental", YearInducted: 1818), + new USState(Id: 14, State: "Indiana", Counties: 92, Population: 6785528, PrimaryIndustry: "Manufacturing", Region: "Midwest", Climate: "Continental", YearInducted: 1816), + new USState(Id: 15, State: "Iowa", Counties: 99, Population: 3190369, PrimaryIndustry: "Agriculture", Region: "Midwest", Climate: "Continental", YearInducted: 1846), + new USState(Id: 16, State: "Kansas", Counties: 105, Population: 2937880, PrimaryIndustry: "Agriculture", Region: "Midwest", Climate: "Continental", YearInducted: 1861), + new USState(Id: 17, State: "Kentucky", Counties: 120, Population: 4505836, PrimaryIndustry: "Manufacturing", Region: "Southeast", Climate: "Humid Continental", YearInducted: 1792), + new USState(Id: 18, State: "Louisiana", Counties: 64, Population: 4657757, PrimaryIndustry: "Oil and Gas", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1812), + new USState(Id: 19, State: "Maine", Counties: 16, Population: 1362359, PrimaryIndustry: "Healthcare", Region: "Northeast", Climate: "Continental", YearInducted: 1820), + new USState(Id: 20, State: "Maryland", Counties: 24, Population: 6177224, PrimaryIndustry: "Healthcare", Region: "Northeast", Climate: "Continental", YearInducted: 1788), + new USState(Id: 21, State: "Massachusetts", Counties: 14, Population: 7029917, PrimaryIndustry: "Healthcare", Region: "Northeast", Climate: "Continental", YearInducted: 1788), + new USState(Id: 22, State: "Michigan", Counties: 83, Population: 10077331, PrimaryIndustry: "Manufacturing", Region: "Midwest", Climate: "Continental", YearInducted: 1837), + new USState(Id: 23, State: "Minnesota", Counties: 87, Population: 5706494, PrimaryIndustry: "Healthcare", Region: "Midwest", Climate: "Continental", YearInducted: 1858), + new USState(Id: 24, State: "Mississippi", Counties: 82, Population: 2961279, PrimaryIndustry: "Agriculture", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1817), + new USState(Id: 25, State: "Missouri", Counties: 115, Population: 6154913, PrimaryIndustry: "Healthcare", Region: "Midwest", Climate: "Continental", YearInducted: 1821), + new USState(Id: 26, State: "Montana", Counties: 56, Population: 1084225, PrimaryIndustry: "Agriculture", Region: "West", Climate: "Continental", YearInducted: 1889), + new USState(Id: 27, State: "Nebraska", Counties: 93, Population: 1961504, PrimaryIndustry: "Agriculture", Region: "Midwest", Climate: "Continental", YearInducted: 1867), + new USState(Id: 28, State: "Nevada", Counties: 17, Population: 3104614, PrimaryIndustry: "Tourism", Region: "West", Climate: "Desert", YearInducted: 1864), + new USState(Id: 29, State: "New Hampshire", Counties: 10, Population: 1377529, PrimaryIndustry: "Healthcare", Region: "Northeast", Climate: "Continental", YearInducted: 1788), + new USState(Id: 30, State: "New Jersey", Counties: 21, Population: 9288994, PrimaryIndustry: "Finance and Insurance", Region: "Northeast", Climate: "Continental", YearInducted: 1787), + new USState(Id: 31, State: "New Mexico", Counties: 33, Population: 2117522, PrimaryIndustry: "Oil and Gas", Region: "Southwest", Climate: "Arid", YearInducted: 1912), + new USState(Id: 32, State: "New York", Counties: 62, Population: 20201249, PrimaryIndustry: "Finance and Insurance", Region: "Northeast", Climate: "Continental", YearInducted: 1788), + new USState(Id: 33, State: "North Carolina", Counties: 100, Population: 10439388, PrimaryIndustry: "Manufacturing", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1789), + new USState(Id: 34, State: "North Dakota", Counties: 53, Population: 779094, PrimaryIndustry: "Agriculture", Region: "Midwest", Climate: "Continental", YearInducted: 1889), + new USState(Id: 35, State: "Ohio", Counties: 88, Population: 11799448, PrimaryIndustry: "Manufacturing", Region: "Midwest", Climate: "Continental", YearInducted: 1803), + new USState(Id: 36, State: "Oklahoma", Counties: 77, Population: 3959353, PrimaryIndustry: "Oil and Gas", Region: "Southwest", Climate: "Continental", YearInducted: 1907), + new USState(Id: 37, State: "Oregon", Counties: 36, Population: 4237256, PrimaryIndustry: "Technology", Region: "West", Climate: "Mediterranean", YearInducted: 1859), + new USState(Id: 38, State: "Pennsylvania", Counties: 67, Population: 13002700, PrimaryIndustry: "Healthcare", Region: "Northeast", Climate: "Continental", YearInducted: 1787), + new USState(Id: 39, State: "Rhode Island", Counties: 5, Population: 1097379, PrimaryIndustry: "Healthcare", Region: "Northeast", Climate: "Continental", YearInducted: 1790), + new USState(Id: 40, State: "South Carolina", Counties: 46, Population: 5118425, PrimaryIndustry: "Manufacturing", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1788), + new USState(Id: 41, State: "South Dakota", Counties: 66, Population: 886667, PrimaryIndustry: "Agriculture", Region: "Midwest", Climate: "Continental", YearInducted: 1889), + new USState(Id: 42, State: "Tennessee", Counties: 95, Population: 6910840, PrimaryIndustry: "Healthcare", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1796), + new USState(Id: 43, State: "Texas", Counties: 254, Population: 29145505, PrimaryIndustry: "Oil and Gas", Region: "Southwest", Climate: "Subtropical", YearInducted: 1845), + new USState(Id: 44, State: "Utah", Counties: 29, Population: 3271616, PrimaryIndustry: "Healthcare", Region: "West", Climate: "Semi-arid", YearInducted: 1896), + new USState(Id: 45, State: "Vermont", Counties: 14, Population: 643077, PrimaryIndustry: "Tourism", Region: "Northeast", Climate: "Continental", YearInducted: 1791), + new USState(Id: 46, State: "Virginia", Counties: 133, Population: 8631393, PrimaryIndustry: "Technology", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1788), + new USState(Id: 47, State: "Washington", Counties: 39, Population: 7705281, PrimaryIndustry: "Technology", Region: "West", Climate: "Oceanic", YearInducted: 1889), + new USState(Id: 48, State: "West Virginia", Counties: 55, Population: 1793716, PrimaryIndustry: "Healthcare", Region: "Southeast", Climate: "Continental", YearInducted: 1863), + new USState(Id: 49, State: "Wisconsin", Counties: 72, Population: 5893718, PrimaryIndustry: "Manufacturing", Region: "Midwest", Climate: "Continental", YearInducted: 1848), + new USState(Id: 50, State: "Wyoming", Counties: 23, Population: 576851, PrimaryIndustry: "Mining", Region: "West", Climate: "Semi-arid", YearInducted: 1890) + ]; + + if (sortDefinitions != null && sortDefinitions.Any()) + { + // on server or actual API would be IQueryable likely + IOrderedEnumerable? orderedQuery = null; + foreach (var sort in sortDefinitions) + { + orderedQuery = data.OrderByDirection(sort.Descending ? SortDirection.Descending : SortDirection.Ascending, + obj => GetPropertyValue(obj, sort.SortBy)); + } + data = orderedQuery!.ToList(); + } + + return data.Where(x => MatchesSearch(x, searchString)).ToList(); + } + + private static bool MatchesSearch(USState obj, string? searchString) + { + if (string.IsNullOrEmpty(searchString)) + { + return true; + } + + return obj.State.Contains(searchString, StringComparison.OrdinalIgnoreCase) || + obj.PrimaryIndustry.Contains(searchString, StringComparison.OrdinalIgnoreCase) || + obj.YearInducted.ToString().Contains(searchString) || + obj.Climate.Contains(searchString, StringComparison.OrdinalIgnoreCase) || + obj.Region.Contains(searchString, StringComparison.OrdinalIgnoreCase); + } + + private static object? GetPropertyValue(TT obj, string propertyName) + { + return typeof(TT).GetProperty(propertyName)?.GetValue(obj); + } + + private record USState(int Id, string State, int Counties, int Population, string PrimaryIndustry, string Region, string Climate, int YearInducted); +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridColumnGroupingTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridColumnGroupingTest.razor index 4a36fae30175..4e6ba3996f66 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridColumnGroupingTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridColumnGroupingTest.razor @@ -1,7 +1,7 @@  - + @@ -17,6 +17,8 @@ ? "(None)" // Customize group name for null values otherwise would be empty group name : x.Profession; + public bool IsNameGrouped { get; private set; } = true; + public bool IsGenderGrouped { get; private set; } public bool IsAgeGrouped { get; private set; } @@ -31,11 +33,29 @@ new("Alice", 32, "Female", "Cook") ]; - private void GroupByGender(MouseEventArgs args) => IsGenderGrouped = true; - - private void GroupByAge(MouseEventArgs args) => IsAgeGrouped = true; - - private void GroupByProfession(MouseEventArgs args) => IsProfessionGrouped = true; + private void GroupByGender(MouseEventArgs args) + { + IsNameGrouped = false; + IsProfessionGrouped = false; + IsAgeGrouped = false; + IsGenderGrouped = true; + } + + private void GroupByAge(MouseEventArgs args) + { + IsNameGrouped = false; + IsProfessionGrouped = false; + IsAgeGrouped = true; + IsGenderGrouped = false; + } + + private void GroupByProfession(MouseEventArgs args) + { + IsNameGrouped = false; + IsProfessionGrouped = true; + IsAgeGrouped = false; + IsGenderGrouped = false; + } public record Model(string Name, int Age, string Gender, string? Profession); } diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupCollapseAllTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupCollapseAllTest.razor index 41ec96f0b0a5..21110d147005 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupCollapseAllTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupCollapseAllTest.razor @@ -1,4 +1,4 @@ - new TestObject(x.Name, x.Category)).ToList(); } - public void ExpandAllGroups() + public Task ExpandAllGroups() { - _dataGrid.ExpandAllGroups(); + return _dataGrid.ExpandAllGroupsAsync(); } - public void CollapseAllGroups() + public Task CollapseAllGroups() { - _dataGrid.CollapseAllGroups(); + return _dataGrid.CollapseAllGroupsAsync(); } public record TestObject(string Name, string Category); -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandAllCollapseAllTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandAllCollapseAllTest.razor index ef9f81346164..d91faf9d49d3 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandAllCollapseAllTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandAllCollapseAllTest.razor @@ -1,4 +1,4 @@ - new Element(x.Group, x.Position, x.Name)).ToList(); } - public void ExpandAllGroups() + public Task ExpandAllGroups() { - _dataGrid.ExpandAllGroups(); + return _dataGrid.ExpandAllGroupsAsync(); } - public void CollapseAllGroups() + public Task CollapseAllGroups() { - _dataGrid.CollapseAllGroups(); + return _dataGrid.CollapseAllGroupsAsync(); } public class Element(string group, int position, string name) @@ -139,4 +139,4 @@ public string Group { get; set; } = group; } -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedAsyncTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedAsyncTest.razor index 375a8c413972..90644b3ce048 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedAsyncTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedAsyncTest.razor @@ -1,4 +1,4 @@ - Fruits @@ -39,15 +39,15 @@ _fruits.Add(new Fruit("Banana", 5, "Musa")); } - public void ExpandAllGroups() + public Task ExpandAllGroups() { - _dataGrid.ExpandAllGroups(); + return _dataGrid.ExpandAllGroupsAsync(); } - public void CollapseAllGroups() + public Task CollapseAllGroups() { - _dataGrid.CollapseAllGroups(); + return _dataGrid.CollapseAllGroupsAsync(); } public record Fruit(string Name, int Count, string Category); -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedFalseAsyncTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedFalseAsyncTest.razor index 8aab110089a7..f0d16995aaac 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedFalseAsyncTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedFalseAsyncTest.razor @@ -40,15 +40,15 @@ _fruits.Add(new Fruit("Banana", 5, "Musa")); } - public void ExpandAllGroups() + public Task ExpandAllGroups() { - _dataGrid.ExpandAllGroups(); + return _dataGrid.ExpandAllGroupsAsync(); } - public void CollapseAllGroups() + public Task CollapseAllGroups() { - _dataGrid.CollapseAllGroups(); + return _dataGrid.CollapseAllGroupsAsync(); } public record Fruit(string Name, int Count, string Category); -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedFalseServerDataTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedFalseServerDataTest.razor index 196ca80fc9d6..aa7e4e2bd9e2 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedFalseServerDataTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedFalseServerDataTest.razor @@ -46,15 +46,15 @@ _fruits.Add(new Fruit("Banana", 5, "Musa")); } - public void ExpandAllGroups() + public Task ExpandAllGroups() { - _dataGrid.ExpandAllGroups(); + return _dataGrid.ExpandAllGroupsAsync(); } - public void CollapseAllGroups() + public Task CollapseAllGroups() { - _dataGrid.CollapseAllGroups(); + return _dataGrid.CollapseAllGroupsAsync(); } public record Fruit(string Name, int Count, string Category); -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedFalseTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedFalseTest.razor index cd7ed6c7d192..0e6e447c0638 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedFalseTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedFalseTest.razor @@ -37,15 +37,15 @@ _fruits.Add(new Fruit("Banana", 5, "Musa")); } - public void ExpandAllGroups() + public Task ExpandAllGroups() { - _dataGrid.ExpandAllGroups(); + return _dataGrid.ExpandAllGroupsAsync(); } - public void CollapseAllGroups() + public Task CollapseAllGroups() { - _dataGrid.CollapseAllGroups(); + return _dataGrid.CollapseAllGroupsAsync(); } public record Fruit(string Name, int Count, string Category); -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedServerDataTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedServerDataTest.razor index 0a251fea0714..a55530f6412e 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedServerDataTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedServerDataTest.razor @@ -1,4 +1,4 @@ - Fruits @@ -48,15 +48,15 @@ _fruits.Add(new Fruit("Banana", 5, "Musa")); } - public void ExpandAllGroups() + public Task ExpandAllGroups() { - _dataGrid.ExpandAllGroups(); + return _dataGrid.ExpandAllGroupsAsync(); } - public void CollapseAllGroups() + public Task CollapseAllGroups() { - _dataGrid.CollapseAllGroups(); + return _dataGrid.CollapseAllGroupsAsync(); } public record Fruit(string Name, int Count, string Category); -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedTest.razor index 24a404458fbb..e96e1dfbd56b 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupExpandedTest.razor @@ -1,4 +1,4 @@ - Fruits @@ -38,14 +38,14 @@ GroupExpanded="true" RowContextMenuClick="@OnRowContextMenuClick"> _fruits.Add(new Fruit("Banana", 5, "Musa")); } - public void ExpandAllGroups() + public Task ExpandAllGroups() { - _dataGrid.ExpandAllGroups(); + return _dataGrid.ExpandAllGroupsAsync(); } - public void CollapseAllGroups() + public Task CollapseAllGroups() { - _dataGrid.CollapseAllGroups(); + return _dataGrid.CollapseAllGroupsAsync(); } private void OnRowContextMenuClick(DataGridRowClickEventArgs args) @@ -61,4 +61,4 @@ GroupExpanded="true" RowContextMenuClick="@OnRowContextMenuClick"> public string Category { get; set; } = category; } -} \ No newline at end of file +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupingMultiLevelTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupingMultiLevelTest.razor new file mode 100644 index 000000000000..eb88375c9738 --- /dev/null +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridGroupingMultiLevelTest.razor @@ -0,0 +1,392 @@ +@using MudBlazor.Utilities + + + + US States Information2025 + + + + + + + + + + @if (_customizeGroupTemplate) + { + var color = context.Grouping.Key?.ToString() switch + { + "Healthcare" => Color.Primary, + "Tech" => Color.Secondary, + "Tourism" => Color.Info, + _ => Color.Dark + }; +
+ + @context.Grouping.Key + + @context.Grouping.Count() states + +
+ } + else + { + @context.Title: @(context.Grouping.Key) + } +
+
+ + + @if (_customizeGroupTemplate) + { + var (icon, color) = context.Grouping.Key?.ToString() switch + { + "Northeast" or "Southeast" => (Icons.Material.Filled.East, Color.Warning), + "West" or "Pacific" => (Icons.Material.Filled.West, Color.Success), + "Midwest" => (Icons.Material.Filled.LocationOn, Color.Info), + "Southwest" => (Icons.Material.Filled.South, Color.Error), + _ => (Icons.Material.Filled.Public, Color.Default) + }; +
+ + @context.Grouping.Key + + @context.Grouping.Count() states + +
+ } + else + { + @context.Title: @(context.Grouping.Key) + } +
+
+ + + @if (_customizeGroupTemplate) + { + var (icon, color) = context.Grouping.Key?.ToString() switch + { + "Continental" => (Icons.Material.Filled.AcUnit, Color.Primary), + "Humid Subtropical" => (Icons.Material.Filled.WbSunny, Color.Warning), + "Mediterranean" or "Tropical" => (Icons.Material.Filled.WaterDrop, Color.Info), + "Desert" or "Semi-arid" or "Arid" => (Icons.Material.Filled.Landscape, Color.Error), + _ => (Icons.Material.Filled.Cloud, Color.Default) + }; +
+ + @context.Grouping.Key + + @context.Grouping.Count() states + +
+ } + else + { + @context.Title: @(context.Grouping.Key) + } +
+
+ + + @if (_customizeGroupTemplate) + { + Year Inducted: @context.Grouping.Key total @context.Grouping.Count() + } + else + { + @context.Title: @(context.Grouping.Key) + } + + +
+
+ +
+ Customize Group Template + Customize Group By Industry + Customize Group Style + Expand All + Collapse All +
+ +@code { +#nullable enable + public static string __description__ = "Multi Level Grouping within DataGrid"; + // For the Grid + private MudDataGrid _dataGrid = null!; + private string? _searchString = string.Empty; + // Bound Properties @bind- + private bool _customizeGroupTemplate; + private bool _customizeGroupBy; + private bool _customizeGroupStyle; + private bool _primaryIndustryGrouping = true; + private bool _regionGrouping; + private bool _climateGrouping = true; + private int _primaryIndustryOrder; + private int _climateOrder = 1; + private int _regionOrder = 2; + // Display Options + private bool _primaryIndustryExpanded = true; + private bool _regionExpanded = true; + private bool _climateExpanded = true; + + // Primary grouping by industry type + private Func? _groupBy1; + + protected override void OnInitialized() + { + _groupBy1 = x => + { + if (_customizeGroupBy) + { + return x.PrimaryIndustry switch + { + "Healthcare" => "Health", + "Technology" => "Tech", + "Tourism" => "Vacay", + _ => "Other" + }; + } + + return x.PrimaryIndustry; + }; + } + + protected override void OnAfterRender(bool firstRender) + { + base.OnAfterRender(firstRender); + + if (firstRender) + { + // Check if the dataGrid is grouped and update the state accordingly (for IsGrouped) + if (_dataGrid.IsGrouped == true) + { + StateHasChanged(); + } + } + } + + private async Task> ServerReload(GridState state) + { + // call API + var data = GetAllStates(_searchString, state.SortDefinitions); + // simulate some wait time + await Task.Delay(150); + + var totalItems = data.Count; + var pagedData = data.Skip(state.Page * state.PageSize).Take(state.PageSize).ToArray(); + return new() + { + TotalItems = totalItems, + Items = pagedData + }; + } + + private static string GroupClassFunc(GroupDefinition item) + { + var key = item.Grouping.Key?.ToString(); + + return item.Title switch + { + "Primary Industry" => key switch + { + "Healthcare" => "mud-theme-primary", + "Tech" => "mud-theme-secondary", + "Tourism" => "mud-theme-info", + _ => "mud-theme-dark" + }, + "Region" => key switch + { + "Northeast" or "Southeast" => "mud-theme-warning", + "West" or "Pacific" => "mud-theme-success", + "Midwest" => "mud-theme-info", + "Southwest" => "mud-theme-error", + _ => string.Empty + }, + _ => key switch + { + "Continental" => "mud-theme-primary", + "Humid Subtropical" => "mud-theme-warning", + "Mediterranean" or "Tropical" => "mud-theme-info", + "Desert" or "Semi-arid" or "Arid" => "mud-theme-error", + _ => string.Empty + } + }; + } + + private static string GroupStyleFunc(GroupDefinition item) + { + var indent = item.Level * 16; // 16px per level + var borderWidth = Math.Max(1, 4 - item.Level); // Decrease border width with depth + + var style = new StyleBuilder() + .AddStyle("padding-left", $"{indent}px") + .AddStyle("border-left", $"{borderWidth}px solid") + .AddStyle("border-color", GetBorderColor(item)) + .AddStyle("opacity", $"{1 - (item.Level - 1) * 0.2}") // Fade out deeper levels slightly + .Build(); + + return style; + } + + private static string GetBorderColor(GroupDefinition item) + { + var key = item.Grouping.Key?.ToString(); + + return item.Title switch + { + // Primary level (Industry) + "Primary Industry" => key switch + { + "Healthcare" => "var(--mud-palette-primary)", + "Tech" => "var(--mud-palette-secondary)", + "Tourism" => "var(--mud-palette-info)", + _ => "var(--mud-palette-dark)" + }, + "Region" => key switch + { + "Northeast" or "Southeast" => "var(--mud-palette-warning)", + "West" or "Pacific" => "var(--mud-palette-success)", + "Midwest" => "var(--mud-palette-info)", + "Southwest" => "var(--mud-palette-error)", + _ => "var(--mud-palette-dark)" + }, + "Climate" => key switch + { + "Continental" => "var(--mud-palette-primary)", + "Humid Subtropical" => "var(--mud-palette-warning)", + "Mediterranean" or "Tropical" => "var(--mud-palette-info)", + "Desert" or "Semi-arid" or "Arid" => "var(--mud-palette-error)", + _ => "var(--mud-palette-dark)" + }, + _ => "var(--mud-palette-dark)" + }; + } + + private Task ExpandAllGroupsAsync() + { + return _dataGrid.ExpandAllGroupsAsync(); + } + + private Task CollapseAllGroupsAsync() + { + return _dataGrid.CollapseAllGroupsAsync(); + } + + private void CustomizeByGroupChanged(bool isChecked) + { + _customizeGroupBy = isChecked; + _dataGrid.GroupItems(); + } + + private static List GetAllStates(string? searchString, ICollection>? sortDefinitions) + { + List data = + [ + new USState(Id: 1, State: "Alabama", Counties: 67, Population: 5024279, PrimaryIndustry: "Manufacturing", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1819), + new USState(Id: 2, State: "Alaska", Counties: 30, Population: 733406, PrimaryIndustry: "Oil and Gas", Region: "Pacific", Climate: "Subarctic", YearInducted: 1959), + new USState(Id: 3, State: "Arizona", Counties: 15, Population: 7151502, PrimaryIndustry: "Healthcare", Region: "Southwest", Climate: "Desert", YearInducted: 1912), + new USState(Id: 4, State: "Arkansas", Counties: 75, Population: 3011524, PrimaryIndustry: "Agriculture", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1836), + new USState(Id: 5, State: "California", Counties: 58, Population: 39538223, PrimaryIndustry: "Technology", Region: "West", Climate: "Mediterranean", YearInducted: 1850), + new USState(Id: 6, State: "Colorado", Counties: 64, Population: 5773714, PrimaryIndustry: "Tourism", Region: "West", Climate: "Semi-arid", YearInducted: 1876), + new USState(Id: 7, State: "Connecticut", Counties: 8, Population: 3605944, PrimaryIndustry: "Finance and Insurance", Region: "Northeast", Climate: "Continental", YearInducted: 1788), + new USState(Id: 8, State: "Delaware", Counties: 3, Population: 989948, PrimaryIndustry: "Finance and Insurance", Region: "Northeast", Climate: "Continental", YearInducted: 1787), + new USState(Id: 9, State: "Florida", Counties: 67, Population: 21538187, PrimaryIndustry: "Tourism", Region: "Southeast", Climate: "Tropical", YearInducted: 1845), + new USState(Id: 10, State: "Georgia", Counties: 159, Population: 10711908, PrimaryIndustry: "Agriculture", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1788), + new USState(Id: 11, State: "Hawaii", Counties: 5, Population: 1455271, PrimaryIndustry: "Tourism", Region: "Pacific", Climate: "Tropical", YearInducted: 1959), + new USState(Id: 12, State: "Idaho", Counties: 44, Population: 1839106, PrimaryIndustry: "Agriculture", Region: "West", Climate: "Continental", YearInducted: 1890), + new USState(Id: 13, State: "Illinois", Counties: 102, Population: 12812508, PrimaryIndustry: "Manufacturing", Region: "Midwest", Climate: "Continental", YearInducted: 1818), + new USState(Id: 14, State: "Indiana", Counties: 92, Population: 6785528, PrimaryIndustry: "Manufacturing", Region: "Midwest", Climate: "Continental", YearInducted: 1816), + new USState(Id: 15, State: "Iowa", Counties: 99, Population: 3190369, PrimaryIndustry: "Agriculture", Region: "Midwest", Climate: "Continental", YearInducted: 1846), + new USState(Id: 16, State: "Kansas", Counties: 105, Population: 2937880, PrimaryIndustry: "Agriculture", Region: "Midwest", Climate: "Continental", YearInducted: 1861), + new USState(Id: 17, State: "Kentucky", Counties: 120, Population: 4505836, PrimaryIndustry: "Manufacturing", Region: "Southeast", Climate: "Humid Continental", YearInducted: 1792), + new USState(Id: 18, State: "Louisiana", Counties: 64, Population: 4657757, PrimaryIndustry: "Oil and Gas", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1812), + new USState(Id: 19, State: "Maine", Counties: 16, Population: 1362359, PrimaryIndustry: "Healthcare", Region: "Northeast", Climate: "Continental", YearInducted: 1820), + new USState(Id: 20, State: "Maryland", Counties: 24, Population: 6177224, PrimaryIndustry: "Healthcare", Region: "Northeast", Climate: "Continental", YearInducted: 1788), + new USState(Id: 21, State: "Massachusetts", Counties: 14, Population: 7029917, PrimaryIndustry: "Healthcare", Region: "Northeast", Climate: "Continental", YearInducted: 1788), + new USState(Id: 22, State: "Michigan", Counties: 83, Population: 10077331, PrimaryIndustry: "Manufacturing", Region: "Midwest", Climate: "Continental", YearInducted: 1837), + new USState(Id: 23, State: "Minnesota", Counties: 87, Population: 5706494, PrimaryIndustry: "Healthcare", Region: "Midwest", Climate: "Continental", YearInducted: 1858), + new USState(Id: 24, State: "Mississippi", Counties: 82, Population: 2961279, PrimaryIndustry: "Agriculture", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1817), + new USState(Id: 25, State: "Missouri", Counties: 115, Population: 6154913, PrimaryIndustry: "Healthcare", Region: "Midwest", Climate: "Continental", YearInducted: 1821), + new USState(Id: 26, State: "Montana", Counties: 56, Population: 1084225, PrimaryIndustry: "Agriculture", Region: "West", Climate: "Continental", YearInducted: 1889), + new USState(Id: 27, State: "Nebraska", Counties: 93, Population: 1961504, PrimaryIndustry: "Agriculture", Region: "Midwest", Climate: "Continental", YearInducted: 1867), + new USState(Id: 28, State: "Nevada", Counties: 17, Population: 3104614, PrimaryIndustry: "Tourism", Region: "West", Climate: "Desert", YearInducted: 1864), + new USState(Id: 29, State: "New Hampshire", Counties: 10, Population: 1377529, PrimaryIndustry: "Healthcare", Region: "Northeast", Climate: "Continental", YearInducted: 1788), + new USState(Id: 30, State: "New Jersey", Counties: 21, Population: 9288994, PrimaryIndustry: "Finance and Insurance", Region: "Northeast", Climate: "Continental", YearInducted: 1787), + new USState(Id: 31, State: "New Mexico", Counties: 33, Population: 2117522, PrimaryIndustry: "Oil and Gas", Region: "Southwest", Climate: "Arid", YearInducted: 1912), + new USState(Id: 32, State: "New York", Counties: 62, Population: 20201249, PrimaryIndustry: "Finance and Insurance", Region: "Northeast", Climate: "Continental", YearInducted: 1788), + new USState(Id: 33, State: "North Carolina", Counties: 100, Population: 10439388, PrimaryIndustry: "Manufacturing", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1789), + new USState(Id: 34, State: "North Dakota", Counties: 53, Population: 779094, PrimaryIndustry: "Agriculture", Region: "Midwest", Climate: "Continental", YearInducted: 1889), + new USState(Id: 35, State: "Ohio", Counties: 88, Population: 11799448, PrimaryIndustry: "Manufacturing", Region: "Midwest", Climate: "Continental", YearInducted: 1803), + new USState(Id: 36, State: "Oklahoma", Counties: 77, Population: 3959353, PrimaryIndustry: "Oil and Gas", Region: "Southwest", Climate: "Continental", YearInducted: 1907), + new USState(Id: 37, State: "Oregon", Counties: 36, Population: 4237256, PrimaryIndustry: "Technology", Region: "West", Climate: "Mediterranean", YearInducted: 1859), + new USState(Id: 38, State: "Pennsylvania", Counties: 67, Population: 13002700, PrimaryIndustry: "Healthcare", Region: "Northeast", Climate: "Continental", YearInducted: 1787), + new USState(Id: 39, State: "Rhode Island", Counties: 5, Population: 1097379, PrimaryIndustry: "Healthcare", Region: "Northeast", Climate: "Continental", YearInducted: 1790), + new USState(Id: 40, State: "South Carolina", Counties: 46, Population: 5118425, PrimaryIndustry: "Manufacturing", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1788), + new USState(Id: 41, State: "South Dakota", Counties: 66, Population: 886667, PrimaryIndustry: "Agriculture", Region: "Midwest", Climate: "Continental", YearInducted: 1889), + new USState(Id: 42, State: "Tennessee", Counties: 95, Population: 6910840, PrimaryIndustry: "Healthcare", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1796), + new USState(Id: 43, State: "Texas", Counties: 254, Population: 29145505, PrimaryIndustry: "Oil and Gas", Region: "Southwest", Climate: "Subtropical", YearInducted: 1845), + new USState(Id: 44, State: "Utah", Counties: 29, Population: 3271616, PrimaryIndustry: "Healthcare", Region: "West", Climate: "Semi-arid", YearInducted: 1896), + new USState(Id: 45, State: "Vermont", Counties: 14, Population: 643077, PrimaryIndustry: "Tourism", Region: "Northeast", Climate: "Continental", YearInducted: 1791), + new USState(Id: 46, State: "Virginia", Counties: 133, Population: 8631393, PrimaryIndustry: "Technology", Region: "Southeast", Climate: "Humid Subtropical", YearInducted: 1788), + new USState(Id: 47, State: "Washington", Counties: 39, Population: 7705281, PrimaryIndustry: "Technology", Region: "West", Climate: "Oceanic", YearInducted: 1889), + new USState(Id: 48, State: "West Virginia", Counties: 55, Population: 1793716, PrimaryIndustry: "Healthcare", Region: "Southeast", Climate: "Continental", YearInducted: 1863), + new USState(Id: 49, State: "Wisconsin", Counties: 72, Population: 5893718, PrimaryIndustry: "Manufacturing", Region: "Midwest", Climate: "Continental", YearInducted: 1848), + new USState(Id: 50, State: "Wyoming", Counties: 23, Population: 576851, PrimaryIndustry: "Mining", Region: "West", Climate: "Semi-arid", YearInducted: 1890) + ]; + + if (sortDefinitions != null && sortDefinitions.Any()) + { + // on server or actual API would be IQueryable likely + IOrderedEnumerable? orderedQuery = null; + foreach (var sort in sortDefinitions) + { + orderedQuery = data.OrderByDirection(sort.Descending ? SortDirection.Descending : SortDirection.Ascending, + obj => GetPropertyValue(obj, sort.SortBy)); + } + data = orderedQuery!.ToList(); + } + + return data.Where(x => MatchesSearch(x, searchString)).ToList(); + } + + private static bool MatchesSearch(USState obj, string? searchString) + { + if (string.IsNullOrEmpty(searchString)) + { + return true; + } + + return obj.State.Contains(searchString, StringComparison.OrdinalIgnoreCase) || + obj.PrimaryIndustry.Contains(searchString, StringComparison.OrdinalIgnoreCase) || + obj.YearInducted.ToString().Contains(searchString) || + obj.Climate.Contains(searchString, StringComparison.OrdinalIgnoreCase) || + obj.Region.Contains(searchString, StringComparison.OrdinalIgnoreCase); + } + + private static object? GetPropertyValue(TT obj, string propertyName) + { + return typeof(TT).GetProperty(propertyName)?.GetValue(obj); + } + + public record USState(int Id, string State, int Counties, int Population, string PrimaryIndustry, string Region, string Climate, int YearInducted); +} diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridServerDataColumnGroupingTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridServerDataColumnGroupingTest.razor index de1562e608bb..3514cc7e54c4 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridServerDataColumnGroupingTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridServerDataColumnGroupingTest.razor @@ -70,14 +70,14 @@ _elements = await _periodicTableService.GetElements(); } - private void ExpandAllGroups() + private Task ExpandAllGroups() { - _dataGrid.ExpandAllGroups(); + return _dataGrid.ExpandAllGroupsAsync(); } - private void CollapseAllGroups() + private Task CollapseAllGroups() { - _dataGrid.CollapseAllGroups(); + return _dataGrid.CollapseAllGroupsAsync(); } private void CustomizeByGroupChanged(bool isChecked) diff --git a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridUniqueRowKeyTest.razor b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridUniqueRowKeyTest.razor index bb8a8b680ff4..dd80cf08b7f0 100644 --- a/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridUniqueRowKeyTest.razor +++ b/src/MudBlazor.UnitTests.Viewer/TestComponents/DataGrid/DataGridUniqueRowKeyTest.razor @@ -1,16 +1,17 @@  @* The input element will be tested to see if it gets recreated when row order changes. *@ - + - + - + @code { + [Parameter] public bool Group { get; set; } diff --git a/src/MudBlazor.UnitTests/Components/DataGridGroupingTests.cs b/src/MudBlazor.UnitTests/Components/DataGridGroupingTests.cs new file mode 100644 index 000000000000..d9613c4de469 --- /dev/null +++ b/src/MudBlazor.UnitTests/Components/DataGridGroupingTests.cs @@ -0,0 +1,544 @@ +// Copyright (c) MudBlazor 2021 +// MudBlazor licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using AngleSharp.Dom; +using Bunit; +using FluentAssertions; +using MudBlazor.UnitTests.TestComponents.DataGrid; +using NUnit.Framework; + +#nullable enable +namespace MudBlazor.UnitTests.Components +{ + public class DataGridGroupingTests : BunitTest + { + [Test] + public async Task DataGridGroupExpandedTrueTest() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + // until a change happens this bool tracks whether GroupExpanded is applied. + dataGrid.Instance._groupInitialExpanded = true; + comp.FindAll("tbody .mud-table-row").Count.Should().Be(7); + await comp.InvokeAsync(() => dataGrid.Instance.CollapseAllGroupsAsync()); + // collapsing group rows counts + dataGrid.Instance._groupInitialExpanded = false; + dataGrid.Render(); + // after all groups are collapsed + comp.FindAll("tbody .mud-table-row").Count.Should().Be(2); + await comp.InvokeAsync(() => + comp.Instance.AddFruit()); + // datagrid should not be expanded with the new category since CollapseAll collapsed it (Even if it was empty) + dataGrid.Render(); + comp.FindAll("tbody .mud-table-row").Count.Should().Be(3); + } + + [Test] + public async Task DataGridGroupExpandedTrueAsyncTest() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + + comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(7)); + await comp.InvokeAsync(() => dataGrid.Instance.CollapseAllGroupsAsync()); + dataGrid.Render(); + // after all groups are collapsed + comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(2)); + await comp.InvokeAsync(() => + comp.Instance.AddFruit()); + // datagrid should not be expanded with the new category since CollapseAll collapsed it (Even if it was empty) + dataGrid.Render(); + comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(3)); + } + + [Test] + public async Task DataGridGroupExpandedTrueServerDataTest() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + + comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(7)); + await comp.InvokeAsync(() => dataGrid.Instance.CollapseAllGroupsAsync()); + dataGrid.Render(); + // after all groups are collapsed + comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(2)); + await comp.InvokeAsync(() => comp.Instance.AddFruit()); + // datagrid should not be expanded with the new category since CollapseAll collapsed it (Even if it was empty) + dataGrid.Render(); + comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(3)); + } + + [Test] + public async Task DataGridGroupExpandedFalseTest() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + + comp.FindAll("tbody .mud-table-row").Count.Should().Be(2); + await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroupsAsync()); + dataGrid.Render(); + // after all groups are expanded + comp.FindAll("tbody .mud-table-row").Count.Should().Be(7); + await comp.InvokeAsync(() => + comp.Instance.AddFruit()); + // datagrid should not be collapsed with the new category since ExpandAll expanded it (Even if it was empty) + dataGrid.Render(); + comp.FindAll("tbody .mud-table-row").Count.Should().Be(10); + } + + [Test] + public async Task DataGridGroupExpandedFalseAsyncTest() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + + comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(2)); + await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroupsAsync()); + dataGrid.Render(); + // after all groups are expanded + comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(7)); + await comp.InvokeAsync(() => + comp.Instance.AddFruit()); + // datagrid should not be collapsed with the new category since ExpandAll expanded it (Even if it was empty) + dataGrid.Render(); + comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(10)); + } + + [Test] + public async Task DataGridGroupExpandedFalseServerDataTest() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + + comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(2)); + await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroupsAsync()); + dataGrid.Render(); + // after all groups are expanded + comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(7)); + await comp.InvokeAsync(() => comp.Instance.AddFruit()); + // datagrid should not be collapsed with the new category since ExpandAll expanded it (Even if it was empty) + dataGrid.Render(); + comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(10)); + } + + [Test] + public async Task DataGridGroupCollapseAllTest() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + + comp.FindAll("tbody .mud-table-row").Count.Should().Be(3); + await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroupsAsync()); + comp.Render(); + comp.FindAll("tbody .mud-table-row").Count.Should().Be(15); + await comp.InvokeAsync(() => dataGrid.Instance.CollapseAllGroupsAsync()); + comp.Render(); + comp.FindAll("tbody .mud-table-row").Count.Should().Be(3); + comp.Instance.RefreshList(); + comp.Render(); + // after all groups are expanded + comp.FindAll("tbody .mud-table-row").Count.Should().Be(3); + } + + [Test] + public async Task DataGridGroupExpandAllCollapseAllTest() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + + comp.FindAll("tbody .mud-table-row").Count.Should().Be(2); + await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroupsAsync()); + comp.Render(); + comp.FindAll("tbody .mud-table-row").Count.Should().Be(14); + await dataGrid.InvokeAsync(() => dataGrid.Instance.NavigateTo(Page.First)); + await dataGrid.InvokeAsync(() => dataGrid.Instance.NavigateTo(Page.Next)); + comp.FindAll("tbody .mud-table-row").Count.Should().Be(18); + await comp.InvokeAsync(() => dataGrid.Instance.CollapseAllGroupsAsync()); + await dataGrid.InvokeAsync(() => dataGrid.Instance.NavigateTo(Page.First)); + comp.FindAll("tbody .mud-table-row").Count.Should().Be(2); + comp.Instance.RefreshList(); + comp.Render(); + comp.FindAll("tbody .mud-table-row").Count.Should().Be(2); + } + + + [Test] + public void ShouldSetIsGenderGroupedToTrueWhenGroupingIsApplied() + { + // Render the DataGridGroupingTest component for testing. + var comp = Context.RenderComponent(); + + // Attempt to find the MudPopoverProvider component within the rendered component. + // MudPopoverProvider is used to manage popovers in the component, including the grouping popover. + var popoverProvider = comp.FindComponent(); + + // Assert that initially, before any user interaction, IsGenderGrouped should be false. + comp.Instance.IsGenderGrouped.Should().Be(false); + + // Find the button within the 'th' element with class 'gender' that triggers the popover for grouping. + var genderHeaderOption = comp.Find("th.gender .mud-menu button"); + + // Simulate a click on the gender header group button to open the popover with grouping options. + genderHeaderOption.Click(); + + // Find all MudListItem components within the popoverProvider. + // These list items represent the individual options within the grouping popover. + var listItems = popoverProvider.FindComponents(); + + // Assert that there are exactly 2 list items (options) available in the popover. + listItems.Count.Should().Be(2); + + // From the list items found, select the second one which is expected to be the clickable option for grouping. + var clickablePopover = listItems[1].Find(".mud-menu-item"); + + // click on the grouping option to apply grouping to the data grid. + clickablePopover.Click(); + + // After clicking the grouping option, assert that IsGenderGrouped is now true, indicating that + // the action of applying grouping has successfully updated the component's state. + comp.Instance.IsGenderGrouped.Should().Be(true); + } + + [Test] + [SetCulture("en-US")] + public void DataGridServerGroupUngroupingTest() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + var popoverProvider = comp.FindComponent(); + + //ungroup Category + var headerCategory = comp.Find("th.group .mud-menu button"); + headerCategory.Click(); + var catListItems = popoverProvider.FindComponents(); + catListItems.Count.Should().Be(4); + var clickableCategoryUngroup = catListItems[3].Find(".mud-menu-item"); + clickableCategoryUngroup.Click(); + + //click name grouping in grid + var headerOption = comp.Find("th.name .mud-menu button"); + headerOption.Click(); + var listItems = popoverProvider.FindComponents(); + listItems.Count.Should().Be(4); + var clickablePopover = listItems[3].Find(".mud-menu-item"); + clickablePopover.Click(); + var cells = dataGrid.FindAll("td"); + + //checking cell content is the most reliable way to verify grouping + cells[0].TextContent.Should().Be("Name: Hydrogen"); + cells[1].TextContent.Should().Be("Name: Helium"); + cells[2].TextContent.Should().Be("Name: Lithium"); + cells[3].TextContent.Should().Be("Name: Beryllium"); + cells[4].TextContent.Should().Be("Name: Boron"); + cells[5].TextContent.Should().Be("Name: Carbon"); + cells[6].TextContent.Should().Be("Name: Nitrogen"); + cells[7].TextContent.Should().Be("Name: Oxygen"); + cells[8].TextContent.Should().Be("Name: Fluorine"); + cells[9].TextContent.Should().Be("Name: Neon"); + dataGrid.Instance.IsGrouped.Should().BeTrue(); + + //click name ungrouping in grid + headerOption = comp.Find("th.name .mud-menu button"); + headerOption.Click(); + listItems = popoverProvider.FindComponents(); + listItems.Count.Should().Be(4); + clickablePopover = listItems[3].Find(".mud-menu-item"); + clickablePopover.Click(); + cells = dataGrid.FindAll("td"); + // We do not need check all 10 rows as it's clear that it's ungrouped if first row pass + cells[0].TextContent.Should().Be("1"); + cells[1].TextContent.Should().Be("H"); + cells[2].TextContent.Should().Be("Hydrogen"); + cells[3].TextContent.Should().Be("0"); + cells[4].TextContent.Should().Be("1.00794"); + cells[5].TextContent.Should().Be("Other"); + dataGrid.Instance.IsGrouped.Should().BeFalse(); + } + + [Test] + public void DataGridGroupingTestBoundAndUnboundScenarios() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + var popoverProvider = comp.FindComponent(); + + IRefreshableElementCollection Rows() => dataGrid.FindAll("tr"); + IRefreshableElementCollection Cells() => dataGrid.FindAll("td"); + + // Assert that initially, before any user interaction, IsGenderGrouped and IsAgeGrouped should be false + // The default grouping is by name + comp.Instance.IsGenderGrouped.Should().Be(false); + comp.Instance.IsAgeGrouped.Should().Be(false); + comp.Instance.IsProfessionGrouped.Should().Be(false); + + var nameCells = Cells(); + nameCells.Count.Should().Be(4, because: "4 data rows"); + nameCells[0].TextContent.Should().Be("Name: John"); + nameCells[1].TextContent.Should().Be("Name: Johanna"); + nameCells[2].TextContent.Should().Be("Name: Steve"); + nameCells[3].TextContent.Should().Be("Name: Alice"); + Rows().Count.Should().Be(6, because: "1 header row + 4 data rows + 1 footer row"); + + var ageGrouping = comp.Find(".GroupByAge"); + ageGrouping.Click(); + comp.Instance.IsAgeGrouped.Should().Be(true); + comp.Instance.IsGenderGrouped.Should().Be(false); + comp.Instance.IsProfessionGrouped.Should().Be(false); + var ageCells = Cells(); + ageCells.Count.Should().Be(3, because: "3 data rows"); + ageCells[0].TextContent.Should().Be("Age: 45"); + ageCells[1].TextContent.Should().Be("Age: 23"); + ageCells[2].TextContent.Should().Be("Age: 32"); + Rows().Count.Should().Be(5, because: "1 header row + 3 data rows + 1 footer row"); + + var genderGrouping = comp.Find(".GroupByGender"); + genderGrouping.Click(); + comp.Instance.IsGenderGrouped.Should().Be(true); + comp.Instance.IsAgeGrouped.Should().Be(false); + comp.Instance.IsProfessionGrouped.Should().Be(false); + var genderCells = Cells(); + genderCells.Count.Should().Be(2, because: "2 data rows"); + genderCells[0].TextContent.Should().Be("Gender: Male"); + genderCells[1].TextContent.Should().Be("Gender: Female"); + Rows().Count.Should().Be(4, because: "1 header row + 2 data rows + 1 footer row"); + + var professionGrouping = comp.Find(".GroupByProfession"); + professionGrouping.Click(); + comp.Instance.IsGenderGrouped.Should().Be(false); + comp.Instance.IsAgeGrouped.Should().Be(false); + comp.Instance.IsProfessionGrouped.Should().Be(true); + var professionCells = Cells(); + professionCells.Count.Should().Be(2, because: "2 data rows"); + professionCells[0].TextContent.Should().Be("Profession: Cook"); + professionCells[1].TextContent.Should().Be("Profession: (None)"); + Rows().Count.Should().Be(4, because: "1 header row + 2 data rows + 1 footer row"); + + //click age grouping in grid + var headerOption = comp.Find("th.age .mud-menu button"); + headerOption.Click(); + var listItems = popoverProvider.FindComponents(); + listItems.Count.Should().Be(2); + var clickablePopover = listItems[1].Find(".mud-menu-item"); + clickablePopover.Click(); + comp.Instance.IsAgeGrouped.Should().Be(false); + comp.Instance.IsGenderGrouped.Should().Be(false); + comp.Instance.IsProfessionGrouped.Should().Be(true); + Rows().Count.Should().Be(5, because: "1 header row + 3 data rows + 1 footer row"); + + //click gender grouping in grid + headerOption = comp.Find("th.gender .mud-menu button"); + headerOption.Click(); + listItems = popoverProvider.FindComponents(); + listItems.Count.Should().Be(2); + clickablePopover = listItems[1].Find(".mud-menu-item"); + clickablePopover.Click(); + comp.Instance.IsGenderGrouped.Should().Be(true); + comp.Instance.IsAgeGrouped.Should().Be(false); + comp.Instance.IsProfessionGrouped.Should().Be(true); + Rows().Count.Should().Be(5, because: "1 header row + 3 data rows + 1 footer row"); + + //click Name grouping in grid + headerOption = comp.Find("th.name .mud-menu button"); + headerOption.Click(); + listItems = popoverProvider.FindComponents(); + listItems.Count.Should().Be(2); + clickablePopover = listItems[1].Find(".mud-menu-item"); + clickablePopover.Click(); + comp.Instance.IsGenderGrouped.Should().Be(true); + comp.Instance.IsAgeGrouped.Should().Be(false); + comp.Instance.IsProfessionGrouped.Should().Be(true); + Rows().Count.Should().Be(6, because: "1 header row + 4 data rows + 1 footer row"); + + //click profession grouping in grid + headerOption = comp.Find("th.profession .mud-menu button"); + headerOption.Click(); + listItems = popoverProvider.FindComponents(); + listItems.Count.Should().Be(2); + clickablePopover = listItems[1].Find(".mud-menu-item"); + clickablePopover.Click(); + comp.Instance.IsGenderGrouped.Should().Be(true); + comp.Instance.IsAgeGrouped.Should().Be(false); + comp.Instance.IsProfessionGrouped.Should().Be(false); + Rows().Count.Should().Be(6, because: "1 header row + 4 data rows + 1 footer row"); + } + + + [Test] + public async Task DataGridGroupedWithServerDataPaginationTest() + { + var comp = Context.RenderComponent(); + var dataGrid = comp.FindComponent>(); + var rows = dataGrid.FindAll("tr"); + rows.Count.Should().Be(12, because: "1 header row + 10 data rows + 1 footer row"); + var cells = dataGrid.FindAll("td"); + cells.Count.Should().Be(10, because: "We have 10 data rows with one group collapsed"); + await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroupsAsync()); + rows = dataGrid.FindAll("tr"); + rows.Count.Should().Be(32, because: "1 header row + 10 data rows + 1 footer row + 10 group rows + 10 footer group rows"); + cells = dataGrid.FindAll("td"); + cells.Count.Should().Be(30, because: "We have 10 data rows with one group + 10*2 cells inside groups"); + //check cells + cells[0].TextContent.Should().Be("Number: 1"); + cells[1].TextContent.Should().Be("Hydrogen"); cells[2].TextContent.Should().Be("1"); + cells[3].TextContent.Should().Be("Number: 2"); + cells[4].TextContent.Should().Be("Helium"); cells[5].TextContent.Should().Be("2"); + cells[6].TextContent.Should().Be("Number: 3"); + cells[7].TextContent.Should().Be("Lithium"); cells[8].TextContent.Should().Be("3"); + cells[9].TextContent.Should().Be("Number: 4"); + cells[10].TextContent.Should().Be("Beryllium"); cells[11].TextContent.Should().Be("4"); + cells[12].TextContent.Should().Be("Number: 5"); + cells[13].TextContent.Should().Be("Boron"); cells[14].TextContent.Should().Be("5"); + cells[15].TextContent.Should().Be("Number: 6"); + cells[16].TextContent.Should().Be("Carbon"); cells[17].TextContent.Should().Be("6"); + cells[18].TextContent.Should().Be("Number: 7"); + cells[19].TextContent.Should().Be("Nitrogen"); cells[20].TextContent.Should().Be("7"); + cells[21].TextContent.Should().Be("Number: 8"); + cells[22].TextContent.Should().Be("Oxygen"); cells[23].TextContent.Should().Be("8"); + cells[24].TextContent.Should().Be("Number: 9"); + cells[25].TextContent.Should().Be("Fluorine"); cells[26].TextContent.Should().Be("9"); + cells[27].TextContent.Should().Be("Number: 10"); + cells[28].TextContent.Should().Be("Neon"); cells[29].TextContent.Should().Be("10"); + //get next page + await comp.InvokeAsync(() => dataGrid.Instance.NavigateTo(Page.Next)); + comp.Render(); + await comp.InvokeAsync(() => dataGrid.Instance.CollapseAllGroupsAsync()); + cells = dataGrid.FindAll("td"); + cells.Count.Should().Be(10, because: "We have 10 data rows with one group collapsed from next page"); + await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroupsAsync()); + cells = dataGrid.FindAll("td"); + cells.Count.Should().Be(30, because: "We have next 10 data rows with one group + 10*2 cells inside groups"); + //cells should have data from next page + cells[0].TextContent.Should().Be("Number: 11"); + cells[1].TextContent.Should().Be("Sodium"); cells[2].TextContent.Should().Be("11"); + cells[3].TextContent.Should().Be("Number: 12"); + cells[4].TextContent.Should().Be("Magnesium"); cells[5].TextContent.Should().Be("12"); + cells[6].TextContent.Should().Be("Number: 13"); + cells[7].TextContent.Should().Be("Aluminium"); cells[8].TextContent.Should().Be("13"); + cells[9].TextContent.Should().Be("Number: 14"); + cells[10].TextContent.Should().Be("Silicon"); cells[11].TextContent.Should().Be("14"); + cells[12].TextContent.Should().Be("Number: 15"); + cells[13].TextContent.Should().Be("Phosphorus"); cells[14].TextContent.Should().Be("15"); + cells[15].TextContent.Should().Be("Number: 16"); + cells[16].TextContent.Should().Be("Sulfur"); cells[17].TextContent.Should().Be("16"); + cells[18].TextContent.Should().Be("Number: 17"); + cells[19].TextContent.Should().Be("Chlorine"); cells[20].TextContent.Should().Be("17"); + cells[21].TextContent.Should().Be("Number: 18"); + cells[22].TextContent.Should().Be("Argon"); cells[23].TextContent.Should().Be("18"); + cells[24].TextContent.Should().Be("Number: 19"); + cells[25].TextContent.Should().Be("Potassium"); cells[26].TextContent.Should().Be("19"); + cells[27].TextContent.Should().Be("Number: 20"); + cells[28].TextContent.Should().Be("Calcium"); cells[29].TextContent.Should().Be("20"); + } + + [Test] + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void TestRtlGroupIconMethod(bool isRightToLeft, bool isExpanded) + { + var test = new MudDataGrid(); + if (isExpanded) + { + test.GetGroupIcon(isExpanded, isRightToLeft).Should().Be(Icons.Material.Filled.ExpandMore); + } + else + { + test.GetGroupIcon(isExpanded, isRightToLeft).Should().Be(isRightToLeft ? Icons.Material.Filled.ChevronLeft : Icons.Material.Filled.ChevronRight); + } + } + + [Test] + public async Task GroupExpandClick_ShouldToggleExpandedState() + { + // Arrange + var component = Context.RenderComponent(); + + var dataGrid = component.FindComponent>(); + await component.InvokeAsync(() => dataGrid.Instance.ReloadServerData()); + + var rows = component.FindComponents>(); + rows.Count.Should().Be(15); + var row = rows[0]; + // Test the method + // Act + void GetCount(bool currExpanded) + { + var defaultExpanded = row.Instance.GroupDefinition.Expanded; + // Whatever the expanded state is if it differs from the default it should be in the dictionary + dataGrid.Instance._groupExpansionsDict.Count.Should().Be(currExpanded != defaultExpanded ? 1 : 0); + } + + // Test the UI + var expandButton = () => row.Find(".mud-datagrid-group-button"); + expandButton.Should().NotBeNull(); + expandButton().Click(); + + row.WaitForAssertion(() => row.Instance._expanded.Should().BeFalse()); + row.WaitForAssertion(() => GetCount(false)); + expandButton().Click(); + + row.WaitForAssertion(() => row.Instance._expanded.Should().BeTrue()); + row.WaitForAssertion(() => GetCount(true)); + } + + [Test] + public async Task DataGrid_Grouping_TestGroupableSets() + { + var component = Context.RenderComponent(); + + var dataGrid = component.FindComponent>(); + // by default has a groupdefinition + dataGrid.WaitForAssertion(() => dataGrid.Instance._groupDefinition.Should().NotBeNull()); + // turn off grouping for the whole grid + dataGrid.SetParam(x => x.Groupable, false); + dataGrid.Render(); + await component.InvokeAsync(() => dataGrid.Instance.ReloadServerData()); + + // grouping shouldn't exist + dataGrid.Instance._groupDefinition.Should().BeNull(); + foreach (var column in dataGrid.Instance.RenderedColumns) + { + column.GroupingState.Value.Should().Be(false); + } + + // no grouping rows + var rows = component.FindComponents>(); + rows.Count.Should().Be(0); + } + + [Test] + public async Task DataGrid_Grouping_GroupDefinition() + { + var component = Context.RenderComponent(); + + var dataGrid = component.FindComponent>(); + await component.InvokeAsync(() => dataGrid.Instance.ReloadServerData()); + // grouping is already setup make sure group definition is not null and it's first inner definition is not null + dataGrid.WaitForAssertion(() => dataGrid.Instance._groupDefinition.Should().NotBeNull()); + dataGrid.Instance._groupDefinition.InnerGroup.Should().NotBeNull(); + dataGrid.Instance._groupDefinition.Grouping.Should().BeNullOrEmpty(); + // _groupDefinition is the definition for all the groups but isn't combined into the items until display so we need to + // check the final definitions from within the DataGridGroupRow + + var rows = component.FindComponents>(); + rows.Count.Should().Be(15); + var row = rows[0]; + + // Only One Manufacturing Primary Industry + row.Instance.GroupDefinition.Title.Should().Be("Primary Industry"); + row.Instance.GroupDefinition.Grouping.Key.Should().Be("Manufacturing"); + row.Instance.Items.Should().NotBeNull(); + row.Instance.Items.Count().Should().Be(1); + // Agriculture should have 2 items + row = rows[6]; + row.Instance.Items.Should().NotBeNull(); + row.Instance.Items.Count().Should().Be(2); + // the next row is a sub group of Agriculture and should also have 2 items + row = rows[7]; + row.Instance.Items.Should().NotBeNull(); + row.Instance.Items.Count().Should().Be(2); + } + } +} diff --git a/src/MudBlazor.UnitTests/Components/DataGridTests.cs b/src/MudBlazor.UnitTests/Components/DataGridTests.cs index 087883b1fc13..02f5082b1da5 100644 --- a/src/MudBlazor.UnitTests/Components/DataGridTests.cs +++ b/src/MudBlazor.UnitTests/Components/DataGridTests.cs @@ -4129,152 +4129,6 @@ public async Task DataGridCustomSortTest() headerCell.Instance.SortDirection.Should().Be(SortDirection.None); } - [Test] - public async Task DataGridGroupExpandedTrueTest() - { - var comp = Context.RenderComponent(); - var dataGrid = comp.FindComponent>(); - - comp.FindAll("tbody .mud-table-row").Count.Should().Be(7); - await comp.InvokeAsync(() => dataGrid.Instance.CollapseAllGroups()); - dataGrid.Render(); - // after all groups are collapsed - comp.FindAll("tbody .mud-table-row").Count.Should().Be(2); - await comp.InvokeAsync(() => - comp.Instance.AddFruit()); - // datagrid should be expanded with the new category - dataGrid.Render(); - comp.FindAll("tbody .mud-table-row").Count.Should().Be(5); - } - - [Test] - public async Task DataGridGroupExpandedTrueAsyncTest() - { - var comp = Context.RenderComponent(); - var dataGrid = comp.FindComponent>(); - - comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(7)); - await comp.InvokeAsync(() => dataGrid.Instance.CollapseAllGroups()); - dataGrid.Render(); - // after all groups are collapsed - comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(2)); - await comp.InvokeAsync(() => - comp.Instance.AddFruit()); - // datagrid should be expanded with the new category - dataGrid.Render(); - comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(5)); - } - - [Test] - public async Task DataGridGroupExpandedTrueServerDataTest() - { - var comp = Context.RenderComponent(); - var dataGrid = comp.FindComponent>(); - - comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(7)); - await comp.InvokeAsync(() => dataGrid.Instance.CollapseAllGroups()); - dataGrid.Render(); - // after all groups are collapsed - comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(2)); - await comp.InvokeAsync(() => comp.Instance.AddFruit()); - // datagrid should be expanded with the new category - dataGrid.Render(); - comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(5)); - } - - [Test] - public async Task DataGridGroupExpandedFalseTest() - { - var comp = Context.RenderComponent(); - var dataGrid = comp.FindComponent>(); - - comp.FindAll("tbody .mud-table-row").Count.Should().Be(2); - await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroups()); - dataGrid.Render(); - // after all groups are expanded - comp.FindAll("tbody .mud-table-row").Count.Should().Be(7); - await comp.InvokeAsync(() => - comp.Instance.AddFruit()); - // datagrid should be collapsed with the new category - dataGrid.Render(); - comp.FindAll("tbody .mud-table-row").Count.Should().Be(8); - } - - [Test] - public async Task DataGridGroupExpandedFalseAsyncTest() - { - var comp = Context.RenderComponent(); - var dataGrid = comp.FindComponent>(); - - comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(2)); - await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroups()); - dataGrid.Render(); - // after all groups are expanded - comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(7)); - await comp.InvokeAsync(() => - comp.Instance.AddFruit()); - // datagrid should be collapsed with the new category - dataGrid.Render(); - comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(8)); - } - - [Test] - public async Task DataGridGroupExpandedFalseServerDataTest() - { - var comp = Context.RenderComponent(); - var dataGrid = comp.FindComponent>(); - - comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(2)); - await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroups()); - dataGrid.Render(); - // after all groups are expanded - comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(7)); - await comp.InvokeAsync(() => comp.Instance.AddFruit()); - // datagrid should be collapsed with the new category - dataGrid.Render(); - comp.WaitForAssertion(() => comp.FindAll("tbody .mud-table-row").Count.Should().Be(8)); - } - - [Test] - public async Task DataGridGroupCollapseAllTest() - { - var comp = Context.RenderComponent(); - var dataGrid = comp.FindComponent>(); - - comp.FindAll("tbody .mud-table-row").Count.Should().Be(3); - await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroups()); - comp.Render(); - comp.FindAll("tbody .mud-table-row").Count.Should().Be(15); - await comp.InvokeAsync(() => dataGrid.Instance.CollapseAllGroups()); - comp.Render(); - comp.FindAll("tbody .mud-table-row").Count.Should().Be(3); - comp.Instance.RefreshList(); - comp.Render(); - // after all groups are expanded - comp.FindAll("tbody .mud-table-row").Count.Should().Be(3); - } - - [Test] - public async Task DataGridGroupExpandAllCollapseAllTest() - { - var comp = Context.RenderComponent(); - var dataGrid = comp.FindComponent>(); - - comp.FindAll("tbody .mud-table-row").Count.Should().Be(2); - await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroups()); - comp.Render(); - comp.FindAll("tbody .mud-table-row").Count.Should().Be(14); - await dataGrid.InvokeAsync(() => dataGrid.Instance.NavigateTo(Page.First)); - await dataGrid.InvokeAsync(() => dataGrid.Instance.NavigateTo(Page.Next)); - comp.FindAll("tbody .mud-table-row").Count.Should().Be(18); - await comp.InvokeAsync(() => dataGrid.Instance.CollapseAllGroups()); - await dataGrid.InvokeAsync(() => dataGrid.Instance.NavigateTo(Page.First)); - comp.FindAll("tbody .mud-table-row").Count.Should().Be(2); - comp.Instance.RefreshList(); - comp.Render(); - comp.FindAll("tbody .mud-table-row").Count.Should().Be(2); - } - [Test] public async Task DataGridPropertyColumnFormatTest() { @@ -4529,43 +4383,6 @@ public void DataGridRedundantMenuTest() columnOptionsSpan.TextContent.Trim().Should().BeEmpty(); } - [Test] - public void ShouldSetIsGenderGroupedToTrueWhenGroupingIsApplied() - { - // Render the DataGridGroupingTest component for testing. - var comp = Context.RenderComponent(); - - // Attempt to find the MudPopoverProvider component within the rendered component. - // MudPopoverProvider is used to manage popovers in the component, including the grouping popover. - var popoverProvider = comp.FindComponent(); - - // Assert that initially, before any user interaction, IsGenderGrouped should be false. - comp.Instance.IsGenderGrouped.Should().Be(false); - - // Find the button within the 'th' element with class 'gender' that triggers the popover for grouping. - var genderHeaderOption = comp.Find("th.gender .mud-menu button"); - - // Simulate a click on the gender header group button to open the popover with grouping options. - genderHeaderOption.Click(); - - // Find all MudListItem components within the popoverProvider. - // These list items represent the individual options within the grouping popover. - var listItems = popoverProvider.FindComponents(); - - // Assert that there are exactly 2 list items (options) available in the popover. - listItems.Count.Should().Be(2); - - // From the list items found, select the second one which is expected to be the clickable option for grouping. - var clickablePopover = listItems[1].Find(".mud-menu-item"); - - // click on the grouping option to apply grouping to the data grid. - clickablePopover.Click(); - - // After clicking the grouping option, assert that IsGenderGrouped is now true, indicating that - // the action of applying grouping has successfully updated the component's state. - comp.Instance.IsGenderGrouped.Should().Be(true); - } - [Test] public void DataGridDynamicColumnsTest() { @@ -4618,161 +4435,6 @@ public void DataGridSelectColumnTest() selectAllCheckboxes[1].Instance.Value.Should().BeFalse(); } - [Test] - [SetCulture("en-US")] - public void DataGridServerGroupUngroupingTest() - { - var comp = Context.RenderComponent(); - var dataGrid = comp.FindComponent>(); - var popoverProvider = comp.FindComponent(); - - //click name grouping in grid - var headerOption = comp.Find("th.name .mud-menu button"); - headerOption.Click(); - var listItems = popoverProvider.FindComponents(); - listItems.Count.Should().Be(4); - var clickablePopover = listItems[3].Find(".mud-menu-item"); - clickablePopover.Click(); - var cells = dataGrid.FindAll("td"); - - //checking cell content is the most reliable way to verify grouping - cells[0].TextContent.Should().Be("Name: Hydrogen"); - cells[1].TextContent.Should().Be("Name: Helium"); - cells[2].TextContent.Should().Be("Name: Lithium"); - cells[3].TextContent.Should().Be("Name: Beryllium"); - cells[4].TextContent.Should().Be("Name: Boron"); - cells[5].TextContent.Should().Be("Name: Carbon"); - cells[6].TextContent.Should().Be("Name: Nitrogen"); - cells[7].TextContent.Should().Be("Name: Oxygen"); - cells[8].TextContent.Should().Be("Name: Fluorine"); - cells[9].TextContent.Should().Be("Name: Neon"); - dataGrid.Instance.GroupedColumn.Should().NotBeNull(); - - //click name ungrouping in grid - headerOption = comp.Find("th.name .mud-menu button"); - headerOption.Click(); - listItems = popoverProvider.FindComponents(); - listItems.Count.Should().Be(4); - clickablePopover = listItems[3].Find(".mud-menu-item"); - clickablePopover.Click(); - cells = dataGrid.FindAll("td"); - // We do not need check all 10 rows as it's clear that it's ungrouped if first row pass - cells[0].TextContent.Should().Be("1"); - cells[1].TextContent.Should().Be("H"); - cells[2].TextContent.Should().Be("Hydrogen"); - cells[3].TextContent.Should().Be("0"); - cells[4].TextContent.Should().Be("1.00794"); - cells[5].TextContent.Should().Be("Other"); - dataGrid.Instance.GroupedColumn.Should().BeNull(); - } - - [Test] - public void DataGridGroupingTestBoundAndUnboundScenarios() - { - var comp = Context.RenderComponent(); - var dataGrid = comp.FindComponent>(); - var popoverProvider = comp.FindComponent(); - - IRefreshableElementCollection Rows() => dataGrid.FindAll("tr"); - IRefreshableElementCollection Cells() => dataGrid.FindAll("td"); - - // Assert that initially, before any user interaction, IsGenderGrouped and IsAgeGrouped should be false - // The default grouping is by name - comp.Instance.IsGenderGrouped.Should().Be(false); - comp.Instance.IsAgeGrouped.Should().Be(false); - comp.Instance.IsProfessionGrouped.Should().Be(false); - - var nameCells = Cells(); - nameCells.Count.Should().Be(4, because: "4 data rows"); - nameCells[0].TextContent.Should().Be("Name: John"); - nameCells[1].TextContent.Should().Be("Name: Johanna"); - nameCells[2].TextContent.Should().Be("Name: Steve"); - nameCells[3].TextContent.Should().Be("Name: Alice"); - Rows().Count.Should().Be(6, because: "1 header row + 4 data rows + 1 footer row"); - - var ageGrouping = comp.Find(".GroupByAge"); - ageGrouping.Click(); - comp.Instance.IsAgeGrouped.Should().Be(true); - comp.Instance.IsGenderGrouped.Should().Be(false); - comp.Instance.IsProfessionGrouped.Should().Be(false); - var ageCells = Cells(); - ageCells.Count.Should().Be(3, because: "3 data rows"); - ageCells[0].TextContent.Should().Be("Age: 45"); - ageCells[1].TextContent.Should().Be("Age: 23"); - ageCells[2].TextContent.Should().Be("Age: 32"); - Rows().Count.Should().Be(5, because: "1 header row + 3 data rows + 1 footer row"); - - var genderGrouping = comp.Find(".GroupByGender"); - genderGrouping.Click(); - comp.Instance.IsGenderGrouped.Should().Be(true); - comp.Instance.IsAgeGrouped.Should().Be(true, because: "Age is not bound"); - comp.Instance.IsProfessionGrouped.Should().Be(false); - var genderCells = Cells(); - genderCells.Count.Should().Be(2, because: "2 data rows"); - genderCells[0].TextContent.Should().Be("Gender: Male"); - genderCells[1].TextContent.Should().Be("Gender: Female"); - Rows().Count.Should().Be(4, because: "1 header row + 2 data rows + 1 footer row"); - - var professionGrouping = comp.Find(".GroupByProfession"); - professionGrouping.Click(); - comp.Instance.IsGenderGrouped.Should().Be(false); - comp.Instance.IsAgeGrouped.Should().Be(true, because: "Age is not bound"); - comp.Instance.IsProfessionGrouped.Should().Be(true); - var professionCells = Cells(); - professionCells.Count.Should().Be(2, because: "2 data rows"); - professionCells[0].TextContent.Should().Be("Profession: Cook"); - professionCells[1].TextContent.Should().Be("Profession: (None)"); - Rows().Count.Should().Be(4, because: "1 header row + 2 data rows + 1 footer row"); - - //click age grouping in grid - var headerOption = comp.Find("th.age .mud-menu button"); - headerOption.Click(); - var listItems = popoverProvider.FindComponents(); - listItems.Count.Should().Be(2); - var clickablePopover = listItems[1].Find(".mud-menu-item"); - clickablePopover.Click(); - comp.Instance.IsAgeGrouped.Should().Be(true); - comp.Instance.IsGenderGrouped.Should().Be(false); - comp.Instance.IsProfessionGrouped.Should().Be(false); - Rows().Count.Should().Be(5, because: "1 header row + 3 data rows + 1 footer row"); - - //click gender grouping in grid - headerOption = comp.Find("th.gender .mud-menu button"); - headerOption.Click(); - listItems = popoverProvider.FindComponents(); - listItems.Count.Should().Be(2); - clickablePopover = listItems[1].Find(".mud-menu-item"); - clickablePopover.Click(); - comp.Instance.IsGenderGrouped.Should().Be(true); - comp.Instance.IsAgeGrouped.Should().Be(true, because: "Age is not bound"); - comp.Instance.IsProfessionGrouped.Should().Be(false); - Rows().Count.Should().Be(4, because: "1 header row + 2 data rows + 1 footer row"); - - //click Name grouping in grid - headerOption = comp.Find("th.name .mud-menu button"); - headerOption.Click(); - listItems = popoverProvider.FindComponents(); - listItems.Count.Should().Be(2); - clickablePopover = listItems[1].Find(".mud-menu-item"); - clickablePopover.Click(); - comp.Instance.IsGenderGrouped.Should().Be(false); - comp.Instance.IsAgeGrouped.Should().Be(true, because: "Age is not bound"); - comp.Instance.IsProfessionGrouped.Should().Be(false); - Rows().Count.Should().Be(6, because: "1 header row + 4 data rows + 1 footer row"); - - //click profession grouping in grid - headerOption = comp.Find("th.profession .mud-menu button"); - headerOption.Click(); - listItems = popoverProvider.FindComponents(); - listItems.Count.Should().Be(2); - clickablePopover = listItems[1].Find(".mud-menu-item"); - clickablePopover.Click(); - comp.Instance.IsGenderGrouped.Should().Be(false); - comp.Instance.IsAgeGrouped.Should().Be(true, because: "Age is not bound"); - comp.Instance.IsProfessionGrouped.Should().Be(true); - Rows().Count.Should().Be(4, because: "1 header row + 2 data rows + 1 footer row"); - } - [Test] public async Task FilterDefinitionTestHasFilterProperty() { @@ -4798,72 +4460,6 @@ await comp.InvokeAsync(() => dataGrid.Instance.AddFilterAsync(new FilterDefiniti statusHeaderCell.Instance.hasFilter.Should().BeFalse(); } - [Test] - public async Task DataGridGroupedWithServerDataPaginationTest() - { - var comp = Context.RenderComponent(); - var dataGrid = comp.FindComponent>(); - var rows = dataGrid.FindAll("tr"); - rows.Count.Should().Be(12, because: "1 header row + 10 data rows + 1 footer row"); - var cells = dataGrid.FindAll("td"); - cells.Count.Should().Be(10, because: "We have 10 data rows with one group collapsed"); - await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroups()); - rows = dataGrid.FindAll("tr"); - rows.Count.Should().Be(32, because: "1 header row + 10 data rows + 1 footer row + 10 group rows + 10 footer group rows"); - cells = dataGrid.FindAll("td"); - cells.Count.Should().Be(30, because: "We have 10 data rows with one group + 10*2 cells inside groups"); - //check cells - cells[0].TextContent.Should().Be("Number: 1"); - cells[1].TextContent.Should().Be("Hydrogen"); cells[2].TextContent.Should().Be("1"); - cells[3].TextContent.Should().Be("Number: 2"); - cells[4].TextContent.Should().Be("Helium"); cells[5].TextContent.Should().Be("2"); - cells[6].TextContent.Should().Be("Number: 3"); - cells[7].TextContent.Should().Be("Lithium"); cells[8].TextContent.Should().Be("3"); - cells[9].TextContent.Should().Be("Number: 4"); - cells[10].TextContent.Should().Be("Beryllium"); cells[11].TextContent.Should().Be("4"); - cells[12].TextContent.Should().Be("Number: 5"); - cells[13].TextContent.Should().Be("Boron"); cells[14].TextContent.Should().Be("5"); - cells[15].TextContent.Should().Be("Number: 6"); - cells[16].TextContent.Should().Be("Carbon"); cells[17].TextContent.Should().Be("6"); - cells[18].TextContent.Should().Be("Number: 7"); - cells[19].TextContent.Should().Be("Nitrogen"); cells[20].TextContent.Should().Be("7"); - cells[21].TextContent.Should().Be("Number: 8"); - cells[22].TextContent.Should().Be("Oxygen"); cells[23].TextContent.Should().Be("8"); - cells[24].TextContent.Should().Be("Number: 9"); - cells[25].TextContent.Should().Be("Fluorine"); cells[26].TextContent.Should().Be("9"); - cells[27].TextContent.Should().Be("Number: 10"); - cells[28].TextContent.Should().Be("Neon"); cells[29].TextContent.Should().Be("10"); - //get next page - dataGrid.Instance.CurrentPage = 1; - await comp.InvokeAsync(async () => await dataGrid.Instance.ReloadServerData()); - cells = dataGrid.FindAll("td"); - cells.Count.Should().Be(10, because: "We have 10 data rows with one group collapsed from next page"); - await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroups()); - cells = dataGrid.FindAll("td"); - cells.Count.Should().Be(30, because: "We have next 10 data rows with one group + 10*2 cells inside groups"); - //cells should have data from next page - cells[0].TextContent.Should().Be("Number: 11"); - cells[1].TextContent.Should().Be("Sodium"); cells[2].TextContent.Should().Be("11"); - cells[3].TextContent.Should().Be("Number: 12"); - cells[4].TextContent.Should().Be("Magnesium"); cells[5].TextContent.Should().Be("12"); - cells[6].TextContent.Should().Be("Number: 13"); - cells[7].TextContent.Should().Be("Aluminium"); cells[8].TextContent.Should().Be("13"); - cells[9].TextContent.Should().Be("Number: 14"); - cells[10].TextContent.Should().Be("Silicon"); cells[11].TextContent.Should().Be("14"); - cells[12].TextContent.Should().Be("Number: 15"); - cells[13].TextContent.Should().Be("Phosphorus"); cells[14].TextContent.Should().Be("15"); - cells[15].TextContent.Should().Be("Number: 16"); - cells[16].TextContent.Should().Be("Sulfur"); cells[17].TextContent.Should().Be("16"); - cells[18].TextContent.Should().Be("Number: 17"); - cells[19].TextContent.Should().Be("Chlorine"); cells[20].TextContent.Should().Be("17"); - cells[21].TextContent.Should().Be("Number: 18"); - cells[22].TextContent.Should().Be("Argon"); cells[23].TextContent.Should().Be("18"); - cells[24].TextContent.Should().Be("Number: 19"); - cells[25].TextContent.Should().Be("Potassium"); cells[26].TextContent.Should().Be("19"); - cells[27].TextContent.Should().Be("Number: 20"); - cells[28].TextContent.Should().Be("Calcium"); cells[29].TextContent.Should().Be("20"); - } - /// /// Reproduce the bug from https://github.com/MudBlazor/MudBlazor/issues/9585 /// When a column is hidden by the menu and the precedent column is resized, then the app crash @@ -5054,24 +4650,6 @@ public async Task TestCurrentPageParameterTwoWayBinding() comp.WaitForAssertion(() => comp.Find(".mud-table-body .mud-table-row .mud-table-cell").TextContent.Should().Be("3")); } - [Test] - [TestCase(true, true)] - [TestCase(true, false)] - [TestCase(false, true)] - [TestCase(false, false)] - public void TestRtlGroupIconMethod(bool isRightToLeft, bool isExpanded) - { - var test = new MudDataGrid(); - if (isExpanded) - { - test.GetGroupIcon(isExpanded, isRightToLeft).Should().Be(Icons.Material.Filled.ExpandMore); - } - else - { - test.GetGroupIcon(isExpanded, isRightToLeft).Should().Be(isRightToLeft ? Icons.Material.Filled.ChevronLeft : Icons.Material.Filled.ChevronRight); - } - } - /// /// Verifies data grid does not reuse row child components for different items (the @key for the row is set to the user supplied item). /// @@ -5091,10 +4669,9 @@ public async Task DataGridUniqueRowKey() before.Should().NotBeSameAs(after, because: "If the @key is correctly set to the row item, child components will be recreated on row reordering."); - //Test the expanded group case comp.SetParametersAndRender(parameters => parameters.Add(p => p.Group, true)); - await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroups()); + await comp.InvokeAsync(() => dataGrid.Instance.ExpandAllGroupsAsync()); await comp.InvokeAsync(() => dataGrid.Instance.SetSortAsync(sortByColumnName, SortDirection.Ascending, x => x)); before = dataGrid.FindComponent>(); diff --git a/src/MudBlazor.UnitTests/Components/UserAttributes/UserAttributesTests.cs b/src/MudBlazor.UnitTests/Components/UserAttributes/UserAttributesTests.cs index 67309a85b955..5b3c8d35a755 100644 --- a/src/MudBlazor.UnitTests/Components/UserAttributes/UserAttributesTests.cs +++ b/src/MudBlazor.UnitTests/Components/UserAttributes/UserAttributesTests.cs @@ -23,6 +23,8 @@ static UserAttributesTests() Exclude(typeof(MudPicker<>)); // Internal component, skip Exclude(typeof(MudRadioGroup<>)); // Wrapping component, skip Exclude(typeof(MudOverlay)); // Sectioned component, skip + Exclude(typeof(DataGridGroupRow<>)); // Internal component, skip + Exclude(typeof(DataGridVirtualizeRow<>)); // Internal component, skip } [Test] diff --git a/src/MudBlazor/Attributes/CategoryAttribute.cs b/src/MudBlazor/Attributes/CategoryAttribute.cs index 27bc6c65e719..b3bf3d031e71 100644 --- a/src/MudBlazor/Attributes/CategoryAttribute.cs +++ b/src/MudBlazor/Attributes/CategoryAttribute.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace MudBlazor +namespace MudBlazor { /// @@ -251,6 +248,22 @@ public static class Container public const string Behavior = "Behavior"; } + public static class DataGrid + { + public const string Data = "Data"; + public const string Behavior = "Behavior"; + public const string Header = "Header"; + public const string Rows = "Rows"; + public const string Footer = "Footer"; + public const string Filtering = "Filtering"; + public const string Grouping = "Grouping"; + public const string Sorting = "Sorting"; + public const string Pagination = "Pagination"; + public const string Selecting = "Selecting"; + public const string Editing = "Editing"; + public const string Appearance = "Appearance"; + } + public static class Dialog { public const string Behavior = "Behavior"; diff --git a/src/MudBlazor/Components/DataGrid/Column.razor.cs b/src/MudBlazor/Components/DataGrid/Column.razor.cs index 05fad1ef2ca3..a66d257d6740 100644 --- a/src/MudBlazor/Components/DataGrid/Column.razor.cs +++ b/src/MudBlazor/Components/DataGrid/Column.razor.cs @@ -22,6 +22,8 @@ public abstract partial class Column<[DynamicallyAccessedMembers(DynamicallyAcce private static readonly RenderFragment> EmptyChildContent = _ => builder => { }; internal ParameterState HiddenState { get; } internal ParameterState GroupingState { get; } + internal ParameterState _groupExpandedState; + internal ParameterState _groupByOrderState; /// /// The data grid which owns this column. @@ -115,6 +117,42 @@ public abstract partial class Column<[DynamicallyAccessedMembers(DynamicallyAcce [Parameter] public Func GroupBy { get; set; } + /// + /// The order in which values are grouped when there are more than one group + /// + /// + /// Defaults to 0. + /// + [Parameter] + public int GroupByOrder { get; set; } + + /// + /// Occurs when the property has changed. + /// + [Parameter] + public EventCallback GroupByOrderChanged { get; set; } + + /// + /// Whether the column is indented 48px beyond it's parent when grouped. + /// + [Parameter] + public bool GroupIndented { get; set; } = true; + + /// + /// Whether groups created from this column are expanded. Toggling the value will Toggle all grouped rows of this column. + /// + /// + /// Defaults to false. + /// + [Parameter] + public bool GroupExpanded { get; set; } + + /// + /// Occurs when the property has changed. + /// + [Parameter] + public EventCallback GroupExpandedChanged { get; set; } + /// /// Requires a value to be set. /// @@ -547,16 +585,38 @@ protected Column() .WithParameter(() => Grouping) .WithEventCallback(() => GroupingChanged) .WithChangeHandler(OnGroupingParameterChangedAsync); + _groupExpandedState = registerScope.RegisterParameter(nameof(GroupExpanded)) + .WithParameter(() => GroupExpanded) + .WithChangeHandler(OnGroupExpandedChangedAsync); + _groupByOrderState = registerScope.RegisterParameter(nameof(GroupByOrder)) + .WithParameter(() => GroupByOrder) + .WithChangeHandler(OnGroupByOrderChangedAsync); } private async Task OnGroupingParameterChangedAsync() { - if (GroupingState.Value) + // Regroup DataGrid + if (DataGrid is not null) { - if (DataGrid is not null) - { - await DataGrid.ChangedGrouping(this); - } + await DataGrid.ChangedGrouping(this); + } + } + + private async Task OnGroupExpandedChangedAsync() + { + // Regroup DataGrid + if (DataGrid is not null) + { + await DataGrid.ChangedGrouping(); + } + } + + private async Task OnGroupByOrderChangedAsync() + { + // Regroup DataGrid + if (DataGrid is not null) + { + await DataGrid.ChangedGrouping(); } } @@ -652,11 +712,6 @@ internal void CompileGroupBy() internal async Task SetGroupingAsync(bool group) { await GroupingState.SetValueAsync(group); - - if (DataGrid is not null) - { - await DataGrid.ChangedGrouping(this); - } } /// diff --git a/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor b/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor new file mode 100644 index 000000000000..aa6a53ed0b05 --- /dev/null +++ b/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor @@ -0,0 +1,57 @@ +@namespace MudBlazor +@inherits MudComponentBase +@typeparam T + + + +
+ + @if (GroupDefinition?.GroupTemplate == null) + { + @GroupDefinition?.Title: @(Items?.Key ?? "null") + } + else + { + @GroupDefinition.GroupTemplate(GroupDefinition) + } +
+ + + +@if (_expanded) +{ + + @if (GroupDefinition.InnerGroup != null) + { + var innerGroupItems = DataGrid?.GetItemsOfGroup(GroupDefinition.InnerGroup, Items); + var groupDefinitions = DataGrid.GetGroupDefinitions(GroupDefinition.InnerGroup, innerGroupItems); + @foreach (var group in groupDefinitions) + { + + } + } + else + { + @if (Items != null) + { + + + + @DataGrid.FooterCells(Items) + + } + } +} diff --git a/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor.cs b/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor.cs new file mode 100644 index 000000000000..f33d2bc2904a --- /dev/null +++ b/src/MudBlazor/Components/DataGrid/DataGridGroupRow.razor.cs @@ -0,0 +1,107 @@ +// Copyright (c) MudBlazor 2021 +// MudBlazor licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor.Utilities; + +#nullable enable + +namespace MudBlazor +{ + public partial class DataGridGroupRow<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : MudComponentBase + { + internal bool _expanded; + + protected string GroupClassname => new CssBuilder("mud-table-cell") + .AddClass("mud-datagrid-group") + .AddClass($"mud-row-group-indented-{(GroupDefinition.Indentation ? Math.Min(GroupDefinition.Level, 5) : 0)}") + .AddClass(GroupClassFunc?.Invoke(GroupDefinition)) + .AddClass(GroupClass) + .Build(); + + protected string GroupStylename => new StyleBuilder() + .AddStyle(GroupStyle) + .AddStyle(GroupStyleFunc?.Invoke(GroupDefinition)) + .Build(); + + [Parameter, EditorRequired] + [Category(CategoryTypes.DataGrid.Grouping)] + public MudDataGrid DataGrid { get; set; } = null!; + + [Parameter] + [Category(CategoryTypes.DataGrid.Selecting)] + public EventCallback<(MouseEventArgs args, T item, int index)> RowClick { get; set; } + + [Parameter] + [Category(CategoryTypes.DataGrid.Selecting)] + public EventCallback<(MouseEventArgs args, T item, int index)> ContextRowClick { get; set; } + + /// + /// The definition for this grouping level + /// + [Parameter, EditorRequired] + [Category(CategoryTypes.DataGrid.Grouping)] + public GroupDefinition GroupDefinition { get; set; } = null!; + + /// + /// The groups and items within this grouping. + /// + [Parameter] + [Category(CategoryTypes.DataGrid.Grouping)] + public IGrouping? Items { get; set; } + + [Parameter] + [Category(CategoryTypes.DataGrid.Appearance)] + public string? GroupClass { get; set; } + + [Parameter] + [Category(CategoryTypes.DataGrid.Appearance)] + public string? GroupStyle { get; set; } + + [Parameter] + [Category(CategoryTypes.DataGrid.Appearance)] + public Func, string>? GroupClassFunc { get; set; } + + [Parameter] + [Category(CategoryTypes.DataGrid.Appearance)] + public Func, string>? GroupStyleFunc { get; set; } + + [Parameter] + [Category(CategoryTypes.DataGrid.Appearance)] + public string? StyleClass { get; set; } + + protected override void OnParametersSet() + { + _expanded = GroupDefinition.Expanded; + base.OnParametersSet(); + } + + internal void GroupExpandClick() + { + _expanded = !_expanded; + // update the expansion state for _groupExpansionsDict + // if it has a key we see if it differs from the definition Expanded State and update accordingly + // if it doesn't we add it if the new state doesn't match the definition + if (Items != null) + { + var key = new { GroupDefinition.Title, Items.Key }; + if (DataGrid._groupExpansionsDict.ContainsKey(key)) + { + if (_expanded == GroupDefinition.Expanded) + DataGrid._groupExpansionsDict.Remove(key); + else + DataGrid._groupExpansionsDict[key] = _expanded; + } + else + { + if (_expanded != GroupDefinition.Expanded) + DataGrid._groupExpansionsDict.TryAdd(key, _expanded); + } + } + DataGrid._groupInitialExpanded = false; + } + } +} diff --git a/src/MudBlazor/Components/DataGrid/DataGridVirtualizeRow.razor b/src/MudBlazor/Components/DataGrid/DataGridVirtualizeRow.razor new file mode 100644 index 000000000000..0aaa5a3b4aa7 --- /dev/null +++ b/src/MudBlazor/Components/DataGrid/DataGridVirtualizeRow.razor @@ -0,0 +1,87 @@ +@namespace MudBlazor +@inherits MudComponentBase +@typeparam T +@using MudBlazor.Utilities +@using MudBlazor.Resources +@inject InternalMudLocalizer Localizer + +@{ + var resolvedItems = new List>(0); + if (DataGrid.IsGrouped) + { + if (GroupedItems is not null) + { + resolvedItems = GroupedItems.Select((item, index) => new IndexBag(index, item)).ToList(); + } + } + else if (!DataGrid.Virtualize || DataGrid.VirtualizeServerData == null || DataGrid.HasFooter) + { + resolvedItems = DataGrid.CurrentPageItems.Select((items, index) => new IndexBag(index, items)) + .ToList(); + } + else + { + resolvedItems = DataGrid.CurrentPageItems.Select((item, index) => new IndexBag(index, item)).ToList(); + } +} + + + + @if (DataGrid.RowLoadingContent != null) + { + @DataGrid.RowLoadingContent + } + else + { + + + @Localizer[LanguageResource.MudDataGrid_Loading] + + + } + + + + @{ + var rowClass = new CssBuilder(DataGrid.RowClass).AddClass(DataGrid.RowClassFunc?.Invoke(itemBag.Item, itemBag.Index)).Build(); + var rowStyle = new StyleBuilder().AddStyle(DataGrid.RowStyle).AddStyle(DataGrid.RowStyleFunc?.Invoke(itemBag.Item, itemBag.Index)).Build(); + } + + + @foreach (var column in DataGrid.RenderedColumns) + { + if (!column.HiddenState.Value) + { + @DataGrid.Cell(column, itemBag.Item) + } + } + + + @if (DataGrid.ChildRowContent != null && (DataGrid._openHierarchies.Contains(itemBag.Item) || !DataGrid.HasHierarchyColumn)) + { + + + @DataGrid.ChildRowContent(new CellContext(DataGrid, itemBag.Item)) + + + } + + + + +
+ @DataGrid.NoRecordsContent +
+ + +
+
diff --git a/src/MudBlazor/Components/DataGrid/DataGridVirtualizeRow.razor.cs b/src/MudBlazor/Components/DataGrid/DataGridVirtualizeRow.razor.cs new file mode 100644 index 000000000000..65efed9fe67e --- /dev/null +++ b/src/MudBlazor/Components/DataGrid/DataGridVirtualizeRow.razor.cs @@ -0,0 +1,30 @@ +// Copyright (c) MudBlazor 2021 +// MudBlazor licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; + +#nullable enable +namespace MudBlazor +{ + public partial class DataGridVirtualizeRow<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : MudComponentBase + { + [Parameter, EditorRequired] + [Category(CategoryTypes.DataGrid.Data)] + public MudDataGrid DataGrid { get; set; } = null!; + + [Parameter] + [Category(CategoryTypes.DataGrid.Grouping)] + public IGrouping? GroupedItems { get; set; } + + [Parameter] + [Category(CategoryTypes.DataGrid.Selecting)] + public EventCallback<(MouseEventArgs args, T item, int index)> RowClick { get; set; } + + [Parameter] + [Category(CategoryTypes.DataGrid.Selecting)] + public EventCallback<(MouseEventArgs args, T item, int index)> ContextRowClick { get; set; } + } +} diff --git a/src/MudBlazor/Components/DataGrid/GroupDefinition.cs b/src/MudBlazor/Components/DataGrid/GroupDefinition.cs index 48b1e6ef9325..31bf117ac7f2 100644 --- a/src/MudBlazor/Components/DataGrid/GroupDefinition.cs +++ b/src/MudBlazor/Components/DataGrid/GroupDefinition.cs @@ -2,6 +2,8 @@ // MudBlazor licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.AspNetCore.Components; + namespace MudBlazor; #nullable enable @@ -11,10 +13,20 @@ namespace MudBlazor; /// public class GroupDefinition { + private GroupDefinition? _innerGroup; + /// /// The LINQ definition of the grouping. /// - public IGrouping Grouping { get; set; } + public required IGrouping Grouping { get; set; } + + /// + /// The function which selects items for this group. + /// + /// + /// Typically used during a LINQ GroupBy() call to group items. + /// + public Func Selector { get; set; } = default!; /// /// Expands this group. @@ -25,13 +37,61 @@ public class GroupDefinition public bool Expanded { get; set; } /// - /// Creates a new instance. + /// The template for the grouped column. /// - /// The LINQ definition of the grouping. - /// Expands this group. - public GroupDefinition(IGrouping grouping, bool expanded) + public RenderFragment>? GroupTemplate { get; set; } + + /// + /// The title of the grouped column + /// + public string Title { get; set; } = string.Empty; + + /// + /// The group definition within this definition. + /// + public GroupDefinition? InnerGroup { - Grouping = grouping; - Expanded = expanded; + get => _innerGroup; + set + { + if (_innerGroup is not null) + { + _innerGroup.Parent = null; + } + + _innerGroup = value; + + if (_innerGroup is not null) + { + _innerGroup.Parent = this; + _innerGroup.Indentation = Indentation; + } + } + } + + /// + /// Indents the each Group beyond the first by 48 px. + /// + public bool Indentation { get; set; } = true; + + /// + /// The parent group definition. + /// + internal GroupDefinition? Parent { get; set; } + + /// + /// Gets the nesting level of this group. + /// + public int Level + { + get + { + if (Parent is null) + { + return 1; + } + + return Parent.Level + 1; + } } } diff --git a/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs b/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs index 041250acdea5..add581867239 100644 --- a/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs +++ b/src/MudBlazor/Components/DataGrid/HeaderCell.razor.cs @@ -538,8 +538,12 @@ internal async Task GroupColumnAsync() if (Column is not null) { await Column.SetGroupingAsync(true); + await DataGrid.ChangedGrouping(Column); + } + else + { + await DataGrid.ChangedGrouping(); } - DataGrid.DropContainerHasChanged(); } @@ -549,7 +553,7 @@ internal async Task UngroupColumnAsync() { await Column.SetGroupingAsync(false); } - + await DataGrid.ChangedGrouping(); DataGrid.DropContainerHasChanged(); } diff --git a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor index f916be204e6b..7af66234909b 100644 --- a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor +++ b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor @@ -4,7 +4,6 @@ @using MudBlazor.Utilities @using MudBlazor.Extensions @using MudBlazor.Resources - @inject InternalMudLocalizer Localizer @{ @@ -41,30 +40,30 @@
- - @if (ColGroup != null) - { - - @ColGroup - - } - - - @Header - - - @foreach (var column in RenderedColumns) - { - - } - +
+ @if (ColGroup != null) + { + + @ColGroup + + } + + + @Header + + + @foreach (var column in RenderedColumns) + { + + } + @if (_filtersMenuVisible && FilterMode == DataGridFilterMode.Simple) { } @if (Filterable && FilterMode == DataGridFilterMode.ColumnFilterRow) - { - - @foreach (var column in RenderedColumns) { - + + @foreach (var column in RenderedColumns) + { + + } + } - - } - @if (Loading) - { - - - - } - - - @{ - var resolvedPageItems = new List>(0); - // resolve the page items only when used - if (!Virtualize || VirtualizeServerData == null || HasFooter) - { - resolvedPageItems = CurrentPageItems.Select((item, index) => new IndexBag(index, item)) - .ToList(); - } - } - @if (Virtualize || resolvedPageItems is { Count: > 0 }) - { - if (GroupedColumn != null) - { - if (_currentPageGroups is { Count: 0 }) + @if (Loading) { - } - foreach (var g in _currentPageGroups) + + + @if (Virtualize || CurrentPageItems.Count() > 0) { - - @{ var groupClass = new CssBuilder(GroupClass).AddClass(GroupClassFunc?.Invoke(g)).Build(); } - @{ var groupStyle = new StyleBuilder().AddStyle(GroupStyle).AddStyle(GroupStyleFunc?.Invoke(g)).Build(); } - - - - - @if (g.Expanded) + if (IsGrouped) { - - - @if (RowLoadingContent != null) - { - @RowLoadingContent - } - else - { - - - - } - - - - @{ var rowClass = new CssBuilder(RowClass).AddClass(RowClassFunc?.Invoke(itemBag.Item, itemBag.Index)).Build(); } - @{ var rowStyle = new StyleBuilder().AddStyle(RowStyle).AddStyle(RowStyleFunc?.Invoke(itemBag.Item, itemBag.Index)).Build(); } - - - @foreach (var column in RenderedColumns) - { - if (!column.HiddenState.Value) - { - @Cell(column, itemBag.Item) - } - } - - - @if (ChildRowContent != null && (_openHierarchies.Contains(itemBag.Item) || !HasHierarchyColumn)) - { - - - - } - - - - - - - - @*Group Footer*@ - - @FooterCells(g.Grouping.ToList()) - - } - } - } - else - { - - - @if (RowLoadingContent != null) + var groupItemsPage = GroupItemsPage; + var groupDefinitions = GetGroupDefinitions(_groupDefinition, groupItemsPage); + if (!groupItemsPage.Any()) { - @RowLoadingContent + + + } - else + foreach (var g in groupDefinitions) { - - - + } - - - - @{ var rowClass = new CssBuilder(RowClass).AddClass(RowClassFunc?.Invoke(itemBag.Item, itemBag.Index)).Build(); } - @{ var rowStyle = new StyleBuilder().AddStyle(RowStyle).AddStyle(RowStyleFunc?.Invoke(itemBag.Item, itemBag.Index)).Build(); } - - - @foreach (var column in RenderedColumns) + } + else + { + + } + } + else if (Loading ? LoadingContent != null : NoRecordsContent != null) + { + + - @if (ChildRowContent != null && (_openHierarchies.Contains(itemBag.Item) || !HasHierarchyColumn)) - { - - - - } - - - - - - - - } - } - else if (Loading ? LoadingContent != null : NoRecordsContent != null) - { - - - - } - - - - @FooterCells(resolvedPageItems.Select(x=>x.Item)) - - -
+ TransformOrigin="Origin.TopLeft" Elevation="4" Style="width:700px;max-width:700px;z-index:13;" MaxHeight="400"> @if (FilterTemplate == null) { @@ -94,18 +93,18 @@
+ AnchorOrigin="Origin.TopLeft" + TransformOrigin="Origin.TopLeft" Elevation="4" + Style="max-width:700px;z-index:11;" MaxHeight="400">
+ Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Search" Margin="Margin.Dense" FullWidth="true" />
+ ItemDisabled="@((item) => !this.ColumnsPanelReordering)" ItemDropped="ColumnOrderUpdated"> @@ -173,8 +172,8 @@ @if (Groupable) { - @Localizer[LanguageResource.MudDataGrid_CollapseAllGroups] - @Localizer[LanguageResource.MudDataGrid_ExpandAllGroups] + @Localizer[LanguageResource.MudDataGrid_CollapseAllGroups] + @Localizer[LanguageResource.MudDataGrid_ExpandAllGroups] } @@ -186,209 +185,84 @@
- -
-
- @NoRecordsContent -
+
+
- - - @if (GroupedColumn.GroupTemplate == null) - { - @GroupedColumn.Title: @g.Grouping.Key - } - else - { - @GroupedColumn.GroupTemplate(@g) - } -
- @Localizer[LanguageResource.MudDataGrid_Loading] -
- @ChildRowContent(new CellContext(this, itemBag.Item)) -
-
- @NoRecordsContent -
-
+
+ @NoRecordsContent +
+
- @Localizer[LanguageResource.MudDataGrid_Loading] -
+
+ @if (Loading) { - if (!column.HiddenState.Value) - { - @Cell(column, itemBag.Item) - } + @LoadingContent } - -
- @ChildRowContent(new CellContext(this, itemBag.Item)) -
-
+ else + { @NoRecordsContent -
-
-
- @if (Loading) - { - @LoadingContent - } - else - { - @NoRecordsContent - } -
-
+ } +
+ + + } + + + + @FooterCells(CurrentPageItems) + + + @context.HeaderCell.TableHeader() @@ -422,12 +296,12 @@ if (propertyType == typeof(string)) { + Required=column.Required Variant="@Variant.Outlined" Disabled="@(!column.Editable || ReadOnly)" Class="mt-4" /> } else if (TypeIdentifier.IsNumber(propertyType)) { + Required=column.Required Variant="@Variant.Outlined" Disabled="@(!column.Editable || ReadOnly)" Class="mt-4" /> } } } @@ -444,13 +318,13 @@ @code { internal RenderFragment ToolbarMenu(MudDataGrid dataGrid) => - @ - + @ + @Localizer[LanguageResource.MudDataGrid_Columns] @if (dataGrid.Groupable) { - @Localizer[LanguageResource.MudDataGrid_ExpandAllGroups] - @Localizer[LanguageResource.MudDataGrid_CollapseAllGroups] + @Localizer[LanguageResource.MudDataGrid_ExpandAllGroups] + @Localizer[LanguageResource.MudDataGrid_CollapseAllGroups] } @if (dataGrid.HasHierarchyColumn) { @@ -462,89 +336,92 @@ @Localizer[LanguageResource.MudDataGrid_RefreshData] } - ; + + ; + + internal RenderFragment FooterCells(IEnumerable currentItems) => @ - @if (currentItems is not null) - { - foreach (var column in RenderedColumns) - { - if (!column.HiddenState.Value) - { - if (column.AggregateDefinition is not null || column.FooterTemplate is not null || HasFooter) - { - - } - } - } - } + @if (currentItems is not null) + { + foreach (var column in RenderedColumns) + { + if (!column.HiddenState.Value) + { + if (column.AggregateDefinition is not null || column.FooterTemplate is not null || HasFooter) + { + + } + } + } + } ; - + internal RenderFragment Cell(Column column, T item) => @ - @{ - var cell = new Cell(this, column, item); - } - - @if (column.Editable && !ReadOnly && EditMode == DataGridEditMode.Cell) - { - if (column.EditTemplate is not null) - { - @column.EditTemplate(cell._cellContext) - } - else - { - if (column.PropertyType == typeof(string)) - { - - } - else if (column.isNumber) - { - - } - } - } - else - { - if (column.CellTemplate is not null) - { - @column.CellTemplate(cell._cellContext) - } - else if (column.Culture is not null && column.isNumber) - { - if (column.ContentFormat is not null) - { - @(((IFormattable)cell._valueNumber)?.ToString(column.ContentFormat, column.Culture)) - } - else - { - @cell._valueNumber?.ToString(column.Culture) - } - } - else - { - if (column.ContentFormat is not null) - { - @(((IFormattable)cell.ComputedValue)?.ToString(column.ContentFormat, column.Culture)) - } - else - { - @cell.ComputedValue - } - } - } - + @{ + var cell = new Cell(this, column, item); + } + + @if (column.Editable && !ReadOnly && EditMode == DataGridEditMode.Cell) + { + if (column.EditTemplate is not null) + { + @column.EditTemplate(cell._cellContext) + } + else + { + if (column.PropertyType == typeof(string)) + { + + } + else if (column.isNumber) + { + + } + } + } + else + { + if (column.CellTemplate is not null) + { + @column.CellTemplate(cell._cellContext) + } + else if (column.Culture is not null && column.isNumber) + { + if (column.ContentFormat is not null) + { + @(((IFormattable)cell._valueNumber)?.ToString(column.ContentFormat, column.Culture)) + } + else + { + @cell._valueNumber?.ToString(column.Culture) + } + } + else + { + if (column.ContentFormat is not null) + { + @(((IFormattable)cell.ComputedValue)?.ToString(column.ContentFormat, column.Culture)) + } + else + { + @cell.ComputedValue + } + } + } + ; internal RenderFragment Filter(IFilterDefinition filterDefinition, Column column) => @@ -626,7 +503,6 @@ } - } else { diff --git a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs index c125394c1db0..05c0d08465ec 100644 --- a/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs +++ b/src/MudBlazor/Components/DataGrid/MudDataGrid.razor.cs @@ -26,7 +26,8 @@ public partial class MudDataGrid<[DynamicallyAccessedMembers(DynamicallyAccessed internal int? _rowsPerPage; private int _currentPage = 0; private IEnumerable _items; - private MudVirtualize> _mudVirtualize; + internal bool _groupInitialExpanded = true; + internal MudVirtualize> _mudVirtualize; private bool _isFirstRendered = false; private bool _filtersMenuVisible = false; private bool _columnsPanelVisible = false; @@ -37,9 +38,8 @@ public partial class MudDataGrid<[DynamicallyAccessedMembers(DynamicallyAccessed private PropertyInfo[] _properties = typeof(T).GetProperties(); private CancellationTokenSource _serverDataCancellationTokenSource; private IEnumerable _currentRenderFilteredItemsCache = null; - internal Dictionary, bool> _groupExpansionsDict = new(); - private List> _currentPageGroups = []; - private List> _allGroups = []; + internal GroupDefinition _groupDefinition; + internal Dictionary, bool> _groupExpansionsDict = []; private GridData _serverData = new() { TotalItems = 0, Items = Array.Empty() }; private Func> _defaultFilterDefinitionFactory = () => new FilterDefinition(); @@ -197,9 +197,7 @@ protected int numPages private static void Swap(List list, int indexA, int indexB) { - var tmp = list[indexA]; - list[indexA] = list[indexB]; - list[indexB] = tmp; + (list[indexB], list[indexA]) = (list[indexA], list[indexB]); } private Task ItemUpdatedAsync(MudItemDropInfo> dropItem) @@ -481,7 +479,7 @@ private Task ItemUpdatedAsync(MudItemDropInfo> dropItem) ///
/// /// - /// This property specifies a group of one or more columns in a table for formatting. For example: + /// This property specifies a groupedColumns of one or more columns in a table for formatting. For example: /// /// /// table @@ -989,7 +987,7 @@ public int CurrentPage /// Allows grouping of columns in this grid. ///
/// - /// Defaults to false. When true, columns can be used to group sets of items. Can be overridden for individual columns via . + /// Defaults to false. When true, columns can be used to groupedColumns sets of items. Can be overridden for individual columns via . /// [Parameter] public bool Groupable @@ -1003,9 +1001,7 @@ public bool Groupable if (!_groupable) { - _currentPageGroups.Clear(); - _allGroups.Clear(); - _groupExpansionsDict.Clear(); + _groupDefinition = null; foreach (var column in RenderedColumns) column.RemoveGrouping().CatchAndLog(); @@ -1017,10 +1013,10 @@ public bool Groupable private bool _groupable = false; /// - /// Expands grouped columns by default. + /// Expands grouped columns by default. Overrides /// /// - /// Defaults to false. Applies when is true. + /// Defaults to false. Applies when is true. /// [Parameter] public bool GroupExpanded { get; set; } @@ -1188,11 +1184,14 @@ public IEnumerable FilteredItems [Parameter] public Interfaces.IForm Validator { get; set; } = new DataGridRowValidator(); - internal Column GroupedColumn + /// + /// Returns true if is true and at least one column has Grouping toggled on. + /// + public bool IsGrouped { get { - return RenderedColumns.FirstOrDefault(x => x.GroupingState.Value); + return Groupable && RenderedColumns.FirstOrDefault(x => x.GroupingState.Value) != null; } } @@ -1200,16 +1199,18 @@ internal Column GroupedColumn #region Computed Properties - internal string GetGroupIcon(bool isExpanded, bool rtl) + internal string GetGroupIcon(bool isExpanded, bool? rtl = null) { + if (rtl == null) + rtl = RightToLeft; if (isExpanded) { return Icons.Material.Filled.ExpandMore; } - return rtl ? Icons.Material.Filled.ChevronLeft : Icons.Material.Filled.ChevronRight; + return rtl.Value ? Icons.Material.Filled.ChevronLeft : Icons.Material.Filled.ChevronRight; } - private bool HasFooter + internal bool HasFooter { get { @@ -1225,7 +1226,7 @@ private bool HasStickyColumns } } - private bool HasHierarchyColumn + internal bool HasHierarchyColumn { get { @@ -1965,7 +1966,7 @@ internal async Task ShowAllColumnsAsync() } /// - /// Shows a panel that lets you show, hide, filter, group, sort and re-arrange columns. + /// Shows a panel that lets you show, hide, filter, groupedColumns, sort and re-arrange columns. /// public void ShowColumnsPanel() { @@ -2018,7 +2019,7 @@ internal void DropContainerHasChanged() _dropContainer?.Refresh(); _columnsPanelDropContainer?.Refresh(); } - +#nullable enable /// /// Performs grouping of the current items. /// @@ -2031,59 +2032,129 @@ public void GroupItems(bool noStateChange = false) if (!noStateChange) DropContainerHasChanged(); - if (GroupedColumn?.groupBy == null) + _groupDefinition = default; + + if (!IsGrouped || GetFilteredItemsCount() == 0) { - _currentPageGroups = new List>(); - _allGroups = new List>(); if (_isFirstRendered && !noStateChange) StateHasChanged(); return; } - var currentPageGroupings = CurrentPageItems.GroupBy(GroupedColumn.groupBy); + // get all columns that are grouped in the order they are grouped + var groupedColumns = RenderedColumns.Where(x => x.GroupingState.Value).OrderBy(x => x._groupByOrderState.Value).ToList(); + + // Initialize with the first group definition + _groupDefinition = ProcessGroup(groupedColumns[0]); - // Maybe group Items to keep groups expanded after clearing a filter? - var allGroupings = FilteredItems.GroupBy(GroupedColumn.groupBy).ToArray(); + // Create a reference to build the hierarchy + var currentGroupDef = _groupDefinition; - if (GetFilteredItemsCount() > 0) + // Start from index 1 since we've already processed the first column + for (var i = 1; i < groupedColumns.Count; i++) { - foreach (var group in allGroupings) - { - _groupExpansionsDict.TryAdd(group.Key, GroupExpanded); - } + var nextGroupDef = ProcessGroup(groupedColumns[i]); + // Connect it to the current level + currentGroupDef.InnerGroup = nextGroupDef; + // Move to the next level for the next iteration + currentGroupDef = nextGroupDef; } - // construct the groups - _currentPageGroups = currentPageGroupings.Select(x => new GroupDefinition(x, - _groupExpansionsDict[x.Key])).ToList(); - - _allGroups = allGroupings.Select(x => new GroupDefinition(x, - _groupExpansionsDict[x.Key])).ToList(); - if ((_isFirstRendered || HasServerData) && !noStateChange) StateHasChanged(); } - internal async Task ChangedGrouping(Column column) + private IEnumerable> GroupItemsPage { - foreach (var c in RenderedColumns) + get { - if (c.PropertyName != column.PropertyName) - await c.RemoveGrouping(); + return GetItemsOfGroup(_groupDefinition, CurrentPageItems); } + } - GroupItems(); + internal IEnumerable> GetItemsOfGroup(GroupDefinition? parent, IEnumerable? sourceList) + { + if (parent is null || sourceList is null) + { + return new List>(); + } + + if (parent.Selector is not null) + { + return sourceList.GroupBy(parent.Selector).ToList(); + } + + return new List>(); } - internal void ToggleGroupExpansion(GroupDefinition g) + private GroupDefinition ProcessGroup(Column column) { - if (_groupExpansionsDict.TryGetValue(g.Grouping.Key, out var value)) + var expanded = _groupInitialExpanded ? + (GroupExpanded || column._groupExpandedState.Value) : + column._groupExpandedState.Value; + return new() { - _groupExpansionsDict[g.Grouping.Key] = !value; + Selector = column.groupBy, + Expanded = expanded, + GroupTemplate = column.GroupTemplate, + Indentation = column.GroupIndented, + Title = column.Title, + Grouping = new EmptyGrouping(null) // Ensure Grouping is not null + }; + } + + internal IEnumerable> GetGroupDefinitions(GroupDefinition groupDef, IEnumerable> groups) + { + List> result = new(); + foreach (var group in groups) + { + var expanded = false; + if (group is { Key: not null }) + { + var key = new { groupDef.Title, group.Key }; + expanded = _groupExpansionsDict.ContainsKey(key) ? + _groupExpansionsDict[key] : + groupDef.Expanded; + } + result.Add(new GroupDefinition + { + Selector = groupDef.Selector, + Expanded = expanded, + GroupTemplate = groupDef.GroupTemplate, + Indentation = groupDef.Indentation, + Title = groupDef.Title, + Parent = groupDef.Parent, + InnerGroup = groupDef.InnerGroup, + Grouping = group, + }); } + return result; + } + internal async Task ChangedGrouping(Column? col = null) + { + // If col is not null add GroupByOrder is not set set it to the end + if (col is { _groupByOrderState.Value: 0 }) + { + var maxOrder = RenderedColumns.Max(x => x._groupByOrderState.Value); + await col._groupByOrderState.SetValueAsync(maxOrder + 1); + } GroupItems(); } +#nullable disable + /// + /// Expands all groups async. + /// + /// + /// Applies when is true. + /// + public async Task ExpandAllGroupsAsync() + { + if (_groupDefinition != null && _groupable) + { + await ToggleGroupExpandRecursively(true); + } + } /// /// Expands all groups. @@ -2091,14 +2162,27 @@ internal void ToggleGroupExpansion(GroupDefinition g) /// /// Applies when is true. /// + [Obsolete("Use ExpandAllGroupsAsync instead")] public void ExpandAllGroups() { - foreach (var group in _allGroups) + if (_groupDefinition != null && _groupable) { - group.Expanded = true; - _groupExpansionsDict[group.Grouping.Key] = true; + ToggleGroupExpandRecursively(true).CatchAndLog(); + } + } + + /// + /// Collapses all groups async. + /// + /// + /// Applies when is true. + /// + public async Task CollapseAllGroupsAsync() + { + if (_groupDefinition != null && _groupable) + { + await ToggleGroupExpandRecursively(false); } - GroupItems(); } /// @@ -2107,13 +2191,26 @@ public void ExpandAllGroups() /// /// Applies when is true. /// + [Obsolete("Use CollapseAllGroupsAsync instead")] public void CollapseAllGroups() { - foreach (var group in _allGroups) + if (_groupDefinition != null && _groupable) { - group.Expanded = false; - _groupExpansionsDict[group.Grouping.Key] = false; + ToggleGroupExpandRecursively(false).CatchAndLog(); } + } + + private async Task ToggleGroupExpandRecursively(bool expanded) + { + _groupExpansionsDict.Clear(); + foreach (var column in RenderedColumns) + { + if (column.GroupingState.Value) + { + await column._groupExpandedState.SetValueAsync(expanded); + } + } + _groupInitialExpanded = false; GroupItems(); } @@ -2194,5 +2291,19 @@ protected virtual void Dispose(bool disposing) // TODO: Use IAsyncDisposable for MudDataGrid _resizeService?.DisposeAsync().CatchAndLog(); } + + private sealed class EmptyGrouping : IGrouping + { + public TKey Key { get; } + + public EmptyGrouping(TKey key) + { + Key = key; + } + + public IEnumerator GetEnumerator() => Enumerable.Empty().GetEnumerator(); + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + } } } diff --git a/src/MudBlazor/Styles/components/_datagrid.scss b/src/MudBlazor/Styles/components/_datagrid.scss index f5f39ad385e4..6ea9a6523465 100644 --- a/src/MudBlazor/Styles/components/_datagrid.scss +++ b/src/MudBlazor/Styles/components/_datagrid.scss @@ -153,6 +153,16 @@ .mud-resizing { border-right: 2px solid var(--mud-palette-primary); } + + &.mud-datagrid-group { + background-color: var(--mud-palette-background-gray); + } + + @for $i from 2 through 5 { + &.mud-row-group-indented-#{$i} { + padding-left: #{$i * 48 - 48}px !important; + } + } } }