Phase 12C: Inferred Location Activity for Continuity Explorer.
This commit is contained in:
parent
dc4324f970
commit
c7e02870cf
@ -152,6 +152,7 @@ public interface IAssetRepository
|
|||||||
Task DeleteDependencyAsync(int assetDependencyId);
|
Task DeleteDependencyAsync(int assetDependencyId);
|
||||||
Task<IReadOnlyList<AssetCustodyEvent>> ListCustodyBySceneAsync(int sceneId);
|
Task<IReadOnlyList<AssetCustodyEvent>> ListCustodyBySceneAsync(int sceneId);
|
||||||
Task<IReadOnlyList<AssetCustodyEvent>> ListCustodyByAssetAsync(int storyAssetId);
|
Task<IReadOnlyList<AssetCustodyEvent>> ListCustodyByAssetAsync(int storyAssetId);
|
||||||
|
Task<IReadOnlyList<AssetCustodyCharacter>> ListCustodyCharactersByProjectAsync(int projectId);
|
||||||
Task<int> SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable<int> custodianCharacterIds, int custodyRoleId);
|
Task<int> SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable<int> custodianCharacterIds, int custodyRoleId);
|
||||||
Task DeleteCustodyEventAsync(int assetCustodyEventId);
|
Task DeleteCustodyEventAsync(int assetCustodyEventId);
|
||||||
Task<(IReadOnlyList<StoryAsset> Assets, IReadOnlyList<AssetEvent> Events)> GetTimelineAsync(int projectId, int? bookId);
|
Task<(IReadOnlyList<StoryAsset> Assets, IReadOnlyList<AssetEvent> Events)> GetTimelineAsync(int projectId, int? bookId);
|
||||||
@ -2775,6 +2776,31 @@ public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : I
|
|||||||
return rows.ToList();
|
return rows.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<AssetCustodyCharacter>> ListCustodyCharactersByProjectAsync(int projectId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<AssetCustodyCharacter>(
|
||||||
|
"""
|
||||||
|
SELECT acec.AssetCustodyEventID,
|
||||||
|
ace.StoryAssetID,
|
||||||
|
ace.SceneID,
|
||||||
|
acec.CharacterID,
|
||||||
|
c.CharacterName,
|
||||||
|
acec.CharacterNameText,
|
||||||
|
cr.RoleName,
|
||||||
|
ace.UpdatedDate
|
||||||
|
FROM dbo.AssetCustodyEventCharacters acec
|
||||||
|
INNER JOIN dbo.AssetCustodyEvents ace ON ace.AssetCustodyEventID = acec.AssetCustodyEventID
|
||||||
|
INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID
|
||||||
|
LEFT JOIN dbo.Characters c ON c.CharacterID = acec.CharacterID
|
||||||
|
LEFT JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = acec.CustodyRoleID
|
||||||
|
WHERE sa.ProjectID = @ProjectID
|
||||||
|
AND sa.IsArchived = 0;
|
||||||
|
""",
|
||||||
|
new { ProjectID = projectId });
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<int> SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable<int> custodianCharacterIds, int custodyRoleId)
|
public async Task<int> SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable<int> custodianCharacterIds, int custodyRoleId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
|||||||
@ -688,6 +688,18 @@ public sealed class AssetCustodyEvent
|
|||||||
public DateTime UpdatedDate { get; set; }
|
public DateTime UpdatedDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class AssetCustodyCharacter
|
||||||
|
{
|
||||||
|
public int AssetCustodyEventID { get; set; }
|
||||||
|
public int StoryAssetID { get; set; }
|
||||||
|
public int SceneID { get; set; }
|
||||||
|
public int? CharacterID { get; set; }
|
||||||
|
public string? CharacterName { get; set; }
|
||||||
|
public string? CharacterNameText { get; set; }
|
||||||
|
public string RoleName { get; set; } = string.Empty;
|
||||||
|
public DateTime UpdatedDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class AssetDependencyIssue
|
public sealed class AssetDependencyIssue
|
||||||
{
|
{
|
||||||
public int AssetDependencyID { get; set; }
|
public int AssetDependencyID { get; set; }
|
||||||
|
|||||||
@ -2448,27 +2448,46 @@ public sealed class StoryStateService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var characterTimeline = await characters.GetTimelineAsync(projectId, null);
|
var characterTimeline = await characters.GetTimelineAsync(projectId, null);
|
||||||
var assetLocations = await GetAssetLocationsThroughCurrentAsync(context.ScopedScenes);
|
var maxScopedSceneIndex = context.ScopedScenes
|
||||||
|
.Select(scene => context.SceneIndexes.GetValueOrDefault(scene.SceneID, -1))
|
||||||
|
.DefaultIfEmpty(-1)
|
||||||
|
.Max();
|
||||||
|
var scenesThroughRange = context.OrderedScenes
|
||||||
|
.Where(scene => context.SceneIndexes.GetValueOrDefault(scene.SceneID, -1) <= maxScopedSceneIndex)
|
||||||
|
.ToList();
|
||||||
|
var assetTimeline = await assets.GetTimelineAsync(projectId, null);
|
||||||
|
var assetLocations = await GetAssetLocationsThroughCurrentAsync(scenesThroughRange);
|
||||||
|
var custodyCharacters = await assets.ListCustodyCharactersByProjectAsync(projectId);
|
||||||
var characterIds = filter.CharacterIDs.ToHashSet();
|
var characterIds = filter.CharacterIDs.ToHashSet();
|
||||||
var assetIds = filter.AssetIDs.ToHashSet();
|
var assetIds = filter.AssetIDs.ToHashSet();
|
||||||
|
var characterAssignments = characterTimeline.Appearances
|
||||||
|
.Where(x => x.LocationID.HasValue)
|
||||||
|
.GroupBy(x => x.CharacterID)
|
||||||
|
.ToDictionary(x => x.Key, x => x.OrderBy(row => context.SceneIndexes.GetValueOrDefault(row.SceneID, -1)).ThenBy(row => row.UpdatedDate).ToList());
|
||||||
|
var assetAssignments = BuildAssetLocationAssignments(assetLocations, custodyCharacters)
|
||||||
|
.GroupBy(x => x.StoryAssetID)
|
||||||
|
.ToDictionary(x => x.Key, x => x.OrderBy(row => context.SceneIndexes.GetValueOrDefault(row.SceneID, -1)).ThenBy(row => row.UpdatedDate).ToList());
|
||||||
var result = new List<LocationActivityViewModel>();
|
var result = new List<LocationActivityViewModel>();
|
||||||
|
|
||||||
foreach (var scene in context.ScopedScenes)
|
foreach (var scene in context.ScopedScenes)
|
||||||
{
|
{
|
||||||
|
var sceneIndex = context.SceneIndexes.GetValueOrDefault(scene.SceneID, -1);
|
||||||
var sceneCharacters = filter.IncludeCharacters
|
var sceneCharacters = filter.IncludeCharacters
|
||||||
? characterTimeline.Appearances
|
? characterTimeline.Characters
|
||||||
.Where(x => x.SceneID == scene.SceneID && x.LocationID == locationId && (!characterIds.Any() || characterIds.Contains(x.CharacterID)))
|
.Where(character => !characterIds.Any() || characterIds.Contains(character.CharacterID))
|
||||||
.Select(x => x.CharacterName)
|
.Select(character => BuildCurrentCharacterPresence(character, characterAssignments.GetValueOrDefault(character.CharacterID, []), locationId, scene.SceneID, sceneIndex, context))
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Where(x => x is not null)
|
||||||
.OrderBy(x => x)
|
.Select(x => x!)
|
||||||
|
.OrderBy(x => x.EntityName)
|
||||||
.ToList()
|
.ToList()
|
||||||
: [];
|
: [];
|
||||||
var sceneAssets = filter.IncludeAssets
|
var sceneAssets = filter.IncludeAssets
|
||||||
? assetLocations
|
? assetTimeline.Assets
|
||||||
.Where(x => x.SceneID == scene.SceneID && x.LocationID == locationId && (!assetIds.Any() || assetIds.Contains(x.StoryAssetID)))
|
.Where(asset => !assetIds.Any() || assetIds.Contains(asset.StoryAssetID))
|
||||||
.Select(x => x.AssetName)
|
.Select(asset => BuildCurrentAssetPresence(asset, assetAssignments.GetValueOrDefault(asset.StoryAssetID, []), characterAssignments, locationId, scene.SceneID, sceneIndex, context))
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Where(x => x is not null)
|
||||||
.OrderBy(x => x)
|
.Select(x => x!)
|
||||||
|
.OrderBy(x => x.EntityName)
|
||||||
.ToList()
|
.ToList()
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@ -2490,8 +2509,10 @@ public sealed class StoryStateService(
|
|||||||
SceneTitle = sceneContext?.SceneTitle ?? scene.SceneTitle,
|
SceneTitle = sceneContext?.SceneTitle ?? scene.SceneTitle,
|
||||||
SceneLabel = SceneLabel(scene.SceneNumber, scene.SceneTitle),
|
SceneLabel = SceneLabel(scene.SceneNumber, scene.SceneTitle),
|
||||||
SceneDisplayName = sceneContext?.SceneDisplayName ?? SceneLabel(scene.SceneNumber, scene.SceneTitle),
|
SceneDisplayName = sceneContext?.SceneDisplayName ?? SceneLabel(scene.SceneNumber, scene.SceneTitle),
|
||||||
CharactersPresent = sceneCharacters.Any() ? string.Join(", ", sceneCharacters) : "None",
|
Characters = sceneCharacters,
|
||||||
AssetsPresent = sceneAssets.Any() ? string.Join(", ", sceneAssets) : "None"
|
Assets = sceneAssets,
|
||||||
|
CharactersPresent = sceneCharacters.Any() ? string.Join(", ", sceneCharacters.Select(x => x.EntityName)) : "None",
|
||||||
|
AssetsPresent = sceneAssets.Any() ? string.Join(", ", sceneAssets.Select(x => x.EntityName)) : "None"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2505,6 +2526,7 @@ public sealed class StoryStateService(
|
|||||||
var orderedScenes = OrderScenes(timeline).ToList();
|
var orderedScenes = OrderScenes(timeline).ToList();
|
||||||
var scopedScenes = ApplySceneRange(orderedScenes, timeline.Chapters, filter).ToList();
|
var scopedScenes = ApplySceneRange(orderedScenes, timeline.Chapters, filter).ToList();
|
||||||
return new RangeContext(
|
return new RangeContext(
|
||||||
|
orderedScenes,
|
||||||
scopedScenes,
|
scopedScenes,
|
||||||
scopedScenes.Select(x => x.SceneID).ToHashSet(),
|
scopedScenes.Select(x => x.SceneID).ToHashSet(),
|
||||||
BuildSceneIndexes(orderedScenes),
|
BuildSceneIndexes(orderedScenes),
|
||||||
@ -2713,6 +2735,146 @@ public sealed class StoryStateService(
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static LocationActivityEntityViewModel? BuildCurrentCharacterPresence(
|
||||||
|
Character character,
|
||||||
|
IReadOnlyList<SceneCharacter> assignments,
|
||||||
|
int locationId,
|
||||||
|
int currentSceneId,
|
||||||
|
int currentSceneIndex,
|
||||||
|
RangeContext context)
|
||||||
|
{
|
||||||
|
var latest = assignments
|
||||||
|
.Where(x => context.SceneIndexes.GetValueOrDefault(x.SceneID, -1) <= currentSceneIndex)
|
||||||
|
.OrderByDescending(x => context.SceneIndexes.GetValueOrDefault(x.SceneID, -1))
|
||||||
|
.ThenByDescending(x => x.UpdatedDate)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (latest?.LocationID != locationId)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LocationActivityEntityViewModel
|
||||||
|
{
|
||||||
|
EntityID = character.CharacterID,
|
||||||
|
EntityName = character.CharacterName,
|
||||||
|
EntityType = "Character",
|
||||||
|
StateType = latest.SceneID == currentSceneId ? "Explicit" : "Inferred",
|
||||||
|
LastConfirmedSceneID = latest.SceneID,
|
||||||
|
LastConfirmedSceneDisplayName = context.SceneLabels.GetValueOrDefault(latest.SceneID, SceneLabel(latest.SceneNumber, latest.SceneTitle))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LocationActivityEntityViewModel? BuildCurrentAssetPresence(
|
||||||
|
StoryAsset asset,
|
||||||
|
IReadOnlyList<AssetLocationActivityAssignment> assignments,
|
||||||
|
IReadOnlyDictionary<int, List<SceneCharacter>> characterAssignments,
|
||||||
|
int locationId,
|
||||||
|
int currentSceneId,
|
||||||
|
int currentSceneIndex,
|
||||||
|
RangeContext context)
|
||||||
|
{
|
||||||
|
var latest = assignments
|
||||||
|
.Where(x => context.SceneIndexes.GetValueOrDefault(x.SceneID, -1) <= currentSceneIndex)
|
||||||
|
.OrderByDescending(x => context.SceneIndexes.GetValueOrDefault(x.SceneID, -1))
|
||||||
|
.ThenByDescending(x => x.Priority)
|
||||||
|
.ThenByDescending(x => x.UpdatedDate)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (latest is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var note = latest.Note;
|
||||||
|
if (latest.LocationID != locationId)
|
||||||
|
{
|
||||||
|
if (!latest.HolderCharacterIDs.Any())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var holderInLocation = latest.HolderCharacterIDs
|
||||||
|
.Select(holderId => ResolveCharacterLocationAtScene(characterAssignments.GetValueOrDefault(holderId, []), currentSceneIndex, context))
|
||||||
|
.FirstOrDefault(x => x?.LocationID == locationId);
|
||||||
|
|
||||||
|
if (holderInLocation is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
note = string.IsNullOrWhiteSpace(holderInLocation.CharacterName)
|
||||||
|
? latest.Note
|
||||||
|
: $"Via {holderInLocation.CharacterName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LocationActivityEntityViewModel
|
||||||
|
{
|
||||||
|
EntityID = asset.StoryAssetID,
|
||||||
|
EntityName = asset.AssetName,
|
||||||
|
EntityType = "Asset",
|
||||||
|
StateType = latest.SceneID == currentSceneId ? "Explicit" : "Inferred",
|
||||||
|
LastConfirmedSceneID = latest.SceneID,
|
||||||
|
LastConfirmedSceneDisplayName = context.SceneLabels.GetValueOrDefault(latest.SceneID, "Unknown scene"),
|
||||||
|
Note = note
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SceneCharacter? ResolveCharacterLocationAtScene(
|
||||||
|
IReadOnlyList<SceneCharacter> assignments,
|
||||||
|
int currentSceneIndex,
|
||||||
|
RangeContext context)
|
||||||
|
{
|
||||||
|
return assignments
|
||||||
|
.Where(x => context.SceneIndexes.GetValueOrDefault(x.SceneID, -1) <= currentSceneIndex)
|
||||||
|
.OrderByDescending(x => context.SceneIndexes.GetValueOrDefault(x.SceneID, -1))
|
||||||
|
.ThenByDescending(x => x.UpdatedDate)
|
||||||
|
.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<AssetLocationActivityAssignment> BuildAssetLocationAssignments(
|
||||||
|
IReadOnlyList<SceneAssetLocation> assetLocations,
|
||||||
|
IReadOnlyList<AssetCustodyCharacter> custodyCharacters)
|
||||||
|
{
|
||||||
|
var locationAssignments = assetLocations.Select(x => new AssetLocationActivityAssignment(
|
||||||
|
x.StoryAssetID,
|
||||||
|
x.SceneID,
|
||||||
|
x.LocationID,
|
||||||
|
[],
|
||||||
|
x.LocationPath,
|
||||||
|
2,
|
||||||
|
x.UpdatedDate,
|
||||||
|
null));
|
||||||
|
var custodyAssignments = custodyCharacters
|
||||||
|
.GroupBy(x => new { x.AssetCustodyEventID, x.StoryAssetID, x.SceneID })
|
||||||
|
.Select(group =>
|
||||||
|
{
|
||||||
|
var first = group.First();
|
||||||
|
var holderIds = group
|
||||||
|
.Select(x => x.CharacterID)
|
||||||
|
.Where(x => x.HasValue)
|
||||||
|
.Select(x => x!.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
var holderNames = group
|
||||||
|
.Select(x => x.CharacterName ?? x.CharacterNameText)
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
return new AssetLocationActivityAssignment(
|
||||||
|
group.Key.StoryAssetID,
|
||||||
|
group.Key.SceneID,
|
||||||
|
null,
|
||||||
|
holderIds,
|
||||||
|
string.Join(", ", holderNames),
|
||||||
|
3,
|
||||||
|
first.UpdatedDate,
|
||||||
|
holderIds.Any() ? null : "Named holder has no known character location");
|
||||||
|
});
|
||||||
|
|
||||||
|
return locationAssignments.Concat(custodyAssignments).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private static void FilterStoryState(StoryStateViewModel storyState, ContinuityFilter filter)
|
private static void FilterStoryState(StoryStateViewModel storyState, ContinuityFilter filter)
|
||||||
{
|
{
|
||||||
storyState.Characters = filter.IncludeCharacters
|
storyState.Characters = filter.IncludeCharacters
|
||||||
@ -2836,7 +2998,18 @@ public sealed class StoryStateService(
|
|||||||
int Priority,
|
int Priority,
|
||||||
DateTime UpdatedDate);
|
DateTime UpdatedDate);
|
||||||
|
|
||||||
|
private sealed record AssetLocationActivityAssignment(
|
||||||
|
int StoryAssetID,
|
||||||
|
int SceneID,
|
||||||
|
int? LocationID,
|
||||||
|
IReadOnlyList<int> HolderCharacterIDs,
|
||||||
|
string Label,
|
||||||
|
int Priority,
|
||||||
|
DateTime UpdatedDate,
|
||||||
|
string? Note);
|
||||||
|
|
||||||
private sealed record RangeContext(
|
private sealed record RangeContext(
|
||||||
|
IReadOnlyList<Scene> OrderedScenes,
|
||||||
IReadOnlyList<Scene> ScopedScenes,
|
IReadOnlyList<Scene> ScopedScenes,
|
||||||
IReadOnlySet<int> AllowedSceneIds,
|
IReadOnlySet<int> AllowedSceneIds,
|
||||||
IReadOnlyDictionary<int, int> SceneIndexes,
|
IReadOnlyDictionary<int, int> SceneIndexes,
|
||||||
|
|||||||
@ -355,6 +355,19 @@ public sealed class LocationActivityViewModel
|
|||||||
public string SceneDisplayName { get; set; } = string.Empty;
|
public string SceneDisplayName { get; set; } = string.Empty;
|
||||||
public string CharactersPresent { get; set; } = string.Empty;
|
public string CharactersPresent { get; set; } = string.Empty;
|
||||||
public string AssetsPresent { get; set; } = string.Empty;
|
public string AssetsPresent { get; set; } = string.Empty;
|
||||||
|
public List<LocationActivityEntityViewModel> Characters { get; set; } = [];
|
||||||
|
public List<LocationActivityEntityViewModel> Assets { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LocationActivityEntityViewModel
|
||||||
|
{
|
||||||
|
public int EntityID { get; set; }
|
||||||
|
public string EntityName { get; set; } = string.Empty;
|
||||||
|
public string EntityType { get; set; } = string.Empty;
|
||||||
|
public string StateType { get; set; } = string.Empty;
|
||||||
|
public int? LastConfirmedSceneID { get; set; }
|
||||||
|
public string LastConfirmedSceneDisplayName { get; set; } = string.Empty;
|
||||||
|
public string? Note { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class SceneWorkflowEditViewModel
|
public sealed class SceneWorkflowEditViewModel
|
||||||
|
|||||||
@ -364,8 +364,54 @@ else
|
|||||||
<td>@row.ChapterDisplayName</td>
|
<td>@row.ChapterDisplayName</td>
|
||||||
}
|
}
|
||||||
<td>@row.SceneDisplayName</td>
|
<td>@row.SceneDisplayName</td>
|
||||||
<td>@row.CharactersPresent</td>
|
<td>
|
||||||
<td>@row.AssetsPresent</td>
|
@if (!row.Characters.Any())
|
||||||
|
{
|
||||||
|
<span class="muted">None</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="d-grid gap-1">
|
||||||
|
@foreach (var character in row.Characters)
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<span>@character.EntityName</span>
|
||||||
|
<span class="badge @StateBadgeClass(character.StateType)">@character.StateType</span>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(character.LastConfirmedSceneDisplayName))
|
||||||
|
{
|
||||||
|
<small class="text-muted d-block">Last confirmed: @character.LastConfirmedSceneDisplayName</small>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (!row.Assets.Any())
|
||||||
|
{
|
||||||
|
<span class="muted">None</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="d-grid gap-1">
|
||||||
|
@foreach (var asset in row.Assets)
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<span>@asset.EntityName</span>
|
||||||
|
<span class="badge @StateBadgeClass(asset.StateType)">@asset.StateType</span>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(asset.Note))
|
||||||
|
{
|
||||||
|
<small class="text-muted"> @asset.Note</small>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(asset.LastConfirmedSceneDisplayName))
|
||||||
|
{
|
||||||
|
<small class="text-muted d-block">Last confirmed: @asset.LastConfirmedSceneDisplayName</small>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user