Add character aliases/identity links, move-beats, keyboard help overlay
- Characters can carry aliases and be linked as the same underlying person (canonical SameCharacterAsId, optional reveal chapter/note), surfaced through the API, MCP tools, agent toolset, and web UI. - Characters page redesigned as a filterable/sortable table (name+ aliases, role, importance, occupation, tags) instead of a sidebar list, to stay usable as the cast grows. - Beats can be moved between chapters (BeatService.MoveAsync + MCP/ agent tool + endpoint). - Add a keyboard-shortcuts help overlay (HelpButton/HelpOverlayContext) wired into the project layout. - CLAUDE.md: require every frontend component to carry a unique id attribute; apply it to CharacterMultiSelect and MarkdownEditor.
This commit is contained in:
@@ -162,6 +162,56 @@ public class BeatServiceTests : ServiceTestFixture
|
||||
Assert.That((await Beats.GetAsync(beat.Id))!.Characters, Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Moving_beats_appends_them_to_the_end_of_the_target_chapter()
|
||||
{
|
||||
var other = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Second landfall"));
|
||||
await Beats.CreateAsync(other.Id, new CreateBeatRequest("Already there"));
|
||||
var first = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
|
||||
var second = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("Second"));
|
||||
|
||||
var moved = await Beats.MoveAsync(_chapterId, new MoveBeatsRequest(other.Id, [second.Id, first.Id]));
|
||||
|
||||
var listed = await Beats.ListAsync(other.Id);
|
||||
var remaining = await Beats.ListAsync(_chapterId);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(moved!.Select(b => b.ChapterId), Is.All.EqualTo(other.Id));
|
||||
Assert.That(listed.Select(b => b.Title), Is.EqualTo(new[] { "Already there", "Second", "First" }));
|
||||
Assert.That(remaining, Is.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Moving_to_an_unknown_chapter_returns_null()
|
||||
{
|
||||
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
|
||||
|
||||
Assert.That(await Beats.MoveAsync(_chapterId, new MoveBeatsRequest(Guid.NewGuid(), [beat.Id])), Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Moving_to_a_chapter_in_another_project_returns_null()
|
||||
{
|
||||
var other = await Projects.CreateAsync(new CreateProjectRequest("Other Book"));
|
||||
var otherChapter = await Chapters.CreateAsync(other.Id, new CreateChapterRequest("Elsewhere"));
|
||||
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
|
||||
|
||||
Assert.That(await Beats.MoveAsync(_chapterId, new MoveBeatsRequest(otherChapter.Id, [beat.Id])), Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Moving_an_unknown_beat_returns_null_rather_than_partially_applying()
|
||||
{
|
||||
var other = await Chapters.CreateAsync(_projectId, new CreateChapterRequest("Second landfall"));
|
||||
var beat = await Beats.CreateAsync(_chapterId, new CreateBeatRequest("First"));
|
||||
|
||||
var result = await Beats.MoveAsync(_chapterId, new MoveBeatsRequest(other.Id, [beat.Id, Guid.NewGuid()]));
|
||||
|
||||
Assert.That(result, Is.Null);
|
||||
Assert.That((await Beats.GetAsync(beat.Id))!.ChapterId, Is.EqualTo(_chapterId));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Updating_leaves_omitted_fields_alone_and_clears_on_empty_string()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user