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:
2026-01-16 23:12:18 -05:00
parent 5f2d7b5b31
commit 8b0451c2ec
16 changed files with 1261 additions and 56 deletions
+143 -7
View File
@@ -30,11 +30,20 @@ public class NotesService : INotesService
return user.FindFirstValue(ClaimTypes.Email) ?? user.Identity?.Name;
}
public async Task<IEnumerable<Note>> GetNotesAsync()
public async Task<IEnumerable<Note>> GetNotesAsync(bool includeDeleted = false)
{
return await _context.Notes
var query = _context.Notes
.AsNoTracking()
.OrderByDescending(n => n.UpdatedAt)
.AsQueryable();
if (!includeDeleted)
{
query = query.Where(n => !n.IsDeleted);
}
return await query
.OrderBy(n => n.Title.StartsWith("@") ? 1 : 0) // Non-page notes first (0), page notes last (1)
.ThenByDescending(n => n.UpdatedAt) // Within each group, order by most recently updated
.ToListAsync();
}
@@ -45,6 +54,15 @@ public class NotesService : INotesService
.FirstOrDefaultAsync(n => n.Id == id);
}
public async Task<Note?> GetPageNoteAsync(string pageIdentifier)
{
var pageNoteTitle = $"@{pageIdentifier}";
return await _context.Notes
.AsNoTracking()
.Where(n => n.Title == pageNoteTitle && !n.IsDeleted)
.FirstOrDefaultAsync();
}
public async Task<IEnumerable<NoteHistory>> GetNoteHistoryAsync(int noteId)
{
return await _context.NoteHistories
@@ -147,16 +165,134 @@ public class NotesService : INotesService
Content = note.Content,
ModifiedBy = userEmail,
ModifiedAt = now,
ChangeType = "Deleted"
ChangeType = "Soft Deleted"
};
_context.NoteHistories.Add(history);
// Delete the note (cascade will handle history, but we want to keep history)
_context.Notes.Remove(note);
// Soft delete - set IsDeleted flag instead of removing
note.IsDeleted = true;
note.UpdatedAt = now;
note.LastModifiedBy = userEmail;
await _context.SaveChangesAsync();
_logger.LogInformation("Note deleted: {NoteId} by {User}", id, userEmail);
_logger.LogInformation("Note soft deleted: {NoteId} by {User}", id, userEmail);
}
public async Task<IEnumerable<Note>> GetPinnedNotesAsync()
{
return await _context.Notes
.AsNoTracking()
.Where(n => n.IsPinned && !n.Title.StartsWith("@") && !n.IsDeleted)
.OrderByDescending(n => n.UpdatedAt)
.Take(3)
.ToListAsync();
}
public async Task TogglePinNoteAsync(int noteId)
{
var note = await _context.Notes
.FirstOrDefaultAsync(n => n.Id == noteId);
if (note == null)
{
throw new InvalidOperationException($"Note with ID {noteId} not found.");
}
// Prevent pinning page notes
if (note.Title.StartsWith("@"))
{
throw new InvalidOperationException("Page notes cannot be pinned.");
}
var userEmail = GetCurrentUserEmail();
var now = DateTime.UtcNow;
// If pinning and already 3 pinned notes exist (excluding this note if it's already pinned)
if (!note.IsPinned)
{
var pinnedCount = await _context.Notes
.CountAsync(n => n.IsPinned && !n.Title.StartsWith("@") && !n.IsDeleted);
if (pinnedCount >= 3)
{
// Unpin the oldest pinned note
var oldestPinned = await _context.Notes
.Where(n => n.IsPinned && !n.Title.StartsWith("@") && !n.IsDeleted)
.OrderBy(n => n.UpdatedAt)
.FirstOrDefaultAsync();
if (oldestPinned != null)
{
oldestPinned.IsPinned = false;
oldestPinned.UpdatedAt = now;
oldestPinned.LastModifiedBy = userEmail;
_logger.LogInformation("Auto-unpinned note {NoteId} (oldest) when pinning note {NewNoteId} by {User}",
oldestPinned.Id, noteId, userEmail);
}
}
}
// Toggle the pin status
note.IsPinned = !note.IsPinned;
note.UpdatedAt = now;
note.LastModifiedBy = userEmail;
await _context.SaveChangesAsync();
_logger.LogInformation("Note {NoteId} {Action} by {User}", noteId,
note.IsPinned ? "pinned" : "unpinned", userEmail);
}
public async Task<IEnumerable<Note>> GetDeletedNotesAsync()
{
return await _context.Notes
.AsNoTracking()
.Where(n => n.IsDeleted)
.OrderBy(n => n.Title.StartsWith("@") ? 1 : 0) // Non-page notes first (0), page notes last (1)
.ThenByDescending(n => n.UpdatedAt) // Within each group, order by most recently updated
.ToListAsync();
}
public async Task RestoreNoteAsync(int noteId)
{
var note = await _context.Notes
.FirstOrDefaultAsync(n => n.Id == noteId);
if (note == null)
{
throw new InvalidOperationException($"Note with ID {noteId} not found.");
}
if (!note.IsDeleted)
{
throw new InvalidOperationException($"Note with ID {noteId} is not deleted.");
}
var userEmail = GetCurrentUserEmail();
var now = DateTime.UtcNow;
// Create restore history entry
var history = new NoteHistory
{
NoteId = note.Id,
Title = note.Title,
Content = note.Content,
ModifiedBy = userEmail,
ModifiedAt = now,
ChangeType = "Restored"
};
_context.NoteHistories.Add(history);
// Restore the note
note.IsDeleted = false;
note.UpdatedAt = now;
note.LastModifiedBy = userEmail;
await _context.SaveChangesAsync();
_logger.LogInformation("Note restored: {NoteId} by {User}", noteId, userEmail);
}
}