Add soft delete + trash, keyboard-first web overhaul, move chapter tags to bottom
CI / build-and-push (push) Failing after 31s
CI / deploy (push) Has been skipped

Adds SoftDelete/Trash across characters, chapters, locations, beats with a
purge schedule and Trash page. Reworks the web client for keyboard-driven
navigation (focus helpers, help overlay, keyboard.md doc). Moves the
ChapterPage tag editor to the bottom of the page to match CharacterDetailPage.
This commit is contained in:
James Wampler
2026-08-20 16:39:09 -07:00
parent 7df1fffdca
commit aca26588f9
59 changed files with 2913 additions and 170 deletions
+6 -1
View File
@@ -2,12 +2,13 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Novelly.Api.Beats;
using Novelly.Api.Chapters;
using Novelly.Api.Common;
using Novelly.Api.Novels;
using Novelly.Api.Tags;
namespace Novelly.Api.Characters;
public class Character
public class Character : ISoftDeletable
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid NovelId { get; set; }
@@ -46,6 +47,7 @@ public class Character
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? DeletedAt { get; set; }
public List<CharacterRelationship> Relationships { get; set; } = [];
public List<Tag> Tags { get; set; } = [];
@@ -79,6 +81,7 @@ public class CharacterEntityTypeConfiguration : IEntityTypeConfiguration<Charact
entity.Property(c => c.Importance).HasConversion<string>().HasMaxLength(32);
entity.HasIndex(c => c.NovelId);
entity.HasIndex(c => c.SameCharacterAsId);
entity.HasQueryFilter(c => c.DeletedAt == null);
entity.HasMany(c => c.Relationships).WithOne(r => r.Character!)
.HasForeignKey(r => r.CharacterId).OnDelete(DeleteBehavior.Cascade);
@@ -102,5 +105,7 @@ public class CharacterRelationshipEntityTypeConfiguration : IEntityTypeConfigura
entity.HasOne(r => r.RelatedCharacter).WithMany()
.HasForeignKey(r => r.RelatedCharacterId).OnDelete(DeleteBehavior.Restrict);
entity.HasQueryFilter(r => r.Character!.DeletedAt == null && r.RelatedCharacter!.DeletedAt == null);
}
}