269 lines
14 KiB
Plaintext
269 lines
14 KiB
Plaintext
@model ImportIndexViewModel
|
|
@{
|
|
ViewData["Title"] = "JSON Import";
|
|
var preview = Model.Preview;
|
|
}
|
|
|
|
<div class="page-heading compact">
|
|
<div>
|
|
<p class="eyebrow">Tools</p>
|
|
<h1>JSON Import</h1>
|
|
<p class="lead-text">Paste or upload a Phase 1D package, preview it, confirm the summary, then import a new project.</p>
|
|
</div>
|
|
<div class="button-row">
|
|
<a class="btn btn-outline-primary" asp-action="Sample">Download sample JSON</a>
|
|
<a class="btn btn-outline-secondary" asp-controller="Projects" asp-action="Index">Back to projects</a>
|
|
</div>
|
|
</div>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(Model.StatusMessage))
|
|
{
|
|
<div class="alert alert-warning">
|
|
<strong>Import paused</strong>
|
|
<p class="mb-0">@Model.StatusMessage</p>
|
|
@if (preview is { Package: not null, HasDuplicateProjectTitle: true })
|
|
{
|
|
<p class="mb-0 mt-2">Duplicate imports will be renamed to <strong>@preview.DuplicateImportProjectTitle</strong>.</p>
|
|
}
|
|
@if (!string.IsNullOrWhiteSpace(Model.TechnicalDetail))
|
|
{
|
|
<details class="mt-2">
|
|
<summary>Technical detail</summary>
|
|
<p class="mb-0 mt-2">@Model.TechnicalDetail</p>
|
|
</details>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<section class="edit-panel import-panel">
|
|
<form asp-action="Preview" method="post" enctype="multipart/form-data">
|
|
<div class="row g-3">
|
|
<div class="col-12">
|
|
<label asp-for="JsonFile" class="form-label">Upload JSON file</label>
|
|
<input asp-for="JsonFile" class="form-control" type="file" accept=".json,application/json" />
|
|
<div class="form-text">Uploading a file replaces the pasted text for this preview.</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<label asp-for="JsonText" class="form-label">JSON package</label>
|
|
<textarea asp-for="JsonText" class="form-control import-json-input" rows="22" spellcheck="false"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="button-row mt-3">
|
|
<button class="btn btn-primary" type="submit">Validate and preview</button>
|
|
<button class="btn btn-outline-secondary" type="button" data-copy-sample>Use sample JSON</button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
|
|
<section class="list-section import-docs mt-3">
|
|
<details>
|
|
<summary>Supported JSON schema</summary>
|
|
<div class="import-doc-grid">
|
|
<div>
|
|
<h3>Required</h3>
|
|
<p><code>project.title</code>, each <code>book.title</code>, each <code>book.seriesOrder</code>, each <code>chapter.title</code>, each <code>chapter.order</code>, each <code>scene.title</code>, and each <code>scene.order</code>.</p>
|
|
</div>
|
|
<div>
|
|
<h3>Optional</h3>
|
|
<p><code>packageVersion</code>, <code>source</code>, top-level <code>characters</code>, top-level <code>locations</code>, top-level <code>plotLines</code>, plot line <code>threads</code>, thread <code>events</code>, POV/location mapping, date/time text, purpose/outcome text, and <code>sceneCharacters</code>.</p>
|
|
</div>
|
|
<div>
|
|
<h3>Not imported yet</h3>
|
|
<p>No AI extraction, Word/text parsing, story assets, asset states/custody, dependencies, relationship graphs, merge/update, or export logic. Thread events link to scenes with <code>bookOrder</code>, <code>chapterOrder</code>, and <code>sceneOrder</code>; <code>sceneRef</code> is accepted as future-friendly text only.</p>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</section>
|
|
|
|
@if (preview is not null)
|
|
{
|
|
<section class="list-section import-preview-section mt-3">
|
|
<div class="import-preview-heading">
|
|
<div>
|
|
<p class="eyebrow">Preview</p>
|
|
<h2>@(preview.Package?.Project.Title ?? "Import package")</h2>
|
|
</div>
|
|
<div class="import-counts">
|
|
<span><strong>@preview.BookCount</strong> books</span>
|
|
<span><strong>@preview.ChapterCount</strong> chapters</span>
|
|
<span><strong>@preview.SceneCount</strong> scenes</span>
|
|
<span><strong>@preview.CharacterCount</strong> characters</span>
|
|
<span><strong>@preview.LocationCount</strong> locations</span>
|
|
<span><strong>@preview.SceneCharacterAppearanceCount</strong> appearances</span>
|
|
<span><strong>@preview.PlotLineCount</strong> plot lines</span>
|
|
<span><strong>@preview.PlotThreadCount</strong> threads</span>
|
|
<span><strong>@preview.ResolvedThreadEventCount</strong> events</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="button-row mb-3">
|
|
<form asp-action="ValidationReport" method="post">
|
|
<input asp-for="JsonText" type="hidden" />
|
|
<button class="btn btn-outline-secondary btn-sm" type="submit">Download validation report</button>
|
|
</form>
|
|
</div>
|
|
|
|
@if (preview.Validation.Errors.Any())
|
|
{
|
|
<div class="alert alert-warning import-message-list">
|
|
<strong>Errors block import</strong>
|
|
<ul class="mb-0 mt-2">
|
|
@foreach (var error in preview.Validation.Errors)
|
|
{
|
|
<li>@error</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
}
|
|
|
|
@if (preview.Validation.Warnings.Any())
|
|
{
|
|
<div class="alert alert-info import-message-list">
|
|
<strong>Warnings require confirmation</strong>
|
|
<ul class="mb-0 mt-2">
|
|
@foreach (var warning in preview.Validation.Warnings)
|
|
{
|
|
<li>@warning</li>
|
|
}
|
|
</ul>
|
|
</div>
|
|
}
|
|
|
|
@if (preview.Package is not null)
|
|
{
|
|
@if (preview.UnmatchedPovNames.Any() || preview.UnmatchedLocationNames.Any() || preview.UnmatchedSceneCharacterNames.Any() || preview.UnresolvedThreadEventReferences.Any())
|
|
{
|
|
<div class="import-mapping-summary">
|
|
@if (preview.UnmatchedPovNames.Any())
|
|
{
|
|
<span>Unmatched POV: @string.Join(", ", preview.UnmatchedPovNames)</span>
|
|
}
|
|
@if (preview.UnmatchedLocationNames.Any())
|
|
{
|
|
<span>Unmatched locations: @string.Join(", ", preview.UnmatchedLocationNames)</span>
|
|
}
|
|
@if (preview.UnmatchedSceneCharacterNames.Any())
|
|
{
|
|
<span>Unmatched scene characters: @string.Join(", ", preview.UnmatchedSceneCharacterNames)</span>
|
|
}
|
|
@if (preview.UnresolvedThreadEventReferences.Any())
|
|
{
|
|
<span>Unresolved thread events: @string.Join(", ", preview.UnresolvedThreadEventReferences)</span>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<div class="import-tree">
|
|
@foreach (var bookPreview in preview.BookPreviews)
|
|
{
|
|
var book = bookPreview.Book;
|
|
<details class="import-tree-book" open>
|
|
<summary>
|
|
<strong>Book @book.SeriesOrder: @(string.IsNullOrWhiteSpace(book.Subtitle) ? book.Title : $"{book.Title}: {book.Subtitle}")</strong>
|
|
<span>@bookPreview.ChapterCount chapters / @bookPreview.SceneCount scenes</span>
|
|
</summary>
|
|
@foreach (var chapterPreview in bookPreview.ChapterPreviews)
|
|
{
|
|
var chapter = chapterPreview.Chapter;
|
|
<details class="import-tree-chapter">
|
|
<summary>
|
|
<strong>Chapter @chapter.ChapterNumber: @chapter.Title</strong>
|
|
<span>@chapterPreview.SceneCount scenes</span>
|
|
</summary>
|
|
<ul>
|
|
@foreach (var scene in chapter.Scenes.OrderBy(x => x.Order))
|
|
{
|
|
<li>
|
|
<strong>Scene @scene.SceneNumber: @scene.Title</strong>
|
|
<span class="import-scene-meta">
|
|
POV: @(scene.PovCharacterName ?? "Not set") /
|
|
Location: @(scene.LocationName ?? "Not set") /
|
|
Characters: @scene.SceneCharacters.Count
|
|
</span>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</details>
|
|
}
|
|
</details>
|
|
}
|
|
</div>
|
|
|
|
@if (preview.Package.PlotLines.Any())
|
|
{
|
|
<div class="import-tree mt-3">
|
|
@foreach (var plotLine in preview.Package.PlotLines)
|
|
{
|
|
<details class="import-tree-book">
|
|
<summary>
|
|
<strong>Plot line: @(string.IsNullOrWhiteSpace(plotLine.DisplayName) ? plotLine.Name : plotLine.DisplayName)</strong>
|
|
<span>@plotLine.Threads.Count threads / @plotLine.Threads.Sum(x => x.Events.Count) events</span>
|
|
</summary>
|
|
@foreach (var thread in plotLine.Threads)
|
|
{
|
|
<details class="import-tree-chapter">
|
|
<summary>
|
|
<strong>@(string.IsNullOrWhiteSpace(thread.DisplayName) ? thread.Name : thread.DisplayName)</strong>
|
|
<span>@thread.Events.Count events</span>
|
|
</summary>
|
|
</details>
|
|
}
|
|
</details>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<section class="import-confirmation mt-3">
|
|
<h3>Confirm import</h3>
|
|
<p>This will create <strong>@preview.Package.Project.Title</strong> with <strong>@preview.BookCount</strong> books, <strong>@preview.ChapterCount</strong> chapters, <strong>@preview.SceneCount</strong> scenes, <strong>@preview.CharacterCount</strong> characters, <strong>@preview.LocationCount</strong> locations, <strong>@preview.SceneCharacterAppearanceCount</strong> scene character appearances, <strong>@preview.PlotLineCount</strong> plot lines, <strong>@preview.PlotThreadCount</strong> plot threads, and <strong>@preview.ResolvedThreadEventCount</strong> resolved thread events.</p>
|
|
@if (preview.HasDuplicateProjectTitle)
|
|
{
|
|
<p class="muted">Because this title already exists, the created project will be named <strong>@preview.DuplicateImportProjectTitle</strong> if you continue.</p>
|
|
}
|
|
|
|
<form asp-action="Import" method="post" class="import-confirm-form">
|
|
<input asp-for="JsonText" type="hidden" />
|
|
@if (preview.Validation.Warnings.Any())
|
|
{
|
|
<div class="form-check import-duplicate-confirm">
|
|
<input asp-for="AcknowledgeWarnings" class="form-check-input" />
|
|
<label asp-for="AcknowledgeWarnings" class="form-check-label">I have reviewed the warnings and still want to import this package.</label>
|
|
</div>
|
|
}
|
|
@if (preview.HasDuplicateProjectTitle)
|
|
{
|
|
<div class="form-check import-duplicate-confirm mt-2">
|
|
<input asp-for="AllowDuplicateProjectTitle" class="form-check-input" />
|
|
<label asp-for="AllowDuplicateProjectTitle" class="form-check-label">
|
|
Import anyway using the renamed project title shown above.
|
|
</label>
|
|
</div>
|
|
}
|
|
<div class="form-check import-duplicate-confirm mt-2">
|
|
<input asp-for="ConfirmImport" class="form-check-input" />
|
|
<label asp-for="ConfirmImport" class="form-check-label">I understand this will create new records in PlotLine.</label>
|
|
</div>
|
|
<button class="btn btn-primary mt-3" type="submit" disabled="@(!preview.Validation.IsValid)">Import project</button>
|
|
</form>
|
|
</section>
|
|
}
|
|
</section>
|
|
}
|
|
|
|
<script type="application/json" id="importSampleJson">@Html.Raw(System.Text.Json.JsonSerializer.Serialize(ImportIndexViewModel.SampleJson))</script>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
(() => {
|
|
const button = document.querySelector("[data-copy-sample]");
|
|
const textarea = document.querySelector("#JsonText");
|
|
const sample = document.querySelector("#importSampleJson");
|
|
if (!button || !textarea || !sample) return;
|
|
button.addEventListener("click", () => {
|
|
textarea.value = JSON.parse(sample.textContent);
|
|
textarea.focus();
|
|
});
|
|
})();
|
|
</script>
|
|
}
|