Dashboard: show 10 chapters titled Chapters; bump chapter mtime on beat changes
Beat create/update/delete/reorder/assign/move now touch the parent chapter's UpdatedAt so the dashboard's recency sort reflects beat edits, not just chapter-level prose/tag changes.
This commit is contained in:
@@ -116,6 +116,8 @@ public class BeatService(
|
|||||||
beat.Tags = await tags.ResolveAsync(chapter.ProjectId, names, ct);
|
beat.Tags = await tags.ResolveAsync(chapter.ProjectId, names, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chapter.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
db.Beats.Add(beat);
|
db.Beats.Add(beat);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
|
|
||||||
@@ -150,6 +152,7 @@ public class BeatService(
|
|||||||
beat.WhatHappened = Patch.Apply(beat.WhatHappened, request.WhatHappened);
|
beat.WhatHappened = Patch.Apply(beat.WhatHappened, request.WhatHappened);
|
||||||
beat.WhatsNext = Patch.Apply(beat.WhatsNext, request.WhatsNext);
|
beat.WhatsNext = Patch.Apply(beat.WhatsNext, request.WhatsNext);
|
||||||
beat.UpdatedAt = DateTimeOffset.UtcNow;
|
beat.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
chapter.UpdatedAt = beat.UpdatedAt;
|
||||||
|
|
||||||
if (request.CharacterIds is { } characterIds)
|
if (request.CharacterIds is { } characterIds)
|
||||||
{
|
{
|
||||||
@@ -179,6 +182,12 @@ public class BeatService(
|
|||||||
|
|
||||||
await RequireBeatAccessAsync(beat, ProjectPermission.DeleteContent, ct);
|
await RequireBeatAccessAsync(beat, ProjectPermission.DeleteContent, ct);
|
||||||
|
|
||||||
|
var chapter = await db.Chapters.FirstOrDefaultAsync(c => c.Id == beat.ChapterId, ct);
|
||||||
|
if (chapter is not null)
|
||||||
|
{
|
||||||
|
chapter.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
db.Beats.Remove(beat);
|
db.Beats.Remove(beat);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
return true;
|
return true;
|
||||||
@@ -215,6 +224,12 @@ public class BeatService(
|
|||||||
beat.SortOrder = order++;
|
beat.SortOrder = order++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var chapter = await db.Chapters.FirstOrDefaultAsync(c => c.Id == chapterId, ct);
|
||||||
|
if (chapter is not null)
|
||||||
|
{
|
||||||
|
chapter.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
return await ListAsync(chapterId, ct);
|
return await ListAsync(chapterId, ct);
|
||||||
}
|
}
|
||||||
@@ -262,6 +277,7 @@ public class BeatService(
|
|||||||
{
|
{
|
||||||
beat.Characters.Add(character);
|
beat.Characters.Add(character);
|
||||||
beat.UpdatedAt = DateTimeOffset.UtcNow;
|
beat.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
chapter.UpdatedAt = beat.UpdatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
@@ -312,14 +328,18 @@ public class BeatService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var nextSortOrder = await NextSortOrderAsync(request.TargetChapterId, ct);
|
var nextSortOrder = await NextSortOrderAsync(request.TargetChapterId, ct);
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
foreach (var beatId in request.BeatIds)
|
foreach (var beatId in request.BeatIds)
|
||||||
{
|
{
|
||||||
var beat = beats.Single(b => b.Id == beatId);
|
var beat = beats.Single(b => b.Id == beatId);
|
||||||
beat.ChapterId = request.TargetChapterId;
|
beat.ChapterId = request.TargetChapterId;
|
||||||
beat.SortOrder = nextSortOrder++;
|
beat.SortOrder = nextSortOrder++;
|
||||||
beat.UpdatedAt = DateTimeOffset.UtcNow;
|
beat.UpdatedAt = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chapter.UpdatedAt = now;
|
||||||
|
targetChapter.UpdatedAt = now;
|
||||||
|
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
return beats;
|
return beats;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useAuth } from '../auth/AuthContext'
|
|||||||
import { AutoField, EmptyState, ErrorNote, Spinner, StatusBadge } from '../components/ui'
|
import { AutoField, EmptyState, ErrorNote, Spinner, StatusBadge } from '../components/ui'
|
||||||
|
|
||||||
const RECENT_COUNT = 5
|
const RECENT_COUNT = 5
|
||||||
|
const RECENT_CHAPTERS_COUNT = 10
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { projectId = '' } = useParams()
|
const { projectId = '' } = useParams()
|
||||||
@@ -61,7 +62,7 @@ function OutliningDashboard({ projectId }: { projectId: string }) {
|
|||||||
<div className="grid gap-6 lg:grid-cols-3">
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
<section className="card p-5 lg:col-span-2">
|
<section className="card p-5 lg:col-span-2">
|
||||||
<div className="mb-4 flex items-center justify-between gap-4">
|
<div className="mb-4 flex items-center justify-between gap-4">
|
||||||
<h2 className="text-lg font-semibold">Outline</h2>
|
<h2 className="text-lg font-semibold">Chapters</h2>
|
||||||
<Link to="chapters" className="text-sm muted hover:underline">
|
<Link to="chapters" className="text-sm muted hover:underline">
|
||||||
View all →
|
View all →
|
||||||
</Link>
|
</Link>
|
||||||
@@ -74,7 +75,7 @@ function OutliningDashboard({ projectId }: { projectId: string }) {
|
|||||||
<EmptyState title="No chapters yet" hint="Add the first chapter to start outlining." />
|
<EmptyState title="No chapters yet" hint="Add the first chapter to start outlining." />
|
||||||
) : (
|
) : (
|
||||||
<ul className="grid grid-cols-1 gap-2">
|
<ul className="grid grid-cols-1 gap-2">
|
||||||
{recentChapters.slice(0, RECENT_COUNT).map((chapter) => (
|
{recentChapters.slice(0, RECENT_CHAPTERS_COUNT).map((chapter) => (
|
||||||
<li key={chapter.id}>
|
<li key={chapter.id}>
|
||||||
<Link
|
<Link
|
||||||
to={`chapters/${chapter.id}`}
|
to={`chapters/${chapter.id}`}
|
||||||
|
|||||||
@@ -35,6 +35,31 @@ public class BeatServiceTests : ServiceTestFixture
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Adding_a_beat_bumps_the_chapters_last_modified_date()
|
||||||
|
{
|
||||||
|
var updatedAtBefore = (await Chapters.GetAsync(_chapterId))!.UpdatedAt;
|
||||||
|
|
||||||
|
await Task.Delay(10);
|
||||||
|
await Beats.CreateAsync(_chapterId, new CreateBeatRequest("She finds the map"));
|
||||||
|
|
||||||
|
var updatedAtAfter = (await Chapters.GetAsync(_chapterId))!.UpdatedAt;
|
||||||
|
Assert.That(updatedAtAfter, Is.GreaterThan(updatedAtBefore));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Editing_a_beats_text_bumps_the_chapters_last_modified_date()
|
||||||
|
{
|
||||||
|
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("She finds the map"));
|
||||||
|
var updatedAtBefore = (await Chapters.GetAsync(_chapterId))!.UpdatedAt;
|
||||||
|
|
||||||
|
await Task.Delay(10);
|
||||||
|
await Beats.UpdateAsync(beat!.Id, new UpdateBeatRequest(WhatHappened: "She reads it by lamplight"));
|
||||||
|
|
||||||
|
var updatedAtAfter = (await Chapters.GetAsync(_chapterId))!.UpdatedAt;
|
||||||
|
Assert.That(updatedAtAfter, Is.GreaterThan(updatedAtBefore));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Reordering_renumbers_to_match_the_order_given()
|
public async Task Reordering_renumbers_to_match_the_order_given()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user