diff --git a/docs/en/tutorials/book-store/part-02.md b/docs/en/tutorials/book-store/part-02.md index 429d7ec17c1..f6d7f708e12 100644 --- a/docs/en/tutorials/book-store/part-02.md +++ b/docs/en/tutorials/book-store/part-02.md @@ -584,14 +584,14 @@ Open the `Books.razor` and replace the content as the following: Field="@nameof(BookDto.Type)" Caption="@L["Type"]"> - @L[$"Enum:BookType.{context.Type}"] + @L[$"Enum:BookType.{(int)context.Item.Type}"] - @context.PublishDate.ToShortDateString() + @context.Item.PublishDate.ToShortDateString() - @context.CreationTime.ToLongDateString() + @context.Item.CreationTime.ToLongDateString() @@ -651,9 +651,11 @@ Open the `Books.razor` and replace the content as the following: - + RowsPerPage="@PageSize" + SortMode="SortMode.None"> @@ -678,12 +680,17 @@ Open the `Books.razor` and replace the content as the following: + + + @code { + private MudDataGrid _booksDataGrid = default!; + public Books() // Constructor { LocalizationResource = typeof(BookStoreResource); @@ -697,6 +704,7 @@ Open the `Books.razor` and replace the content as the following: * `Entities`, `TotalCount`, `PageSize`, `OnDataGridReadAsync` are defined in the base class. * `LocalizationResource` is set to the `BookStoreResource` to localize the texts. * This page uses the standard MudBlazor `MudDataGrid` with `` definitions. ABP also ships an `AbpMudExtensibleDataGrid` that integrates with the [data table column extension system](../../framework/ui/blazor/data-table-column-extensions.md) when you need to extend module pages. +* `AbpMudCrudPageBase` uses its own `AbpMudExtensibleDataGrid` to reload the data and to resolve the sort columns. Since this page uses the standard `MudDataGrid`, it keeps a reference to the grid (`_booksDataGrid`) to reload it after the create, update and delete operations in the next part, and disables sorting with `SortMode.None`. While the code above is pretty easy to understand, you can check the MudBlazor [Card](https://mudblazor.com/components/card) and [DataGrid](https://mudblazor.com/components/datagrid) documents to understand them better. @@ -712,6 +720,14 @@ You can run the application! The final UI of this part is shown below: ![blazor-bookstore-book-list](images/blazor-bookstore-book-list-2.png) +{{if BlazorUI == "MudBlazor"}} + +This is a fully working, server side paged and localized table of books. + +{{else}} + This is a fully working, server side paged, sorted and localized table of books. +{{end}} + {{end # UI }} diff --git a/docs/en/tutorials/book-store/part-03.md b/docs/en/tutorials/book-store/part-03.md index 259475b27db..45319f8409a 100644 --- a/docs/en/tutorials/book-store/part-03.md +++ b/docs/en/tutorials/book-store/part-03.md @@ -1147,9 +1147,8 @@ Now, we can add a modal that will be opened when we click the button. Open the `Books.razor` and add the following code to the end of the page: ````xml - - - + +
@L["NewBook"] @@ -1160,16 +1159,16 @@ Open the `Books.razor` and add the following code to the end of the page: @L["Name"] - + - + @L["Type"] - @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType))) { @@ -1180,11 +1179,11 @@ Open the `Books.razor` and add the following code to the end of the page: @L["PublishDate"] - + @L["Price"] - + @@ -1236,7 +1235,7 @@ Open the `Books.razor` and add the following code to the end of the page: @L[$"Enum:BookType.{(int)bookTypeValue}"] } - NewEntity?.PublishDate; + set { if (NewEntity != null && value.HasValue) NewEntity.PublishDate = value.Value; } +} +```` + +`AbpMudCrudPageBase` reloads its own `AbpMudExtensibleDataGrid` after creating a book. Override the `OnCreatedEntityAsync` method in the `@code` block to reload the `MudDataGrid` of this page instead: + +````csharp +protected override async Task OnCreatedEntityAsync() +{ + await InvokeAsync(() => _booksDataGrid.ReloadServerData()); + await CloseCreateDialogAsync(); + Snackbar.Add(GetCreateMessage(), Severity.Success); +} +```` {{end}} @@ -1281,7 +1303,7 @@ Open the `Books.razor` and add the following `DataGridEntityActionsColumn` secti + Clicked="() => OpenEditModalAsync(context.Item)" /> @@ -1324,29 +1346,28 @@ We can now define a modal to edit the book. Add the following code to the end of {{if BlazorUI == "Blazorise"}} ````xml - - - + + @EditingEntity.Name - + @L["Name"] - + - + @L["Type"] - @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType))) { @@ -1357,11 +1378,11 @@ We can now define a modal to edit the book. Add the following code to the end of @L["PublishDate"] - + @L["Price"] - + @@ -1402,7 +1423,7 @@ We can now define a modal to edit the book. Add the following code to the end of @L[$"Enum:BookType.{(int)bookTypeValue}"] } - ```` +Add the following property to the `@code` block of the `Books.razor` file, as the edit dialog's date picker binds to it: + +````csharp +private DateTime? EditingEntityPublishDate +{ + get => EditingEntity?.PublishDate; + set { if (EditingEntity != null && value.HasValue) EditingEntity.PublishDate = value.Value; } +} +```` + +Also override the `OnUpdatedEntityAsync` method to reload the grid after updating a book: + +````csharp +protected override async Task OnUpdatedEntityAsync() +{ + await InvokeAsync(() => _booksDataGrid.ReloadServerData()); + await CloseEditDialogAsync(); + Snackbar.Add(GetUpdateMessage(), Severity.Success); +} +```` + {{end}} ### Mapperly Configuration @@ -1430,6 +1472,7 @@ Open the `BookStoreBlazorMappers` inside the {{ if UI == "BlazorServer" }}`Acme. ```csharp using Riok.Mapperly.Abstractions; using Volo.Abp.Mapperly; +using Acme.BookStore.Books; {{ if UI == "BlazorServer" }}namespace Acme.BookStore.Blazor; {{ else if UI == "MAUIBlazor" }}namespace Acme.BookStore.MauiBlazor; {{ else }}namespace Acme.BookStore.Blazor.Client;{{ end }} @@ -1461,8 +1504,8 @@ Add the following `EntityAction` code under the "Edit" action inside `EntityActi ````xml + Clicked="() => DeleteEntityAsync(context.Item)" + ConfirmationMessage="() => GetDeleteConfirmationMessage(context.Item)" /> ```` {{end}} @@ -1472,16 +1515,32 @@ Add the following `EntityAction` code under the "Edit" action inside `EntityActi Add the following `MudMenuItem` after the "Edit" item inside the actions `MudMenu`: ````razor - + @L["Delete"] ```` +`DeleteEntityAsync` asks the user to confirm (with the message returned by `GetDeleteConfirmationMessage`) before deleting the book. + +Override the `OnDeletedEntityAsync` method to reload the grid after deleting a book (and go back to the previous page if the current page becomes empty): + +````csharp +protected override async Task OnDeletedEntityAsync() +{ + // Go back to the previous page if the deleted book was the only one on the current page + if (_booksDataGrid.CurrentPage > 0 && _booksDataGrid.ServerItems.Count() == 1) + { + await InvokeAsync(() => _booksDataGrid.NavigateTo(Page.Previous)); + } + else + { + await InvokeAsync(() => _booksDataGrid.ReloadServerData()); + } + + Snackbar.Add(GetDeleteMessage(), Severity.Success); +} +```` + {{end}} * `DeleteEntityAsync` is defined in the base class that deletes the entity by performing a call to the server. @@ -1526,7 +1585,7 @@ Here's the complete code to create the book management CRUD page, that has been @@ -1536,11 +1595,11 @@ Here's the complete code to create the book management CRUD page, that has been + Clicked="() => OpenEditModalAsync(context.Item)" /> + Clicked="() => DeleteEntityAsync(context.Item)" + ConfirmationMessage="()=>GetDeleteConfirmationMessage(context.Item)" /> @@ -1551,14 +1610,14 @@ Here's the complete code to create the book management CRUD page, that has been Field="@nameof(BookDto.Type)" Caption="@L["Type"]"> - @L[$"Enum:BookType.{context.Type:D}"] + @L[$"Enum:BookType.{(int)context.Item.Type}"] - @context.PublishDate.ToShortDateString() + @context.Item.PublishDate.ToShortDateString() - @context.CreationTime.ToLongDateString() + @context.Item.CreationTime.ToLongDateString() @@ -1577,9 +1636,8 @@ Here's the complete code to create the book management CRUD page, that has been - - - + + @L["NewBook"] @@ -1590,16 +1648,16 @@ Here's the complete code to create the book management CRUD page, that has been @L["Name"] - + - + @L["Type"] - @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType))) { @@ -1610,11 +1668,11 @@ Here's the complete code to create the book management CRUD page, that has been @L["PublishDate"] - + @L["Price"] - + @@ -1630,29 +1688,28 @@ Here's the complete code to create the book management CRUD page, that has been - - - + + @EditingEntity.Name - + @L["Name"] - + - + @L["Type"] - @foreach (int bookTypeValue in Enum.GetValues(typeof(BookType))) { @@ -1663,11 +1720,11 @@ Here's the complete code to create the book management CRUD page, that has been @L["PublishDate"] - + @L["Price"] - + @@ -1716,15 +1773,17 @@ Here's the complete code to create the book management CRUD page, that has been - + RowsPerPage="@PageSize" + SortMode="SortMode.None"> @L["Edit"] - @L["Delete"] + @L["Delete"] @@ -1746,6 +1805,9 @@ Here's the complete code to create the book management CRUD page, that has been + + + @@ -1769,7 +1831,7 @@ Here's the complete code to create the book management CRUD page, that has been @L[$"Enum:BookType.{(int)bookTypeValue}"] } - @L[$"Enum:BookType.{(int)bookTypeValue}"] } - NewEntity?.PublishDate; + set { if (NewEntity != null && value.HasValue) NewEntity.PublishDate = value.Value; } + } + + private DateTime? EditingEntityPublishDate + { + get => EditingEntity?.PublishDate; + set { if (EditingEntity != null && value.HasValue) EditingEntity.PublishDate = value.Value; } + } + + private MudDataGrid _booksDataGrid = default!; + + protected override async Task OnCreatedEntityAsync() + { + await InvokeAsync(() => _booksDataGrid.ReloadServerData()); + await CloseCreateDialogAsync(); + Snackbar.Add(GetCreateMessage(), Severity.Success); + } + + protected override async Task OnUpdatedEntityAsync() + { + await InvokeAsync(() => _booksDataGrid.ReloadServerData()); + await CloseEditDialogAsync(); + Snackbar.Add(GetUpdateMessage(), Severity.Success); + } + + protected override async Task OnDeletedEntityAsync() + { + // Go back to the previous page if the deleted book was the only one on the current page + if (_booksDataGrid.CurrentPage > 0 && _booksDataGrid.ServerItems.Count() == 1) + { + await InvokeAsync(() => _booksDataGrid.NavigateTo(Page.Previous)); + } + else + { + await InvokeAsync(() => _booksDataGrid.ReloadServerData()); + } + + Snackbar.Add(GetDeleteMessage(), Severity.Success); + } + public Books() // Constructor { LocalizationResource = typeof(BookStoreResource); diff --git a/docs/en/tutorials/book-store/part-05.md b/docs/en/tutorials/book-store/part-05.md index 7449b4b65d9..e21f1209b34 100644 --- a/docs/en/tutorials/book-store/part-05.md +++ b/docs/en/tutorials/book-store/part-05.md @@ -80,10 +80,6 @@ public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvide { var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore")); - //Dashboard permissions - bookStoreGroup.AddPermission(BookStorePermissions.Dashboard.Host, L("Permission:Dashboard"), MultiTenancySides.Host); - bookStoreGroup.AddPermission(BookStorePermissions.Dashboard.Tenant, L("Permission:Dashboard"), MultiTenancySides.Tenant); - //Books permissions var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books")); booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create")); @@ -421,19 +417,16 @@ The base `AbpCrudPageBase` class already has the necessary functionality for the #### Set the Policy (Permission) Names -Add the following code block to the end of the `Books.razor` file: +Update the constructor in the `@code` block of the `Books.razor` file as shown below: ````csharp -@code +public Books() // Constructor { - public Books() // Constructor - { - LocalizationResource = typeof(BookStoreResource); - - CreatePolicyName = BookStorePermissions.Books.Create; - UpdatePolicyName = BookStorePermissions.Books.Edit; - DeletePolicyName = BookStorePermissions.Books.Delete; - } + LocalizationResource = typeof(BookStoreResource); + + CreatePolicyName = BookStorePermissions.Books.Create; + UpdatePolicyName = BookStorePermissions.Books.Edit; + DeletePolicyName = BookStorePermissions.Books.Delete; } ```` @@ -449,6 +442,8 @@ The base `AbpCrudPageBase` class automatically checks these permissions on the r Wrap the *New Book* button by an `if` block as shown below: +{{if BlazorUI == "Blazorise"}} + ````xml @if (HasCreatePermission) { @@ -457,8 +452,25 @@ Wrap the *New Book* button by an `if` block as shown below: } ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +````razor +@if (HasCreatePermission) +{ + @L["NewBook"] +} +```` + +{{end}} + #### Hide the Edit/Delete Actions +{{if BlazorUI == "Blazorise"}} + `EntityAction` component defines `Visible` attribute (parameter) to conditionally show the action. Update the `EntityActions` section as shown below: @@ -468,15 +480,36 @@ Update the `EntityActions` section as shown below: + Clicked="() => OpenEditModalAsync(context.Item)" /> + Clicked="() => DeleteEntityAsync(context.Item)" + ConfirmationMessage="()=>GetDeleteConfirmationMessage(context.Item)" /> ```` +{{end}} + +{{if BlazorUI == "MudBlazor"}} + +Wrap the menu items inside the actions `MudMenu` by `if` blocks as shown below: + +````razor + + @if (HasUpdatePermission) + { + @L["Edit"] + } + @if (HasDeletePermission) + { + @L["Delete"] + } + +```` + +{{end}} + #### About the Permission Caching You can run and test the permissions. Remove a book related permission from the admin role to see the related button/action disappears from the UI. diff --git a/docs/en/tutorials/book-store/part-09.md b/docs/en/tutorials/book-store/part-09.md index 22c144dd14d..29a5acbae39 100644 --- a/docs/en/tutorials/book-store/part-09.md +++ b/docs/en/tutorials/book-store/part-09.md @@ -873,7 +873,7 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "B ShowPager="true" PageSize="PageSize"> - @if (CanEditAuthor) { - + @L["Edit"] } @if (CanDeleteAuthor) { - + @L["Delete"] } @@ -907,7 +907,7 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "B Field="@nameof(AuthorDto.BirthDate)" Caption="@L["BirthDate"]"> - @context.BirthDate.ToShortDateString() + @context.Item.BirthDate.ToShortDateString() @@ -915,9 +915,8 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "B - - - + + @L["NewAuthor"] @@ -928,25 +927,25 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "B @L["Name"] - + - + @L["BirthDate"] - + @L["ShortBio"] - + - + @@ -967,9 +966,8 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "B - - - + + @EditingAuthor.Name @@ -980,25 +978,25 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "B @L["Name"] - + - + @L["BirthDate"] - + @L["ShortBio"] - + - + @@ -1048,7 +1046,8 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "B - @@ -1079,6 +1078,9 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the {{ if UI == "B + + + @@ -1238,26 +1240,26 @@ public partial class Authors await InvokeAsync(StateHasChanged); } - private void OpenCreateAuthorModal() + private async Task OpenCreateAuthorModal() { - CreateValidationsRef.ClearAll(); + await CreateValidationsRef.ClearAll(); NewAuthor = new CreateAuthorDto(); - CreateAuthorModal.Show(); + await CreateAuthorModal.Show(); } - private void CloseCreateAuthorModal() + private async Task CloseCreateAuthorModal() { - CreateAuthorModal.Hide(); + await CreateAuthorModal.Hide(); } - private void OpenEditAuthorModal(AuthorDto author) + private async Task OpenEditAuthorModal(AuthorDto author) { - EditValidationsRef.ClearAll(); + await EditValidationsRef.ClearAll(); EditingAuthorId = author.Id; EditingAuthor = ObjectMapper.Map(author); - EditAuthorModal.Show(); + await EditAuthorModal.Show(); } private async Task DeleteAuthorAsync(AuthorDto author) @@ -1279,9 +1281,9 @@ public partial class Authors } } - private void CloseEditAuthorModal() + private async Task CloseEditAuthorModal() { - EditAuthorModal.Hide(); + await EditAuthorModal.Hide(); } private async Task CreateAuthorAsync() @@ -1292,7 +1294,7 @@ public partial class Authors { await AuthorAppService.CreateAsync(NewAuthor); await GetAuthorsAsync(); - CreateAuthorModal.Hide(); + await CreateAuthorModal.Hide(); } } catch(Exception ex) @@ -1309,7 +1311,7 @@ public partial class Authors { await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor); await GetAuthorsAsync(); - EditAuthorModal.Hide(); + await EditAuthorModal.Hide(); } } catch(Exception ex) @@ -1328,6 +1330,7 @@ public partial class Authors using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Acme.BookStore.Authors; using Acme.BookStore.Permissions; @@ -1355,6 +1358,8 @@ public partial class Authors private Guid EditingAuthorId { get; set; } private UpdateAuthorDto EditingAuthor { get; set; } + private MudDataGrid AuthorsGrid { get; set; } + private MudDialog CreateAuthorDialog { get; set; } private MudDialog EditAuthorDialog { get; set; } @@ -1365,7 +1370,7 @@ public partial class Authors // Bind to a nullable wrapper and sync back to the DTO before saving. private DateTime? NewAuthorBirthDate { - get => NewAuthor?.BirthDate; + get => NewAuthor == null || NewAuthor.BirthDate == default ? null : NewAuthor.BirthDate; set { if (NewAuthor != null && value.HasValue) NewAuthor.BirthDate = value.Value; } } @@ -1414,7 +1419,7 @@ public partial class Authors TotalCount = (int)result.TotalCount; } - private async Task> OnDataGridReadAsync(GridState state) + private async Task> OnDataGridReadAsync(GridState state, CancellationToken cancellationToken = default) { CurrentSorting = state.SortDefinitions .Where(s => !string.IsNullOrWhiteSpace(s.SortBy?.ToString())) @@ -1463,7 +1468,16 @@ public partial class Authors } await AuthorAppService.DeleteAsync(author.Id); - await GetAuthorsAsync(); + + // Go back to the previous page if the deleted author was the only one on the current page + if (AuthorsGrid.CurrentPage > 0 && AuthorsGrid.ServerItems.Count() == 1) + { + await InvokeAsync(() => AuthorsGrid.NavigateTo(Page.Previous)); + } + else + { + await InvokeAsync(() => AuthorsGrid.ReloadServerData()); + } } catch(Exception ex) { @@ -1475,11 +1489,11 @@ public partial class Authors { try { - await CreateFormRef.Validate(); + await CreateFormRef.ValidateAsync(); if (CreateFormRef.IsValid) { await AuthorAppService.CreateAsync(NewAuthor); - await GetAuthorsAsync(); + await InvokeAsync(() => AuthorsGrid.ReloadServerData()); await InvokeAsync(() => CreateAuthorDialog.CloseAsync()); } } @@ -1493,11 +1507,11 @@ public partial class Authors { try { - await EditFormRef.Validate(); + await EditFormRef.ValidateAsync(); if (EditFormRef.IsValid) { await AuthorAppService.UpdateAsync(EditingAuthorId, EditingAuthor); - await GetAuthorsAsync(); + await InvokeAsync(() => AuthorsGrid.ReloadServerData()); await InvokeAsync(() => EditAuthorDialog.CloseAsync()); } } @@ -1540,11 +1554,11 @@ public partial class AuthorDtoToUpdateAuthorDtoMapper : MapperBase NewEntity?.PublishDate; + set { if (NewEntity != null && value.HasValue) NewEntity.PublishDate = value.Value; } + } + + private DateTime? EditingEntityPublishDate + { + get => EditingEntity?.PublishDate; + set { if (EditingEntity != null && value.HasValue) EditingEntity.PublishDate = value.Value; } + } + + private MudDataGrid _booksDataGrid = default!; + + protected override async Task OnCreatedEntityAsync() + { + await InvokeAsync(() => _booksDataGrid.ReloadServerData()); + await CloseCreateDialogAsync(); + Snackbar.Add(GetCreateMessage(), Severity.Success); + } + + protected override async Task OnUpdatedEntityAsync() + { + await InvokeAsync(() => _booksDataGrid.ReloadServerData()); + await CloseEditDialogAsync(); + Snackbar.Add(GetUpdateMessage(), Severity.Success); + } + + protected override async Task OnDeletedEntityAsync() + { + // Go back to the previous page if the deleted book was the only one on the current page + if (_booksDataGrid.CurrentPage > 0 && _booksDataGrid.ServerItems.Count() == 1) + { + await InvokeAsync(() => _booksDataGrid.NavigateTo(Page.Previous)); + } + else + { + await InvokeAsync(() => _booksDataGrid.ReloadServerData()); + } + + Snackbar.Add(GetDeleteMessage(), Severity.Success); + } + //GET AUTHORS ON INITIALIZATION protected override async Task OnInitializedAsync() { @@ -1279,7 +1330,7 @@ Finally, add the following field definition into the *Create* modal/dialog, as t ````xml @L["Author"] - @foreach (var author in authorList) { @@ -1324,7 +1375,7 @@ Add the following field definition into the *Edit* modal/dialog, as the first it ````xml @L["Author"] - @foreach (var author in authorList) {