PlotDirector Phase 17H - Character Knowledge Editor Migration

This commit is contained in:
Nick Beckley 2026-06-30 15:16:13 +01:00
parent 0adca386d9
commit 36a4c76921
7 changed files with 234 additions and 3 deletions

View File

@ -124,6 +124,15 @@ public sealed class ScenesController(
: PartialView("~/Views/Shared/SceneInspectorV2/_SceneCharactersEditor.cshtml", model);
}
[HttpGet]
public async Task<IActionResult> SceneKnowledgeEditor(int sceneId)
{
var model = await scenes.GetEditAsync(sceneId);
return model is null
? NotFound()
: PartialView("~/Views/Shared/SceneInspectorV2/_SceneKnowledgeEditor.cshtml", model);
}
[HttpGet]
public async Task<IActionResult> SceneLocationsEditor(int sceneId)
{
@ -1123,6 +1132,24 @@ public sealed class ScenesController(
public async Task<IActionResult> AddCharacterKnowledge(CharacterKnowledgeCreateViewModel model)
{
await characters.AddKnowledgeAsync(model);
if (IsAjaxRequest())
{
return await SceneKnowledgeRefreshJsonAsync(model.SceneID);
}
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UpdateCharacterKnowledge(CharacterKnowledgeCreateViewModel model)
{
await characters.UpdateKnowledgeAsync(model);
if (IsAjaxRequest())
{
return await SceneKnowledgeRefreshJsonAsync(model.SceneID);
}
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
@ -1131,6 +1158,11 @@ public sealed class ScenesController(
public async Task<IActionResult> DeleteCharacterKnowledge(int id, int sceneId, int? projectId, int? bookId)
{
await characters.DeleteKnowledgeAsync(id);
if (IsAjaxRequest())
{
return await SceneKnowledgeRefreshJsonAsync(sceneId);
}
return RedirectForScene(sceneId, projectId, bookId);
}
@ -1359,6 +1391,30 @@ public sealed class ScenesController(
});
}
private async Task<IActionResult> SceneKnowledgeRefreshJsonAsync(int sceneId)
{
var updated = await scenes.GetEditAsync(sceneId);
if (updated is null)
{
return NotFound();
}
return Json(new
{
success = true,
html = new
{
health = await RenderPartialViewToStringAsync("~/Views/Shared/SceneInspectorV2/_SceneInspectorHealthStrip.cshtml", updated),
people = await RenderPartialViewToStringAsync("~/Views/Shared/SceneInspectorV2/_ScenePeopleCard.cshtml", updated),
continuity = await RenderPartialViewToStringAsync("~/Views/Shared/SceneInspectorV2/_SceneContinuityCard.cshtml", updated)
},
scene = new
{
updated.SceneID
}
});
}
private IActionResult RedirectForScene(int sceneId, int? projectId, int? bookId)
{
if (TryGetLocalReturnUrl(out var returnUrl))

View File

@ -261,6 +261,7 @@ public interface ICharacterService
Task RejectSceneCharacterSuggestionAsync(int suggestionId);
Task<int> AddAttributeEventAsync(CharacterAttributeEventCreateViewModel model);
Task<int> AddKnowledgeAsync(CharacterKnowledgeCreateViewModel model);
Task<int> UpdateKnowledgeAsync(CharacterKnowledgeCreateViewModel model);
Task DeleteKnowledgeAsync(int characterKnowledgeId);
Task<int> AddRelationshipEventAsync(RelationshipEventCreateViewModel model);
Task DeleteRelationshipEventAsync(int relationshipEventId);
@ -9521,13 +9522,19 @@ public sealed class CharacterService(
Description = model.Description
});
public Task<int> AddKnowledgeAsync(CharacterKnowledgeCreateViewModel model) => characters.SaveKnowledgeAsync(new CharacterKnowledgeItem
public Task<int> AddKnowledgeAsync(CharacterKnowledgeCreateViewModel model) => SaveKnowledgeAsync(model);
public Task<int> UpdateKnowledgeAsync(CharacterKnowledgeCreateViewModel model) => SaveKnowledgeAsync(model);
private Task<int> SaveKnowledgeAsync(CharacterKnowledgeCreateViewModel model) => characters.SaveKnowledgeAsync(new CharacterKnowledgeItem
{
CharacterKnowledgeID = model.CharacterKnowledgeID,
CharacterID = model.CharacterID,
StoryAssetID = model.StoryAssetID,
PlotThreadID = model.PlotThreadID,
SceneID = model.SceneID,
KnowledgeStateID = model.KnowledgeStateID,
SourceEventID = model.SourceEventID,
Description = model.Description
});

View File

@ -59,6 +59,7 @@ public sealed class ProjectAccessFilter(IProjectAccessService access, ICurrentUs
["ParentPlotLineID"] = "PlotLine",
["PlotThreadID"] = "PlotThread",
["CharacterRelationshipID"] = "CharacterRelationship",
["CharacterKnowledgeID"] = "CharacterKnowledge",
["TimelineViewPresetID"] = "TimelineViewPreset",
["ScenarioID"] = "Scenario"
};

View File

@ -2025,11 +2025,13 @@ public sealed class CharacterAttributeEventCreateViewModel
public sealed class CharacterKnowledgeCreateViewModel
{
public int CharacterKnowledgeID { get; set; }
public int SceneID { get; set; }
public int CharacterID { get; set; }
public int? StoryAssetID { get; set; }
public int? PlotThreadID { get; set; }
public int KnowledgeStateID { get; set; }
public int? SourceEventID { get; set; }
public string? Description { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }

View File

@ -7,7 +7,7 @@
: $"{warningCount} warning{(warningCount == 1 ? "" : "s")} need review.";
}
<article class="scene-inspector-v2-card">
<article class="scene-inspector-v2-card" data-scene-summary-part="continuity">
<div class="scene-inspector-v2-card-header">
<div>
<p class="eyebrow">Continuity</p>

View File

@ -0,0 +1,165 @@
@model SceneEditViewModel
<div class="scene-inspector-editor-form">
<div class="scene-inspector-editor-fields">
<section class="scene-structure-editor-section">
<div class="scene-structure-editor-heading">
<span>Knowledge changes</span>
<strong>@Model.CharacterKnowledge.Count</strong>
</div>
@if (!Model.CharacterKnowledge.Any())
{
<p class="muted">No knowledge changes are recorded for this scene yet.</p>
}
else
{
<div class="scene-structure-editor-list">
@foreach (var item in Model.CharacterKnowledge)
{
<article class="scene-structure-editor-card">
<div class="scene-structure-card-header">
<div>
<h4>@item.CharacterName</h4>
<p>@(item.AssetName ?? item.ThreadTitle ?? "Scene knowledge")</p>
</div>
<span class="scene-structure-marker">@item.KnowledgeStateName</span>
</div>
@if (!string.IsNullOrWhiteSpace(item.Description))
{
<p>@item.Description</p>
}
<form asp-controller="Scenes" asp-action="UpdateCharacterKnowledge" method="post" class="scene-structure-editor-form" data-scene-editor-form>
@Html.AntiForgeryToken()
<input type="hidden" name="CharacterKnowledgeID" value="@item.CharacterKnowledgeID" />
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="SourceEventID" value="@item.SourceEventID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="scene-inspector-editor-error" data-scene-editor-error hidden></div>
<div class="form-group">
<label for="Knowledge_@(item.CharacterKnowledgeID)__CharacterID">Character</label>
<select class="form-control form-control-sm" id="Knowledge_@(item.CharacterKnowledgeID)__CharacterID" name="CharacterID">
@foreach (var option in Model.CharacterOptions)
{
<option value="@option.Value" selected="@(option.Value == item.CharacterID.ToString())">@option.Text</option>
}
</select>
</div>
<div class="scene-inspector-editor-grid">
<div class="form-group">
<label for="Knowledge_@(item.CharacterKnowledgeID)__StoryAssetID">Related asset</label>
<select class="form-control form-control-sm" id="Knowledge_@(item.CharacterKnowledgeID)__StoryAssetID" name="StoryAssetID">
<option value="">No asset</option>
@foreach (var option in Model.AssetOptions)
{
<option value="@option.Value" selected="@(item.StoryAssetID?.ToString() == option.Value)">@option.Text</option>
}
</select>
</div>
<div class="form-group">
<label for="Knowledge_@(item.CharacterKnowledgeID)__PlotThreadID">Related plot thread</label>
<select class="form-control form-control-sm" id="Knowledge_@(item.CharacterKnowledgeID)__PlotThreadID" name="PlotThreadID">
<option value="">No plot thread</option>
@foreach (var option in Model.PlotThreadOptions)
{
<option value="@option.Value" selected="@(item.PlotThreadID?.ToString() == option.Value)">@option.Text</option>
}
</select>
</div>
</div>
<div class="form-group">
<label for="Knowledge_@(item.CharacterKnowledgeID)__KnowledgeStateID">Knowledge state</label>
<select class="form-control form-control-sm" id="Knowledge_@(item.CharacterKnowledgeID)__KnowledgeStateID" name="KnowledgeStateID">
@foreach (var option in Model.KnowledgeStateOptions)
{
<option value="@option.Value" selected="@(option.Value == item.KnowledgeStateID.ToString())">@option.Text</option>
}
</select>
</div>
<div class="form-group">
<label for="Knowledge_@(item.CharacterKnowledgeID)__Description">Notes</label>
<textarea class="form-control form-control-sm" id="Knowledge_@(item.CharacterKnowledgeID)__Description" name="Description" rows="3">@item.Description</textarea>
</div>
<div class="scene-structure-action-row">
<button class="btn btn-sm btn-primary" type="submit" data-scene-editor-save>Save knowledge</button>
</div>
</form>
<form asp-controller="Scenes" asp-action="DeleteCharacterKnowledge" method="post" class="scene-structure-delete-form" data-scene-editor-form>
@Html.AntiForgeryToken()
<input type="hidden" name="id" value="@item.CharacterKnowledgeID" />
<input type="hidden" name="sceneId" value="@Model.SceneID" />
<input type="hidden" name="projectId" value="@Model.ReturnProjectID" />
<input type="hidden" name="bookId" value="@Model.ReturnBookID" />
<div class="scene-inspector-editor-error" data-scene-editor-error hidden></div>
<button class="btn btn-sm btn-outline-danger" type="submit" data-scene-editor-save>Remove knowledge</button>
</form>
</article>
}
</div>
}
</section>
<section class="scene-structure-editor-section">
<div class="scene-structure-editor-heading">
<span>Add knowledge change</span>
</div>
<form asp-controller="Scenes" asp-action="AddCharacterKnowledge" method="post" class="scene-structure-editor-card scene-structure-editor-form" data-scene-editor-form>
@Html.AntiForgeryToken()
<input type="hidden" name="SceneID" value="@Model.SceneID" />
<input type="hidden" name="ReturnProjectID" value="@Model.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.ReturnBookID" />
<div class="scene-inspector-editor-error" data-scene-editor-error hidden></div>
<div class="form-group">
<label for="KnowledgeCharacterID">Character</label>
<select class="form-control form-control-sm" id="KnowledgeCharacterID" name="CharacterID" asp-items="Model.CharacterOptions"></select>
</div>
<div class="scene-inspector-editor-grid">
<div class="form-group">
<label for="KnowledgeStoryAssetID">Related asset</label>
<select class="form-control form-control-sm" id="KnowledgeStoryAssetID" name="StoryAssetID" asp-items="Model.AssetOptions">
<option value="">No asset</option>
</select>
</div>
<div class="form-group">
<label for="KnowledgePlotThreadID">Related plot thread</label>
<select class="form-control form-control-sm" id="KnowledgePlotThreadID" name="PlotThreadID" asp-items="Model.PlotThreadOptions">
<option value="">No plot thread</option>
</select>
</div>
</div>
<div class="form-group">
<label for="KnowledgeStateID">Knowledge state</label>
<select class="form-control form-control-sm" id="KnowledgeStateID" name="KnowledgeStateID" asp-items="Model.KnowledgeStateOptions"></select>
</div>
<div class="form-group">
<label for="KnowledgeDescription">Notes</label>
<textarea class="form-control form-control-sm" id="KnowledgeDescription" name="Description" rows="3" placeholder="Reveal or discovery context"></textarea>
</div>
<div class="scene-structure-action-row">
<button class="btn btn-sm btn-primary" type="submit" data-scene-editor-save>Add knowledge</button>
</div>
</form>
</section>
</div>
<div class="scene-inspector-editor-footer">
<button type="button" class="btn btn-sm btn-outline-secondary" data-scene-editor-close>Close</button>
</div>
</div>

View File

@ -53,7 +53,7 @@
<div class="scene-inspector-v2-actions">
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="characters" data-scene-id="@Model.SceneID" data-scene-editor-url="@Url.Action("SceneCharactersEditor", "Scenes", new { sceneId = Model.SceneID })">Manage characters</button>
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="knowledge" data-scene-id="@Model.SceneID">Manage knowledge</button>
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="knowledge" data-scene-id="@Model.SceneID" data-scene-editor-url="@Url.Action("SceneKnowledgeEditor", "Scenes", new { sceneId = Model.SceneID })">Manage knowledge</button>
<button type="button" class="btn btn-sm btn-outline-primary" data-scene-editor="relationships" data-scene-id="@Model.SceneID">Manage relationships</button>
</div>
</article>