Add preferred name alias semantics
This commit is contained in:
parent
3c016c904e
commit
bb62762b78
@ -194,6 +194,7 @@ var tests = new (string Name, Action Test)[]
|
|||||||
("Character merge is a reusable stored procedure workflow", CharacterMergeIsReusableStoredProcedureWorkflow),
|
("Character merge is a reusable stored procedure workflow", CharacterMergeIsReusableStoredProcedureWorkflow),
|
||||||
("Story Intelligence character review suppresses resolved narrator POV", StoryIntelligenceCharacterReviewSuppressesResolvedNarratorPov),
|
("Story Intelligence character review suppresses resolved narrator POV", StoryIntelligenceCharacterReviewSuppressesResolvedNarratorPov),
|
||||||
("Story Intelligence character review reuses structured appearance aliases", StoryIntelligenceCharacterReviewReusesStructuredAppearanceAliases),
|
("Story Intelligence character review reuses structured appearance aliases", StoryIntelligenceCharacterReviewReusesStructuredAppearanceAliases),
|
||||||
|
("Entity preferred names are peer aliases", EntityPreferredNamesArePeerAliases),
|
||||||
("Story Intelligence review evidence includes chapter scene labels", StoryIntelligenceReviewEvidenceIncludesChapterSceneLabels),
|
("Story Intelligence review evidence includes chapter scene labels", StoryIntelligenceReviewEvidenceIncludesChapterSceneLabels),
|
||||||
("Story Intelligence import enables project metric defaults before save", StoryIntelligenceImportEnablesProjectMetricDefaultsBeforeSave),
|
("Story Intelligence import enables project metric defaults before save", StoryIntelligenceImportEnablesProjectMetricDefaultsBeforeSave),
|
||||||
("Project metrics add defaults backfills scene values", ProjectMetricsAddDefaultsBackfillsSceneValues),
|
("Project metrics add defaults backfills scene values", ProjectMetricsAddDefaultsBackfillsSceneValues),
|
||||||
@ -5599,6 +5600,63 @@ static void StoryIntelligenceCharacterReviewReusesStructuredAppearanceAliases()
|
|||||||
Assert(service.Contains("Aliases = appearance.Aliases", StringComparison.Ordinal), "Structured appearance aliases should be reused without another AI call.");
|
Assert(service.Contains("Aliases = appearance.Aliases", StringComparison.Ordinal), "Structured appearance aliases should be reused without another AI call.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void EntityPreferredNamesArePeerAliases()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
var sql = File.ReadAllText(Path.Combine(root, "Sql/180_Phase27G_EntityPreferredNameAliases.sql"));
|
||||||
|
var repository = File.ReadAllText(Path.Combine(root, "Data/Repositories.cs"));
|
||||||
|
var services = File.ReadAllText(Path.Combine(root, "Services/CoreServices.cs"));
|
||||||
|
var characterController = File.ReadAllText(Path.Combine(root, "Controllers/CharactersController.cs"));
|
||||||
|
var locationController = File.ReadAllText(Path.Combine(root, "Controllers/LocationsController.cs"));
|
||||||
|
var characterDetails = File.ReadAllText(Path.Combine(root, "Views/Characters/Details.cshtml"));
|
||||||
|
var characterEdit = File.ReadAllText(Path.Combine(root, "Views/Characters/Edit.cshtml"));
|
||||||
|
var locationDetails = File.ReadAllText(Path.Combine(root, "Views/Locations/Details.cshtml"));
|
||||||
|
var locationEdit = File.ReadAllText(Path.Combine(root, "Views/Locations/Edit.cshtml"));
|
||||||
|
|
||||||
|
Assert(sql.Contains("INSERT dbo.CharacterAliases (CharacterID, Alias, SortOrder)", StringComparison.Ordinal), "Migration should backfill preferred character names into CharacterAliases.");
|
||||||
|
Assert(sql.Contains("INSERT dbo.LocationAliases (LocationID, Alias, SortOrder)", StringComparison.Ordinal), "Migration should backfill preferred location names into LocationAliases.");
|
||||||
|
Assert(sql.Contains("CREATE OR ALTER PROCEDURE dbo.Character_SetPreferredName", StringComparison.Ordinal), "Character preferred-name procedure should exist.");
|
||||||
|
Assert(sql.Contains("CREATE OR ALTER PROCEDURE dbo.Location_SetPreferredName", StringComparison.Ordinal), "Location preferred-name procedure should exist.");
|
||||||
|
Assert(sql.Contains("The preferred name must already belong to this character.", StringComparison.Ordinal), "Character preferred-name changes must verify the name belongs to the CharacterID.");
|
||||||
|
Assert(sql.Contains("The preferred name must already belong to this location.", StringComparison.Ordinal), "Location preferred-name changes must verify the name belongs to the LocationID.");
|
||||||
|
Assert(!sql.Contains("TargetAliasID", StringComparison.OrdinalIgnoreCase), "Alias migration must not introduce alias-to-alias links.");
|
||||||
|
Assert(!sql.Contains("ParentAliasID", StringComparison.OrdinalIgnoreCase), "Alias migration must not introduce alias chain parents.");
|
||||||
|
|
||||||
|
Assert(repository.Contains("Task SetPreferredNameAsync(int characterId, string preferredName)", StringComparison.Ordinal), "Character repository should expose preferred-name update by CharacterID.");
|
||||||
|
Assert(repository.Contains("\"dbo.Character_SetPreferredName\"", StringComparison.Ordinal), "Character repository should use the stored procedure.");
|
||||||
|
Assert(repository.Contains("Task SetPreferredNameAsync(int locationId, string preferredName)", StringComparison.Ordinal), "Location repository should expose preferred-name update by LocationID.");
|
||||||
|
Assert(repository.Contains("\"dbo.Location_SetPreferredName\"", StringComparison.Ordinal), "Location repository should use the stored procedure.");
|
||||||
|
Assert(services.Contains("SyncCharacterAliasesAsync(characterId, model.Aliases, model.CharacterName)", StringComparison.Ordinal), "Character saves should include the preferred display name in known names.");
|
||||||
|
Assert(services.Contains("SyncLocationAliasesAsync(locationId, model.Aliases, model.LocationName)", StringComparison.Ordinal), "Location saves should include the preferred display name in known names.");
|
||||||
|
Assert(services.Contains("await characters.SetPreferredNameAsync(characterId, preferredName)", StringComparison.Ordinal), "Character preferred-name service should only update the display label.");
|
||||||
|
Assert(services.Contains("await locations.SetPreferredNameAsync(locationId, preferredName)", StringComparison.Ordinal), "Location preferred-name service should only update the display label.");
|
||||||
|
|
||||||
|
Assert(characterController.Contains("SetPreferredName(int id, string preferredName)", StringComparison.Ordinal), "Character controller should expose Set as preferred.");
|
||||||
|
Assert(locationController.Contains("SetPreferredName(int id, string preferredName)", StringComparison.Ordinal), "Location controller should expose Set as preferred.");
|
||||||
|
Assert(characterDetails.Contains("Known names", StringComparison.Ordinal) && characterDetails.Contains("Set as preferred", StringComparison.Ordinal), "Character details should render known names and Set as preferred.");
|
||||||
|
Assert(locationDetails.Contains("Known names", StringComparison.Ordinal) && locationDetails.Contains("Set as preferred", StringComparison.Ordinal), "Location details should render known names and Set as preferred.");
|
||||||
|
Assert(characterEdit.Contains("Known names", StringComparison.Ordinal) && !characterEdit.Contains(">Aliases</label>", StringComparison.Ordinal), "Character edit terminology should avoid alias-chain wording.");
|
||||||
|
Assert(locationEdit.Contains("Known names", StringComparison.Ordinal) && !locationEdit.Contains(">Aliases</label>", StringComparison.Ordinal), "Location edit terminology should avoid alias-chain wording.");
|
||||||
|
|
||||||
|
var characterId = 28;
|
||||||
|
var characterNames = new HashSet<string>(["Maggie", "Margaret", "Margaret Grant", "Miss Grant", "Grace"], StringComparer.OrdinalIgnoreCase);
|
||||||
|
var originalCharacterLinks = (Scenes: 4, Relationships: 3, Knowledge: 5, PlotEvents: 2);
|
||||||
|
var preferredCharacterName = "Margaret Grant";
|
||||||
|
Assert(characterId == 28 && characterNames.Contains(preferredCharacterName), "Fixture preferred name should belong directly to CharacterID 28.");
|
||||||
|
Assert(characterNames.SetEquals(["Maggie", "Margaret", "Margaret Grant", "Miss Grant", "Grace"]), "Changing preferred character name must leave all known names attached.");
|
||||||
|
Assert(originalCharacterLinks == (Scenes: 4, Relationships: 3, Knowledge: 5, PlotEvents: 2), "Changing preferred character name must not alter related foreign-key counts.");
|
||||||
|
preferredCharacterName = "Maggie";
|
||||||
|
Assert(characterId == 28 && characterNames.Contains(preferredCharacterName), "Preferred character name should be reversible without changing CharacterID.");
|
||||||
|
|
||||||
|
var locationId = 57;
|
||||||
|
var locationNames = new HashSet<string>(["Grant family home", "Maggie's flat", "Maggie's home", "23 The Doweries"], StringComparer.OrdinalIgnoreCase);
|
||||||
|
var originalLocationLinks = (ParentLocationID: 12, Scenes: 7, Assets: 3);
|
||||||
|
var preferredLocationName = "23 The Doweries";
|
||||||
|
Assert(locationId == 57 && locationNames.Contains(preferredLocationName), "Fixture preferred name should belong directly to LocationID 57.");
|
||||||
|
Assert(locationNames.SetEquals(["Grant family home", "Maggie's flat", "Maggie's home", "23 The Doweries"]), "Changing preferred location name must leave all known names attached.");
|
||||||
|
Assert(originalLocationLinks == (ParentLocationID: 12, Scenes: 7, Assets: 3), "Changing preferred location name must not alter parent, scene, or asset references.");
|
||||||
|
}
|
||||||
|
|
||||||
static void StoryIntelligenceReviewEvidenceIncludesChapterSceneLabels()
|
static void StoryIntelligenceReviewEvidenceIncludesChapterSceneLabels()
|
||||||
{
|
{
|
||||||
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
|||||||
@ -134,6 +134,23 @@ public sealed class CharactersController(ICharacterService characters) : Control
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> SetPreferredName(int id, string preferredName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await characters.SetPreferredNameAsync(id, preferredName);
|
||||||
|
TempData["CharacterMessage"] = $"Preferred name set to {preferredName}.";
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
TempData["CharacterMessage"] = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
private void ValidateSexWorkflow(CharacterEditViewModel model)
|
private void ValidateSexWorkflow(CharacterEditViewModel model)
|
||||||
{
|
{
|
||||||
if (model.SexValueID == CharacterEditViewModel.CustomSexValue)
|
if (model.SexValueID == CharacterEditViewModel.CustomSexValue)
|
||||||
|
|||||||
@ -61,6 +61,23 @@ public sealed class LocationsController(ILocationService locations) : Controller
|
|||||||
return RedirectToAction(nameof(Details), new { id = locationId });
|
return RedirectToAction(nameof(Details), new { id = locationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> SetPreferredName(int id, string preferredName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await locations.SetPreferredNameAsync(id, preferredName);
|
||||||
|
TempData["LocationMessage"] = $"Preferred name set to {preferredName}.";
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
TempData["LocationMessage"] = ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(Details), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> Archive(int id, int projectId)
|
public async Task<IActionResult> Archive(int id, int projectId)
|
||||||
|
|||||||
@ -218,6 +218,7 @@ public interface ICharacterRepository
|
|||||||
Task<int> AddAliasAsync(int characterId, string alias, int? sortOrder);
|
Task<int> AddAliasAsync(int characterId, string alias, int? sortOrder);
|
||||||
Task UpdateAliasAsync(int characterAliasId, string alias, int? sortOrder);
|
Task UpdateAliasAsync(int characterAliasId, string alias, int? sortOrder);
|
||||||
Task DeleteAliasAsync(int characterAliasId);
|
Task DeleteAliasAsync(int characterAliasId);
|
||||||
|
Task SetPreferredNameAsync(int characterId, string preferredName);
|
||||||
Task<CharacterMergeResult?> MergeAsync(int sourceCharacterId, int targetCharacterId, int? userId);
|
Task<CharacterMergeResult?> MergeAsync(int sourceCharacterId, int targetCharacterId, int? userId);
|
||||||
Task<IReadOnlyList<CharacterSexValue>> ListSexValuesAsync(int ownerUserId);
|
Task<IReadOnlyList<CharacterSexValue>> ListSexValuesAsync(int ownerUserId);
|
||||||
Task<CharacterSexValue?> GetSexValueAsync(int characterSexValueId, int ownerUserId);
|
Task<CharacterSexValue?> GetSexValueAsync(int characterSexValueId, int ownerUserId);
|
||||||
@ -267,6 +268,7 @@ public interface ILocationRepository
|
|||||||
Task<int> AddAliasAsync(int locationId, string alias, int? sortOrder);
|
Task<int> AddAliasAsync(int locationId, string alias, int? sortOrder);
|
||||||
Task UpdateAliasAsync(int locationAliasId, string alias, int? sortOrder);
|
Task UpdateAliasAsync(int locationAliasId, string alias, int? sortOrder);
|
||||||
Task DeleteAliasAsync(int locationAliasId);
|
Task DeleteAliasAsync(int locationAliasId);
|
||||||
|
Task SetPreferredNameAsync(int locationId, string preferredName);
|
||||||
Task ArchiveAsync(int locationId);
|
Task ArchiveAsync(int locationId);
|
||||||
Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByProjectAsync(int projectId);
|
Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByProjectAsync(int projectId);
|
||||||
Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByLocationAsync(int locationId);
|
Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByLocationAsync(int locationId);
|
||||||
@ -564,6 +566,15 @@ public sealed class LocationRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetPreferredNameAsync(int locationId, string preferredName)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
"dbo.Location_SetPreferredName",
|
||||||
|
new { LocationID = locationId, PreferredName = preferredName },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ArchiveAsync(int locationId)
|
public async Task ArchiveAsync(int locationId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
@ -3754,6 +3765,15 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetPreferredNameAsync(int characterId, string preferredName)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
"dbo.Character_SetPreferredName",
|
||||||
|
new { CharacterID = characterId, PreferredName = preferredName },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<CharacterMergeResult?> MergeAsync(int sourceCharacterId, int targetCharacterId, int? userId)
|
public async Task<CharacterMergeResult?> MergeAsync(int sourceCharacterId, int targetCharacterId, int? userId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
|||||||
@ -13,9 +13,11 @@ namespace PlotLine.Services;
|
|||||||
|
|
||||||
internal static class AliasInput
|
internal static class AliasInput
|
||||||
{
|
{
|
||||||
|
public static string CleanOne(string? alias) => (alias ?? string.Empty).Trim();
|
||||||
|
|
||||||
public static IReadOnlyList<string> Clean(IEnumerable<string>? aliases) =>
|
public static IReadOnlyList<string> Clean(IEnumerable<string>? aliases) =>
|
||||||
(aliases ?? [])
|
(aliases ?? [])
|
||||||
.Select(alias => (alias ?? string.Empty).Trim())
|
.Select(CleanOne)
|
||||||
.Where(alias => alias.Length > 0)
|
.Where(alias => alias.Length > 0)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.Take(50)
|
.Take(50)
|
||||||
@ -209,6 +211,7 @@ public interface ILocationService
|
|||||||
Task<LocationEditViewModel?> GetEditAsync(int locationId);
|
Task<LocationEditViewModel?> GetEditAsync(int locationId);
|
||||||
Task<LocationDetailViewModel?> GetDetailAsync(int locationId);
|
Task<LocationDetailViewModel?> GetDetailAsync(int locationId);
|
||||||
Task<int> SaveAsync(LocationEditViewModel model);
|
Task<int> SaveAsync(LocationEditViewModel model);
|
||||||
|
Task SetPreferredNameAsync(int locationId, string preferredName);
|
||||||
Task ArchiveAsync(int locationId);
|
Task ArchiveAsync(int locationId);
|
||||||
Task<int> SaveRelationshipAsync(LocationRelationshipEditViewModel model);
|
Task<int> SaveRelationshipAsync(LocationRelationshipEditViewModel model);
|
||||||
Task ArchiveRelationshipAsync(int locationRelationshipId);
|
Task ArchiveRelationshipAsync(int locationRelationshipId);
|
||||||
@ -265,6 +268,7 @@ public interface ICharacterService
|
|||||||
Task<CharacterMergeViewModel?> GetMergeAsync(int sourceCharacterId);
|
Task<CharacterMergeViewModel?> GetMergeAsync(int sourceCharacterId);
|
||||||
Task<CharacterAvatarCropViewModel?> GetAvatarCropAsync(int characterImageId);
|
Task<CharacterAvatarCropViewModel?> GetAvatarCropAsync(int characterImageId);
|
||||||
Task<CharacterSaveResult> SaveCharacterAsync(CharacterEditViewModel model);
|
Task<CharacterSaveResult> SaveCharacterAsync(CharacterEditViewModel model);
|
||||||
|
Task SetPreferredNameAsync(int characterId, string preferredName);
|
||||||
Task<CharacterMergeResult?> MergeAsync(CharacterMergeForm form);
|
Task<CharacterMergeResult?> MergeAsync(CharacterMergeForm form);
|
||||||
Task<VisualIdentityImageResult> UploadCharacterImageAsync(CharacterImageUploadViewModel model);
|
Task<VisualIdentityImageResult> UploadCharacterImageAsync(CharacterImageUploadViewModel model);
|
||||||
Task<VisualIdentityImageResult> SaveCharacterAvatarAsync(CharacterAvatarCropViewModel model);
|
Task<VisualIdentityImageResult> SaveCharacterAvatarAsync(CharacterAvatarCropViewModel model);
|
||||||
@ -7991,6 +7995,7 @@ public sealed class LocationService(
|
|||||||
{
|
{
|
||||||
Project = project,
|
Project = project,
|
||||||
Location = location,
|
Location = location,
|
||||||
|
Aliases = await locations.ListAliasesAsync(location.LocationID),
|
||||||
ChildLocations = allLocations.Where(x => x.ParentLocationID == location.LocationID).ToList(),
|
ChildLocations = allLocations.Where(x => x.ParentLocationID == location.LocationID).ToList(),
|
||||||
Relationships = await locations.ListRelationshipsByLocationAsync(location.LocationID),
|
Relationships = await locations.ListRelationshipsByLocationAsync(location.LocationID),
|
||||||
Scenes = await locations.ListScenesByLocationAsync(location.LocationID),
|
Scenes = await locations.ListScenesByLocationAsync(location.LocationID),
|
||||||
@ -8024,11 +8029,32 @@ public sealed class LocationService(
|
|||||||
ExcludeFromCompanionDetection = model.ExcludeFromCompanionDetection,
|
ExcludeFromCompanionDetection = model.ExcludeFromCompanionDetection,
|
||||||
DetectionPriority = model.DetectionPriority
|
DetectionPriority = model.DetectionPriority
|
||||||
});
|
});
|
||||||
await SyncLocationAliasesAsync(locationId, model.Aliases);
|
await SyncLocationAliasesAsync(locationId, model.Aliases, model.LocationName);
|
||||||
await activity.RecordAsync(model.ProjectID, isNew ? "Created" : "Updated", "Location", locationId, model.LocationName);
|
await activity.RecordAsync(model.ProjectID, isNew ? "Created" : "Updated", "Location", locationId, model.LocationName);
|
||||||
return locationId;
|
return locationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetPreferredNameAsync(int locationId, string preferredName)
|
||||||
|
{
|
||||||
|
var location = await locations.GetAsync(locationId);
|
||||||
|
if (location is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Location not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var knownNames = (await locations.ListAliasesAsync(locationId))
|
||||||
|
.Select(alias => alias.Alias)
|
||||||
|
.Append(location.LocationName)
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (!knownNames.Contains(AliasInput.CleanOne(preferredName)))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("The preferred name must already belong to this location.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await locations.SetPreferredNameAsync(locationId, preferredName);
|
||||||
|
await activity.RecordAsync(location.ProjectID, "Updated", "Location", locationId, preferredName, "Preferred location name changed.");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ArchiveAsync(int locationId)
|
public async Task ArchiveAsync(int locationId)
|
||||||
{
|
{
|
||||||
var location = await locations.GetAsync(locationId);
|
var location = await locations.GetAsync(locationId);
|
||||||
@ -8114,9 +8140,9 @@ public sealed class LocationService(
|
|||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SyncLocationAliasesAsync(int locationId, IEnumerable<string> postedAliases)
|
private async Task SyncLocationAliasesAsync(int locationId, IEnumerable<string> postedAliases, string preferredName)
|
||||||
{
|
{
|
||||||
var aliases = AliasInput.Clean(postedAliases);
|
var aliases = AliasInput.Clean(postedAliases.Append(preferredName));
|
||||||
var existing = await locations.ListAliasesAsync(locationId);
|
var existing = await locations.ListAliasesAsync(locationId);
|
||||||
var existingByAlias = existing.ToDictionary(x => x.Alias, StringComparer.OrdinalIgnoreCase);
|
var existingByAlias = existing.ToDictionary(x => x.Alias, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
@ -9364,7 +9390,7 @@ public sealed class CharacterService(
|
|||||||
ImagePath = model.ImagePath ?? existing?.ImagePath,
|
ImagePath = model.ImagePath ?? existing?.ImagePath,
|
||||||
ThumbnailPath = model.ThumbnailPath ?? existing?.ThumbnailPath
|
ThumbnailPath = model.ThumbnailPath ?? existing?.ThumbnailPath
|
||||||
});
|
});
|
||||||
await SyncCharacterAliasesAsync(characterId, model.Aliases);
|
await SyncCharacterAliasesAsync(characterId, model.Aliases, model.CharacterName);
|
||||||
var imageHost = existing ?? new Character { CharacterID = characterId, ProjectID = model.ProjectID };
|
var imageHost = existing ?? new Character { CharacterID = characterId, ProjectID = model.ProjectID };
|
||||||
imageHost.CharacterID = characterId;
|
imageHost.CharacterID = characterId;
|
||||||
imageHost.ProjectID = model.ProjectID;
|
imageHost.ProjectID = model.ProjectID;
|
||||||
@ -9387,6 +9413,27 @@ public sealed class CharacterService(
|
|||||||
return new CharacterSaveResult(characterId, uploadedCharacterImageId);
|
return new CharacterSaveResult(characterId, uploadedCharacterImageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetPreferredNameAsync(int characterId, string preferredName)
|
||||||
|
{
|
||||||
|
var character = await characters.GetCharacterAsync(characterId);
|
||||||
|
if (character is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Character not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var knownNames = (await characters.ListAliasesAsync(characterId))
|
||||||
|
.Select(alias => alias.Alias)
|
||||||
|
.Append(character.CharacterName)
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (!knownNames.Contains(AliasInput.CleanOne(preferredName)))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("The preferred name must already belong to this character.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await characters.SetPreferredNameAsync(characterId, preferredName);
|
||||||
|
await activity.RecordAsync(character.ProjectID, "Updated", "Character", characterId, preferredName, "Preferred character name changed.");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<CharacterMergeResult?> MergeAsync(CharacterMergeForm form)
|
public async Task<CharacterMergeResult?> MergeAsync(CharacterMergeForm form)
|
||||||
{
|
{
|
||||||
if (!form.Confirm)
|
if (!form.Confirm)
|
||||||
@ -9407,9 +9454,9 @@ public sealed class CharacterService(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SyncCharacterAliasesAsync(int characterId, IEnumerable<string> postedAliases)
|
private async Task SyncCharacterAliasesAsync(int characterId, IEnumerable<string> postedAliases, string preferredName)
|
||||||
{
|
{
|
||||||
var aliases = AliasInput.Clean(postedAliases);
|
var aliases = AliasInput.Clean(postedAliases.Append(preferredName));
|
||||||
var existing = await characters.ListAliasesAsync(characterId);
|
var existing = await characters.ListAliasesAsync(characterId);
|
||||||
var existingByAlias = existing.ToDictionary(x => x.Alias, StringComparer.OrdinalIgnoreCase);
|
var existingByAlias = existing.ToDictionary(x => x.Alias, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
|||||||
353
PlotLine/Sql/180_Phase27G_EntityPreferredNameAliases.sql
Normal file
353
PlotLine/Sql/180_Phase27G_EntityPreferredNameAliases.sql
Normal file
@ -0,0 +1,353 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
INSERT dbo.CharacterAliases (CharacterID, Alias, SortOrder)
|
||||||
|
SELECT c.CharacterID,
|
||||||
|
LTRIM(RTRIM(c.CharacterName)),
|
||||||
|
ISNULL((SELECT MAX(ca.SortOrder) FROM dbo.CharacterAliases ca WHERE ca.CharacterID = c.CharacterID), 0) + 10
|
||||||
|
FROM dbo.Characters c
|
||||||
|
WHERE NULLIF(LTRIM(RTRIM(c.CharacterName)), N'') IS NOT NULL
|
||||||
|
AND NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.CharacterAliases ca
|
||||||
|
WHERE ca.CharacterID = c.CharacterID
|
||||||
|
AND UPPER(LTRIM(RTRIM(ca.Alias))) = UPPER(LTRIM(RTRIM(c.CharacterName)))
|
||||||
|
);
|
||||||
|
GO
|
||||||
|
|
||||||
|
INSERT dbo.LocationAliases (LocationID, Alias, SortOrder)
|
||||||
|
SELECT l.LocationID,
|
||||||
|
LTRIM(RTRIM(l.LocationName)),
|
||||||
|
ISNULL((SELECT MAX(la.SortOrder) FROM dbo.LocationAliases la WHERE la.LocationID = l.LocationID), 0) + 10
|
||||||
|
FROM dbo.Locations l
|
||||||
|
WHERE NULLIF(LTRIM(RTRIM(l.LocationName)), N'') IS NOT NULL
|
||||||
|
AND NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.LocationAliases la
|
||||||
|
WHERE la.LocationID = l.LocationID
|
||||||
|
AND UPPER(LTRIM(RTRIM(la.Alias))) = UPPER(LTRIM(RTRIM(l.LocationName)))
|
||||||
|
);
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.CharacterAlias_Add
|
||||||
|
@CharacterID int,
|
||||||
|
@Alias nvarchar(200),
|
||||||
|
@SortOrder int = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SET @Alias = LTRIM(RTRIM(@Alias));
|
||||||
|
IF NULLIF(@Alias, N'') IS NULL
|
||||||
|
THROW 51000, 'Enter an alias before saving.', 1;
|
||||||
|
|
||||||
|
DECLARE @ExistingCharacterAliasID int;
|
||||||
|
SELECT TOP (1) @ExistingCharacterAliasID = CharacterAliasID
|
||||||
|
FROM dbo.CharacterAliases
|
||||||
|
WHERE CharacterID = @CharacterID
|
||||||
|
AND UPPER(LTRIM(RTRIM(Alias))) = UPPER(@Alias)
|
||||||
|
ORDER BY CharacterAliasID;
|
||||||
|
|
||||||
|
IF @ExistingCharacterAliasID IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
IF @SortOrder IS NOT NULL
|
||||||
|
UPDATE dbo.CharacterAliases SET SortOrder = @SortOrder WHERE CharacterAliasID = @ExistingCharacterAliasID;
|
||||||
|
SELECT @ExistingCharacterAliasID AS CharacterAliasID;
|
||||||
|
RETURN;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF @SortOrder IS NULL
|
||||||
|
SELECT @SortOrder = ISNULL(MAX(SortOrder), 0) + 10 FROM dbo.CharacterAliases WHERE CharacterID = @CharacterID;
|
||||||
|
|
||||||
|
INSERT dbo.CharacterAliases (CharacterID, Alias, SortOrder)
|
||||||
|
VALUES (@CharacterID, @Alias, @SortOrder);
|
||||||
|
|
||||||
|
SELECT CAST(SCOPE_IDENTITY() AS int) AS CharacterAliasID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.CharacterAlias_Update
|
||||||
|
@CharacterAliasID int,
|
||||||
|
@Alias nvarchar(200),
|
||||||
|
@SortOrder int = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SET @Alias = LTRIM(RTRIM(@Alias));
|
||||||
|
IF NULLIF(@Alias, N'') IS NULL
|
||||||
|
THROW 51000, 'Enter an alias before saving.', 1;
|
||||||
|
|
||||||
|
DECLARE @CharacterID int;
|
||||||
|
SELECT @CharacterID = CharacterID FROM dbo.CharacterAliases WHERE CharacterAliasID = @CharacterAliasID;
|
||||||
|
|
||||||
|
IF EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.CharacterAliases
|
||||||
|
WHERE CharacterID = @CharacterID
|
||||||
|
AND CharacterAliasID <> @CharacterAliasID
|
||||||
|
AND UPPER(LTRIM(RTRIM(Alias))) = UPPER(@Alias)
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
DELETE dbo.CharacterAliases WHERE CharacterAliasID = @CharacterAliasID;
|
||||||
|
RETURN;
|
||||||
|
END
|
||||||
|
|
||||||
|
UPDATE dbo.CharacterAliases
|
||||||
|
SET Alias = @Alias,
|
||||||
|
SortOrder = COALESCE(@SortOrder, SortOrder)
|
||||||
|
WHERE CharacterAliasID = @CharacterAliasID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.LocationAlias_Add
|
||||||
|
@LocationID int,
|
||||||
|
@Alias nvarchar(200),
|
||||||
|
@SortOrder int = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SET @Alias = LTRIM(RTRIM(@Alias));
|
||||||
|
IF NULLIF(@Alias, N'') IS NULL
|
||||||
|
THROW 51000, 'Enter an alias before saving.', 1;
|
||||||
|
|
||||||
|
DECLARE @ExistingLocationAliasID int;
|
||||||
|
SELECT TOP (1) @ExistingLocationAliasID = LocationAliasID
|
||||||
|
FROM dbo.LocationAliases
|
||||||
|
WHERE LocationID = @LocationID
|
||||||
|
AND UPPER(LTRIM(RTRIM(Alias))) = UPPER(@Alias)
|
||||||
|
ORDER BY LocationAliasID;
|
||||||
|
|
||||||
|
IF @ExistingLocationAliasID IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
IF @SortOrder IS NOT NULL
|
||||||
|
UPDATE dbo.LocationAliases SET SortOrder = @SortOrder WHERE LocationAliasID = @ExistingLocationAliasID;
|
||||||
|
SELECT @ExistingLocationAliasID AS LocationAliasID;
|
||||||
|
RETURN;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF @SortOrder IS NULL
|
||||||
|
SELECT @SortOrder = ISNULL(MAX(SortOrder), 0) + 10 FROM dbo.LocationAliases WHERE LocationID = @LocationID;
|
||||||
|
|
||||||
|
INSERT dbo.LocationAliases (LocationID, Alias, SortOrder)
|
||||||
|
VALUES (@LocationID, @Alias, @SortOrder);
|
||||||
|
|
||||||
|
SELECT CAST(SCOPE_IDENTITY() AS int) AS LocationAliasID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.LocationAlias_Update
|
||||||
|
@LocationAliasID int,
|
||||||
|
@Alias nvarchar(200),
|
||||||
|
@SortOrder int = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SET @Alias = LTRIM(RTRIM(@Alias));
|
||||||
|
IF NULLIF(@Alias, N'') IS NULL
|
||||||
|
THROW 51000, 'Enter an alias before saving.', 1;
|
||||||
|
|
||||||
|
DECLARE @LocationID int;
|
||||||
|
SELECT @LocationID = LocationID FROM dbo.LocationAliases WHERE LocationAliasID = @LocationAliasID;
|
||||||
|
|
||||||
|
IF EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.LocationAliases
|
||||||
|
WHERE LocationID = @LocationID
|
||||||
|
AND LocationAliasID <> @LocationAliasID
|
||||||
|
AND UPPER(LTRIM(RTRIM(Alias))) = UPPER(@Alias)
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
DELETE dbo.LocationAliases WHERE LocationAliasID = @LocationAliasID;
|
||||||
|
RETURN;
|
||||||
|
END
|
||||||
|
|
||||||
|
UPDATE dbo.LocationAliases
|
||||||
|
SET Alias = @Alias,
|
||||||
|
SortOrder = COALESCE(@SortOrder, SortOrder)
|
||||||
|
WHERE LocationAliasID = @LocationAliasID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Character_SetPreferredName
|
||||||
|
@CharacterID int,
|
||||||
|
@PreferredName nvarchar(200)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SET @PreferredName = LTRIM(RTRIM(@PreferredName));
|
||||||
|
IF NULLIF(@PreferredName, N'') IS NULL
|
||||||
|
THROW 51000, 'Choose a known name before setting the preferred name.', 1;
|
||||||
|
|
||||||
|
IF NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.CharacterAliases
|
||||||
|
WHERE CharacterID = @CharacterID
|
||||||
|
AND UPPER(LTRIM(RTRIM(Alias))) = UPPER(@PreferredName)
|
||||||
|
)
|
||||||
|
THROW 51000, 'The preferred name must already belong to this character.', 1;
|
||||||
|
|
||||||
|
UPDATE dbo.Characters
|
||||||
|
SET CharacterName = @PreferredName,
|
||||||
|
UpdatedDate = SYSUTCDATETIME()
|
||||||
|
WHERE CharacterID = @CharacterID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Location_SetPreferredName
|
||||||
|
@LocationID int,
|
||||||
|
@PreferredName nvarchar(200)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SET @PreferredName = LTRIM(RTRIM(@PreferredName));
|
||||||
|
IF NULLIF(@PreferredName, N'') IS NULL
|
||||||
|
THROW 51000, 'Choose a known name before setting the preferred name.', 1;
|
||||||
|
|
||||||
|
IF NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.LocationAliases
|
||||||
|
WHERE LocationID = @LocationID
|
||||||
|
AND UPPER(LTRIM(RTRIM(Alias))) = UPPER(@PreferredName)
|
||||||
|
)
|
||||||
|
THROW 51000, 'The preferred name must already belong to this location.', 1;
|
||||||
|
|
||||||
|
UPDATE dbo.Locations
|
||||||
|
SET LocationName = @PreferredName,
|
||||||
|
UpdatedDate = SYSUTCDATETIME()
|
||||||
|
WHERE LocationID = @LocationID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Character_Save
|
||||||
|
@CharacterID int = NULL,
|
||||||
|
@ProjectID int,
|
||||||
|
@CharacterName nvarchar(200),
|
||||||
|
@ShortName nvarchar(100) = NULL,
|
||||||
|
@SexValueID int = NULL,
|
||||||
|
@Sex nvarchar(100) = NULL,
|
||||||
|
@BirthDate date = NULL,
|
||||||
|
@AgeAtSeriesStart int = NULL,
|
||||||
|
@AgeReferenceSceneID int = NULL,
|
||||||
|
@Height nvarchar(50) = NULL,
|
||||||
|
@EyeColour nvarchar(50) = NULL,
|
||||||
|
@CharacterImportance int = NULL,
|
||||||
|
@ShowInQuickAddBar bit = 0,
|
||||||
|
@DefaultDescription nvarchar(max) = NULL,
|
||||||
|
@ImagePath nvarchar(500) = NULL,
|
||||||
|
@ThumbnailPath nvarchar(500) = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SET @CharacterName = LTRIM(RTRIM(@CharacterName));
|
||||||
|
|
||||||
|
IF @CharacterID IS NULL OR @CharacterID = 0
|
||||||
|
BEGIN
|
||||||
|
INSERT dbo.Characters (ProjectID, CharacterName, ShortName, SexValueID, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, CharacterImportance, ShowInQuickAddBar, DefaultDescription, ImagePath, ThumbnailPath)
|
||||||
|
VALUES (@ProjectID, @CharacterName, @ShortName, @SexValueID, @Sex, @BirthDate, @AgeAtSeriesStart, @AgeReferenceSceneID, @Height, @EyeColour, @CharacterImportance, @ShowInQuickAddBar, @DefaultDescription, @ImagePath, @ThumbnailPath);
|
||||||
|
SET @CharacterID = CAST(SCOPE_IDENTITY() AS int);
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
UPDATE dbo.Characters
|
||||||
|
SET CharacterName = @CharacterName, ShortName = @ShortName, SexValueID = @SexValueID, Sex = @Sex, BirthDate = @BirthDate,
|
||||||
|
AgeAtSeriesStart = @AgeAtSeriesStart, AgeReferenceSceneID = @AgeReferenceSceneID,
|
||||||
|
Height = @Height, EyeColour = @EyeColour, CharacterImportance = @CharacterImportance,
|
||||||
|
ShowInQuickAddBar = @ShowInQuickAddBar, DefaultDescription = @DefaultDescription,
|
||||||
|
ImagePath = @ImagePath, ThumbnailPath = @ThumbnailPath,
|
||||||
|
UpdatedDate = SYSUTCDATETIME()
|
||||||
|
WHERE CharacterID = @CharacterID;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NULLIF(@CharacterName, N'') IS NOT NULL
|
||||||
|
AND NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.CharacterAliases
|
||||||
|
WHERE CharacterID = @CharacterID
|
||||||
|
AND UPPER(LTRIM(RTRIM(Alias))) = UPPER(@CharacterName)
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
INSERT dbo.CharacterAliases (CharacterID, Alias, SortOrder)
|
||||||
|
SELECT @CharacterID,
|
||||||
|
@CharacterName,
|
||||||
|
ISNULL(MAX(SortOrder), 0) + 10
|
||||||
|
FROM dbo.CharacterAliases
|
||||||
|
WHERE CharacterID = @CharacterID;
|
||||||
|
END
|
||||||
|
|
||||||
|
SELECT @CharacterID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Location_Save
|
||||||
|
@LocationID int = NULL,
|
||||||
|
@ProjectID int,
|
||||||
|
@ParentLocationID int = NULL,
|
||||||
|
@LocationName nvarchar(200),
|
||||||
|
@LocationTypeID int = NULL,
|
||||||
|
@Description nvarchar(max) = NULL,
|
||||||
|
@ShowInQuickAddBar bit = 0,
|
||||||
|
@ExcludeFromCompanionDetection bit = 0,
|
||||||
|
@DetectionPriority int = 50
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SET @LocationName = LTRIM(RTRIM(@LocationName));
|
||||||
|
SET @DetectionPriority = COALESCE(@DetectionPriority, 50);
|
||||||
|
|
||||||
|
IF @LocationID IS NULL OR @LocationID = 0
|
||||||
|
BEGIN
|
||||||
|
INSERT dbo.Locations (ProjectID, ParentLocationID, LocationName, LocationTypeID, Description, ShowInQuickAddBar, ExcludeFromCompanionDetection, DetectionPriority)
|
||||||
|
VALUES (@ProjectID, @ParentLocationID, @LocationName, @LocationTypeID, @Description, @ShowInQuickAddBar, @ExcludeFromCompanionDetection, @DetectionPriority);
|
||||||
|
|
||||||
|
SET @LocationID = CAST(SCOPE_IDENTITY() AS int);
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
UPDATE dbo.Locations
|
||||||
|
SET ParentLocationID = @ParentLocationID,
|
||||||
|
LocationName = @LocationName,
|
||||||
|
LocationTypeID = @LocationTypeID,
|
||||||
|
Description = @Description,
|
||||||
|
ShowInQuickAddBar = @ShowInQuickAddBar,
|
||||||
|
ExcludeFromCompanionDetection = @ExcludeFromCompanionDetection,
|
||||||
|
DetectionPriority = @DetectionPriority,
|
||||||
|
UpdatedDate = SYSUTCDATETIME()
|
||||||
|
WHERE LocationID = @LocationID;
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NULLIF(@LocationName, N'') IS NOT NULL
|
||||||
|
AND NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.LocationAliases
|
||||||
|
WHERE LocationID = @LocationID
|
||||||
|
AND UPPER(LTRIM(RTRIM(Alias))) = UPPER(@LocationName)
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
INSERT dbo.LocationAliases (LocationID, Alias, SortOrder)
|
||||||
|
SELECT @LocationID,
|
||||||
|
@LocationName,
|
||||||
|
ISNULL(MAX(SortOrder), 0) + 10
|
||||||
|
FROM dbo.LocationAliases
|
||||||
|
WHERE LocationID = @LocationID;
|
||||||
|
END
|
||||||
|
|
||||||
|
SELECT @LocationID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -2160,6 +2160,7 @@ public sealed class LocationDetailViewModel
|
|||||||
{
|
{
|
||||||
public Project Project { get; set; } = new();
|
public Project Project { get; set; } = new();
|
||||||
public LocationItem Location { get; set; } = new();
|
public LocationItem Location { get; set; } = new();
|
||||||
|
public IReadOnlyList<LocationAlias> Aliases { get; set; } = [];
|
||||||
public IReadOnlyList<LocationItem> ChildLocations { get; set; } = [];
|
public IReadOnlyList<LocationItem> ChildLocations { get; set; } = [];
|
||||||
public IReadOnlyList<LocationRelationship> Relationships { get; set; } = [];
|
public IReadOnlyList<LocationRelationship> Relationships { get; set; } = [];
|
||||||
public IReadOnlyList<Scene> Scenes { get; set; } = [];
|
public IReadOnlyList<Scene> Scenes { get; set; } = [];
|
||||||
|
|||||||
@ -11,6 +11,14 @@
|
|||||||
_ => "Timeline fallback"
|
_ => "Timeline fallback"
|
||||||
};
|
};
|
||||||
var characterLookup = Model.ProjectCharacters.ToDictionary(x => x.CharacterID);
|
var characterLookup = Model.ProjectCharacters.ToDictionary(x => x.CharacterID);
|
||||||
|
var knownNames = Model.Aliases
|
||||||
|
.Select(x => x.Alias)
|
||||||
|
.Append(Model.Character.CharacterName)
|
||||||
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(name => !string.Equals(name, Model.Character.CharacterName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ThenBy(name => name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
||||||
@ -81,13 +89,37 @@
|
|||||||
<section id="character-overview" class="asset-detail-grid">
|
<section id="character-overview" class="asset-detail-grid">
|
||||||
<article class="plain-card">
|
<article class="plain-card">
|
||||||
<h2>Profile</h2>
|
<h2>Profile</h2>
|
||||||
<p><strong>Aliases:</strong> @(Model.Aliases.Any() ? string.Join(", ", Model.Aliases.Select(x => x.Alias)) : "None")</p>
|
<p><strong>Preferred name:</strong> @Model.Character.CharacterName</p>
|
||||||
<p><strong>Sex:</strong> @Model.Character.Sex</p>
|
<p><strong>Sex:</strong> @Model.Character.Sex</p>
|
||||||
<p><strong>Age:</strong> @Model.DisplayAge</p>
|
<p><strong>Age:</strong> @Model.DisplayAge</p>
|
||||||
<p><strong>Height:</strong> @Model.Character.Height</p>
|
<p><strong>Height:</strong> @Model.Character.Height</p>
|
||||||
<p><strong>Eye colour:</strong> @Model.Character.EyeColour</p>
|
<p><strong>Eye colour:</strong> @Model.Character.EyeColour</p>
|
||||||
<p><strong>Story importance:</strong> @storyImportance</p>
|
<p><strong>Story importance:</strong> @storyImportance</p>
|
||||||
</article>
|
</article>
|
||||||
|
<article class="plain-card">
|
||||||
|
<h2>Known names</h2>
|
||||||
|
<div class="asset-mini-list">
|
||||||
|
@foreach (var name in knownNames)
|
||||||
|
{
|
||||||
|
var isPreferred = string.Equals(name, Model.Character.CharacterName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
<div class="asset-mini-item">
|
||||||
|
<strong>@name</strong>
|
||||||
|
@if (isPreferred)
|
||||||
|
{
|
||||||
|
<span class="status-pill">Preferred</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<form asp-action="SetPreferredName" method="post">
|
||||||
|
<input type="hidden" name="id" value="@Model.Character.CharacterID" />
|
||||||
|
<input type="hidden" name="preferredName" value="@name" />
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="submit">Set as preferred</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
<article class="plain-card">
|
<article class="plain-card">
|
||||||
<h2>Initial Relationships <help-icon key="characters.initialRelationships" /></h2>
|
<h2>Initial Relationships <help-icon key="characters.initialRelationships" /></h2>
|
||||||
@if (!Model.InitialRelationships.Any())
|
@if (!Model.InitialRelationships.Any())
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
<span asp-validation-for="CharacterName" class="text-danger"></span>
|
<span asp-validation-for="CharacterName" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label">Aliases</label>
|
<label class="form-label">Known names</label>
|
||||||
<div class="d-flex flex-wrap gap-2 mb-2" data-alias-list>
|
<div class="d-flex flex-wrap gap-2 mb-2" data-alias-list>
|
||||||
@for (var i = 0; i < Model.Aliases.Count; i++)
|
@for (var i = 0; i < Model.Aliases.Count; i++)
|
||||||
{
|
{
|
||||||
@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="form-control" type="text" maxlength="200" placeholder="Add an alias" data-alias-input />
|
<input class="form-control" type="text" maxlength="200" placeholder="Add another name" data-alias-input />
|
||||||
<button class="btn btn-outline-secondary" type="button" data-add-alias>Add</button>
|
<button class="btn btn-outline-secondary" type="button" data-add-alias>Add</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,6 +3,14 @@
|
|||||||
ViewData["Title"] = Model.Location.LocationName;
|
ViewData["Title"] = Model.Location.LocationName;
|
||||||
ViewData["ProjectSection"] = "Locations";
|
ViewData["ProjectSection"] = "Locations";
|
||||||
var firstScene = Model.Scenes.FirstOrDefault();
|
var firstScene = Model.Scenes.FirstOrDefault();
|
||||||
|
var knownNames = Model.Aliases
|
||||||
|
.Select(x => x.Alias)
|
||||||
|
.Append(Model.Location.LocationName)
|
||||||
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(name => !string.Equals(name, Model.Location.LocationName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ThenBy(name => name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
||||||
@ -49,6 +57,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@if (TempData["LocationMessage"] is string locationMessage)
|
||||||
|
{
|
||||||
|
<div class="alert alert-info">@locationMessage</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<section class="edit-panel">
|
||||||
|
<h2>Known names</h2>
|
||||||
|
<div class="asset-mini-list">
|
||||||
|
@foreach (var name in knownNames)
|
||||||
|
{
|
||||||
|
var isPreferred = string.Equals(name, Model.Location.LocationName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
<div class="asset-mini-item">
|
||||||
|
<strong>@name</strong>
|
||||||
|
@if (isPreferred)
|
||||||
|
{
|
||||||
|
<span class="status-pill">Preferred</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<form asp-action="SetPreferredName" method="post">
|
||||||
|
<input type="hidden" name="id" value="@Model.Location.LocationID" />
|
||||||
|
<input type="hidden" name="preferredName" value="@name" />
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" type="submit">Set as preferred</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="edit-panel">
|
<section class="edit-panel">
|
||||||
<h2>Child locations <help-icon key="locations.childLocations" /></h2>
|
<h2>Child locations <help-icon key="locations.childLocations" /></h2>
|
||||||
@if (!Model.ChildLocations.Any())
|
@if (!Model.ChildLocations.Any())
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
<span asp-validation-for="LocationName" class="text-danger"></span>
|
<span asp-validation-for="LocationName" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label">Aliases</label>
|
<label class="form-label">Known names</label>
|
||||||
<div class="d-flex flex-wrap gap-2 mb-2" data-alias-list>
|
<div class="d-flex flex-wrap gap-2 mb-2" data-alias-list>
|
||||||
@for (var i = 0; i < Model.Aliases.Count; i++)
|
@for (var i = 0; i < Model.Aliases.Count; i++)
|
||||||
{
|
{
|
||||||
@ -46,7 +46,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="form-control" type="text" maxlength="200" placeholder="Add an alias" data-alias-input />
|
<input class="form-control" type="text" maxlength="200" placeholder="Add another name" data-alias-input />
|
||||||
<button class="btn btn-outline-secondary" type="button" data-add-alias>Add</button>
|
<button class="btn btn-outline-secondary" type="button" data-add-alias>Add</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user