b4c11cd0a6
This commit adds MudTooltip components to action buttons in the Calendar, Events, Students, and Teams features, improving user experience by providing contextual information on button actions. The changes ensure that users receive helpful hints when hovering over buttons, enhancing accessibility and usability throughout the application.
487 lines
17 KiB
Plaintext
487 lines
17 KiB
Plaintext
@page "/notes"
|
|
@attribute [Authorize]
|
|
@implements IAsyncDisposable
|
|
@using Core.Entities
|
|
@using WebApp.Services
|
|
@using WebApp.Components.Shared.Components
|
|
@inject INotesService NotesService
|
|
@inject IDialogService DialogService
|
|
@inject ISnackbar Snackbar
|
|
@inject NavigationManager NavigationManager
|
|
|
|
<PageHeader Title="Notes">
|
|
<ActionButtons>
|
|
<MudTooltip Text="@(_showRemoved ? "Show Active" : "Show Removed")">
|
|
<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>
|
|
</MudTooltip>
|
|
<MudTooltip Text="Create Note">
|
|
<MudButton StartIcon="@Icons.Material.Filled.Add"
|
|
OnClick="OpenCreateDialog"
|
|
Variant="Variant.Filled"
|
|
Color="Color.Primary">
|
|
Create Note
|
|
</MudButton>
|
|
</MudTooltip>
|
|
</ActionButtons>
|
|
</PageHeader>
|
|
|
|
<MudPaper Elevation="2" Class="pa-3 pa-md-6">
|
|
@if (_isLoading)
|
|
{
|
|
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-4" />
|
|
}
|
|
else if (!_notes.Any())
|
|
{
|
|
<MudText Typo="Typo.h6" Align="Align.Center" Class="my-8">
|
|
No notes yet. Create your first note to get started!
|
|
</MudText>
|
|
}
|
|
else
|
|
{
|
|
<ClientSidePagination T="Note" Items="@_notes" DefaultPageSize="@DefaultPageSize">
|
|
<ChildContent Context="paginatedNotes">
|
|
<MudExpansionPanels MultiExpansion="true">
|
|
@foreach (var note in paginatedNotes)
|
|
{
|
|
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))"
|
|
Class="@MarkdownHelper.GetNoteColorClass(noteId)">
|
|
<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);">
|
|
<div class="markdown-content">
|
|
@((MarkupString)MarkdownHelper.ToHtml(note.Content))
|
|
</div>
|
|
</MudPaper>
|
|
}
|
|
<MudStack Row="true" Spacing="2" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
|
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
|
Last updated: @note.UpdatedAt.ToString("g") by @(note.LastModifiedBy ?? "Unknown")
|
|
</MudText>
|
|
<MudStack Row="true" Spacing="2">
|
|
@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>
|
|
</ChildContent>
|
|
</ClientSidePagination>
|
|
}
|
|
</MudPaper>
|
|
|
|
@code {
|
|
// Constants
|
|
private const int MaxPreviewLength = 60;
|
|
private const int MaxPinnedNotes = 3;
|
|
private const int DefaultPageSize = 25;
|
|
|
|
private List<Note> _notes = [];
|
|
private bool _isLoading = true;
|
|
private bool _showRemoved = false;
|
|
private int _pinnedCount = 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()
|
|
{
|
|
_cancellationTokenSource = new CancellationTokenSource();
|
|
}
|
|
|
|
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;
|
|
|
|
_isLoading = true;
|
|
try
|
|
{
|
|
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
|
|
|
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);
|
|
}
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
// Component was disposed, ignore
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
// JS connection lost, ignore
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_isDisposed)
|
|
{
|
|
Snackbar.Add($"Error loading notes: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (!_isDisposed)
|
|
{
|
|
_isLoading = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
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 async Task ToggleRemovedFilter()
|
|
{
|
|
if (_isDisposed) return;
|
|
|
|
_showRemoved = !_showRemoved;
|
|
await LoadNotes();
|
|
}
|
|
|
|
private async Task OpenCreateDialog()
|
|
{
|
|
if (_isDisposed) return;
|
|
|
|
var parameters = new DialogParameters
|
|
{
|
|
["Note"] = new Note { Title = "", Content = "" },
|
|
["IsEdit"] = false
|
|
};
|
|
|
|
var dialog = await DialogService.ShowAsync<NoteEditDialog>("Create Note", parameters, GetDefaultDialogOptions());
|
|
var result = await dialog.Result;
|
|
|
|
if (!result.Canceled && !_isDisposed)
|
|
{
|
|
await LoadNotes();
|
|
}
|
|
}
|
|
|
|
private async Task OpenEditDialog(Note note)
|
|
{
|
|
if (_isDisposed) return;
|
|
|
|
var parameters = new DialogParameters
|
|
{
|
|
["Note"] = note,
|
|
["IsEdit"] = true
|
|
};
|
|
|
|
var options = new DialogOptions
|
|
{
|
|
MaxWidth = MaxWidth.Medium,
|
|
FullWidth = true,
|
|
CloseButton = true
|
|
};
|
|
|
|
var dialog = await DialogService.ShowAsync<NoteEditDialog>("Edit Note", parameters, options);
|
|
var result = await dialog.Result;
|
|
|
|
if (!result.Canceled && !_isDisposed)
|
|
{
|
|
await LoadNotes();
|
|
}
|
|
}
|
|
|
|
private async Task OpenHistoryDialog(int noteId)
|
|
{
|
|
if (_isDisposed) return;
|
|
|
|
var parameters = new DialogParameters
|
|
{
|
|
["NoteId"] = noteId
|
|
};
|
|
|
|
await DialogService.ShowAsync<NoteHistoryDialog>("Note History", parameters, GetDefaultDialogOptions());
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (_isDisposed) return;
|
|
|
|
try
|
|
{
|
|
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
|
|
|
var result = await DialogService.ShowMessageBox(
|
|
"Delete Note",
|
|
(MarkupString)$"Are you sure you want to delete <b>{note.Title}</b>? You can restore it later from the 'Show Removed' view.",
|
|
yesText: "Yes",
|
|
noText: "Cancel");
|
|
|
|
if (_isDisposed) return;
|
|
|
|
if (result == true)
|
|
{
|
|
await NotesService.DeleteNoteAsync(note.Id);
|
|
|
|
if (!_isDisposed)
|
|
{
|
|
Snackbar.Add($"Note '{note.Title}' deleted", Severity.Info);
|
|
await LoadNotes();
|
|
}
|
|
}
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
// Component was disposed, ignore
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
// JS connection lost, ignore
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_isDisposed)
|
|
{
|
|
Snackbar.Add($"Error deleting note: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
_isDisposed = true;
|
|
_cancellationTokenSource?.Cancel();
|
|
_cancellationTokenSource?.Dispose();
|
|
_cancellationTokenSource = null;
|
|
}
|
|
await ValueTask.CompletedTask;
|
|
}
|
|
}
|