From 4550188830ab566f3b99b7016a216d6fa38f8e58 Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Sun, 2 Feb 2025 10:48:48 +0800 Subject: [PATCH 01/13] Roles.razor --- src/Infrastructure/Services/ExcelService.cs | 242 ++++++++-------- .../LoadingButton/MudLoadingButton.razor | 103 +++++-- .../Pages/Identity/Roles/Roles.razor | 274 ++++++++++++------ .../Pages/Identity/Users/Users.razor | 6 +- .../Pages/PicklistSets/PicklistSets.razor | 204 +++++++------ src/Server.UI/Server.UI.csproj | 2 +- src/Server.UI/Services/DialogServiceHelper.cs | 45 +-- 7 files changed, 539 insertions(+), 337 deletions(-) diff --git a/src/Infrastructure/Services/ExcelService.cs b/src/Infrastructure/Services/ExcelService.cs index 6add75d9f..243f3da51 100644 --- a/src/Infrastructure/Services/ExcelService.cs +++ b/src/Infrastructure/Services/ExcelService.cs @@ -16,152 +16,164 @@ public ExcelService(IStringLocalizer localizer) _localizer = localizer; } - public Task CreateTemplateAsync(IEnumerable fields, string sheetName = "Sheet1") + /// + /// Applies the header cell style. + /// + /// The cell to style. + private void ApplyHeaderStyle(IXLCell cell) { - using (var workbook = new XLWorkbook()) - { - workbook.Properties.Author = ""; - var ws = workbook.Worksheets.Add(sheetName); - var colIndex = 1; - var rowIndex = 1; - foreach (var header in fields) - { - var cell = ws.Cell(rowIndex, colIndex); - var style = cell.Style; - style.Fill.PatternType = XLFillPatternValues.Solid; - style.Fill.BackgroundColor = XLColor.LightBlue; - style.Border.BottomBorder = XLBorderStyleValues.Thin; - - cell.Value = header; + var style = cell.Style; + style.Fill.PatternType = XLFillPatternValues.Solid; + style.Fill.BackgroundColor = XLColor.LightBlue; + style.Border.BottomBorder = XLBorderStyleValues.Thin; + } - colIndex++; - } + /// + /// Saves the given workbook to a byte array. + /// + /// The workbook to save. + /// A byte array representing the workbook. + private static byte[] SaveWorkbookToByteArray(XLWorkbook workbook) + { + using var stream = new MemoryStream(); + workbook.SaveAs(stream); + stream.Seek(0, SeekOrigin.Begin); + return stream.ToArray(); + } - using (var stream = new MemoryStream()) - { - workbook.SaveAs(stream); - stream.Seek(0, SeekOrigin.Begin); - return Task.FromResult(stream.ToArray()); - } + public Task CreateTemplateAsync(IEnumerable fields, string sheetName = "Sheet1") + { + using var workbook = new XLWorkbook(); + workbook.Properties.Author = string.Empty; + var ws = workbook.Worksheets.Add(sheetName); + int rowIndex = 1; + int colIndex = 1; + foreach (var header in fields) + { + var cell = ws.Cell(rowIndex, colIndex++); + ApplyHeaderStyle(cell); + cell.Value = header; } + + return Task.FromResult(SaveWorkbookToByteArray(workbook)); } public Task ExportAsync(IEnumerable data, Dictionary> mappers, string sheetName = "Sheet1") { - using (var workbook = new XLWorkbook()) + using var workbook = new XLWorkbook(); + workbook.Properties.Author = string.Empty; + var ws = workbook.Worksheets.Add(sheetName); + int rowIndex = 1; + int colIndex = 1; + var headers = mappers.Keys.ToList(); + + // Write header row + foreach (var header in headers) { - workbook.Properties.Author = ""; - var ws = workbook.Worksheets.Add(sheetName); - var colIndex = 1; - var rowIndex = 1; - var headers = mappers.Keys.ToList(); - foreach (var header in headers) - { - var cell = ws.Cell(rowIndex, colIndex); - var style = cell.Style; - style.Fill.PatternType = XLFillPatternValues.Solid; - style.Fill.BackgroundColor = XLColor.LightBlue; - style.Border.BottomBorder = XLBorderStyleValues.Thin; - - cell.Value = header; - - colIndex++; - } - - var dataList = data.ToList(); - foreach (var item in dataList) - { - colIndex = 1; - rowIndex++; - - var result = headers.Select(header => mappers[header](item)); + var cell = ws.Cell(rowIndex, colIndex++); + ApplyHeaderStyle(cell); + cell.Value = header; + } - foreach (var value in result) - { - ws.Cell(rowIndex, colIndex).Value = value == null ? Blank.Value : value.ToString(); - colIndex++; - } - } + // Write data rows + var dataList = data.ToList(); + foreach (var item in dataList) + { + colIndex = 1; + rowIndex++; - using (var stream = new MemoryStream()) + foreach (var header in headers) { - workbook.SaveAs(stream); - stream.Seek(0, SeekOrigin.Begin); - return Task.FromResult(stream.ToArray()); + var value = mappers[header](item); + // If the value is null, write a Blank.Value; otherwise, use its string representation. + ws.Cell(rowIndex, colIndex++).Value = value == null ? Blank.Value : value.ToString(); } } + + return Task.FromResult(SaveWorkbookToByteArray(workbook)); } - public async Task>> ImportAsync(byte[] data, Dictionary> mappers, string sheetName = "Sheet1") + public async Task>> ImportAsync( + byte[] data, + Dictionary> mappers, + string sheetName = "Sheet1") { - using (var workbook = new XLWorkbook(new MemoryStream(data))) + using var workbook = new XLWorkbook(new MemoryStream(data)); + if (!workbook.Worksheets.TryGetWorksheet(sheetName, out var ws)) { - if (!workbook.Worksheets.TryGetWorksheet(sheetName, out var ws)) - { - return await Result>.FailureAsync(string.Format(_localizer["Sheet with name {0} does not exist!"], sheetName)); - } - - var dt = new DataTable(); - var titlesInFirstRow = true; - - foreach (var firstRowCell in ws.Range(1, 1, 1, ws.LastCellUsed().Address.ColumnNumber).Cells()) - { - dt.Columns.Add(titlesInFirstRow ? firstRowCell.GetString() : $"Column {firstRowCell.Address.ColumnNumber}"); - } + var msg = string.Format(_localizer["Sheet with name {0} does not exist!"], sheetName); + return await Result>.FailureAsync(msg); + } - var startRow = titlesInFirstRow ? 2 : 1; - var headers = mappers.Keys.ToList(); - var errors = new List(); + // Check if the worksheet contains any cells. + var lastCellUsed = ws.LastCellUsed()?.Address.ColumnNumber ?? 0; + if (lastCellUsed == 0) + { + var msg = string.Format(_localizer["Sheet with name {0} is empty!"], sheetName); + return await Result>.FailureAsync(msg); + } - foreach (var header in headers) - { - if (!dt.Columns.Contains(header)) - { - errors.Add(string.Format(_localizer["Header '{0}' does not exist in table!"], header)); - } - } + // Create a DataTable from the header row. + var dt = new DataTable(); + bool titlesInFirstRow = true; + foreach (var cell in ws.Range(1, 1, 1, lastCellUsed).Cells()) + { + string colName = titlesInFirstRow ? cell.GetString() : $"Column {cell.Address.ColumnNumber}"; + dt.Columns.Add(colName); + } + int startRow = titlesInFirstRow ? 2 : 1; - if (errors.Any()) + // Validate that all expected headers exist. + var headers = mappers.Keys.ToList(); + var errors = new List(); + foreach (var header in headers) + { + if (!dt.Columns.Contains(header)) { - return await Result>.FailureAsync(errors.ToArray()); + errors.Add(string.Format(_localizer["Header '{0}' does not exist in table!"], header)); } + } + if (errors.Any()) + { + return await Result>.FailureAsync(errors.ToArray()); + } - var lastRow = ws.LastRowUsed(); - var list = new List(); + var lastRowNumber = ws.LastRowUsed()?.RowNumber() ?? 0; + var list = new List(); - foreach (var row in ws.Rows(startRow, lastRow.RowNumber())) + // Process each row in the worksheet. + for (int rowIndex = startRow; rowIndex <= lastRowNumber; rowIndex++) + { + var row = ws.Row(rowIndex); + try { - try + var dataRow = dt.NewRow(); + // Populate the DataRow with cell values. + foreach (var cell in row.Cells(1, dt.Columns.Count)) { - var dataRow = dt.Rows.Add(); - var item = (TEntity?)Activator.CreateInstance(typeof(TEntity)) ?? throw new NullReferenceException($"{nameof(TEntity)}"); - - foreach (var cell in row.Cells()) - { - if (cell.DataType == XLDataType.DateTime) - { - dataRow[cell.Address.ColumnNumber - 1] = cell.GetDateTime().ToString("yyyy-MM-dd HH:mm:ss"); - } - else - { - dataRow[cell.Address.ColumnNumber - 1] = cell.Value.ToString(); - } - } - - foreach (var header in headers) - { - mappers[header](dataRow, item); - } - - list.Add(item); + int colIndex = cell.Address.ColumnNumber - 1; + dataRow[colIndex] = cell.DataType == XLDataType.DateTime + ? cell.GetDateTime().ToString("yyyy-MM-dd HH:mm:ss") + : cell.Value.ToString(); } - catch (Exception e) + dt.Rows.Add(dataRow); + + // Create an instance of TEntity and apply the mapping functions. + var item = (TEntity)Activator.CreateInstance(typeof(TEntity))!; // using null-forgiving operator + foreach (var header in headers) { - return await Result>.FailureAsync(string.Format(_localizer["Sheet name {0}:{1}"], sheetName, e.Message)); + mappers[header](dataRow, item); } + list.Add(item); + } + catch (Exception e) + { + var errorMsg = string.Format(_localizer["Error in sheet {0}: {1}"], sheetName, e.Message); + return await Result>.FailureAsync(errorMsg); } - - return await Result>.SuccessAsync(list); } + + return await Result>.SuccessAsync(list); } } + diff --git a/src/Server.UI/Components/LoadingButton/MudLoadingButton.razor b/src/Server.UI/Components/LoadingButton/MudLoadingButton.razor index adb104abc..90f7f1c03 100644 --- a/src/Server.UI/Components/LoadingButton/MudLoadingButton.razor +++ b/src/Server.UI/Components/LoadingButton/MudLoadingButton.razor @@ -1,14 +1,23 @@ @inherits MudBaseButton - - @if (_loading) + + @if (Loading) { - if (LoadingAdornment == Adornment.Start) + @if (LoadingAdornment == Adornment.Start) { - + } - if (LoadingContent != null) + @if (LoadingContent is not null) { @LoadingContent } @@ -17,9 +26,12 @@ @ChildContent } - if (LoadingAdornment == Adornment.End) + @if (LoadingAdornment == Adornment.End) { - + } } else @@ -27,81 +39,116 @@ @ChildContent } -@code{ + +@code { + // Private field backing the Loading property. private bool _loading; + /// - /// If applied the text will be added to the component. + /// Optional label text for the button. /// [Parameter] public string? Label { get; set; } + /// - /// The color of the icon. It supports the theme colors. + /// Color of the icon; supports theme colors. /// [Parameter] public Color IconColor { get; set; } = Color.Inherit; + /// - /// Icon placed before the text if set. + /// Icon displayed at the start of the button. /// [Parameter] public string? StartIcon { get; set; } + /// - /// The color of the component. It supports the theme colors. + /// Button color; supports theme colors. /// [Parameter] public Color Color { get; set; } = Color.Default; + /// - /// Placement of the loading adornment. Default is start. + /// Position of the loading adornment; defaults to Start. /// [Parameter] public Adornment LoadingAdornment { get; set; } = Adornment.Start; /// - /// The Size of the component. + /// Size of the button. /// [Parameter] public Size Size { get; set; } = Size.Medium; + /// - /// The variant to use. + /// Variant of the button. /// [Parameter] public Variant Variant { get; set; } = Variant.Text; + + /// + /// Indicates if the button is in a loading state. When true, displays a loading indicator and may disable the button. + /// [Parameter] public bool Loading { get => _loading; set { - if (_loading == value) - return; - - _loading = value; - StateHasChanged(); + if (_loading != value) + { + _loading = value; + StateHasChanged(); + } } } + + /// + /// When true, disables the button while it is loading. Default is true. + /// [Parameter] - public RenderFragment ChildContent { get; set; } // Adding ChildContent parameter + public bool DisableWhenLoading { get; set; } = true; + /// + /// The content displayed within the button. + /// [Parameter] - public RenderFragment LoadingContent { get; set; } + public RenderFragment? ChildContent { get; set; } + /// + /// Optional content displayed when the button is loading. If not provided, ChildContent is used. + /// [Parameter] - public Color LoadingCircularColor { get; set; } = Color.Primary; + public RenderFragment? LoadingContent { get; set; } /// - /// Determines if the button should be disabled when loading. + /// Color for the loading circular progress indicator. /// [Parameter] - public bool DisableWhenLoading { get; set; } = true; + public Color LoadingCircularColor { get; set; } = Color.Primary; + /// + /// Determines whether the button is disabled based on its Disabled property and loading state. + /// private bool IsDisabled => Disabled || (Loading && DisableWhenLoading); + /// + /// Handles the button click event, setting the loading state during asynchronous execution. + /// Uses try/finally to ensure the loading state is reset even if an exception occurs. + /// private async Task OnClickAsync() { if (IsDisabled) return; - _loading = true; - await base.OnClick.InvokeAsync(null); - _loading = false; + try + { + Loading = true; + await base.OnClick.InvokeAsync(null); + } + finally + { + Loading = false; + } } } diff --git a/src/Server.UI/Pages/Identity/Roles/Roles.razor b/src/Server.UI/Pages/Identity/Roles/Roles.razor index 42a343983..45d45bc62 100644 --- a/src/Server.UI/Pages/Identity/Roles/Roles.razor +++ b/src/Server.UI/Pages/Identity/Roles/Roles.razor @@ -30,39 +30,39 @@ ServerData="@(ServerReload)"> + @Title - + @L["ALL"] - @foreach (var item in TenantsService.DataSource) + @foreach (var tenant in TenantsService.DataSource) { - @item.Name + @tenant.Name } + - @ConstantString.Refresh @if (_canCreate) { - @ConstantString.New } - + @if (_canDelete) { - @ConstantString.Delete @@ -79,7 +79,8 @@ - @ConstantString.Import @@ -90,20 +91,18 @@ } - - @if (_canSearch) - { - - - } - + @if (_canSearch) + { + + + } - + @if (_canEdit || _canDelete || _canManagePermissions) @@ -137,7 +136,7 @@ - @context.Item.TenantName + @context.Item.TenantName @L["Selected"]: @_selectedItems.Count @@ -157,9 +156,17 @@ - + + @code { + #region Fields and Properties + [CascadingParameter] private Task AuthState { get; set; } = default!; private RoleManager RoleManager = null!; private string? Title { get; set; } @@ -167,6 +174,7 @@ private string _currentRoleName = string.Empty; private int _defaultPageSize = 15; private HashSet _selectedItems = new(); + // Used only for retrieving member descriptions private readonly ApplicationRoleDto _currentDto = new(); private string _searchString = string.Empty; private string _selectedTenantId = " "; @@ -185,6 +193,11 @@ private bool _uploading; private bool _exporting; private PermissionHelper PermissionHelper = null!; + + #endregion + + #region Lifecycle + protected override async Task OnInitializedAsync() { InitializeServices(); @@ -198,68 +211,103 @@ _canImport = (await AuthService.AuthorizeAsync(state.User, Permissions.Roles.Import)).Succeeded; _canExport = (await AuthService.AuthorizeAsync(state.User, Permissions.Roles.Export)).Succeeded; } + private void InitializeServices() { RoleManager = ScopedServices.GetRequiredService>(); PermissionHelper = ScopedServices.GetRequiredService(); } + + #endregion + + #region Grid and Search + + /// + /// Creates the predicate used to filter roles by search text and tenant. + /// private Expression> CreateSearchPredicate() { - return x => - (x.Name!.ToLower().Contains(_searchString) || x.Description!.ToLower().Contains(_searchString)) && - (_selectedTenantId == " " || (_selectedTenantId != " " && x.TenantId == _selectedTenantId)); + return role => + (role.Name!.ToLower().Contains(_searchString) || role.Description!.ToLower().Contains(_searchString)) && + (_selectedTenantId == " " || role.TenantId == _selectedTenantId); } + /// + /// Retrieves the grid data from the server. + /// private async Task> ServerReload(GridState state) { + _loading = true; try { - _loading = true; - var searchPredicate = CreateSearchPredicate(); - var count = await RoleManager.Roles.CountAsync(searchPredicate); - var data = await RoleManager.Roles + var totalCount = await RoleManager.Roles.CountAsync(searchPredicate); + var roles = await RoleManager.Roles .Where(searchPredicate) .EfOrderBySortDefinitions(state) - .Skip(state.Page * state.PageSize).Take(state.PageSize) + .Skip(state.Page * state.PageSize) + .Take(state.PageSize) .ProjectTo() .ToListAsync(); - return new GridData { TotalItems = count, Items = data }; + return new GridData { TotalItems = totalCount, Items = roles }; } finally { _loading = false; } } + + /// + /// Handles tenant selection changes. + /// private async Task OnChangedListView(string tenantId) { _selectedTenantId = tenantId; await _table.ReloadServerData(); } + + /// + /// Triggers a new search. + /// private async Task OnSearch(string text) { - if (_loading) - return; + if (_loading) return; _searchString = text.ToLower(); await _table.ReloadServerData(); } + /// + /// Refreshes the grid data. + /// private async Task OnRefresh() { - await _table.ReloadServerData(); + await InvokeAsync(async () => + { + _selectedItems = new HashSet(); + await _table.ReloadServerData(); + }); } + #endregion + + #region Create / Edit Roles + + /// + /// Opens the dialog to create a new role. + /// private async Task OnCreate() { - var model = new ApplicationRoleDto { Name = "" }; - await ShowRoleDialog(model, L["Create a new role"], async role => + var newRoleDto = new ApplicationRoleDto { Name = string.Empty }; + await ShowRoleDialog(newRoleDto, L["Create a new role"], async role => { - var state = await RoleManager.CreateAsync(role); - return state; + return await RoleManager.CreateAsync(role); }); } + /// + /// Opens the dialog to edit an existing role. + /// private async Task OnEdit(ApplicationRoleDto item) { await ShowRoleDialog(item, L["Edit the role"], async role => @@ -270,16 +318,21 @@ existingRole.TenantId = item.TenantId; existingRole.Description = item.Description; existingRole.Name = item.Name; - var state = await RoleManager.UpdateAsync(existingRole); - return state; + return await RoleManager.UpdateAsync(existingRole); } return IdentityResult.Failed(new IdentityError { Description = "Role not found." }); }); } + /// + /// Displays a role form dialog and processes the save action. + /// private async Task ShowRoleDialog(ApplicationRoleDto model, string title, Func> saveAction) { - var parameters = new DialogParameters { { x => x.Model, model } }; + var parameters = new DialogParameters + { + { x => x.Model, model } + }; var options = new DialogOptions { CloseButton = true, CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small, FullWidth = true }; var dialog = await DialogService.ShowAsync(title, parameters, options); var result = await dialog.Result; @@ -293,19 +346,26 @@ Description = model.Description }; - var state = await saveAction(applicationRole); - if (state.Succeeded) + var resultState = await saveAction(applicationRole); + if (resultState.Succeeded) { - Snackbar.Add($"{ConstantString.CreateSuccess}", Severity.Info); + Snackbar.Add(ConstantString.CreateSuccess, Severity.Info); await OnRefresh(); } else { - Snackbar.Add($"{string.Join(",", state.Errors.Select(x => x.Description).ToArray())}", Severity.Error); + Snackbar.Add(string.Join(",", resultState.Errors.Select(x => x.Description)), Severity.Error); } } } + #endregion + + #region Permissions Handling + + /// + /// Opens the permissions drawer and loads permissions for the selected role. + /// private async Task OnSetPermissions(ApplicationRoleDto item) { _showPermissionsDrawer = true; @@ -313,57 +373,70 @@ _permissions = await PermissionHelper.GetAllPermissionsByRoleId(item.Id); } - - private Task OnOpenChangedHandler(bool state) { _showPermissionsDrawer = state; return Task.CompletedTask; } + /// + /// Handles a single permission change. + /// private async Task OnAssignChangedHandler(PermissionModel model) { await ProcessPermissionChange(model, async (roleId, claim, assigned) => { - var role = await RoleManager.FindByIdAsync(roleId) ?? throw new NotFoundException($"Application role {roleId} Not Found."); + var role = await RoleManager.FindByIdAsync(roleId) + ?? throw new NotFoundException($"Application role {roleId} not found."); if (assigned) { await RoleManager.AddClaimAsync(role, claim); - Snackbar.Add($"{L["Permission assigned successfully"]}", Severity.Info); + Snackbar.Add(L["Permission assigned successfully"], Severity.Info); } else { await RoleManager.RemoveClaimAsync(role, claim); - Snackbar.Add($"{L["Permission removed successfully"]}", Severity.Info); + Snackbar.Add(L["Permission removed successfully"], Severity.Info); } }); } + /// + /// Processes batch permission assignment changes. + /// private async Task OnAssignAllChangedHandler(List models) { - var roleClaimsDict = models.GroupBy(m => m.RoleId) - .ToDictionary(g => g.Key!, g => g.ToList()); + var roleGroups = models + .GroupBy(m => m.RoleId) + .ToDictionary(g => g.Key!, g => g.ToList()); - foreach (var roleClaims in roleClaimsDict) + foreach (var (roleId, permissionModels) in roleGroups) { - var roleId = roleClaims.Key!; - var claims = roleClaims.Value.Select(m => new Claim(m.ClaimType, m.ClaimValue)).ToList(); - var assignedClaims = roleClaims.Value.Where(m => m.Assigned).Select(m => new Claim(m.ClaimType, m.ClaimValue)).ToList(); - var unassignedClaims = roleClaims.Value.Where(m => !m.Assigned).Select(m => new Claim(m.ClaimType, m.ClaimValue)).ToList(); + var assignedClaims = permissionModels + .Where(m => m.Assigned) + .Select(m => new Claim(m.ClaimType, m.ClaimValue)) + .ToList(); + var unassignedClaims = permissionModels + .Where(m => !m.Assigned) + .Select(m => new Claim(m.ClaimType, m.ClaimValue)) + .ToList(); await ProcessPermissionsBatch(roleId, assignedClaims, unassignedClaims); } - Snackbar.Add($"{L["Authorization has been changed"]}", Severity.Info); + Snackbar.Add(L["Authorization has been changed"], Severity.Info); } + /// + /// Processes batch permission changes for a role. + /// private async Task ProcessPermissionsBatch(string roleId, List assignedClaims, List unassignedClaims) { _processing = true; try { - var role = await RoleManager.FindByIdAsync(roleId) ?? throw new NotFoundException($"Application role {roleId} Not Found."); + var role = await RoleManager.FindByIdAsync(roleId) + ?? throw new NotFoundException($"Application role {roleId} not found."); - // 批量添加和移除声明 foreach (var claim in assignedClaims) { await RoleManager.AddClaimAsync(role, claim); @@ -376,10 +449,9 @@ FusionCache.Remove($"get-claims-by-{roleId}"); } - catch (Exception ex) + catch (Exception) { - // Log the exception here - throw ex; + throw; } finally { @@ -387,6 +459,9 @@ } } + /// + /// Toggles a permission assignment and applies the change. + /// private async Task ProcessPermissionChange(PermissionModel model, Func changeAction) { _processing = true; @@ -395,82 +470,99 @@ var roleId = model.RoleId!; var claim = new Claim(model.ClaimType, model.ClaimValue); model.Assigned = !model.Assigned; - var assigned = model.Assigned; - await changeAction(roleId, claim, assigned); - - - + await changeAction(roleId, claim, model.Assigned); FusionCache.Remove($"get-claims-by-{roleId}"); } - catch (Exception ex) + catch (Exception) { - // Log the exception here - throw ex; + throw; } finally { _processing = false; } } + + #endregion + + #region Deletion + + /// + /// Deletes a single role after confirmation. + /// private async Task OnDelete(ApplicationRoleDto dto) { - await DialogServiceHelper.ShowConfirmationDialogAsync(ConstantString.DeleteConfirmationTitle, string.Format(ConstantString.DeleteConfirmation, dto.Name), async () => - { - await InvokeAsync(async () => + await DialogServiceHelper.ShowConfirmationDialogAsync( + ConstantString.DeleteConfirmationTitle, + string.Format(ConstantString.DeleteConfirmation, dto.Name), + async () => { - var deleteRoles = await RoleManager.Roles.Where(x => x.Id == dto.Id).ToListAsync(); - foreach (var role in deleteRoles) + var rolesToDelete = await RoleManager.Roles.Where(x => x.Id == dto.Id).ToListAsync(); + foreach (var role in rolesToDelete) { var deleteResult = await RoleManager.DeleteAsync(role); if (!deleteResult.Succeeded) { - Snackbar.Add($"{string.Join(",", deleteResult.Errors.Select(x => x.Description).ToArray())}", Severity.Error); + Snackbar.Add(string.Join(",", deleteResult.Errors.Select(x => x.Description)), Severity.Error); return; } } - - Snackbar.Add($"{ConstantString.DeleteSuccess}", Severity.Info); + Snackbar.Add(ConstantString.DeleteSuccess, Severity.Info); await OnRefresh(); }); - }); } + /// + /// Deletes all selected roles after confirmation. + /// private async Task OnDeleteChecked() { - await DialogServiceHelper.ShowConfirmationDialogAsync(ConstantString.DeleteConfirmationTitle, string.Format(ConstantString.DeleteConfirmation, $"{_selectedItems.Count}"), async () => - { - await InvokeAsync(async () => + await DialogServiceHelper.ShowConfirmationDialogAsync( + ConstantString.DeleteConfirmationTitle, + string.Format(ConstantString.DeleteConfirmation, _selectedItems.Count), + async () => { - var deleteId = _selectedItems.Select(x => x.Id).ToArray(); - var deleteRoles = await RoleManager.Roles.Where(x => deleteId.Contains(x.Id)).ToListAsync(); - foreach (var role in deleteRoles) + var deleteIds = _selectedItems.Select(x => x.Id).ToArray(); + var rolesToDelete = await RoleManager.Roles.Where(x => deleteIds.Contains(x.Id)).ToListAsync(); + foreach (var role in rolesToDelete) { var deleteResult = await RoleManager.DeleteAsync(role); if (!deleteResult.Succeeded) { - Snackbar.Add($"{string.Join(",", deleteResult.Errors.Select(x => x.Description).ToArray())}", Severity.Error); + Snackbar.Add(string.Join(",", deleteResult.Errors.Select(x => x.Description)), Severity.Error); return; } } - Snackbar.Add($"{ConstantString.DeleteSuccess}", Severity.Info); + Snackbar.Add(ConstantString.DeleteSuccess, Severity.Info); await OnRefresh(); }); - }); } + #endregion + + #region Export / Import + /// + /// Handles exporting of role data. + /// private Task OnExport() { _exporting = true; + // Export logic to be implemented. _exporting = false; return Task.CompletedTask; } + /// + /// Handles importing role data from a file. + /// private Task OnImportData(IBrowserFile file) { _uploading = true; + // Import logic to be implemented. _uploading = false; return Task.CompletedTask; } -} \ No newline at end of file + #endregion +} diff --git a/src/Server.UI/Pages/Identity/Users/Users.razor b/src/Server.UI/Pages/Identity/Users/Users.razor index ef2124c4d..66bc96346 100644 --- a/src/Server.UI/Pages/Identity/Users/Users.razor +++ b/src/Server.UI/Pages/Identity/Users/Users.razor @@ -381,7 +381,11 @@ private async Task OnRefresh() { - await _table.ReloadServerData(); + await InvokeAsync(async () => + { + _selectedItems = new HashSet(); + await _table.ReloadServerData(); + }); } #region User Creation, Editing, and Deletion private async Task ShowUserDialog(ApplicationUserDto model, string title, Func processAction) diff --git a/src/Server.UI/Pages/PicklistSets/PicklistSets.razor b/src/Server.UI/Pages/PicklistSets/PicklistSets.razor index c8710d5e1..d5360bb06 100644 --- a/src/Server.UI/Pages/PicklistSets/PicklistSets.razor +++ b/src/Server.UI/Pages/PicklistSets/PicklistSets.razor @@ -29,45 +29,46 @@ EditMode="DataGridEditMode.Cell" T="PicklistSetDto" CommittedItemChanges="@CommittedItemChanges" - ServerData="@(ServerReload)"> + ServerData="ServerReload"> + @Title - + + - @ConstantString.Refresh @if (_canCreate) { - + @ConstantString.New } - + @if (_canDelete) { - + @ConstantString.Delete } @if (_canExport) { + OnClick="OnExport"> @ConstantString.Export } @@ -76,9 +77,10 @@ - + @ConstantString.Import @@ -90,10 +92,10 @@ @if (_canSearch) { - - - + + } @@ -102,7 +104,7 @@ - + @@ -111,10 +113,10 @@ - + - + @@ -123,8 +125,9 @@ - @code { + #region Fields & Properties + [CascadingParameter] private Task AuthState { get; set; } = default!; [CascadingParameter] private UserProfile? UserProfile { get; set; } private MudDataGrid _table = null!; @@ -139,6 +142,7 @@ private int _defaultPageSize = 15; private PicklistSetsWithPaginationQuery Query { get; set; } = new(); + private bool _canCreate; private bool _canSearch; private bool _canEdit; @@ -150,6 +154,10 @@ private bool _uploading; private bool _exporting; + #endregion + + #region Lifecycle Methods + protected override async Task OnInitializedAsync() { Title = L[SelectedItem.GetClassDescription()]; @@ -162,6 +170,10 @@ _canExport = (await AuthService.AuthorizeAsync(state.User, Permissions.PicklistSets.Export)).Succeeded; } + #endregion + + #region Grid Data & Search + private async Task> ServerReload(GridState state) { try @@ -171,10 +183,12 @@ Query.Keyword = _searchString; Query.Picklist = _searchPicklist; Query.OrderBy = state.SortDefinitions.FirstOrDefault()?.SortBy ?? "Id"; - Query.SortDirection = state.SortDefinitions.FirstOrDefault()?.Descending ?? true ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString(); + Query.SortDirection = (state.SortDefinitions.FirstOrDefault()?.Descending ?? true) + ? SortDirection.Descending.ToString() + : SortDirection.Ascending.ToString(); Query.PageNumber = state.Page + 1; Query.PageSize = state.PageSize; - var result = await Mediator.Send(Query).ConfigureAwait(false); + var result = await Mediator.Send(Query); return new GridData { TotalItems = result.TotalItems, Items = result.Items }; } finally @@ -182,18 +196,20 @@ _loading = false; } } + private async Task OnChangedListView(PickListView listview) { Query.ListView = listview; await _table.ReloadServerData(); } - private async Task OnSearch(string text) + + private async Task OnKeywordSearch(string text) { _searchString = text; await _table.ReloadServerData(); } - private async Task OnSearch(Picklist? val) + private async Task OnPicklistSearch(Picklist? val) { _searchPicklist = val; await _table.ReloadServerData(); @@ -203,10 +219,16 @@ { PicklistSetCacheKey.Refresh(); _searchString = string.Empty; - await _table.ReloadServerData(); + await InvokeAsync(() => _table.ReloadServerData()); } + + #endregion + + #region Edit & Commit Changes + private void PicklistChanged(PicklistSetDto item) { + // Trigger the commit of cell changes. _table.CommittedItemChanges.InvokeAsync(item); } @@ -218,40 +240,26 @@ var result = await Mediator.Send(command); if (!result.Succeeded) { - Snackbar.Add($"{result.ErrorMessage}", Severity.Error); + Snackbar.Add(result.ErrorMessage, Severity.Error); } - StateHasChanged(); }); } - private async Task DeleteItem(PicklistSetDto item) - { - var command = new DeletePicklistSetCommand(new[] { item.Id }); - var contentText = string.Format(ConstantString.DeleteConfirmation, item.Name); - await DialogServiceHelper.ShowDeleteConfirmationDialogAsync(command, ConstantString.DeleteConfirmationTitle, contentText, async () => - { - await InvokeAsync(async () => - { - await _table.ReloadServerData(); - _selectedItems.Clear(); - }); - }); - } + #endregion - private async Task OnDeleteChecked() + #region Create & Delete + + private async Task OnCreate() { - var command = new DeletePicklistSetCommand(_selectedItems.Select(x => x.Id).ToArray()); - var contentText = string.Format(ConstantString.DeleteConfirmation, _selectedItems.Count); - await DialogServiceHelper.ShowDeleteConfirmationDialogAsync(command, ConstantString.DeleteConfirmationTitle, contentText, async () => - { - await InvokeAsync(async () => + var command = new AddEditPicklistSetCommand { - await _table.ReloadServerData(); - _selectedItems.Clear(); - }); - }); + Name = SelectedItem.Name, + Description = SelectedItem?.Description + }; + await ShowCreateDialog(command, L["Create a picklist"]); } + private async Task ShowCreateDialog(AddEditPicklistSetCommand command, string title) { var parameters = new DialogParameters @@ -266,50 +274,82 @@ await _table.ReloadServerData(); } } - private async Task OnCreate() + + private async Task DeleteItem(PicklistSetDto item) { - var command = new AddEditPicklistSetCommand + var command = new DeletePicklistSetCommand(new[] { item.Id }); + var contentText = string.Format(ConstantString.DeleteConfirmation, item.Name); + await DialogServiceHelper.ShowDeleteConfirmationDialogAsync(command, ConstantString.DeleteConfirmationTitle, contentText, async () => + { + await InvokeAsync(async () => { - Name = SelectedItem.Name, - Description = SelectedItem?.Description - }; - await ShowCreateDialog(command, L["Create a picklist"]); + await _table.ReloadServerData(); + _selectedItems.Clear(); + }); + }); } + private async Task OnDeleteChecked() + { + var command = new DeletePicklistSetCommand(_selectedItems.Select(x => x.Id).ToArray()); + var contentText = string.Format(ConstantString.DeleteConfirmation, _selectedItems.Count); + await DialogServiceHelper.ShowDeleteConfirmationDialogAsync(command, ConstantString.DeleteConfirmationTitle, contentText, async () => + { + await InvokeAsync(async () => + { + await _table.ReloadServerData(); + _selectedItems.Clear(); + }); + }); + } + + #endregion + + #region Export & Import + private async Task OnExport() { _exporting = true; - var request = new ExportPicklistSetsQuery - { - Keyword = _searchString - }; - var result = await Mediator.Send(request); - var downloadResult = await BlazorDownloadFileService.DownloadFile($"{L["Picklist"]}.xlsx", result, "application/octet-stream"); - Snackbar.Add($"{ConstantString.ExportSuccess}", Severity.Info); - _exporting = false; + try + { + var request = new ExportPicklistSetsQuery { Keyword = _searchString }; + var result = await Mediator.Send(request); + await BlazorDownloadFileService.DownloadFile($"{L["Picklist"]}.xlsx", result, "application/octet-stream"); + Snackbar.Add(ConstantString.ExportSuccess, Severity.Info); + } + finally + { + _exporting = false; + } } private async Task OnImportData(IBrowserFile file) { _uploading = true; - var stream = new MemoryStream(); - await file.OpenReadStream(GlobalVariable.MaxAllowedSize).CopyToAsync(stream); - var command = new ImportPicklistSetsCommand(file.Name, stream.ToArray()); - var result = await Mediator.Send(command); - if (result.Succeeded) - { - await OnRefresh(); - Snackbar.Add($"{ConstantString.ImportSuccess}", Severity.Info); - } - else + try { - foreach (var msg in result.Errors) + using var stream = new MemoryStream(); + await file.OpenReadStream(GlobalVariable.MaxAllowedSize).CopyToAsync(stream); + var command = new ImportPicklistSetsCommand(file.Name, stream.ToArray()); + var result = await Mediator.Send(command); + if (result.Succeeded) + { + await OnRefresh(); + Snackbar.Add(ConstantString.ImportSuccess, Severity.Info); + } + else { - Snackbar.Add($"{msg}", Severity.Error); + foreach (var msg in result.Errors) + { + Snackbar.Add(msg, Severity.Error); + } } } - - _uploading = false; + finally + { + _uploading = false; + } } -} \ No newline at end of file + #endregion +} diff --git a/src/Server.UI/Server.UI.csproj b/src/Server.UI/Server.UI.csproj index 49c6f3eac..bdd68f33e 100644 --- a/src/Server.UI/Server.UI.csproj +++ b/src/Server.UI/Server.UI.csproj @@ -20,7 +20,7 @@ - + diff --git a/src/Server.UI/Services/DialogServiceHelper.cs b/src/Server.UI/Services/DialogServiceHelper.cs index 526ee762c..90b842a8e 100644 --- a/src/Server.UI/Services/DialogServiceHelper.cs +++ b/src/Server.UI/Services/DialogServiceHelper.cs @@ -10,7 +10,6 @@ public class DialogServiceHelper { private readonly IDialogService _dialogService; - /// /// Initializes a new instance of the class. /// @@ -29,24 +28,29 @@ public DialogServiceHelper(IDialogService dialogService) /// The action to perform on confirmation. /// The action to perform on cancellation (optional). /// A task representing the asynchronous operation. - public async Task ShowDeleteConfirmationDialogAsync(IRequest> command, string title, string contentText, Func onConfirm, Func? onCancel = null) + public async Task ShowDeleteConfirmationDialogAsync( + IRequest> command, + string title, + string contentText, + Func onConfirm, + Func? onCancel = null) { var parameters = new DialogParameters - { - { nameof(DeleteConfirmation.ContentText), contentText }, - { nameof(DeleteConfirmation.Command), command } - }; + { + { nameof(DeleteConfirmation.ContentText), contentText }, + { nameof(DeleteConfirmation.Command), command } + }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall, FullWidth = true }; - var dialog = await _dialogService.ShowAsync(title, parameters, options).ConfigureAwait(false); - var result = await dialog.Result.ConfigureAwait(false); + var dialog = await _dialogService.ShowAsync(title, parameters, options); + var result = await dialog.Result; if (result is not null && !result.Canceled) { - await onConfirm().ConfigureAwait(false); + await onConfirm(); } else if (onCancel != null) { - await onCancel().ConfigureAwait(false); + await onCancel(); } } @@ -58,24 +62,27 @@ public async Task ShowDeleteConfirmationDialogAsync(IRequest> comman /// The action to perform on confirmation. /// The action to perform on cancellation (optional). /// A task representing the asynchronous operation. - public async Task ShowConfirmationDialogAsync(string title, string contentText, Func onConfirm, Func? onCancel = null) + public async Task ShowConfirmationDialogAsync( + string title, + string contentText, + Func onConfirm, + Func? onCancel = null) { var parameters = new DialogParameters - { - { nameof(ConfirmationDialog.ContentText), contentText } - }; + { + { nameof(ConfirmationDialog.ContentText), contentText } + }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall, FullWidth = true }; - var dialog = await _dialogService.ShowAsync(title, parameters, options).ConfigureAwait(false); - var result = await dialog.Result.ConfigureAwait(false); - + var dialog = await _dialogService.ShowAsync(title, parameters, options); + var result = await dialog.Result; if (result is not null && !result.Canceled) { - await onConfirm().ConfigureAwait(false); + await onConfirm(); } else if (onCancel != null) { - await onCancel().ConfigureAwait(false); + await onCancel(); } } } From 2f1ca27f6f9b5487314a4b0cce9473bbfa845b36 Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Sun, 2 Feb 2025 11:38:52 +0800 Subject: [PATCH 02/13] Update PicklistSets.razor --- .../Pages/PicklistSets/PicklistSets.razor | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/Server.UI/Pages/PicklistSets/PicklistSets.razor b/src/Server.UI/Pages/PicklistSets/PicklistSets.razor index d5360bb06..b52e9eca7 100644 --- a/src/Server.UI/Pages/PicklistSets/PicklistSets.razor +++ b/src/Server.UI/Pages/PicklistSets/PicklistSets.razor @@ -52,50 +52,50 @@ @if (_canCreate) { - + @ConstantString.New - + } @if (_canDelete) { - + @ConstantString.Delete - + } @if (_canExport) { - + @ConstantString.Export - + } @if (_canImport) { - - - - + + + + @ConstantString.Import - - - - + + + + } @if (_canSearch) { - - + + } @@ -253,10 +253,10 @@ private async Task OnCreate() { var command = new AddEditPicklistSetCommand - { - Name = SelectedItem.Name, - Description = SelectedItem?.Description - }; + { + Name = SelectedItem.Name, + Description = SelectedItem?.Description + }; await ShowCreateDialog(command, L["Create a picklist"]); } From 96000f9f5faff3d561117435a6a96dff89209a47 Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Sun, 2 Feb 2025 19:47:40 +0800 Subject: [PATCH 03/13] commit --- .../Localization/LanguageSelector.razor | 2 +- src/Server.UI/Pages/Dashboard/Dashboard.razor | 24 ++ src/Server.UI/Pages/Public/Index.razor | 1 + .../Services/Layout/LayoutService.cs | 241 +++++++----------- .../UserPreferences/UserPreference.cs | 169 ++++++++---- src/Server.UI/Themes/Theme.cs | 7 +- src/Server.UI/wwwroot/css/app.css | 1 - 7 files changed, 254 insertions(+), 191 deletions(-) diff --git a/src/Server.UI/Components/Localization/LanguageSelector.razor b/src/Server.UI/Components/Localization/LanguageSelector.razor index cc3dfeb03..e848f9863 100644 --- a/src/Server.UI/Components/Localization/LanguageSelector.razor +++ b/src/Server.UI/Components/Localization/LanguageSelector.razor @@ -12,7 +12,7 @@ { if (language.Name == CurrentLanguage) { - @language.DisplayName + @language.DisplayName } else { diff --git a/src/Server.UI/Pages/Dashboard/Dashboard.razor b/src/Server.UI/Pages/Dashboard/Dashboard.razor index b6ac400b7..a1bb3d9d6 100644 --- a/src/Server.UI/Pages/Dashboard/Dashboard.razor +++ b/src/Server.UI/Pages/Dashboard/Dashboard.razor @@ -280,6 +280,30 @@ “Never had such a good time making a site“ + + H1 Title + H2 Title + H3 Title + H4 Title + H5 Title + H6 Title + + Subtitle1 Blazor + Subtitle2 Blazor + Button Text + + Here we add each class to the divs to change the background color, text color and both background and text color with one class only. + Here we add each class to the divs to change the background color, text color and both background and text color with one class only. + Here we add each class to the divs to change the background color, text color and both background and text color with one class only. + Here we add each class to the divs to change the background color, text color and both background and text color with one class only. + + Here we add each class to the divs to change the background color, text color and both background and text color with one class only. + Here we add each class to the divs to change the background color, text color and both background and text color with one class only. + Here we add each class to the divs to change the background color, text color and both background and text color with one class only. + Here we add each class to the divs to change the background color, text color and both background and text color with one class only. + Here we add each class to the divs to change the background color, text color and both background and text color with one class only. + Here we add each class to the divs to change the background color, text color and both background and text color with one class only. + Here we add each class to the divs to change the background color, text color and both background and text color with one class only.