Replace chapter setting with multi-select locations; add chapter character summary
Chapters now carry many Locations (new Tags-style entity with cross-referencing) instead of a single free-text Setting field, with a Locations tab on the novel for browsing them and seeing every chapter set at each one. Also surfaces the distinct characters appearing in a chapter's beats, linked, under the beat/word count on the outline tab.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using Novelly.Api.Beats;
|
||||
using Novelly.Api.Common;
|
||||
using Novelly.Api.Common.Validation;
|
||||
using Novelly.Api.Locations;
|
||||
using Novelly.Api.Tags;
|
||||
|
||||
namespace Novelly.Api.Chapters;
|
||||
@@ -11,7 +12,7 @@ public record ChapterSummaryResponse(
|
||||
int Number,
|
||||
string Title,
|
||||
string? Summary,
|
||||
string? Setting,
|
||||
IReadOnlyList<LocationResponse> Locations,
|
||||
DraftStatus Status,
|
||||
int? TargetWordCount,
|
||||
int BeatCount,
|
||||
@@ -25,7 +26,7 @@ public record ChapterResponse(
|
||||
int Number,
|
||||
string Title,
|
||||
string? Summary,
|
||||
string? Setting,
|
||||
IReadOnlyList<LocationResponse> Locations,
|
||||
string? Notes,
|
||||
DraftStatus Status,
|
||||
int? TargetWordCount,
|
||||
@@ -39,7 +40,7 @@ public record CreateChapterRequest(
|
||||
string Title,
|
||||
int? Number = null,
|
||||
string? Summary = null,
|
||||
string? Setting = null,
|
||||
IReadOnlyList<string>? Locations = null,
|
||||
string? Notes = null,
|
||||
DraftStatus Status = DraftStatus.Planned,
|
||||
int? TargetWordCount = null,
|
||||
@@ -53,7 +54,7 @@ public class CreateChapterRequestValidator : IModelValidator<CreateChapterReques
|
||||
var result = new ValidationResult();
|
||||
|
||||
result.AddRequiredTextErrors("Title", "Title", model.Title, 200);
|
||||
ChapterValidation.OptionalFields(model.Number, model.Summary, model.Setting, model.Notes, model.TargetWordCount, model.Prose, model.Tags, result);
|
||||
ChapterValidation.OptionalFields(model.Number, model.Summary, model.Locations, model.Notes, model.TargetWordCount, model.Prose, model.Tags, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -63,7 +64,7 @@ public record UpdateChapterRequest(
|
||||
string? Title = null,
|
||||
int? Number = null,
|
||||
string? Summary = null,
|
||||
string? Setting = null,
|
||||
IReadOnlyList<string>? Locations = null,
|
||||
string? Notes = null,
|
||||
DraftStatus? Status = null,
|
||||
int? TargetWordCount = null,
|
||||
@@ -77,7 +78,7 @@ public class UpdateChapterRequestValidator : IModelValidator<UpdateChapterReques
|
||||
var result = new ValidationResult();
|
||||
|
||||
result.AddUnclearableTextErrors("Title", "Title", model.Title, "a chapter", 200);
|
||||
ChapterValidation.OptionalFields(model.Number, model.Summary, model.Setting, model.Notes, model.TargetWordCount, model.Prose, model.Tags, result);
|
||||
ChapterValidation.OptionalFields(model.Number, model.Summary, model.Locations, model.Notes, model.TargetWordCount, model.Prose, model.Tags, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -86,7 +87,7 @@ public class UpdateChapterRequestValidator : IModelValidator<UpdateChapterReques
|
||||
file static class ChapterValidation
|
||||
{
|
||||
public static void OptionalFields(
|
||||
int? number, string? summary, string? setting, string? notes, int? targetWordCount, string? prose,
|
||||
int? number, string? summary, IReadOnlyList<string>? locations, string? notes, int? targetWordCount, string? prose,
|
||||
IReadOnlyList<string>? tags, ValidationResult result)
|
||||
{
|
||||
if (number is <= 0)
|
||||
@@ -95,8 +96,8 @@ file static class ChapterValidation
|
||||
if (summary is { Length: > 20000 })
|
||||
result.AddError("Summary", "'Summary' must be 20,000 characters or fewer.");
|
||||
|
||||
if (setting is { Length: > 500 })
|
||||
result.AddError("Setting", "'Setting' must be 500 characters or fewer.");
|
||||
if (locations is not null && locations.Any(string.IsNullOrWhiteSpace))
|
||||
result.AddError("Locations", "'Locations' must not contain blank entries.");
|
||||
|
||||
if (notes is { Length: > 20000 })
|
||||
result.AddError("Notes", "'Notes' must be 20,000 characters or fewer.");
|
||||
@@ -116,7 +117,8 @@ public static class ChapterMapping
|
||||
{
|
||||
public static ChapterResponse ToResponse(this Chapter c) => new(
|
||||
c.Id, c.NovelId, c.Number, c.Title, c.Summary,
|
||||
c.Setting, c.Notes,
|
||||
[.. c.Locations.OrderBy(l => l.Name).Select(l => l.ToResponse())],
|
||||
c.Notes,
|
||||
c.Status, c.TargetWordCount,
|
||||
[.. c.Beats.OrderBy(b => b.SortOrder).Select(b => b.ToResponse())],
|
||||
c.Prose, c.WordCount,
|
||||
@@ -125,7 +127,8 @@ public static class ChapterMapping
|
||||
|
||||
public static ChapterSummaryResponse ToSummaryResponse(this Chapter c) => new(
|
||||
c.Id, c.NovelId, c.Number, c.Title, c.Summary,
|
||||
c.Setting, c.Status, c.TargetWordCount,
|
||||
[.. c.Locations.OrderBy(l => l.Name).Select(l => l.ToResponse())],
|
||||
c.Status, c.TargetWordCount,
|
||||
c.Beats.Count, c.WordCount,
|
||||
[.. c.Tags.OrderBy(t => t.Name).Select(t => t.ToResponse())],
|
||||
c.UpdatedAt);
|
||||
|
||||
Reference in New Issue
Block a user