From b6841462480d9749e7090bee41687b6fd365c497 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Tue, 2 Jun 2026 12:51:47 +0100 Subject: [PATCH] Import Phase 1F --- PlotLine/Data/ImportRepository.cs | 126 +++++++++++++++++++++++- PlotLine/Models/ImportModels.cs | 23 +++++ PlotLine/Services/ImportServices.cs | 123 ++++++++++++++++++++++- PlotLine/ViewModels/ImportViewModels.cs | 40 +++++++- PlotLine/Views/Imports/Index.cshtml | 50 +++++++++- PlotLine/Views/Imports/Result.cshtml | 1 + 6 files changed, 354 insertions(+), 9 deletions(-) diff --git a/PlotLine/Data/ImportRepository.cs b/PlotLine/Data/ImportRepository.cs index c03684d..c6a2e07 100644 --- a/PlotLine/Data/ImportRepository.cs +++ b/PlotLine/Data/ImportRepository.cs @@ -13,7 +13,7 @@ public interface IImportRepository public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : IImportRepository { - private const string ImportMarker = "Imported via JSON Import Phase 1E"; + private const string ImportMarker = "Imported via JSON Import Phase 1F"; public async Task ProjectTitleExistsAsync(string projectTitle) { @@ -50,6 +50,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : var assetStatesCreated = 0; var assetEventsCreated = 0; var assetCustodyEventsCreated = 0; + var sceneDependenciesCreated = 0; var skippedFields = GetSkippedOptionalFields(package); var unresolvedWarnings = GetUnresolvedWarnings(package); var sceneMap = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -491,6 +492,45 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : } } + var importedSceneDependencyKeys = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var dependency in package.SceneDependencies) + { + if (!TryResolveSceneReference(sceneMap, dependency.FromScene, out var sourceSceneId) + || !TryResolveSceneReference(sceneMap, dependency.ToScene, out var targetSceneId) + || sourceSceneId == targetSceneId) + { + continue; + } + + var dependencyTypeId = ResolveSceneDependencyTypeId(lookupIds, dependency.DependencyType); + var dependencyKey = $"{sourceSceneId}/{targetSceneId}/{dependencyTypeId}"; + if (!importedSceneDependencyKeys.Add(dependencyKey)) + { + continue; + } + + await connection.QuerySingleAsync( + "dbo.SceneDependency_Save", + new + { + SceneDependencyID = 0, + ProjectID = projectId, + SourceSceneID = sourceSceneId, + TargetSceneID = targetSceneId, + SceneDependencyTypeID = dependencyTypeId, + Description = CombineText( + dependency.Description, + MetadataLine("Dependency type", dependency.DependencyType), + MetadataLine("Strength", dependency.Strength), + dependency.Notes, + ImportMarker), + IsHardDependency = ResolveSceneDependencyStrength(dependency.Strength) + }, + transaction, + commandType: CommandType.StoredProcedure); + sceneDependenciesCreated++; + } + transaction.Commit(); return new ImportResult { @@ -510,6 +550,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : AssetStatesCreated = assetStatesCreated, AssetEventsCreated = assetEventsCreated, AssetCustodyEventsCreated = assetCustodyEventsCreated, + SceneDependenciesCreated = sceneDependenciesCreated, SkippedOptionalFields = skippedFields, UnresolvedWarnings = unresolvedWarnings, Message = "Import complete." @@ -555,6 +596,9 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : var threadEventTypes = (await connection.QueryAsync( "SELECT ThreadEventTypeID AS LookupID, TypeName FROM dbo.ThreadEventTypes WHERE IsActive = 1;", transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var sceneDependencyTypes = (await connection.QueryAsync( + "SELECT SceneDependencyTypeID AS LookupID, TypeName FROM dbo.SceneDependencyTypes WHERE IsActive = 1;", + transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); var assetKinds = (await connection.QueryAsync( "SELECT AssetKindID AS LookupID, KindName AS TypeName FROM dbo.AssetKinds WHERE IsActive = 1;", transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); @@ -587,6 +631,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : threadTypes, threadStatuses, threadEventTypes, + sceneDependencyTypes, assetKinds, assetStates, assetEventTypes, @@ -640,6 +685,11 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : skipped.Add("Asset event character names were stored in event descriptions."); } + if (package.SceneDependencies.Any(dependency => !string.IsNullOrWhiteSpace(dependency.DependencyType) || !string.IsNullOrWhiteSpace(dependency.Strength))) + { + skipped.Add("Scene dependency type and strength labels were mapped to existing dependency fields and preserved in descriptions."); + } + return skipped; } @@ -729,6 +779,21 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : warnings.Add($"Unresolved asset event scene reference: {reference}"); } + foreach (var reference in package.SceneDependencies.SelectMany((dependency, index) => new[] + { + new { Label = $"Dependency {index + 1} fromScene", Reference = dependency.FromScene }, + new { Label = $"Dependency {index + 1} toScene", Reference = dependency.ToScene } + }) + .Where(x => x.Reference?.BookOrder.HasValue != true + || !x.Reference.ChapterOrder.HasValue + || !x.Reference.SceneOrder.HasValue + || !sceneKeys.Contains(SceneKey(x.Reference.BookOrder.GetValueOrDefault(), x.Reference.ChapterOrder.GetValueOrDefault(), x.Reference.SceneOrder.GetValueOrDefault()))) + .Select(x => $"{x.Label} -> {SceneReferenceKey(x.Reference)}") + .Distinct(StringComparer.OrdinalIgnoreCase)) + { + warnings.Add($"Unresolved scene dependency reference: {reference}"); + } + return warnings; } @@ -863,6 +928,48 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : : lookupIds.ThreadEventTypeIds.Values.First(); } + private static int ResolveSceneDependencyTypeId(ImportLookupIds lookupIds, string? dependencyType) + { + var normalized = Clean(dependencyType); + if (normalized.Length > 0 && lookupIds.SceneDependencyTypeIds.TryGetValue(normalized, out var id)) + { + return id; + } + + var mappedType = normalized.ToLowerInvariant() switch + { + "causes" or "requires" or "blocks" or "follows" => "Must Occur Before", + "reveals" or "foreshadows" or "paysoff" or "pays off" => "Reveal Setup", + "contradicts" => "Knowledge Dependency", + "mirrors" => "Emotional Setup", + _ => "Must Occur Before" + }; + + return lookupIds.SceneDependencyTypeIds.TryGetValue(mappedType, out var mappedId) + ? mappedId + : lookupIds.SceneDependencyTypeIds.Values.First(); + } + + private static bool ResolveSceneDependencyStrength(string? strength) + { + var normalized = Clean(strength); + if (normalized.Length == 0) + { + return false; + } + + if (int.TryParse(normalized, out var numeric)) + { + return numeric >= 7; + } + + return normalized.Equals("Hard", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("Strong", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("Required", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("High", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("Critical", StringComparison.OrdinalIgnoreCase); + } + private static int ResolveAssetKindId(ImportLookupIds lookupIds, string? type) { if (!string.IsNullOrWhiteSpace(type) && lookupIds.AssetKindIds.TryGetValue(type.Trim(), out var id)) @@ -955,9 +1062,25 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : return false; } + private static bool TryResolveSceneReference(IReadOnlyDictionary sceneMap, ImportSceneReferenceDto? reference, out int sceneId) + { + if (reference?.BookOrder.HasValue == true && reference.ChapterOrder.HasValue && reference.SceneOrder.HasValue) + { + return sceneMap.TryGetValue(SceneKey(reference.BookOrder.Value, reference.ChapterOrder.Value, reference.SceneOrder.Value), out sceneId); + } + + sceneId = 0; + return false; + } + private static string SceneKey(int bookOrder, int chapterOrder, int sceneOrder) => $"{bookOrder}/{chapterOrder}/{sceneOrder}"; + private static string SceneReferenceKey(ImportSceneReferenceDto? reference) + => reference is null + ? "(missing)" + : $"{reference.BookOrder?.ToString() ?? "?"}/{reference.ChapterOrder?.ToString() ?? "?"}/{reference.SceneOrder?.ToString() ?? "?"}"; + private static string Clean(string? value) => (value ?? string.Empty).Trim(); private static string? MetadataLine(string label, string? value) @@ -1010,6 +1133,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : IReadOnlyDictionary ThreadTypeIds, IReadOnlyDictionary ThreadStatusIds, IReadOnlyDictionary ThreadEventTypeIds, + IReadOnlyDictionary SceneDependencyTypeIds, IReadOnlyDictionary AssetKindIds, IReadOnlyDictionary AssetStateIds, IReadOnlyDictionary AssetEventTypeIds, diff --git a/PlotLine/Models/ImportModels.cs b/PlotLine/Models/ImportModels.cs index ea81f26..3e1d3ac 100644 --- a/PlotLine/Models/ImportModels.cs +++ b/PlotLine/Models/ImportModels.cs @@ -11,6 +11,7 @@ public sealed class PlotLineImportPackage public List Locations { get; set; } = []; public List PlotLines { get; set; } = []; public List StoryAssets { get; set; } = []; + public List SceneDependencies { get; set; } = []; public List Books { get; set; } = []; [JsonExtensionData] @@ -132,6 +133,24 @@ public sealed class ImportAssetEventDto public string? Notes { get; set; } } +public sealed class ImportSceneDependencyDto +{ + public ImportSceneReferenceDto? FromScene { get; set; } + public ImportSceneReferenceDto? ToScene { get; set; } + public string? DependencyType { get; set; } + public string? Strength { get; set; } + public string? Description { get; set; } + public string? Notes { get; set; } +} + +public sealed class ImportSceneReferenceDto +{ + public string? SceneRef { get; set; } + public int? BookOrder { get; set; } + public int? ChapterOrder { get; set; } + public int? SceneOrder { get; set; } +} + public sealed class ImportChapterDto { public decimal ChapterNumber { get; set; } @@ -186,6 +205,7 @@ public sealed class ImportPreview public IReadOnlyList UnresolvedAssetReferences { get; init; } = []; public IReadOnlyList UnmatchedAssetOwnerNames { get; init; } = []; public IReadOnlyList UnmatchedAssetLocationNames { get; init; } = []; + public IReadOnlyList UnresolvedSceneDependencyReferences { get; init; } = []; public int BookCount => Package?.Books.Count ?? 0; public int CharacterCount => Package?.Characters.Count ?? 0; public int LocationCount => Package?.Locations.Count ?? 0; @@ -200,6 +220,8 @@ public sealed class ImportPreview public int ResolvedAssetStateCount => Math.Max(0, AssetStateCount - UnresolvedAssetReferences.Count(x => x.Contains("state", StringComparison.OrdinalIgnoreCase))); public int ResolvedAssetEventCount => Math.Max(0, AssetEventCount - UnresolvedAssetReferences.Count(x => x.Contains("event", StringComparison.OrdinalIgnoreCase))); public int ResolvedAssetCustodyCount => Math.Max(0, AssetCustodyCount - Package?.StoryAssets.Count(asset => !string.IsNullOrWhiteSpace(asset.InitialOwnerCharacterName) && UnmatchedAssetOwnerNames.Contains(asset.InitialOwnerCharacterName.Trim(), StringComparer.OrdinalIgnoreCase)) ?? 0); + public int SceneDependencyCount => Package?.SceneDependencies.Count ?? 0; + public int ResolvedSceneDependencyCount => Math.Max(0, SceneDependencyCount - UnresolvedSceneDependencyReferences.Count); public int ChapterCount => Package?.Books.Sum(book => book.Chapters.Count) ?? 0; public int SceneCount => Package?.Books.Sum(book => book.Chapters.Sum(chapter => chapter.Scenes.Count)) ?? 0; public int SceneCharacterAppearanceCount => Package?.Books.Sum(book => book.Chapters.Sum(chapter => chapter.Scenes.Sum(scene => scene.SceneCharacters.Count + AutoPovAppearanceCount(scene)))) ?? 0; @@ -243,6 +265,7 @@ public sealed class ImportResult public int AssetStatesCreated { get; init; } public int AssetEventsCreated { get; init; } public int AssetCustodyEventsCreated { get; init; } + public int SceneDependenciesCreated { get; init; } public IReadOnlyList SkippedOptionalFields { get; init; } = []; public IReadOnlyList UnresolvedWarnings { get; init; } = []; public string? TechnicalDetail { get; init; } diff --git a/PlotLine/Services/ImportServices.cs b/PlotLine/Services/ImportServices.cs index 7a9ed68..a64e979 100644 --- a/PlotLine/Services/ImportServices.cs +++ b/PlotLine/Services/ImportServices.cs @@ -112,6 +112,7 @@ public sealed class ImportService(IImportRepository imports, ILogger HasSceneOrderReference(dependency.FromScene) && HasSceneOrderReference(dependency.ToScene)) + .GroupBy(SceneDependencyKey, StringComparer.OrdinalIgnoreCase) + .Where(group => group.Count() > 1)) + { + result.Warnings.Add($"Duplicate scene dependency will be imported once: {duplicate.Key}."); + } + + for (var index = 0; index < package.SceneDependencies.Count; index++) + { + var dependency = package.SceneDependencies[index]; + var label = $"Scene dependency {index + 1}"; + + if (dependency.FromScene is null) + { + result.Warnings.Add($"{label} is missing fromScene and will be skipped."); + } + else if (!HasSceneOrderReference(dependency.FromScene) && string.IsNullOrWhiteSpace(dependency.FromScene.SceneRef)) + { + result.Warnings.Add($"{label} fromScene is missing bookOrder/chapterOrder/sceneOrder."); + } + + if (dependency.ToScene is null) + { + result.Warnings.Add($"{label} is missing toScene and will be skipped."); + } + else if (!HasSceneOrderReference(dependency.ToScene) && string.IsNullOrWhiteSpace(dependency.ToScene.SceneRef)) + { + result.Warnings.Add($"{label} toScene is missing bookOrder/chapterOrder/sceneOrder."); + } + + if (HasSceneOrderReference(dependency.FromScene) && HasSceneOrderReference(dependency.ToScene) + && SceneReferenceKey(dependency.FromScene!) == SceneReferenceKey(dependency.ToScene!)) + { + result.Warnings.Add($"{label} links a scene to itself and will be skipped."); + } + + if (!string.IsNullOrWhiteSpace(dependency.DependencyType) && dependency.DependencyType.Trim().Length > 100) + { + result.Warnings.Add($"{label} dependencyType is longer than 100 characters and may not match an existing dependency type."); + } + + if (!string.IsNullOrWhiteSpace(dependency.Strength) && !IsSupportedDependencyStrength(dependency.Strength)) + { + result.Warnings.Add($"{label} strength \"{dependency.Strength.Trim()}\" is not a recognized hard/soft value. It will be preserved in the description."); + } + + ValidateLargeText(result, $"{label} description", dependency.Description); + ValidateLargeText(result, $"{label} notes", dependency.Notes); + } + } + private static void ValidateStoryAssets(PlotLineImportPackage package, ImportValidationResult result) { foreach (var group in package.StoryAssets @@ -776,6 +839,33 @@ public sealed class ImportService(IImportRepository imports, ILogger FindUnresolvedSceneDependencyReferences(PlotLineImportPackage package) + { + var sceneKeys = package.Books + .SelectMany(book => book.Chapters.SelectMany(chapter => chapter.Scenes.Select(scene => $"{book.SeriesOrder}/{chapter.Order}/{scene.Order}"))) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + return package.SceneDependencies + .SelectMany((dependency, index) => new[] + { + new + { + Label = $"Dependency {index + 1} fromScene", + Reference = dependency.FromScene + }, + new + { + Label = $"Dependency {index + 1} toScene", + Reference = dependency.ToScene + } + }) + .Where(x => !HasSceneOrderReference(x.Reference) || !sceneKeys.Contains(SceneReferenceKey(x.Reference!))) + .Select(x => $"{x.Label} -> {SceneReferenceKey(x.Reference)}") + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(x => x) + .ToList(); + } + private static bool HasSceneOrderReference(ImportThreadEventDto threadEvent) => threadEvent.BookOrder.HasValue && threadEvent.ChapterOrder.HasValue && threadEvent.SceneOrder.HasValue; @@ -794,6 +884,35 @@ public sealed class ImportService(IImportRepository imports, ILogger $"{assetEvent.BookOrder?.ToString() ?? "?"}/{assetEvent.ChapterOrder?.ToString() ?? "?"}/{assetEvent.SceneOrder?.ToString() ?? "?"}"; + private static bool HasSceneOrderReference(ImportSceneReferenceDto? reference) + => reference?.BookOrder.HasValue == true && reference.ChapterOrder.HasValue && reference.SceneOrder.HasValue; + + private static string SceneReferenceKey(ImportSceneReferenceDto? reference) + => reference is null + ? "(missing)" + : $"{reference.BookOrder?.ToString() ?? "?"}/{reference.ChapterOrder?.ToString() ?? "?"}/{reference.SceneOrder?.ToString() ?? "?"}"; + + private static string SceneDependencyKey(ImportSceneDependencyDto dependency) + => $"{SceneReferenceKey(dependency.FromScene)} -> {SceneReferenceKey(dependency.ToScene)} / {CleanDependencyType(dependency.DependencyType)}"; + + private static string CleanDependencyType(string? dependencyType) + => string.IsNullOrWhiteSpace(dependencyType) ? "Must Occur Before" : dependencyType.Trim(); + + private static bool IsSupportedDependencyStrength(string strength) + { + var normalized = strength.Trim(); + return normalized.Equals("hard", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("soft", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("strong", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("weak", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("required", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("optional", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("high", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("medium", StringComparison.OrdinalIgnoreCase) + || normalized.Equals("low", StringComparison.OrdinalIgnoreCase) + || (int.TryParse(normalized, out var numeric) && numeric is >= 1 and <= 10); + } + private static HashSet CharacterNames(PlotLineImportPackage package) => package.Characters .Where(x => !string.IsNullOrWhiteSpace(x.Name)) diff --git a/PlotLine/ViewModels/ImportViewModels.cs b/PlotLine/ViewModels/ImportViewModels.cs index 7d617be..c5c8843 100644 --- a/PlotLine/ViewModels/ImportViewModels.cs +++ b/PlotLine/ViewModels/ImportViewModels.cs @@ -21,7 +21,7 @@ public sealed class ImportIndexViewModel public static string SampleJson => """ { "packageVersion": "1.0", - "source": "JSON Import Phase 1E sample", + "source": "JSON Import Phase 1F sample", "project": { "title": "The Glass Meridian", "description": "A placeholder two-book fantasy outline.", @@ -148,6 +148,44 @@ public sealed class ImportIndexViewModel ] } ], + "sceneDependencies": [ + { + "fromScene": { "bookOrder": 1, "chapterOrder": 1, "sceneOrder": 1 }, + "toScene": { "bookOrder": 1, "chapterOrder": 1, "sceneOrder": 2 }, + "dependencyType": "Requires", + "strength": "Hard", + "description": "Mara needs to find the arcade door before choosing to descend.", + "notes": "Opening cause and effect." + }, + { + "fromScene": { "bookOrder": 1, "chapterOrder": 1, "sceneOrder": 2 }, + "toScene": { "bookOrder": 1, "chapterOrder": 2, "sceneOrder": 1 }, + "dependencyType": "Reveals", + "strength": "Strong", + "description": "The descent sets up the first view of Lantern Court." + }, + { + "fromScene": { "bookOrder": 1, "chapterOrder": 2, "sceneOrder": 2 }, + "toScene": { "bookOrder": 2, "chapterOrder": 1, "sceneOrder": 1 }, + "dependencyType": "Follows", + "strength": "Medium", + "description": "Ilen's bargain motivates the later departure." + }, + { + "fromScene": { "bookOrder": 2, "chapterOrder": 1, "sceneOrder": 2 }, + "toScene": { "bookOrder": 2, "chapterOrder": 2, "sceneOrder": 1 }, + "dependencyType": "Reveals", + "strength": "Hard", + "description": "The damaged signal plate must be recovered before Mara can translate it." + }, + { + "fromScene": { "bookOrder": 2, "chapterOrder": 2, "sceneOrder": 1 }, + "toScene": { "bookOrder": 2, "chapterOrder": 2, "sceneOrder": 2 }, + "dependencyType": "Blocks", + "strength": "7", + "description": "The warning turns the causeway crossing into a race." + } + ], "books": [ { "title": "Book One", diff --git a/PlotLine/Views/Imports/Index.cshtml b/PlotLine/Views/Imports/Index.cshtml index 056735b..d78885a 100644 --- a/PlotLine/Views/Imports/Index.cshtml +++ b/PlotLine/Views/Imports/Index.cshtml @@ -8,7 +8,7 @@

Tools

JSON Import

-

Paste or upload a Phase 1E package, preview it, confirm the summary, then import a new project.

+

Paste or upload a Phase 1F package, preview it, confirm the summary, then import a new project.

Download sample JSON @@ -65,11 +65,15 @@

Optional

-

packageVersion, source, top-level characters, top-level locations, top-level plotLines, top-level storyAssets, plot line threads, thread events, asset states, asset events, initial asset owner/location mapping, POV/location mapping, date/time text, purpose/outcome text, and sceneCharacters.

+

packageVersion, source, top-level characters, top-level locations, top-level plotLines, top-level storyAssets, top-level sceneDependencies, plot line threads, thread events, asset states, asset events, initial asset owner/location mapping, POV/location mapping, date/time text, purpose/outcome text, and sceneCharacters.

+
+
+

Scene links

+

sceneDependencies use fromScene and toScene objects. Link scenes with bookOrder, chapterOrder, and sceneOrder; sceneRef is optional future-friendly text. dependencyType, strength, description, and notes are optional. Warnings can be acknowledged; errors block import. Unknown fields are ignored safely.

Not imported yet

-

No AI extraction, Word/text parsing, manuscript parsing, scene dependency import, relationship graphs, merge/update, or export logic. Thread events and asset states/events link to scenes with bookOrder, chapterOrder, and sceneOrder; sceneRef is accepted as future-friendly text only.

+

No AI extraction, Word/text parsing, manuscript parsing, metrics import, continuity warning import, relationship graphs, merge/update, or export logic.

@@ -97,6 +101,7 @@ @preview.ResolvedAssetStateCount asset states @preview.ResolvedAssetEventCount asset events @preview.ResolvedAssetCustodyCount custody links + @preview.ResolvedSceneDependencyCount dependencies @@ -135,7 +140,7 @@ @if (preview.Package is not null) { - @if (preview.UnmatchedPovNames.Any() || preview.UnmatchedLocationNames.Any() || preview.UnmatchedSceneCharacterNames.Any() || preview.UnresolvedThreadEventReferences.Any() || preview.UnresolvedAssetReferences.Any() || preview.UnmatchedAssetOwnerNames.Any() || preview.UnmatchedAssetLocationNames.Any()) + @if (preview.UnmatchedPovNames.Any() || preview.UnmatchedLocationNames.Any() || preview.UnmatchedSceneCharacterNames.Any() || preview.UnresolvedThreadEventReferences.Any() || preview.UnresolvedAssetReferences.Any() || preview.UnmatchedAssetOwnerNames.Any() || preview.UnmatchedAssetLocationNames.Any() || preview.UnresolvedSceneDependencyReferences.Any()) {
@if (preview.UnmatchedPovNames.Any()) @@ -166,6 +171,10 @@ { Unmatched asset locations: @string.Join(", ", preview.UnmatchedAssetLocationNames) } + @if (preview.UnresolvedSceneDependencyReferences.Any()) + { + Unresolved dependencies: @string.Join(", ", preview.UnresolvedSceneDependencyReferences) + }
} @@ -229,6 +238,37 @@ } + @if (preview.Package.SceneDependencies.Any()) + { +
+ @foreach (var group in preview.Package.SceneDependencies.Select((dependency, index) => new { Dependency = dependency, Index = index + 1 }).GroupBy(x => $"{x.Dependency.FromScene?.BookOrder?.ToString() ?? "?"}/{x.Dependency.FromScene?.ChapterOrder?.ToString() ?? "?"}").OrderBy(x => x.Key)) + { +
+ + Dependencies from @group.Key + @group.Count() links + +
    + @foreach (var item in group.Take(12)) + { + var dependency = item.Dependency; + var fromScene = $"{dependency.FromScene?.BookOrder?.ToString() ?? "?"}/{dependency.FromScene?.ChapterOrder?.ToString() ?? "?"}/{dependency.FromScene?.SceneOrder?.ToString() ?? "?"}"; + var toScene = $"{dependency.ToScene?.BookOrder?.ToString() ?? "?"}/{dependency.ToScene?.ChapterOrder?.ToString() ?? "?"}/{dependency.ToScene?.SceneOrder?.ToString() ?? "?"}"; +
  • + @fromScene -> @toScene + @((dependency.DependencyType ?? "Dependency")) / @(dependency.Strength ?? "soft") +
  • + } + @if (group.Count() > 12) + { +
  • Showing first 12 links in this group.
  • + } +
+
+ } +
+ } + @if (preview.Package.StoryAssets.Any()) {
@@ -249,7 +289,7 @@

Confirm import

-

This will create @preview.Package.Project.Title with @preview.BookCount books, @preview.ChapterCount chapters, @preview.SceneCount scenes, @preview.CharacterCount characters, @preview.LocationCount locations, @preview.SceneCharacterAppearanceCount scene character appearances, @preview.PlotLineCount plot lines, @preview.PlotThreadCount plot threads, @preview.ResolvedThreadEventCount resolved thread events, @preview.StoryAssetCount story assets, @preview.ResolvedAssetStateCount resolved asset states, @preview.ResolvedAssetEventCount resolved asset events, and @preview.ResolvedAssetCustodyCount custody links.

+

This will create @preview.Package.Project.Title with @preview.BookCount books, @preview.ChapterCount chapters, @preview.SceneCount scenes, @preview.CharacterCount characters, @preview.LocationCount locations, @preview.SceneCharacterAppearanceCount scene character appearances, @preview.PlotLineCount plot lines, @preview.PlotThreadCount plot threads, @preview.ResolvedThreadEventCount resolved thread events, @preview.StoryAssetCount story assets, @preview.ResolvedAssetStateCount resolved asset states, @preview.ResolvedAssetEventCount resolved asset events, @preview.ResolvedAssetCustodyCount custody links, and @preview.ResolvedSceneDependencyCount scene dependencies.

@if (preview.HasDuplicateProjectTitle) {

Because this title already exists, the created project will be named @preview.DuplicateImportProjectTitle if you continue.

diff --git a/PlotLine/Views/Imports/Result.cshtml b/PlotLine/Views/Imports/Result.cshtml index 00d116d..cc1a4b2 100644 --- a/PlotLine/Views/Imports/Result.cshtml +++ b/PlotLine/Views/Imports/Result.cshtml @@ -30,6 +30,7 @@ @Model.AssetStatesCreated asset states created @Model.AssetEventsCreated asset events created @Model.AssetCustodyEventsCreated custody records created + @Model.SceneDependenciesCreated scene dependencies created
@if (Model.SkippedOptionalFields.Any())