Review all Save, Add, Update and Delete actions used by the Scene Inspector.
This commit is contained in:
parent
d4c886ce2f
commit
59c5630950
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
@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>
|
||||
|
||||
@ -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;
|
||||
|
||||
2
PlotLine/wwwroot/js/site.min.js
vendored
2
PlotLine/wwwroot/js/site.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user