Add IsPinned and IsDeleted properties to Note entity with corresponding database configurations and migrations
This commit enhances the Note entity by introducing two new properties: IsPinned and IsDeleted, allowing for better management of note visibility and status. The NoteConfiguration class has been updated to include indexes for these properties, improving query performance. Additionally, new migrations have been created to reflect these changes in the database schema. The UI components have been updated to support pinning and restoring notes, enhancing user interaction and functionality within the note management system.
This commit is contained in:
@@ -2,8 +2,14 @@
|
||||
@attribute [Authorize]
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using WebApp.Models
|
||||
@using Core.Entities
|
||||
@using WebApp.Services
|
||||
@using WebApp.Components.Shared.Components
|
||||
@inject IConfiguration Configuration
|
||||
@inject AppDbContext Context
|
||||
@inject INotesService NotesService
|
||||
@inject IDialogService DialogService
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<PageTitle>@Configuration["ChapterSettings:Name"] - TSA Chapter Organizer</PageTitle>
|
||||
|
||||
@@ -26,6 +32,19 @@
|
||||
</div>
|
||||
</MudPaper>
|
||||
|
||||
@if (_pinnedNotes.Any())
|
||||
{
|
||||
<MudPaper Elevation="0" Class="mb-4">
|
||||
<MudText Typo="Typo.h4">Pinned Notes</MudText>
|
||||
</MudPaper>
|
||||
<MudGrid>
|
||||
@foreach (var note in _pinnedNotes)
|
||||
{
|
||||
<NoteCard Note="@note" OnNoteChanged="HandleNoteChanged" />
|
||||
}
|
||||
</MudGrid>
|
||||
}
|
||||
|
||||
@if (!_hasStudents)
|
||||
{
|
||||
<!-- Getting Started: No students yet -->
|
||||
@@ -182,13 +201,54 @@ else
|
||||
private int _teamCount;
|
||||
private int _individualTeamsCount;
|
||||
private int _groupTeamsCount;
|
||||
private List<Note> _pinnedNotes = [];
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private bool _isDisposed = false;
|
||||
|
||||
private bool _hasStudents => _studentCount > 0;
|
||||
private bool _hasTeams => _teamCount > 0;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadStatistics();
|
||||
await LoadPinnedNotes();
|
||||
}
|
||||
|
||||
private async Task HandleNoteChanged()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
await LoadPinnedNotes();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPinnedNotes()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
try
|
||||
{
|
||||
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
||||
_pinnedNotes = (await NotesService.GetPinnedNotesAsync()).ToList();
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Component was disposed, ignore
|
||||
}
|
||||
catch (JSDisconnectedException)
|
||||
{
|
||||
// JS connection lost, ignore
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Error loading pinned notes - ignore
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadStatistics()
|
||||
@@ -225,4 +285,16 @@ else
|
||||
_individualTeamsCount = contextTeams.Count(e => e.Event.EventFormat == EventFormat.Individual);
|
||||
_groupTeamsCount = contextTeams.Count(e => e.Event.EventFormat == EventFormat.Team);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_isDisposed = true;
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
await ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +97,8 @@
|
||||
"Created" => Color.Success,
|
||||
"Updated" => Color.Info,
|
||||
"Deleted" => Color.Error,
|
||||
"Soft Deleted" => Color.Error,
|
||||
"Restored" => Color.Success,
|
||||
_ => Color.Default
|
||||
};
|
||||
}
|
||||
@@ -110,7 +112,7 @@
|
||||
|
||||
var options = new DialogOptions
|
||||
{
|
||||
MaxWidth = MaxWidth.Large,
|
||||
MaxWidth = MaxWidth.Medium,
|
||||
FullWidth = true,
|
||||
CloseButton = true
|
||||
};
|
||||
|
||||
@@ -7,9 +7,16 @@
|
||||
@inject INotesService NotesService
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<PageHeader Title="Notes">
|
||||
<ActionButtons>
|
||||
<MudButton StartIcon="@(_showRemoved ? Icons.Material.Filled.List : Icons.Material.Filled.DeleteOutline)"
|
||||
OnClick="ToggleRemovedFilter"
|
||||
Variant="Variant.Outlined"
|
||||
Color="@(_showRemoved ? Color.Warning : Color.Default)">
|
||||
@(_showRemoved ? "Show Active" : "Show Removed")
|
||||
</MudButton>
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="OpenCreateDialog"
|
||||
Variant="Variant.Filled"
|
||||
@@ -33,11 +40,42 @@
|
||||
else
|
||||
{
|
||||
<MudExpansionPanels MultiExpansion="true">
|
||||
@foreach (var note in _notes)
|
||||
@foreach (var note in _paginatedNotes)
|
||||
{
|
||||
<MudExpansionPanel Text="@note.Title"
|
||||
Icon="@Icons.Material.Filled.Note">
|
||||
<MudStack Spacing="2">
|
||||
var noteId = note.Id;
|
||||
var initialExpanded = _selectedNoteId.HasValue && noteId == _selectedNoteId.Value;
|
||||
if (!_expandedNotes.ContainsKey(noteId))
|
||||
{
|
||||
_expandedNotes[noteId] = initialExpanded;
|
||||
}
|
||||
var isExpanded = GetNoteExpanded(noteId);
|
||||
<MudExpansionPanel @key="@($"note-{noteId}")"
|
||||
Icon="@Icons.Material.Filled.Note"
|
||||
IsExpanded="@isExpanded"
|
||||
ExpandedChanged="@((bool expanded) => OnPanelExpandedChanged(noteId, expanded))">
|
||||
<TitleContent>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="flex-grow-1">
|
||||
<MudText Typo="Typo.body1" Class="flex-grow-1" Style="min-width: 0;">
|
||||
<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block;">
|
||||
@GetNoteHeaderText(note, isExpanded)
|
||||
</span>
|
||||
</MudText>
|
||||
@if (!note.Title.StartsWith("@") && !note.IsDeleted && !isExpanded)
|
||||
{
|
||||
<MudButton StartIcon="@(note.IsPinned ? Icons.Material.Filled.PushPin : Icons.Material.Outlined.PushPin)"
|
||||
OnClick="() => TogglePin(note)"
|
||||
OnClick:StopPropagation="true"
|
||||
Variant="Variant.Text"
|
||||
Size="Size.Small"
|
||||
Color="@(note.IsPinned ? Color.Primary : Color.Default)"
|
||||
Disabled="@(IsPinDisabled(note))"
|
||||
Title="@(note.IsPinned ? "Unpin note" : "Pin note")"
|
||||
Class="flex-shrink-0" />
|
||||
}
|
||||
</MudStack>
|
||||
</TitleContent>
|
||||
<ChildContent>
|
||||
<MudStack Spacing="2">
|
||||
@if (!string.IsNullOrWhiteSpace(note.Content))
|
||||
{
|
||||
<MudPaper Elevation="0" Class="pa-3" Style="background-color: var(--mud-palette-background-grey);">
|
||||
@@ -51,40 +89,124 @@
|
||||
Last updated: @note.UpdatedAt.ToString("g") by @(note.LastModifiedBy ?? "Unknown")
|
||||
</MudText>
|
||||
<MudStack Row="true" Spacing="2">
|
||||
<MudButton StartIcon="@Icons.Material.Filled.History"
|
||||
OnClick="() => OpenHistoryDialog(note.Id)"
|
||||
Variant="Variant.Text"
|
||||
Size="Size.Small">
|
||||
History
|
||||
</MudButton>
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Edit"
|
||||
OnClick="() => OpenEditDialog(note)"
|
||||
Variant="Variant.Text"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton StartIcon="@Icons.Material.Outlined.Delete"
|
||||
OnClick="() => DeleteNote(note)"
|
||||
Variant="Variant.Text"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error">
|
||||
Delete
|
||||
</MudButton>
|
||||
@if (!note.IsDeleted)
|
||||
{
|
||||
<MudButton StartIcon="@Icons.Material.Filled.History"
|
||||
OnClick="() => OpenHistoryDialog(note.Id)"
|
||||
Variant="Variant.Text"
|
||||
Size="Size.Small">
|
||||
History
|
||||
</MudButton>
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Edit"
|
||||
OnClick="() => OpenEditDialog(note)"
|
||||
Variant="Variant.Text"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton StartIcon="@Icons.Material.Outlined.Delete"
|
||||
OnClick="() => DeleteNote(note)"
|
||||
Variant="Variant.Text"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error">
|
||||
Delete
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Restore"
|
||||
OnClick="() => RestoreNote(note)"
|
||||
Variant="Variant.Text"
|
||||
Size="Size.Small"
|
||||
Color="Color.Success">
|
||||
Restore
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</ChildContent>
|
||||
</MudExpansionPanel>
|
||||
}
|
||||
</MudExpansionPanels>
|
||||
|
||||
@if (_totalPages > 1 || _currentPageSize != DefaultPageSize)
|
||||
{
|
||||
<MudStack Row="true" Spacing="2" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mt-4">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudText Typo="Typo.body2">Items per page:</MudText>
|
||||
<MudSelect T="int" @bind-Value="CurrentPageSize" Variant="Variant.Outlined" Dense="true" Style="width: 100px;">
|
||||
<MudSelectItem Value="10">10</MudSelectItem>
|
||||
<MudSelectItem Value="25">25</MudSelectItem>
|
||||
<MudSelectItem Value="50">50</MudSelectItem>
|
||||
<MudSelectItem Value="100">100</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudStack>
|
||||
|
||||
@if (_totalPages > 1)
|
||||
{
|
||||
<MudPagination Count="@_totalPages"
|
||||
Selected="@(_currentPage + 1)"
|
||||
SelectedChanged="OnPageChanged"
|
||||
ShowFirstButton="true"
|
||||
ShowLastButton="true" />
|
||||
}
|
||||
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
Showing @_displayStart to @_displayEnd of @_totalNotes
|
||||
</MudText>
|
||||
</MudStack>
|
||||
}
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
// Constants
|
||||
private const int MaxPreviewLength = 60;
|
||||
private const int MaxPinnedNotes = 3;
|
||||
private const int DefaultPageSize = 25;
|
||||
|
||||
private List<Note> _notes = [];
|
||||
private List<Note> _paginatedNotes = [];
|
||||
private bool _isLoading = true;
|
||||
private bool _showRemoved = false;
|
||||
private int _currentPage = 0;
|
||||
private int _currentPageSize = DefaultPageSize;
|
||||
private int _pinnedCount = 0;
|
||||
|
||||
private int CurrentPageSize
|
||||
{
|
||||
get => _currentPageSize;
|
||||
set
|
||||
{
|
||||
if (_currentPageSize != value)
|
||||
{
|
||||
_currentPageSize = value;
|
||||
_currentPage = 0; // Reset to first page when page size changes
|
||||
UpdatePagination();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
private int _totalNotes = 0;
|
||||
private int _totalPages = 0;
|
||||
private int _displayStart = 0;
|
||||
private int _displayEnd = 0;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private bool _isDisposed = false;
|
||||
private int? _selectedNoteId;
|
||||
private Dictionary<int, bool> _expandedNotes = new();
|
||||
|
||||
|
||||
private static DialogOptions GetDefaultDialogOptions() => new()
|
||||
{
|
||||
MaxWidth = MaxWidth.Medium,
|
||||
FullWidth = true,
|
||||
CloseButton = true
|
||||
};
|
||||
|
||||
[SupplyParameterFromQuery]
|
||||
private int? Id { get; set; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
@@ -93,9 +215,34 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_selectedNoteId = Id;
|
||||
await LoadNotes();
|
||||
|
||||
// Clear the query parameter after loading
|
||||
if (Id.HasValue)
|
||||
{
|
||||
NavigationManager.NavigateTo("/notes", replace: true);
|
||||
}
|
||||
}
|
||||
|
||||
private bool GetNoteExpanded(int noteId)
|
||||
{
|
||||
if (_expandedNotes.ContainsKey(noteId))
|
||||
{
|
||||
return _expandedNotes[noteId];
|
||||
}
|
||||
return _selectedNoteId.HasValue && noteId == _selectedNoteId.Value;
|
||||
}
|
||||
|
||||
private void OnPanelExpandedChanged(int noteId, bool expanded)
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
_expandedNotes[noteId] = expanded;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
|
||||
private async Task LoadNotes()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
@@ -104,7 +251,28 @@
|
||||
try
|
||||
{
|
||||
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
||||
_notes = (await NotesService.GetNotesAsync()).ToList();
|
||||
|
||||
if (_showRemoved)
|
||||
{
|
||||
_notes = (await NotesService.GetDeletedNotesAsync()).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
_notes = (await NotesService.GetNotesAsync(false)).ToList();
|
||||
}
|
||||
|
||||
// Update pinned count cache
|
||||
_pinnedCount = _notes.Count(n => n.IsPinned && !n.Title.StartsWith("@") && !n.IsDeleted);
|
||||
|
||||
// Clean up expanded state for notes that no longer exist
|
||||
var existingNoteIds = _notes.Select(n => n.Id).ToHashSet();
|
||||
var notesToRemove = _expandedNotes.Keys.Where(id => !existingNoteIds.Contains(id)).ToList();
|
||||
foreach (var id in notesToRemove)
|
||||
{
|
||||
_expandedNotes.Remove(id);
|
||||
}
|
||||
|
||||
UpdatePagination();
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
@@ -131,6 +299,69 @@
|
||||
}
|
||||
}
|
||||
|
||||
private string GetNoteHeaderText(Note note, bool isExpanded)
|
||||
{
|
||||
// When expanded, only show title (no preview)
|
||||
if (isExpanded || string.IsNullOrWhiteSpace(note.Content))
|
||||
{
|
||||
return note.Title;
|
||||
}
|
||||
|
||||
var preview = GetPreview(note.Content);
|
||||
if (string.IsNullOrWhiteSpace(preview))
|
||||
{
|
||||
return note.Title;
|
||||
}
|
||||
|
||||
return $"{note.Title} — {preview}";
|
||||
}
|
||||
|
||||
private string GetPreview(string? content)
|
||||
{
|
||||
return MarkdownHelper.StripMarkdownPreview(content, MaxPreviewLength);
|
||||
}
|
||||
|
||||
private void UpdatePagination()
|
||||
{
|
||||
_totalNotes = _notes.Count;
|
||||
_totalPages = (int)Math.Ceiling((double)_totalNotes / _currentPageSize);
|
||||
|
||||
// Ensure current page is valid
|
||||
if (_currentPage >= _totalPages && _totalPages > 0)
|
||||
{
|
||||
_currentPage = _totalPages - 1;
|
||||
}
|
||||
else if (_currentPage < 0)
|
||||
{
|
||||
_currentPage = 0;
|
||||
}
|
||||
|
||||
// Calculate paginated notes
|
||||
var startIndex = _currentPage * _currentPageSize;
|
||||
_paginatedNotes = _notes.Skip(startIndex).Take(_currentPageSize).ToList();
|
||||
|
||||
// Calculate display range
|
||||
_displayStart = _totalNotes > 0 ? startIndex + 1 : 0;
|
||||
_displayEnd = Math.Min(startIndex + _currentPageSize, _totalNotes);
|
||||
}
|
||||
|
||||
private void OnPageChanged(int newPage)
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
_currentPage = newPage - 1; // MudPagination is 1-based, we use 0-based
|
||||
UpdatePagination();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task ToggleRemovedFilter()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
_showRemoved = !_showRemoved;
|
||||
await LoadNotes();
|
||||
}
|
||||
|
||||
private async Task OpenCreateDialog()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
@@ -141,14 +372,7 @@
|
||||
["IsEdit"] = false
|
||||
};
|
||||
|
||||
var options = new DialogOptions
|
||||
{
|
||||
MaxWidth = MaxWidth.Large,
|
||||
FullWidth = true,
|
||||
CloseButton = true
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<NoteEditDialog>("Create Note", parameters, options);
|
||||
var dialog = await DialogService.ShowAsync<NoteEditDialog>("Create Note", parameters, GetDefaultDialogOptions());
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (!result.Canceled && !_isDisposed)
|
||||
@@ -169,7 +393,7 @@
|
||||
|
||||
var options = new DialogOptions
|
||||
{
|
||||
MaxWidth = MaxWidth.Large,
|
||||
MaxWidth = MaxWidth.Medium,
|
||||
FullWidth = true,
|
||||
CloseButton = true
|
||||
};
|
||||
@@ -192,14 +416,61 @@
|
||||
["NoteId"] = noteId
|
||||
};
|
||||
|
||||
var options = new DialogOptions
|
||||
{
|
||||
MaxWidth = MaxWidth.Large,
|
||||
FullWidth = true,
|
||||
CloseButton = true
|
||||
};
|
||||
await DialogService.ShowAsync<NoteHistoryDialog>("Note History", parameters, GetDefaultDialogOptions());
|
||||
}
|
||||
|
||||
await DialogService.ShowAsync<NoteHistoryDialog>("Note History", parameters, options);
|
||||
private async Task TogglePin(Note note)
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
try
|
||||
{
|
||||
await NotesService.TogglePinNoteAsync(note.Id);
|
||||
|
||||
if (!_isDisposed)
|
||||
{
|
||||
// Reload to get updated state before showing message
|
||||
await LoadNotes();
|
||||
var updatedNote = _notes.FirstOrDefault(n => n.Id == note.Id);
|
||||
if (updatedNote != null)
|
||||
{
|
||||
Snackbar.Add($"Note '{updatedNote.Title}' {(updatedNote.IsPinned ? "pinned" : "unpinned")}", Severity.Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Component was disposed, ignore
|
||||
}
|
||||
catch (JSDisconnectedException)
|
||||
{
|
||||
// JS connection lost, ignore
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
Snackbar.Add($"Error toggling pin: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPinDisabled(Note note)
|
||||
{
|
||||
// Can always unpin
|
||||
if (note.IsPinned)
|
||||
return false;
|
||||
|
||||
// Can't pin page notes
|
||||
if (note.Title.StartsWith("@"))
|
||||
return true;
|
||||
|
||||
// Can't pin deleted notes
|
||||
if (note.IsDeleted)
|
||||
return true;
|
||||
|
||||
// Check if already at max pinned notes (using cached count)
|
||||
return _pinnedCount >= MaxPinnedNotes;
|
||||
}
|
||||
|
||||
private async Task DeleteNote(Note note)
|
||||
@@ -246,6 +517,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RestoreNote(Note note)
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
try
|
||||
{
|
||||
await NotesService.RestoreNoteAsync(note.Id);
|
||||
|
||||
if (!_isDisposed)
|
||||
{
|
||||
Snackbar.Add($"Note '{note.Title}' restored", Severity.Success);
|
||||
await LoadNotes();
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Component was disposed, ignore
|
||||
}
|
||||
catch (JSDisconnectedException)
|
||||
{
|
||||
// JS connection lost, ignore
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
Snackbar.Add($"Error restoring note: {ex.Message}", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
|
||||
Reference in New Issue
Block a user