diff --git a/WebApp/Components/Features/Events/CareerMapping.razor b/WebApp/Components/Features/Events/CareerMapping.razor index 3f7ebdc..4bd4e7b 100644 --- a/WebApp/Components/Features/Events/CareerMapping.razor +++ b/WebApp/Components/Features/Events/CareerMapping.razor @@ -37,7 +37,11 @@ else Events are shown in blue, career fields in green. Career fields are clusters of related careers. Click on a node to see details. Use mouse to zoom and pan the graph. - + +
+ +
+ @if (_selectedNodeInfo != null) { @@ -61,9 +65,6 @@ else } -
- -
} diff --git a/WebApp/Components/Features/Students/Index.razor b/WebApp/Components/Features/Students/Index.razor index 47fbb65..b840e37 100644 --- a/WebApp/Components/Features/Students/Index.razor +++ b/WebApp/Components/Features/Students/Index.razor @@ -30,7 +30,7 @@ -
+ @@ -40,7 +40,7 @@ { @context.Item.OfficerRole } -
+
t?.Event is { RegionalEvent: true }).OrderBy(t => t.Event.Name) : context.Item.Teams.Where(t => t?.Event != null).OrderBy(t => t.Event.Name); } -
+ @foreach (var team in teamsToDisplay) { var isCaptain = team.Captain != null && team.Captain.Equals(context.Item.Student); @@ -98,7 +98,7 @@ } -
+
diff --git a/WebApp/Components/Features/Teams/Components/TeamStudents.razor b/WebApp/Components/Features/Teams/Components/TeamStudents.razor index 58cba4a..51c9f0f 100644 --- a/WebApp/Components/Features/Teams/Components/TeamStudents.razor +++ b/WebApp/Components/Features/Teams/Components/TeamStudents.razor @@ -1,5 +1,5 @@ @using WebApp.Models -
+ @foreach (var student in Team.Students .OrderBy(e => @@ -31,7 +31,7 @@ } -
+ @code { [Parameter] diff --git a/WebApp/Components/Features/Teams/Components/TeamToggleSelector.razor b/WebApp/Components/Features/Teams/Components/TeamToggleSelector.razor index 32f3709..443ff2b 100644 --- a/WebApp/Components/Features/Teams/Components/TeamToggleSelector.razor +++ b/WebApp/Components/Features/Teams/Components/TeamToggleSelector.razor @@ -15,13 +15,13 @@ { -
+ @team.ToString() @if (ShowEventAttributes) { } -
+
} diff --git a/WebApp/Components/Features/Teams/Index.razor b/WebApp/Components/Features/Teams/Index.razor index 04a3d59..a13fb84 100644 --- a/WebApp/Components/Features/Teams/Index.razor +++ b/WebApp/Components/Features/Teams/Index.razor @@ -3,6 +3,7 @@ @using WebApp.Models @page "/teams" @attribute [Authorize] +@implements IAsyncDisposable @inject AppDbContext Context @inject IDialogService DialogService @inject ISnackbar Snackbar @@ -74,30 +75,58 @@ MudDataGrid _dataGrid = null!; private bool _isLoading = true; private bool _showRegionalOnly = false; + private CancellationTokenSource? _cancellationTokenSource; + private bool _isDisposed = false; + + protected override void OnInitialized() + { + _cancellationTokenSource = new CancellationTokenSource(); + } private async Task ToggleRegionalFilter() { + if (_isDisposed) return; + try { _showRegionalOnly = !_showRegionalOnly; - if (_dataGrid != null) + if (_dataGrid != null && !_isDisposed) { await _dataGrid.ReloadServerData(); } } + catch (TaskCanceledException) + { + // Component was disposed, ignore + } + catch (JSDisconnectedException) + { + // JS connection lost, ignore + } catch (Exception ex) { - Snackbar.Add($"Error applying filter: {ex.Message}", Severity.Error); + if (!_isDisposed) + { + Snackbar.Add($"Error applying filter: {ex.Message}", Severity.Error); + } } } private async Task> ServerReload(GridState state) { + if (_isDisposed) + { + return new GridData { TotalItems = 0, Items = [] }; + } + _isLoading = true; try { + var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None; + IQueryable query = Context.Teams + .AsNoTracking() .Include(e => e.Event) .Include(e => e.Students) .ThenInclude(e => e.EventRankings); @@ -113,7 +142,7 @@ query = query.Where(state.FilterDefinitions); // Load all data first - var allTeams = await query.ToArrayAsync(); + var allTeams = await query.ToArrayAsync(cancellationToken); // Always sort by EventFormat FIRST to separate group/individual teams // Sort in memory to ensure this ordering is maintained regardless of user sorts @@ -173,45 +202,100 @@ Items = pagedData }; } + catch (TaskCanceledException) + { + // Component was disposed, return empty result + return new GridData { TotalItems = 0, Items = [] }; + } + catch (JSDisconnectedException) + { + // JS connection lost, return empty result + return new GridData { TotalItems = 0, Items = [] }; + } finally { - _isLoading = false; + if (!_isDisposed) + { + _isLoading = false; + } } } private async Task DeleteTeam(Team team) { - //_isRowBlocked = true; + if (_isDisposed) return; - var result = await DialogService - .ShowMessageBox("Delete team", - (MarkupString)$"Are you sure want to delete {team}? This cannot be undone.", - yesText: "Yes", - noText: "Cancel"); - - if (result == true) + try { - // If deleting a numbered team (1 or 2), clear the identifier of the remaining team - if (team.Identifier == "1" || team.Identifier == "2") - { - var remainingTeam = await Context.Teams - .Include(t => t.Event) - .FirstOrDefaultAsync(t => t.Event.Id == team.Event.Id && t.Id != team.Id); + var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None; - if (remainingTeam != null) + var result = await DialogService + .ShowMessageBox("Delete team", + (MarkupString)$"Are you sure want to delete {team}? This cannot be undone.", + yesText: "Yes", + noText: "Cancel"); + + if (_isDisposed) return; + + if (result == true) + { + // If deleting a numbered team (1 or 2), clear the identifier of the remaining team + if (team.Identifier == "1" || team.Identifier == "2") { - remainingTeam.Identifier = null; - Context.Teams.Update(remainingTeam); + var remainingTeam = await Context.Teams + .Include(t => t.Event) + .FirstOrDefaultAsync(t => t.Event.Id == team.Event.Id && t.Id != team.Id, cancellationToken); + + if (_isDisposed) return; + + if (remainingTeam != null) + { + remainingTeam.Identifier = null; + Context.Teams.Update(remainingTeam); + } + } + + Context.Teams.Remove(team!); + await Context.SaveChangesAsync(cancellationToken); + + if (!_isDisposed) + { + Snackbar.Add($"Team {team} deleted", Severity.Info); } } - Context.Teams.Remove(team!); - await Context.SaveChangesAsync(); - Snackbar.Add($"Delete event: Delete of Team {team}", Severity.Info); + if (!_isDisposed) + { + StateHasChanged(); + await _dataGrid.ReloadServerData(); + } } + catch (TaskCanceledException) + { + // Component was disposed, ignore + } + catch (JSDisconnectedException) + { + // JS connection lost, ignore + } + catch (Exception ex) + { + if (!_isDisposed) + { + Snackbar.Add($"Error deleting team: {ex.Message}", Severity.Error); + } + } + } - //_isRowBlocked = false; - StateHasChanged(); - await _dataGrid.ReloadServerData(); + public async ValueTask DisposeAsync() + { + if (!_isDisposed) + { + _isDisposed = true; + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + } + await ValueTask.CompletedTask; } }