Review all Save, Add, Update and Delete actions used by the Scene Inspector.

This commit is contained in:
Nick Beckley 2026-06-18 21:16:49 +01:00
parent d4c886ce2f
commit 59c5630950
6 changed files with 149 additions and 47 deletions

View File

@ -66,6 +66,11 @@ public sealed class ScenesController(
return View("Edit", createModel);
}
if (TryGetLocalReturnUrl(out var returnUrl))
{
return LocalRedirect(returnUrl);
}
if (model.ReturnToTimeline && model.ReturnProjectID.HasValue)
{
return RedirectToAction("Index", "Timeline", new
@ -85,6 +90,11 @@ public sealed class ScenesController(
{
await scenes.ArchiveAsync(id);
TempData["ArchiveMessage"] = "Scene archived. You can restore it from Archived Items.";
if (TryGetLocalReturnUrl(out var returnUrl))
{
return LocalRedirect(RemoveSelectedScene(returnUrl));
}
return RedirectToAction("Details", "Chapters", new { id = chapterId });
}
@ -141,12 +151,7 @@ public sealed class ScenesController(
public async Task<IActionResult> DeleteThreadEvent(int id, int sceneId, int? projectId, int? bookId)
{
await plots.DeleteThreadEventAsync(id);
if (projectId.HasValue)
{
return RedirectToAction("Index", "Timeline", new { projectId = projectId.Value, bookId, selectedSceneId = sceneId });
}
return RedirectToAction(nameof(Edit), new { id = sceneId });
return RedirectForScene(sceneId, projectId, bookId);
}
[HttpPost]
@ -154,17 +159,7 @@ public sealed class ScenesController(
public async Task<IActionResult> AddAssetEvent(AssetEventCreateViewModel model)
{
await assets.AddAssetEventAsync(model);
if (model.ReturnProjectID.HasValue)
{
return RedirectToAction("Index", "Timeline", new
{
projectId = model.ReturnProjectID.Value,
bookId = model.ReturnBookID,
selectedSceneId = model.SceneID
});
}
return RedirectToAction(nameof(Edit), new { id = model.SceneID });
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
[HttpPost]
@ -172,12 +167,7 @@ public sealed class ScenesController(
public async Task<IActionResult> DeleteAssetEvent(int id, int sceneId, int? projectId, int? bookId)
{
await assets.DeleteAssetEventAsync(id);
if (projectId.HasValue)
{
return RedirectToAction("Index", "Timeline", new { projectId = projectId.Value, bookId, selectedSceneId = sceneId });
}
return RedirectToAction(nameof(Edit), new { id = sceneId });
return RedirectForScene(sceneId, projectId, bookId);
}
[HttpPost]
@ -185,17 +175,7 @@ public sealed class ScenesController(
public async Task<IActionResult> AddAssetCustodyEvent(AssetCustodyCreateViewModel model)
{
await assets.AddCustodyEventAsync(model);
if (model.ReturnProjectID.HasValue)
{
return RedirectToAction("Index", "Timeline", new
{
projectId = model.ReturnProjectID.Value,
bookId = model.ReturnBookID,
selectedSceneId = model.SceneID
});
}
return RedirectToAction(nameof(Edit), new { id = model.SceneID });
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
[HttpPost]
@ -219,12 +199,7 @@ public sealed class ScenesController(
public async Task<IActionResult> DeleteAssetCustodyEvent(int id, int sceneId, int? projectId, int? bookId)
{
await assets.DeleteCustodyEventAsync(id);
if (projectId.HasValue)
{
return RedirectToAction("Index", "Timeline", new { projectId = projectId.Value, bookId, selectedSceneId = sceneId });
}
return RedirectToAction(nameof(Edit), new { id = sceneId });
return RedirectForScene(sceneId, projectId, bookId);
}
[HttpPost]
@ -279,6 +254,7 @@ public sealed class ScenesController(
[ValidateAntiForgeryToken]
public async Task<IActionResult> PreviewMove(SceneMoveRequestViewModel model)
{
model.ReturnUrl = GetLocalReturnUrl();
var preview = await scenes.GetMovePreviewAsync(model);
return preview is null ? NotFound() : View("MovePreview", preview);
}
@ -288,6 +264,11 @@ public sealed class ScenesController(
public async Task<IActionResult> ConfirmMove(SceneMoveRequestViewModel model)
{
await scenes.MoveToChapterAsync(model);
if (TryGetLocalReturnUrl(model.ReturnUrl, out var returnUrl))
{
return LocalRedirect(returnUrl);
}
return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID);
}
@ -475,6 +456,11 @@ public sealed class ScenesController(
private IActionResult RedirectForScene(int sceneId, int? projectId, int? bookId)
{
if (TryGetLocalReturnUrl(out var returnUrl))
{
return LocalRedirect(returnUrl);
}
if (projectId.HasValue)
{
return RedirectToAction("Index", "Timeline", new { projectId = projectId.Value, bookId, selectedSceneId = sceneId });
@ -483,6 +469,61 @@ public sealed class ScenesController(
return RedirectToAction(nameof(Edit), new { id = sceneId });
}
private string? GetLocalReturnUrl(string? returnUrl = null)
{
if (string.IsNullOrWhiteSpace(returnUrl)
&& Request.HasFormContentType
&& Request.Form.TryGetValue("ReturnUrl", out var postedReturnUrl))
{
returnUrl = postedReturnUrl.ToString();
}
return !string.IsNullOrWhiteSpace(returnUrl) && Url.IsLocalUrl(returnUrl)
? returnUrl
: null;
}
private bool TryGetLocalReturnUrl(out string returnUrl) =>
TryGetLocalReturnUrl(null, out returnUrl);
private bool TryGetLocalReturnUrl(string? candidate, out string returnUrl)
{
var localReturnUrl = GetLocalReturnUrl(candidate);
if (!string.IsNullOrWhiteSpace(localReturnUrl))
{
returnUrl = localReturnUrl;
return true;
}
returnUrl = string.Empty;
return false;
}
private static string RemoveSelectedScene(string returnUrl)
{
var hashIndex = returnUrl.IndexOf('#');
var hash = hashIndex >= 0 ? returnUrl[hashIndex..] : string.Empty;
var withoutHash = hashIndex >= 0 ? returnUrl[..hashIndex] : returnUrl;
var queryIndex = withoutHash.IndexOf('?');
if (queryIndex < 0)
{
return returnUrl;
}
var path = withoutHash[..queryIndex];
var query = withoutHash[(queryIndex + 1)..]
.Split('&', StringSplitOptions.RemoveEmptyEntries)
.Where(part =>
{
var equalsIndex = part.IndexOf('=');
var key = equalsIndex >= 0 ? part[..equalsIndex] : part;
return !string.Equals(Uri.UnescapeDataString(key), "selectedSceneId", StringComparison.OrdinalIgnoreCase);
})
.ToList();
return query.Count == 0 ? path + hash : $"{path}?{string.Join("&", query)}{hash}";
}
private static void CopyPostedSceneFields(SceneEditViewModel source, SceneEditViewModel target)
{
target.SceneTitle = source.SceneTitle;

View File

@ -49,6 +49,11 @@ public sealed class WarningsController(IContinuityValidationService validation)
public async Task<IActionResult> ValidateScene(int projectId, int? bookId, int sceneId, bool returnToTimeline = false)
{
await validation.ValidateSceneAsync(sceneId);
if (TryGetLocalReturnUrl(out var returnUrl))
{
return LocalRedirect(returnUrl);
}
if (returnToTimeline)
{
return RedirectToAction("Index", "Timeline", new { projectId, bookId, selectedSceneId = sceneId });
@ -137,6 +142,11 @@ public sealed class WarningsController(IContinuityValidationService validation)
private IActionResult RedirectAfterAction(int projectId, int? bookId, int? sceneId, bool returnToTimeline)
{
if (TryGetLocalReturnUrl(out var returnUrl))
{
return LocalRedirect(returnUrl);
}
if (returnToTimeline && sceneId.HasValue)
{
return RedirectToAction("Index", "Timeline", new { projectId, bookId, selectedSceneId = sceneId.Value });
@ -145,6 +155,20 @@ public sealed class WarningsController(IContinuityValidationService validation)
return RedirectToAction(nameof(Index), new { projectId, bookId, sceneId });
}
private bool TryGetLocalReturnUrl(out string returnUrl)
{
if (Request.HasFormContentType
&& Request.Form.TryGetValue("ReturnUrl", out var postedReturnUrl)
&& Url.IsLocalUrl(postedReturnUrl.ToString()))
{
returnUrl = postedReturnUrl.ToString();
return true;
}
returnUrl = string.Empty;
return false;
}
private IActionResult RedirectToDashboard(WarningDashboardReturnFilter filter)
{
return RedirectToAction(nameof(Index), new

View File

@ -1917,6 +1917,7 @@ public sealed class SceneMoveRequestViewModel
public bool RenumberScenes { get; set; }
public int? ReturnProjectID { get; set; }
public int? ReturnBookID { get; set; }
public string? ReturnUrl { get; set; }
}
public sealed class SceneMovePreviewViewModel

View File

@ -55,13 +55,21 @@
<input type="hidden" name="RenumberScenes" value="@Model.Request.RenumberScenes.ToString().ToLowerInvariant()" />
<input type="hidden" name="ReturnProjectID" value="@Model.Request.ReturnProjectID" />
<input type="hidden" name="ReturnBookID" value="@Model.Request.ReturnBookID" />
<input type="hidden" name="ReturnUrl" value="@Model.Request.ReturnUrl" />
<button class="btn btn-primary" type="submit">Confirm move</button>
</form>
<a class="btn btn-outline-secondary"
asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Request.ReturnProjectID"
asp-route-bookId="@Model.Request.ReturnBookID"
asp-route-selectedSceneId="@Model.Request.SceneID">Cancel</a>
@if (!string.IsNullOrWhiteSpace(Model.Request.ReturnUrl))
{
<a class="btn btn-outline-secondary" href="@Model.Request.ReturnUrl">Cancel</a>
}
else
{
<a class="btn btn-outline-secondary"
asp-controller="Timeline"
asp-action="Index"
asp-route-projectId="@Model.Request.ReturnProjectID"
asp-route-bookId="@Model.Request.ReturnBookID"
asp-route-selectedSceneId="@Model.Request.SceneID">Cancel</a>
}
</div>
</section>

View File

@ -63,6 +63,34 @@
});
})();
(() => {
const inspectorRoots = document.querySelectorAll(".scene-inspector-root");
if (!inspectorRoots.length) {
return;
}
const currentReturnUrl = () => `${window.location.pathname}${window.location.search}${window.location.hash}`;
inspectorRoots.forEach((root) => {
root.addEventListener("submit", (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement)) {
return;
}
let returnUrlInput = form.querySelector("input[name='ReturnUrl']");
if (!returnUrlInput) {
returnUrlInput = document.createElement("input");
returnUrlInput.type = "hidden";
returnUrlInput.name = "ReturnUrl";
form.appendChild(returnUrlInput);
}
returnUrlInput.value = currentReturnUrl();
}, true);
});
})();
(() => {
const modalElement = document.getElementById("plotlineConfirmModal");
const modal = modalElement && window.bootstrap ? new bootstrap.Modal(modalElement) : null;

File diff suppressed because one or more lines are too long