8b0451c2ec
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.
299 lines
8.9 KiB
C#
299 lines
8.9 KiB
C#
using Core.Entities;
|
|
using Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Security.Claims;
|
|
|
|
namespace WebApp.Services;
|
|
|
|
public class NotesService : INotesService
|
|
{
|
|
private readonly AppDbContext _context;
|
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
private readonly ILogger<NotesService> _logger;
|
|
|
|
public NotesService(
|
|
AppDbContext context,
|
|
IHttpContextAccessor httpContextAccessor,
|
|
ILogger<NotesService> logger)
|
|
{
|
|
_context = context;
|
|
_httpContextAccessor = httpContextAccessor;
|
|
_logger = logger;
|
|
}
|
|
|
|
private string? GetCurrentUserEmail()
|
|
{
|
|
var user = _httpContextAccessor.HttpContext?.User;
|
|
if (user == null)
|
|
return null;
|
|
|
|
return user.FindFirstValue(ClaimTypes.Email) ?? user.Identity?.Name;
|
|
}
|
|
|
|
public async Task<IEnumerable<Note>> GetNotesAsync(bool includeDeleted = false)
|
|
{
|
|
var query = _context.Notes
|
|
.AsNoTracking()
|
|
.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();
|
|
}
|
|
|
|
public async Task<Note?> GetNoteAsync(int id)
|
|
{
|
|
return await _context.Notes
|
|
.Include(n => n.NoteHistories.OrderByDescending(h => h.ModifiedAt))
|
|
.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
|
|
.AsNoTracking()
|
|
.Where(h => h.NoteId == noteId)
|
|
.OrderByDescending(h => h.ModifiedAt)
|
|
.Take(10)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<Note> CreateNoteAsync(Note note)
|
|
{
|
|
var userEmail = GetCurrentUserEmail();
|
|
var now = DateTime.UtcNow;
|
|
|
|
note.CreatedAt = now;
|
|
note.UpdatedAt = now;
|
|
note.CreatedBy = userEmail;
|
|
note.LastModifiedBy = userEmail;
|
|
|
|
_context.Notes.Add(note);
|
|
await _context.SaveChangesAsync();
|
|
|
|
// Create initial history entry
|
|
var history = new NoteHistory
|
|
{
|
|
NoteId = note.Id,
|
|
Title = note.Title,
|
|
Content = note.Content,
|
|
ModifiedBy = userEmail,
|
|
ModifiedAt = now,
|
|
ChangeType = "Created"
|
|
};
|
|
|
|
_context.NoteHistories.Add(history);
|
|
await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Note created: {NoteId} by {User}", note.Id, userEmail);
|
|
|
|
return note;
|
|
}
|
|
|
|
public async Task<Note> UpdateNoteAsync(Note note)
|
|
{
|
|
var existingNote = await _context.Notes
|
|
.FirstOrDefaultAsync(n => n.Id == note.Id);
|
|
|
|
if (existingNote == null)
|
|
{
|
|
throw new InvalidOperationException($"Note with ID {note.Id} not found.");
|
|
}
|
|
|
|
var userEmail = GetCurrentUserEmail();
|
|
var now = DateTime.UtcNow;
|
|
|
|
// Create history entry with previous state before updating
|
|
var history = new NoteHistory
|
|
{
|
|
NoteId = existingNote.Id,
|
|
Title = existingNote.Title,
|
|
Content = existingNote.Content,
|
|
ModifiedBy = userEmail,
|
|
ModifiedAt = now,
|
|
ChangeType = "Updated"
|
|
};
|
|
|
|
_context.NoteHistories.Add(history);
|
|
|
|
// Update the note
|
|
existingNote.Title = note.Title;
|
|
existingNote.Content = note.Content;
|
|
existingNote.UpdatedAt = now;
|
|
existingNote.LastModifiedBy = userEmail;
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
_logger.LogInformation("Note updated: {NoteId} by {User}", note.Id, userEmail);
|
|
|
|
return existingNote;
|
|
}
|
|
|
|
public async Task DeleteNoteAsync(int id)
|
|
{
|
|
var note = await _context.Notes
|
|
.FirstOrDefaultAsync(n => n.Id == id);
|
|
|
|
if (note == null)
|
|
{
|
|
throw new InvalidOperationException($"Note with ID {id} not found.");
|
|
}
|
|
|
|
var userEmail = GetCurrentUserEmail();
|
|
var now = DateTime.UtcNow;
|
|
|
|
// Create deletion history entry
|
|
var history = new NoteHistory
|
|
{
|
|
NoteId = note.Id,
|
|
Title = note.Title,
|
|
Content = note.Content,
|
|
ModifiedBy = userEmail,
|
|
ModifiedAt = now,
|
|
ChangeType = "Soft Deleted"
|
|
};
|
|
|
|
_context.NoteHistories.Add(history);
|
|
|
|
// Soft delete - set IsDeleted flag instead of removing
|
|
note.IsDeleted = true;
|
|
note.UpdatedAt = now;
|
|
note.LastModifiedBy = userEmail;
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
_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);
|
|
}
|
|
}
|