Phase 16A - Base Identity Model Cleanup and Chapter/Scene Foundation
This commit is contained in:
parent
62bf231f65
commit
3c48301732
@ -67,6 +67,7 @@ public sealed class BooksController(IBookService books) : Controller
|
||||
createModel.WritingStartDate = model.WritingStartDate;
|
||||
createModel.TargetCompletionDate = model.TargetCompletionDate;
|
||||
createModel.PublicationDate = model.PublicationDate;
|
||||
createModel.UsesExplicitScenes = model.UsesExplicitScenes;
|
||||
await books.PopulateEditContextAsync(createModel);
|
||||
return View("Edit", createModel);
|
||||
}
|
||||
|
||||
@ -118,7 +118,7 @@ public sealed class CharactersController(ICharacterService characters) : Control
|
||||
private static void CopyCharacterFormValues(CharacterEditViewModel source, CharacterEditViewModel target)
|
||||
{
|
||||
target.CharacterName = source.CharacterName;
|
||||
target.ShortName = source.ShortName;
|
||||
target.Aliases = source.Aliases;
|
||||
target.SexValueID = source.SexValueID;
|
||||
target.Sex = source.Sex;
|
||||
target.CustomSex = source.CustomSex;
|
||||
|
||||
@ -38,7 +38,21 @@ public sealed class LocationsController(ILocationService locations) : Controller
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View("Edit", model);
|
||||
var editModel = model.LocationID == 0
|
||||
? await locations.GetCreateAsync(model.ProjectID)
|
||||
: await locations.GetEditAsync(model.LocationID);
|
||||
if (editModel is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
editModel.ParentLocationID = model.ParentLocationID;
|
||||
editModel.LocationName = model.LocationName;
|
||||
editModel.Aliases = model.Aliases;
|
||||
editModel.LocationTypeID = model.LocationTypeID;
|
||||
editModel.Description = model.Description;
|
||||
editModel.ShowInQuickAddBar = model.ShowInQuickAddBar;
|
||||
return View("Edit", editModel);
|
||||
}
|
||||
|
||||
var locationId = await locations.SaveAsync(model);
|
||||
|
||||
@ -88,7 +88,16 @@ public sealed class ScenesController(
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Archive(int id, int chapterId)
|
||||
{
|
||||
await scenes.ArchiveAsync(id);
|
||||
try
|
||||
{
|
||||
await scenes.ArchiveAsync(id);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
TempData["ArchiveMessage"] = ex.Message;
|
||||
return RedirectToAction("Details", "Chapters", new { id = chapterId });
|
||||
}
|
||||
|
||||
TempData["ArchiveMessage"] = "Scene archived. You can restore it from Archived Items.";
|
||||
if (TryGetLocalReturnUrl(out var returnUrl))
|
||||
{
|
||||
|
||||
@ -110,6 +110,7 @@ public sealed class StoryAssetsController(IAssetService assets) : Controller
|
||||
}
|
||||
|
||||
model.AssetName = posted.AssetName;
|
||||
model.Aliases = posted.Aliases;
|
||||
model.AssetKindID = posted.AssetKindID;
|
||||
model.Description = posted.Description;
|
||||
model.Importance = posted.Importance;
|
||||
|
||||
@ -287,15 +287,31 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
||||
commandType: CommandType.StoredProcedure);
|
||||
chaptersCreated++;
|
||||
|
||||
foreach (var scene in chapter.Scenes.OrderBy(x => x.Order))
|
||||
var orderedScenes = chapter.Scenes.OrderBy(x => x.Order).ToList();
|
||||
var defaultSceneId = 0;
|
||||
if (orderedScenes.Count > 0)
|
||||
{
|
||||
defaultSceneId = (await connection.QueryAsync<Scene>(
|
||||
"dbo.Scene_ListByChapter",
|
||||
new { ChapterID = chapterId },
|
||||
transaction,
|
||||
commandType: CommandType.StoredProcedure))
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.ThenBy(x => x.SceneID)
|
||||
.Select(x => x.SceneID)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
for (var sceneIndex = 0; sceneIndex < orderedScenes.Count; sceneIndex++)
|
||||
{
|
||||
var scene = orderedScenes[sceneIndex];
|
||||
var primaryLocationId = ResolveId(locationMap, scene.LocationName, package.Aliases.Locations);
|
||||
var povCharacterId = ResolveId(characterMap, scene.PovCharacterName, package.Aliases.Characters);
|
||||
var sceneId = await connection.QuerySingleAsync<int>(
|
||||
"dbo.Scene_Save",
|
||||
new
|
||||
{
|
||||
SceneID = 0,
|
||||
SceneID = sceneIndex == 0 ? defaultSceneId : 0,
|
||||
ChapterID = chapterId,
|
||||
scene.SceneNumber,
|
||||
SceneTitle = Clean(scene.Title),
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using PlotLine.Models;
|
||||
|
||||
namespace PlotLine.Data;
|
||||
@ -159,6 +160,10 @@ public interface IAssetRepository
|
||||
Task<IReadOnlyList<StoryAsset>> ListAssetsAsync(int projectId);
|
||||
Task<StoryAsset?> GetAssetAsync(int storyAssetId);
|
||||
Task<int> SaveAssetAsync(StoryAsset asset);
|
||||
Task<IReadOnlyList<AssetAlias>> ListAliasesAsync(int storyAssetId);
|
||||
Task<int> AddAliasAsync(int storyAssetId, string alias, int? sortOrder);
|
||||
Task UpdateAliasAsync(int assetAliasId, string alias, int? sortOrder);
|
||||
Task DeleteAliasAsync(int assetAliasId);
|
||||
Task UpdateAssetImageAsync(int storyAssetId, string? imagePath, string? thumbnailPath);
|
||||
Task ArchiveAssetAsync(int storyAssetId);
|
||||
Task<IReadOnlyList<AssetEvent>> ListEventsBySceneAsync(int sceneId);
|
||||
@ -183,6 +188,10 @@ public interface ICharacterRepository
|
||||
Task<IReadOnlyList<Character>> ListCharactersAsync(int projectId);
|
||||
Task<Character?> GetCharacterAsync(int characterId);
|
||||
Task<int> SaveCharacterAsync(Character character);
|
||||
Task<IReadOnlyList<CharacterAlias>> ListAliasesAsync(int characterId);
|
||||
Task<int> AddAliasAsync(int characterId, string alias, int? sortOrder);
|
||||
Task UpdateAliasAsync(int characterAliasId, string alias, int? sortOrder);
|
||||
Task DeleteAliasAsync(int characterAliasId);
|
||||
Task<IReadOnlyList<CharacterSexValue>> ListSexValuesAsync(int ownerUserId);
|
||||
Task<CharacterSexValue?> GetSexValueAsync(int characterSexValueId, int ownerUserId);
|
||||
Task<int> GetOrCreateSexValueAsync(int ownerUserId, string sexName);
|
||||
@ -223,6 +232,10 @@ public interface ILocationRepository
|
||||
Task<IReadOnlyList<LocationItem>> ListByProjectAsync(int projectId);
|
||||
Task<LocationItem?> GetAsync(int locationId);
|
||||
Task<int> SaveAsync(LocationItem location);
|
||||
Task<IReadOnlyList<LocationAlias>> ListAliasesAsync(int locationId);
|
||||
Task<int> AddAliasAsync(int locationId, string alias, int? sortOrder);
|
||||
Task UpdateAliasAsync(int locationAliasId, string alias, int? sortOrder);
|
||||
Task DeleteAliasAsync(int locationAliasId);
|
||||
Task ArchiveAsync(int locationId);
|
||||
Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByProjectAsync(int projectId);
|
||||
Task<IReadOnlyList<LocationRelationship>> ListRelationshipsByLocationAsync(int locationId);
|
||||
@ -477,6 +490,43 @@ public sealed class LocationRepository(ISqlConnectionFactory connectionFactory)
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<LocationAlias>> ListAliasesAsync(int locationId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<LocationAlias>(
|
||||
"dbo.LocationAlias_ListByLocation",
|
||||
new { LocationID = locationId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<int> AddAliasAsync(int locationId, string alias, int? sortOrder)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
"dbo.LocationAlias_Add",
|
||||
new { LocationID = locationId, Alias = alias, SortOrder = sortOrder },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task UpdateAliasAsync(int locationAliasId, string alias, int? sortOrder)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
await connection.ExecuteAsync(
|
||||
"dbo.LocationAlias_Update",
|
||||
new { LocationAliasID = locationAliasId, Alias = alias, SortOrder = sortOrder },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task DeleteAliasAsync(int locationAliasId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
await connection.ExecuteAsync(
|
||||
"dbo.LocationAlias_Delete",
|
||||
new { LocationAliasID = locationAliasId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task ArchiveAsync(int locationId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
@ -2178,7 +2228,8 @@ public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IB
|
||||
book.TargetWordCount,
|
||||
book.WritingStartDate,
|
||||
book.TargetCompletionDate,
|
||||
book.PublicationDate
|
||||
book.PublicationDate,
|
||||
book.UsesExplicitScenes
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
@ -2337,7 +2388,14 @@ public sealed class SceneRepository(ISqlConnectionFactory connectionFactory) : I
|
||||
public async Task ArchiveAsync(int sceneId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
await connection.ExecuteAsync("dbo.Scene_Archive", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
|
||||
try
|
||||
{
|
||||
await connection.ExecuteAsync("dbo.Scene_Archive", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
catch (SqlException ex) when (ex.Number == 51000)
|
||||
{
|
||||
throw new InvalidOperationException(ex.Message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MoveAsync(int sceneId, string direction)
|
||||
@ -2977,6 +3035,43 @@ public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : I
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AssetAlias>> ListAliasesAsync(int storyAssetId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<AssetAlias>(
|
||||
"dbo.AssetAlias_ListByAsset",
|
||||
new { StoryAssetID = storyAssetId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<int> AddAliasAsync(int storyAssetId, string alias, int? sortOrder)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
"dbo.AssetAlias_Add",
|
||||
new { StoryAssetID = storyAssetId, Alias = alias, SortOrder = sortOrder },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task UpdateAliasAsync(int assetAliasId, string alias, int? sortOrder)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
await connection.ExecuteAsync(
|
||||
"dbo.AssetAlias_Update",
|
||||
new { AssetAliasID = assetAliasId, Alias = alias, SortOrder = sortOrder },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task DeleteAliasAsync(int assetAliasId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
await connection.ExecuteAsync(
|
||||
"dbo.AssetAlias_Delete",
|
||||
new { AssetAliasID = assetAliasId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task UpdateAssetImageAsync(int storyAssetId, string? imagePath, string? thumbnailPath)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
@ -3228,6 +3323,43 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CharacterAlias>> ListAliasesAsync(int characterId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<CharacterAlias>(
|
||||
"dbo.CharacterAlias_ListByCharacter",
|
||||
new { CharacterID = characterId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<int> AddAliasAsync(int characterId, string alias, int? sortOrder)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<int>(
|
||||
"dbo.CharacterAlias_Add",
|
||||
new { CharacterID = characterId, Alias = alias, SortOrder = sortOrder },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task UpdateAliasAsync(int characterAliasId, string alias, int? sortOrder)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
await connection.ExecuteAsync(
|
||||
"dbo.CharacterAlias_Update",
|
||||
new { CharacterAliasID = characterAliasId, Alias = alias, SortOrder = sortOrder },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task DeleteAliasAsync(int characterAliasId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
await connection.ExecuteAsync(
|
||||
"dbo.CharacterAlias_Delete",
|
||||
new { CharacterAliasID = characterAliasId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CharacterSexValue>> ListSexValuesAsync(int ownerUserId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
|
||||
@ -238,6 +238,7 @@ public sealed class Book
|
||||
public DateTime? WritingStartDate { get; set; }
|
||||
public DateTime? TargetCompletionDate { get; set; }
|
||||
public DateTime? PublicationDate { get; set; }
|
||||
public bool UsesExplicitScenes { get; set; }
|
||||
public string? CoverThumbnailPath { get; set; }
|
||||
public string? CoverStandardPath { get; set; }
|
||||
public int CurrentWordCount { get; set; }
|
||||
@ -254,6 +255,33 @@ public sealed class Book
|
||||
: null;
|
||||
}
|
||||
|
||||
public sealed class CharacterAlias
|
||||
{
|
||||
public int CharacterAliasID { get; set; }
|
||||
public int CharacterID { get; set; }
|
||||
public string Alias { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AssetAlias
|
||||
{
|
||||
public int AssetAliasID { get; set; }
|
||||
public int StoryAssetID { get; set; }
|
||||
public string Alias { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class LocationAlias
|
||||
{
|
||||
public int LocationAliasID { get; set; }
|
||||
public int LocationID { get; set; }
|
||||
public string Alias { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
}
|
||||
|
||||
public static class BookStatuses
|
||||
{
|
||||
public const string Planning = "Planning";
|
||||
|
||||
@ -10,6 +10,17 @@ using PlotLine.ViewModels;
|
||||
|
||||
namespace PlotLine.Services;
|
||||
|
||||
internal static class AliasInput
|
||||
{
|
||||
public static IReadOnlyList<string> Clean(IEnumerable<string>? aliases) =>
|
||||
(aliases ?? [])
|
||||
.Select(alias => (alias ?? string.Empty).Trim())
|
||||
.Where(alias => alias.Length > 0)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Take(50)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public interface IProjectService
|
||||
{
|
||||
Task<ProjectIndexViewModel> ListAsync();
|
||||
@ -1181,6 +1192,7 @@ public sealed class BookService(
|
||||
WritingStartDate = book.WritingStartDate,
|
||||
TargetCompletionDate = book.TargetCompletionDate,
|
||||
PublicationDate = book.PublicationDate,
|
||||
UsesExplicitScenes = book.UsesExplicitScenes,
|
||||
CoverThumbnailPath = book.CoverThumbnailPath ?? BookCoverService.PlaceholderPath,
|
||||
CoverStandardPath = book.CoverStandardPath,
|
||||
Project = await projects.GetAsync(book.ProjectID),
|
||||
@ -1227,7 +1239,8 @@ public sealed class BookService(
|
||||
TargetWordCount = model.TargetWordCount,
|
||||
WritingStartDate = model.WritingStartDate,
|
||||
TargetCompletionDate = model.TargetCompletionDate,
|
||||
PublicationDate = model.PublicationDate
|
||||
PublicationDate = model.PublicationDate,
|
||||
UsesExplicitScenes = model.UsesExplicitScenes
|
||||
});
|
||||
await activity.RecordAsync(model.ProjectID, isNew ? "Created" : "Updated", "Book", bookId, model.BookTitle);
|
||||
|
||||
@ -7479,6 +7492,7 @@ public sealed class AssetService(
|
||||
StoryAssetID = asset.StoryAssetID,
|
||||
ProjectID = asset.ProjectID,
|
||||
AssetName = asset.AssetName,
|
||||
Aliases = (await assets.ListAliasesAsync(asset.StoryAssetID)).Select(x => x.Alias).ToList(),
|
||||
AssetKindID = asset.AssetKindID,
|
||||
Description = asset.Description,
|
||||
Importance = asset.Importance,
|
||||
@ -7549,6 +7563,7 @@ public sealed class AssetService(
|
||||
ImagePath = model.ImagePath ?? existing?.ImagePath,
|
||||
ThumbnailPath = model.ThumbnailPath ?? existing?.ThumbnailPath
|
||||
});
|
||||
await SyncAssetAliasesAsync(assetId, model.Aliases);
|
||||
var imageHost = existing ?? new StoryAsset { StoryAssetID = assetId, ProjectID = model.ProjectID };
|
||||
imageHost.StoryAssetID = assetId;
|
||||
imageHost.ProjectID = model.ProjectID;
|
||||
@ -7664,6 +7679,33 @@ public sealed class AssetService(
|
||||
return model;
|
||||
}
|
||||
|
||||
private async Task SyncAssetAliasesAsync(int storyAssetId, IEnumerable<string> postedAliases)
|
||||
{
|
||||
var aliases = AliasInput.Clean(postedAliases);
|
||||
var existing = await assets.ListAliasesAsync(storyAssetId);
|
||||
var existingByAlias = existing.ToDictionary(x => x.Alias, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
for (var index = 0; index < aliases.Count; index++)
|
||||
{
|
||||
var alias = aliases[index];
|
||||
var sortOrder = (index + 1) * 10;
|
||||
if (existingByAlias.TryGetValue(alias, out var row))
|
||||
{
|
||||
await assets.UpdateAliasAsync(row.AssetAliasID, alias, sortOrder);
|
||||
}
|
||||
else
|
||||
{
|
||||
await assets.AddAliasAsync(storyAssetId, alias, sortOrder);
|
||||
}
|
||||
}
|
||||
|
||||
var postedSet = aliases.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var alias in existing.Where(x => !postedSet.Contains(x.Alias)))
|
||||
{
|
||||
await assets.DeleteAliasAsync(alias.AssetAliasID);
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<SelectListItem> OptionalStateList(IEnumerable<AssetState> states)
|
||||
=> states.Select(x => new SelectListItem(x.StateName, x.AssetStateID.ToString())).ToList();
|
||||
|
||||
@ -7710,6 +7752,7 @@ public sealed class LocationService(IProjectRepository projects, ILocationReposi
|
||||
ProjectID = location.ProjectID,
|
||||
ParentLocationID = location.ParentLocationID,
|
||||
LocationName = location.LocationName,
|
||||
Aliases = (await locations.ListAliasesAsync(location.LocationID)).Select(x => x.Alias).ToList(),
|
||||
LocationTypeID = location.LocationTypeID,
|
||||
Description = location.Description,
|
||||
ShowInQuickAddBar = location.ShowInQuickAddBar,
|
||||
@ -7768,6 +7811,7 @@ public sealed class LocationService(IProjectRepository projects, ILocationReposi
|
||||
Description = model.Description,
|
||||
ShowInQuickAddBar = model.ShowInQuickAddBar
|
||||
});
|
||||
await SyncLocationAliasesAsync(locationId, model.Aliases);
|
||||
await activity.RecordAsync(model.ProjectID, isNew ? "Created" : "Updated", "Location", locationId, model.LocationName);
|
||||
return locationId;
|
||||
}
|
||||
@ -7828,6 +7872,33 @@ public sealed class LocationService(IProjectRepository projects, ILocationReposi
|
||||
model.LocationTypeOptions = ChapterService.ToSelectList(lookups.LocationTypes, x => x.LocationTypeID, x => x.TypeName);
|
||||
return model;
|
||||
}
|
||||
|
||||
private async Task SyncLocationAliasesAsync(int locationId, IEnumerable<string> postedAliases)
|
||||
{
|
||||
var aliases = AliasInput.Clean(postedAliases);
|
||||
var existing = await locations.ListAliasesAsync(locationId);
|
||||
var existingByAlias = existing.ToDictionary(x => x.Alias, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
for (var index = 0; index < aliases.Count; index++)
|
||||
{
|
||||
var alias = aliases[index];
|
||||
var sortOrder = (index + 1) * 10;
|
||||
if (existingByAlias.TryGetValue(alias, out var row))
|
||||
{
|
||||
await locations.UpdateAliasAsync(row.LocationAliasID, alias, sortOrder);
|
||||
}
|
||||
else
|
||||
{
|
||||
await locations.AddAliasAsync(locationId, alias, sortOrder);
|
||||
}
|
||||
}
|
||||
|
||||
var postedSet = aliases.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var alias in existing.Where(x => !postedSet.Contains(x.Alias)))
|
||||
{
|
||||
await locations.DeleteAliasAsync(alias.LocationAliasID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FloorPlanService(
|
||||
@ -8880,7 +8951,7 @@ public sealed class CharacterService(
|
||||
CharacterID = character.CharacterID,
|
||||
ProjectID = character.ProjectID,
|
||||
CharacterName = character.CharacterName,
|
||||
ShortName = character.ShortName,
|
||||
Aliases = (await characters.ListAliasesAsync(character.CharacterID)).Select(x => x.Alias).ToList(),
|
||||
SexValueID = character.SexValueID,
|
||||
Sex = character.Sex,
|
||||
BirthDate = character.BirthDate,
|
||||
@ -8928,6 +8999,7 @@ public sealed class CharacterService(
|
||||
{
|
||||
Project = project,
|
||||
Character = character,
|
||||
Aliases = await characters.ListAliasesAsync(character.CharacterID),
|
||||
ProjectCharacters = projectCharacters,
|
||||
Images = await characters.ListCharacterImagesAsync(character.CharacterID),
|
||||
DisplayAge = DisplayAge(character),
|
||||
@ -9009,7 +9081,7 @@ public sealed class CharacterService(
|
||||
CharacterID = model.CharacterID,
|
||||
ProjectID = model.ProjectID,
|
||||
CharacterName = model.CharacterName,
|
||||
ShortName = model.ShortName,
|
||||
ShortName = existing?.ShortName,
|
||||
SexValueID = sex.SexValueID,
|
||||
Sex = sex.SexName,
|
||||
BirthDate = model.BirthDate,
|
||||
@ -9022,6 +9094,7 @@ public sealed class CharacterService(
|
||||
ImagePath = model.ImagePath ?? existing?.ImagePath,
|
||||
ThumbnailPath = model.ThumbnailPath ?? existing?.ThumbnailPath
|
||||
});
|
||||
await SyncCharacterAliasesAsync(characterId, model.Aliases);
|
||||
var imageHost = existing ?? new Character { CharacterID = characterId, ProjectID = model.ProjectID };
|
||||
imageHost.CharacterID = characterId;
|
||||
imageHost.ProjectID = model.ProjectID;
|
||||
@ -9044,6 +9117,33 @@ public sealed class CharacterService(
|
||||
return new CharacterSaveResult(characterId, uploadedCharacterImageId);
|
||||
}
|
||||
|
||||
private async Task SyncCharacterAliasesAsync(int characterId, IEnumerable<string> postedAliases)
|
||||
{
|
||||
var aliases = AliasInput.Clean(postedAliases);
|
||||
var existing = await characters.ListAliasesAsync(characterId);
|
||||
var existingByAlias = existing.ToDictionary(x => x.Alias, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
for (var index = 0; index < aliases.Count; index++)
|
||||
{
|
||||
var alias = aliases[index];
|
||||
var sortOrder = (index + 1) * 10;
|
||||
if (existingByAlias.TryGetValue(alias, out var row))
|
||||
{
|
||||
await characters.UpdateAliasAsync(row.CharacterAliasID, alias, sortOrder);
|
||||
}
|
||||
else
|
||||
{
|
||||
await characters.AddAliasAsync(characterId, alias, sortOrder);
|
||||
}
|
||||
}
|
||||
|
||||
var postedSet = aliases.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var alias in existing.Where(x => !postedSet.Contains(x.Alias)))
|
||||
{
|
||||
await characters.DeleteAliasAsync(alias.CharacterAliasID);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<CharacterEditViewModel> PopulateCharacterEditOptionsAsync(CharacterEditViewModel model)
|
||||
{
|
||||
var ownerUserId = await collaboration.GetOwnerUserIdAsync(model.ProjectID);
|
||||
|
||||
607
PlotLine/Sql/093_Phase16A_BaseIdentityAndSceneFoundation.sql
Normal file
607
PlotLine/Sql/093_Phase16A_BaseIdentityAndSceneFoundation.sql
Normal file
@ -0,0 +1,607 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.CharacterAliases', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.CharacterAliases
|
||||
(
|
||||
CharacterAliasID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_CharacterAliases PRIMARY KEY,
|
||||
CharacterID int NOT NULL,
|
||||
Alias nvarchar(200) NOT NULL,
|
||||
SortOrder int NOT NULL CONSTRAINT DF_CharacterAliases_SortOrder DEFAULT (0),
|
||||
CreatedUtc datetime2 NOT NULL CONSTRAINT DF_CharacterAliases_CreatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT FK_CharacterAliases_Characters FOREIGN KEY (CharacterID) REFERENCES dbo.Characters(CharacterID) ON DELETE CASCADE
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.AssetAliases', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.AssetAliases
|
||||
(
|
||||
AssetAliasID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_AssetAliases PRIMARY KEY,
|
||||
StoryAssetID int NOT NULL,
|
||||
Alias nvarchar(200) NOT NULL,
|
||||
SortOrder int NOT NULL CONSTRAINT DF_AssetAliases_SortOrder DEFAULT (0),
|
||||
CreatedUtc datetime2 NOT NULL CONSTRAINT DF_AssetAliases_CreatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT FK_AssetAliases_StoryAssets FOREIGN KEY (StoryAssetID) REFERENCES dbo.StoryAssets(StoryAssetID) ON DELETE CASCADE
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.LocationAliases', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.LocationAliases
|
||||
(
|
||||
LocationAliasID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_LocationAliases PRIMARY KEY,
|
||||
LocationID int NOT NULL,
|
||||
Alias nvarchar(200) NOT NULL,
|
||||
SortOrder int NOT NULL CONSTRAINT DF_LocationAliases_SortOrder DEFAULT (0),
|
||||
CreatedUtc datetime2 NOT NULL CONSTRAINT DF_LocationAliases_CreatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT FK_LocationAliases_Locations FOREIGN KEY (LocationID) REFERENCES dbo.Locations(LocationID) ON DELETE CASCADE
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_CharacterAliases_CharacterAlias' AND object_id = OBJECT_ID(N'dbo.CharacterAliases'))
|
||||
CREATE UNIQUE INDEX UX_CharacterAliases_CharacterAlias ON dbo.CharacterAliases(CharacterID, Alias);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_CharacterAliases_CharacterSort' AND object_id = OBJECT_ID(N'dbo.CharacterAliases'))
|
||||
CREATE INDEX IX_CharacterAliases_CharacterSort ON dbo.CharacterAliases(CharacterID, SortOrder, CharacterAliasID);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_AssetAliases_AssetAlias' AND object_id = OBJECT_ID(N'dbo.AssetAliases'))
|
||||
CREATE UNIQUE INDEX UX_AssetAliases_AssetAlias ON dbo.AssetAliases(StoryAssetID, Alias);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_AssetAliases_AssetSort' AND object_id = OBJECT_ID(N'dbo.AssetAliases'))
|
||||
CREATE INDEX IX_AssetAliases_AssetSort ON dbo.AssetAliases(StoryAssetID, SortOrder, AssetAliasID);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_LocationAliases_LocationAlias' AND object_id = OBJECT_ID(N'dbo.LocationAliases'))
|
||||
CREATE UNIQUE INDEX UX_LocationAliases_LocationAlias ON dbo.LocationAliases(LocationID, Alias);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_LocationAliases_LocationSort' AND object_id = OBJECT_ID(N'dbo.LocationAliases'))
|
||||
CREATE INDEX IX_LocationAliases_LocationSort ON dbo.LocationAliases(LocationID, SortOrder, LocationAliasID);
|
||||
GO
|
||||
|
||||
INSERT dbo.CharacterAliases (CharacterID, Alias, SortOrder)
|
||||
SELECT c.CharacterID, LTRIM(RTRIM(c.ShortName)), 10
|
||||
FROM dbo.Characters c
|
||||
WHERE NULLIF(LTRIM(RTRIM(c.ShortName)), N'') IS NOT NULL
|
||||
AND NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.CharacterAliases ca
|
||||
WHERE ca.CharacterID = c.CharacterID
|
||||
AND ca.Alias = LTRIM(RTRIM(c.ShortName))
|
||||
);
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.Books', N'UsesExplicitScenes') IS NULL
|
||||
ALTER TABLE dbo.Books ADD UsesExplicitScenes bit NOT NULL CONSTRAINT DF_Books_UsesExplicitScenes DEFAULT (0);
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.CharacterAlias_ListByCharacter
|
||||
@CharacterID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT CharacterAliasID, CharacterID, Alias, SortOrder, CreatedUtc
|
||||
FROM dbo.CharacterAliases
|
||||
WHERE CharacterID = @CharacterID
|
||||
ORDER BY SortOrder, CharacterAliasID;
|
||||
END;
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
UPDATE dbo.CharacterAliases
|
||||
SET Alias = @Alias,
|
||||
SortOrder = COALESCE(@SortOrder, SortOrder)
|
||||
WHERE CharacterAliasID = @CharacterAliasID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.CharacterAlias_Delete
|
||||
@CharacterAliasID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
DELETE dbo.CharacterAliases WHERE CharacterAliasID = @CharacterAliasID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.AssetAlias_ListByAsset
|
||||
@StoryAssetID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT AssetAliasID, StoryAssetID, Alias, SortOrder, CreatedUtc
|
||||
FROM dbo.AssetAliases
|
||||
WHERE StoryAssetID = @StoryAssetID
|
||||
ORDER BY SortOrder, AssetAliasID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.AssetAlias_Add
|
||||
@StoryAssetID 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;
|
||||
|
||||
IF @SortOrder IS NULL
|
||||
SELECT @SortOrder = ISNULL(MAX(SortOrder), 0) + 10 FROM dbo.AssetAliases WHERE StoryAssetID = @StoryAssetID;
|
||||
|
||||
INSERT dbo.AssetAliases (StoryAssetID, Alias, SortOrder)
|
||||
VALUES (@StoryAssetID, @Alias, @SortOrder);
|
||||
|
||||
SELECT CAST(SCOPE_IDENTITY() AS int) AS AssetAliasID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.AssetAlias_Update
|
||||
@AssetAliasID 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;
|
||||
|
||||
UPDATE dbo.AssetAliases
|
||||
SET Alias = @Alias,
|
||||
SortOrder = COALESCE(@SortOrder, SortOrder)
|
||||
WHERE AssetAliasID = @AssetAliasID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.AssetAlias_Delete
|
||||
@AssetAliasID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
DELETE dbo.AssetAliases WHERE AssetAliasID = @AssetAliasID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.LocationAlias_ListByLocation
|
||||
@LocationID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT LocationAliasID, LocationID, Alias, SortOrder, CreatedUtc
|
||||
FROM dbo.LocationAliases
|
||||
WHERE LocationID = @LocationID
|
||||
ORDER BY SortOrder, LocationAliasID;
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
UPDATE dbo.LocationAliases
|
||||
SET Alias = @Alias,
|
||||
SortOrder = COALESCE(@SortOrder, SortOrder)
|
||||
WHERE LocationAliasID = @LocationAliasID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.LocationAlias_Delete
|
||||
@LocationAliasID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
DELETE dbo.LocationAliases WHERE LocationAliasID = @LocationAliasID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Book_ListByProject
|
||||
@ProjectID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT b.BookID, b.ProjectID, b.BookTitle, b.Subtitle, b.Tagline, b.ShortDescription, b.Status,
|
||||
b.BookNumber, b.Description, b.TargetWordCount, b.WritingStartDate, b.TargetCompletionDate,
|
||||
b.PublicationDate, b.UsesExplicitScenes, b.CoverThumbnailPath, b.CoverStandardPath,
|
||||
ISNULL(wordCounts.CurrentWordCount, 0) AS CurrentWordCount,
|
||||
ISNULL(counts.ChapterCount, 0) AS ChapterCount,
|
||||
ISNULL(counts.SceneCount, 0) AS SceneCount,
|
||||
b.SortOrder, b.CreatedDate, b.UpdatedDate, b.IsArchived, b.ArchivedDate, b.ArchivedReason
|
||||
FROM dbo.Books b
|
||||
OUTER APPLY
|
||||
(
|
||||
SELECT COUNT(DISTINCT c.ChapterID) AS ChapterCount, COUNT(s.SceneID) AS SceneCount
|
||||
FROM dbo.Chapters c
|
||||
LEFT JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||
) counts
|
||||
OUTER APPLY
|
||||
(
|
||||
SELECT SUM(ISNULL(sw.ActualWordCount, 0)) AS CurrentWordCount
|
||||
FROM dbo.Chapters c
|
||||
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
|
||||
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||
) wordCounts
|
||||
WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0
|
||||
ORDER BY b.SortOrder, b.BookNumber, b.BookTitle;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Book_ListByProjects
|
||||
@ProjectIDs nvarchar(max)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
;WITH ProjectIDs AS
|
||||
(
|
||||
SELECT DISTINCT TRY_CONVERT(int, value) AS ProjectID
|
||||
FROM STRING_SPLIT(COALESCE(@ProjectIDs, N''), N',')
|
||||
WHERE TRY_CONVERT(int, value) IS NOT NULL
|
||||
)
|
||||
SELECT b.BookID, b.ProjectID, b.BookTitle, b.Subtitle, b.Tagline, b.ShortDescription, b.Status,
|
||||
b.BookNumber, b.Description, b.TargetWordCount, b.WritingStartDate, b.TargetCompletionDate,
|
||||
b.PublicationDate, b.UsesExplicitScenes, b.CoverThumbnailPath, b.CoverStandardPath,
|
||||
ISNULL(wordCounts.CurrentWordCount, 0) AS CurrentWordCount,
|
||||
ISNULL(counts.ChapterCount, 0) AS ChapterCount,
|
||||
ISNULL(counts.SceneCount, 0) AS SceneCount,
|
||||
b.SortOrder, b.CreatedDate, b.UpdatedDate, b.IsArchived, b.ArchivedDate, b.ArchivedReason
|
||||
FROM dbo.Books b
|
||||
INNER JOIN ProjectIDs ids ON ids.ProjectID = b.ProjectID
|
||||
OUTER APPLY
|
||||
(
|
||||
SELECT COUNT(DISTINCT c.ChapterID) AS ChapterCount, COUNT(s.SceneID) AS SceneCount
|
||||
FROM dbo.Chapters c
|
||||
LEFT JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||
) counts
|
||||
OUTER APPLY
|
||||
(
|
||||
SELECT SUM(ISNULL(sw.ActualWordCount, 0)) AS CurrentWordCount
|
||||
FROM dbo.Chapters c
|
||||
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
|
||||
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||
) wordCounts
|
||||
WHERE b.IsArchived = 0
|
||||
ORDER BY b.ProjectID, b.SortOrder, b.BookNumber, b.BookTitle;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Book_Get
|
||||
@BookID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT b.BookID, b.ProjectID, b.BookTitle, b.Subtitle, b.Tagline, b.ShortDescription, b.Status,
|
||||
b.BookNumber, b.Description, b.TargetWordCount, b.WritingStartDate, b.TargetCompletionDate,
|
||||
b.PublicationDate, b.UsesExplicitScenes, b.CoverThumbnailPath, b.CoverStandardPath,
|
||||
ISNULL(wordCounts.CurrentWordCount, 0) AS CurrentWordCount,
|
||||
ISNULL(counts.ChapterCount, 0) AS ChapterCount,
|
||||
ISNULL(counts.SceneCount, 0) AS SceneCount,
|
||||
b.SortOrder, b.CreatedDate, b.UpdatedDate, b.IsArchived, b.ArchivedDate, b.ArchivedReason
|
||||
FROM dbo.Books b
|
||||
OUTER APPLY
|
||||
(
|
||||
SELECT COUNT(DISTINCT c.ChapterID) AS ChapterCount, COUNT(s.SceneID) AS SceneCount
|
||||
FROM dbo.Chapters c
|
||||
LEFT JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||
) counts
|
||||
OUTER APPLY
|
||||
(
|
||||
SELECT SUM(ISNULL(sw.ActualWordCount, 0)) AS CurrentWordCount
|
||||
FROM dbo.Chapters c
|
||||
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
|
||||
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||
) wordCounts
|
||||
WHERE b.BookID = @BookID AND b.IsArchived = 0;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Book_Save
|
||||
@BookID int = NULL,
|
||||
@ProjectID int,
|
||||
@BookTitle nvarchar(200),
|
||||
@Subtitle nvarchar(200) = NULL,
|
||||
@Tagline nvarchar(300) = NULL,
|
||||
@ShortDescription nvarchar(max) = NULL,
|
||||
@Status nvarchar(50) = N'Planning',
|
||||
@BookNumber int,
|
||||
@Description nvarchar(max) = NULL,
|
||||
@TargetWordCount int = NULL,
|
||||
@WritingStartDate date = NULL,
|
||||
@TargetCompletionDate date = NULL,
|
||||
@PublicationDate date = NULL,
|
||||
@UsesExplicitScenes bit = 0
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @TargetWordCount IS NOT NULL AND @TargetWordCount <= 0
|
||||
THROW 51000, 'Target word count must be greater than zero.', 1;
|
||||
|
||||
IF NULLIF(LTRIM(RTRIM(@Status)), N'') IS NULL
|
||||
SET @Status = N'Planning';
|
||||
|
||||
IF @BookID IS NULL OR @BookID = 0
|
||||
BEGIN
|
||||
DECLARE @NextBookOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Books WHERE ProjectID = @ProjectID), 0) + 10;
|
||||
|
||||
INSERT dbo.Books
|
||||
(
|
||||
ProjectID, BookTitle, Subtitle, Tagline, ShortDescription, Status, BookNumber, Description,
|
||||
TargetWordCount, WritingStartDate, TargetCompletionDate, PublicationDate, UsesExplicitScenes, SortOrder
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@ProjectID, @BookTitle, @Subtitle, @Tagline, @ShortDescription, @Status, @BookNumber, @Description,
|
||||
@TargetWordCount, @WritingStartDate, @TargetCompletionDate, @PublicationDate, @UsesExplicitScenes, @NextBookOrder
|
||||
);
|
||||
|
||||
SELECT CAST(SCOPE_IDENTITY() AS int) AS BookID;
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
UPDATE dbo.Books
|
||||
SET BookTitle = @BookTitle,
|
||||
Subtitle = @Subtitle,
|
||||
Tagline = @Tagline,
|
||||
ShortDescription = @ShortDescription,
|
||||
Status = @Status,
|
||||
BookNumber = @BookNumber,
|
||||
Description = @Description,
|
||||
TargetWordCount = @TargetWordCount,
|
||||
WritingStartDate = @WritingStartDate,
|
||||
TargetCompletionDate = @TargetCompletionDate,
|
||||
PublicationDate = @PublicationDate,
|
||||
UsesExplicitScenes = @UsesExplicitScenes,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE BookID = @BookID;
|
||||
|
||||
SELECT @BookID AS BookID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Chapter_Save
|
||||
@ChapterID int = NULL,
|
||||
@BookID int,
|
||||
@ChapterNumber decimal(9,2),
|
||||
@ChapterTitle nvarchar(200),
|
||||
@Summary nvarchar(max) = NULL,
|
||||
@RevisionStatusID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @ChapterID IS NULL OR @ChapterID = 0
|
||||
BEGIN
|
||||
DECLARE @NextChapterOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Chapters WHERE BookID = @BookID), 0) + 10;
|
||||
DECLARE @NewChapterID int;
|
||||
|
||||
INSERT dbo.Chapters (BookID, ChapterNumber, ChapterTitle, Summary, SortOrder, RevisionStatusID)
|
||||
VALUES (@BookID, @ChapterNumber, @ChapterTitle, @Summary, @NextChapterOrder, @RevisionStatusID);
|
||||
|
||||
SET @NewChapterID = CAST(SCOPE_IDENTITY() AS int);
|
||||
|
||||
SELECT @NewChapterID AS ChapterID;
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
UPDATE dbo.Chapters
|
||||
SET ChapterNumber = @ChapterNumber,
|
||||
ChapterTitle = @ChapterTitle,
|
||||
Summary = @Summary,
|
||||
RevisionStatusID = @RevisionStatusID,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE ChapterID = @ChapterID;
|
||||
|
||||
SELECT @ChapterID AS ChapterID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER TRIGGER dbo.TR_Chapters_CreateDefaultScene
|
||||
ON dbo.Chapters
|
||||
AFTER INSERT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @DefaultTimeModeID int;
|
||||
DECLARE @DefaultTimeConfidenceID int;
|
||||
|
||||
SELECT TOP (1) @DefaultTimeModeID = TimeModeID
|
||||
FROM dbo.TimeModes
|
||||
ORDER BY CASE WHEN TimeModeName = N'Unknown' THEN 0 ELSE 1 END, SortOrder, TimeModeID;
|
||||
|
||||
SELECT TOP (1) @DefaultTimeConfidenceID = TimeConfidenceID
|
||||
FROM dbo.TimeConfidences
|
||||
ORDER BY CASE WHEN TimeConfidenceName = N'Unknown' THEN 0 ELSE 1 END, SortOrder, TimeConfidenceID;
|
||||
|
||||
INSERT dbo.Scenes
|
||||
(
|
||||
ChapterID, SceneNumber, SceneTitle, Summary, SortOrder, TimeModeID, TimeConfidenceID, RevisionStatusID
|
||||
)
|
||||
SELECT i.ChapterID, 1, N'Scene 1', NULL, 10, @DefaultTimeModeID, @DefaultTimeConfidenceID, i.RevisionStatusID
|
||||
FROM inserted i
|
||||
WHERE i.IsArchived = 0
|
||||
AND NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.Scenes s
|
||||
WHERE s.ChapterID = i.ChapterID
|
||||
AND s.IsArchived = 0
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
DECLARE @MigrationTimeModeID int;
|
||||
DECLARE @MigrationTimeConfidenceID int;
|
||||
|
||||
SELECT TOP (1) @MigrationTimeModeID = TimeModeID
|
||||
FROM dbo.TimeModes
|
||||
ORDER BY CASE WHEN TimeModeName = N'Unknown' THEN 0 ELSE 1 END, SortOrder, TimeModeID;
|
||||
|
||||
SELECT TOP (1) @MigrationTimeConfidenceID = TimeConfidenceID
|
||||
FROM dbo.TimeConfidences
|
||||
ORDER BY CASE WHEN TimeConfidenceName = N'Unknown' THEN 0 ELSE 1 END, SortOrder, TimeConfidenceID;
|
||||
|
||||
INSERT dbo.Scenes
|
||||
(
|
||||
ChapterID, SceneNumber, SceneTitle, Summary, SortOrder, TimeModeID, TimeConfidenceID, RevisionStatusID
|
||||
)
|
||||
SELECT c.ChapterID, 1, N'Scene 1', NULL, 10, @MigrationTimeModeID, @MigrationTimeConfidenceID, c.RevisionStatusID
|
||||
FROM dbo.Chapters c
|
||||
WHERE c.IsArchived = 0
|
||||
AND NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.Scenes s
|
||||
WHERE s.ChapterID = c.ChapterID
|
||||
AND s.IsArchived = 0
|
||||
);
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Scene_Archive
|
||||
@SceneID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @ChapterID int;
|
||||
SELECT @ChapterID = ChapterID
|
||||
FROM dbo.Scenes
|
||||
WHERE SceneID = @SceneID AND IsArchived = 0;
|
||||
|
||||
IF @ChapterID IS NULL
|
||||
RETURN;
|
||||
|
||||
IF (SELECT COUNT(*) FROM dbo.Scenes WHERE ChapterID = @ChapterID AND IsArchived = 0) <= 1
|
||||
THROW 51000, 'A chapter must contain at least one scene.', 1;
|
||||
|
||||
UPDATE dbo.Scenes
|
||||
SET IsArchived = 1,
|
||||
ArchivedDate = SYSUTCDATETIME(),
|
||||
ArchivedReason = NULL,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE SceneID = @SceneID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Archive_Entity
|
||||
@EntityType nvarchar(80),
|
||||
@EntityID int,
|
||||
@ArchivedReason nvarchar(500) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @EntityType = N'Project' UPDATE dbo.Projects SET IsArchived = 1, ArchivedDate = SYSUTCDATETIME(), ArchivedReason = @ArchivedReason, UpdatedDate = SYSUTCDATETIME() WHERE ProjectID = @EntityID;
|
||||
ELSE IF @EntityType = N'Book' UPDATE dbo.Books SET IsArchived = 1, ArchivedDate = SYSUTCDATETIME(), ArchivedReason = @ArchivedReason, UpdatedDate = SYSUTCDATETIME() WHERE BookID = @EntityID;
|
||||
ELSE IF @EntityType = N'Chapter' UPDATE dbo.Chapters SET IsArchived = 1, ArchivedDate = SYSUTCDATETIME(), ArchivedReason = @ArchivedReason, UpdatedDate = SYSUTCDATETIME() WHERE ChapterID = @EntityID;
|
||||
ELSE IF @EntityType = N'Scene'
|
||||
BEGIN
|
||||
DECLARE @ChapterID int;
|
||||
SELECT @ChapterID = ChapterID FROM dbo.Scenes WHERE SceneID = @EntityID AND IsArchived = 0;
|
||||
|
||||
IF @ChapterID IS NULL
|
||||
RETURN;
|
||||
|
||||
IF (SELECT COUNT(*) FROM dbo.Scenes WHERE ChapterID = @ChapterID AND IsArchived = 0) <= 1
|
||||
THROW 51000, 'A chapter must contain at least one scene.', 1;
|
||||
|
||||
UPDATE dbo.Scenes SET IsArchived = 1, ArchivedDate = SYSUTCDATETIME(), ArchivedReason = @ArchivedReason, UpdatedDate = SYSUTCDATETIME() WHERE SceneID = @EntityID;
|
||||
END
|
||||
ELSE IF @EntityType = N'PlotLine' UPDATE dbo.PlotLines SET IsArchived = 1, ArchivedDate = SYSUTCDATETIME(), ArchivedReason = @ArchivedReason, UpdatedDate = SYSUTCDATETIME() WHERE PlotLineID = @EntityID;
|
||||
ELSE IF @EntityType = N'PlotThread' UPDATE dbo.PlotThreads SET IsArchived = 1, ArchivedDate = SYSUTCDATETIME(), ArchivedReason = @ArchivedReason, UpdatedDate = SYSUTCDATETIME() WHERE PlotThreadID = @EntityID;
|
||||
ELSE IF @EntityType = N'StoryAsset' UPDATE dbo.StoryAssets SET IsArchived = 1, ArchivedDate = SYSUTCDATETIME(), ArchivedReason = @ArchivedReason, UpdatedDate = SYSUTCDATETIME() WHERE StoryAssetID = @EntityID;
|
||||
ELSE IF @EntityType = N'Character' UPDATE dbo.Characters SET IsArchived = 1, ArchivedDate = SYSUTCDATETIME(), ArchivedReason = @ArchivedReason, UpdatedDate = SYSUTCDATETIME() WHERE CharacterID = @EntityID;
|
||||
ELSE IF @EntityType = N'Location' UPDATE dbo.Locations SET IsArchived = 1, ArchivedDate = SYSUTCDATETIME(), ArchivedReason = @ArchivedReason, UpdatedDate = SYSUTCDATETIME() WHERE LocationID = @EntityID;
|
||||
ELSE IF @EntityType = N'Scenario' UPDATE dbo.Scenarios SET IsArchived = 1, ArchivedDate = SYSUTCDATETIME(), ArchivedReason = @ArchivedReason, UpdatedDate = SYSUTCDATETIME() WHERE ScenarioID = @EntityID;
|
||||
ELSE IF @EntityType = N'Relationship' UPDATE dbo.CharacterRelationships SET IsArchived = 1, ArchivedDate = SYSUTCDATETIME(), ArchivedReason = @ArchivedReason, UpdatedDate = SYSUTCDATETIME() WHERE CharacterRelationshipID = @EntityID;
|
||||
END;
|
||||
GO
|
||||
@ -214,6 +214,9 @@ public sealed class BookEditViewModel
|
||||
[Display(Name = "Publication date")]
|
||||
public DateTime? PublicationDate { get; set; }
|
||||
|
||||
[Display(Name = "Uses explicit scenes")]
|
||||
public bool UsesExplicitScenes { get; set; }
|
||||
|
||||
[Display(Name = "Cover image")]
|
||||
public IFormFile? CoverUpload { get; set; }
|
||||
|
||||
@ -1475,6 +1478,8 @@ public sealed class StoryAssetEditViewModel
|
||||
[Display(Name = "Asset name")]
|
||||
public string AssetName { get; set; } = string.Empty;
|
||||
|
||||
public List<string> Aliases { get; set; } = [];
|
||||
|
||||
[Display(Name = "Kind")]
|
||||
public int AssetKindID { get; set; }
|
||||
|
||||
@ -1602,11 +1607,10 @@ public sealed class CharacterEditViewModel
|
||||
public int ProjectID { get; set; }
|
||||
|
||||
[Required, StringLength(200)]
|
||||
[Display(Name = "Character name")]
|
||||
[Display(Name = "Display name")]
|
||||
public string CharacterName { get; set; } = string.Empty;
|
||||
|
||||
[Display(Name = "Short name")]
|
||||
public string? ShortName { get; set; }
|
||||
public List<string> Aliases { get; set; } = [];
|
||||
|
||||
[Display(Name = "Sex")]
|
||||
public int? SexValueID { get; set; }
|
||||
@ -1666,6 +1670,7 @@ public sealed class CharacterDetailViewModel
|
||||
{
|
||||
public Project Project { get; set; } = new();
|
||||
public Character Character { get; set; } = new();
|
||||
public IReadOnlyList<CharacterAlias> Aliases { get; set; } = [];
|
||||
public IReadOnlyList<Character> ProjectCharacters { get; set; } = [];
|
||||
public IReadOnlyList<CharacterImage> Images { get; set; } = [];
|
||||
public string DisplayAge { get; set; } = "Unknown";
|
||||
@ -1915,6 +1920,8 @@ public sealed class LocationEditViewModel
|
||||
[Display(Name = "Location name")]
|
||||
public string LocationName { get; set; } = string.Empty;
|
||||
|
||||
public List<string> Aliases { get; set; } = [];
|
||||
|
||||
[Display(Name = "Location type")]
|
||||
public int? LocationTypeID { get; set; }
|
||||
|
||||
|
||||
@ -53,6 +53,12 @@
|
||||
<select asp-for="Status" asp-items="Model.StatusOptions" class="form-select"></select>
|
||||
<span asp-validation-for="Status" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check mt-4">
|
||||
<input asp-for="UsesExplicitScenes" class="form-check-input" />
|
||||
<label asp-for="UsesExplicitScenes" class="form-check-label"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="TargetWordCount" class="form-label"></label>
|
||||
<input asp-for="TargetWordCount" class="form-control" min="1" />
|
||||
|
||||
@ -76,7 +76,7 @@
|
||||
<section id="character-overview" class="asset-detail-grid">
|
||||
<article class="plain-card">
|
||||
<h2>Profile</h2>
|
||||
<p><strong>Short name:</strong> @Model.Character.ShortName</p>
|
||||
<p><strong>Aliases:</strong> @(Model.Aliases.Any() ? string.Join(", ", Model.Aliases.Select(x => x.Alias)) : "None")</p>
|
||||
<p><strong>Sex:</strong> @Model.Character.Sex</p>
|
||||
<p><strong>Age:</strong> @Model.DisplayAge</p>
|
||||
<p><strong>Height:</strong> @Model.Character.Height</p>
|
||||
@ -508,4 +508,3 @@
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
|
||||
@ -30,9 +30,22 @@
|
||||
<input asp-for="CharacterName" class="form-control" />
|
||||
<span asp-validation-for="CharacterName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="ShortName" class="form-label"></label>
|
||||
<input asp-for="ShortName" class="form-control" />
|
||||
<div class="col-12">
|
||||
<label class="form-label">Aliases</label>
|
||||
<div class="d-flex flex-wrap gap-2 mb-2" data-alias-list>
|
||||
@for (var i = 0; i < Model.Aliases.Count; i++)
|
||||
{
|
||||
<span class="badge rounded-pill text-bg-light border d-inline-flex align-items-center gap-2">
|
||||
<span>@Model.Aliases[i]</span>
|
||||
<button class="btn-close btn-close-sm" type="button" aria-label="Remove @Model.Aliases[i]" data-remove-alias></button>
|
||||
<input type="hidden" name="Aliases" value="@Model.Aliases[i]" />
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" maxlength="200" placeholder="Add an alias" data-alias-input />
|
||||
<button class="btn btn-outline-secondary" type="button" data-add-alias>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="SexValueID" class="form-label"></label>
|
||||
@ -130,5 +143,52 @@
|
||||
select.addEventListener("change", syncCustomSex);
|
||||
syncCustomSex();
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const list = document.querySelector("[data-alias-list]");
|
||||
const input = document.querySelector("[data-alias-input]");
|
||||
const add = document.querySelector("[data-add-alias]");
|
||||
if (!list || !input || !add) return;
|
||||
|
||||
const aliases = () => [...list.querySelectorAll("input[name='Aliases']")].map(x => x.value.toLowerCase());
|
||||
const appendAlias = (value) => {
|
||||
const alias = value.trim();
|
||||
if (!alias || aliases().includes(alias.toLowerCase())) return;
|
||||
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "badge rounded-pill text-bg-light border d-inline-flex align-items-center gap-2";
|
||||
|
||||
const text = document.createElement("span");
|
||||
text.textContent = alias;
|
||||
|
||||
const remove = document.createElement("button");
|
||||
remove.className = "btn-close btn-close-sm";
|
||||
remove.type = "button";
|
||||
remove.setAttribute("aria-label", `Remove ${alias}`);
|
||||
remove.dataset.removeAlias = "";
|
||||
|
||||
const hidden = document.createElement("input");
|
||||
hidden.type = "hidden";
|
||||
hidden.name = "Aliases";
|
||||
hidden.value = alias;
|
||||
|
||||
chip.append(text, remove, hidden);
|
||||
list.append(chip);
|
||||
input.value = "";
|
||||
};
|
||||
|
||||
add.addEventListener("click", () => appendAlias(input.value));
|
||||
input.addEventListener("keydown", event => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
appendAlias(input.value);
|
||||
}
|
||||
});
|
||||
list.addEventListener("click", event => {
|
||||
if (event.target instanceof HTMLElement && event.target.matches("[data-remove-alias]")) {
|
||||
event.target.closest(".badge")?.remove();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
@ -42,10 +42,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="asset-card-footer">
|
||||
@if (!string.IsNullOrWhiteSpace(character.ShortName))
|
||||
{
|
||||
<span class="purpose-chip">@character.ShortName</span>
|
||||
}
|
||||
@if (character.AgeAtSeriesStart.HasValue)
|
||||
{
|
||||
<span class="status-pill">Age @character.AgeAtSeriesStart</span>
|
||||
|
||||
@ -33,6 +33,23 @@
|
||||
<input asp-for="LocationName" class="form-control" />
|
||||
<span asp-validation-for="LocationName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Aliases</label>
|
||||
<div class="d-flex flex-wrap gap-2 mb-2" data-alias-list>
|
||||
@for (var i = 0; i < Model.Aliases.Count; i++)
|
||||
{
|
||||
<span class="badge rounded-pill text-bg-light border d-inline-flex align-items-center gap-2">
|
||||
<span>@Model.Aliases[i]</span>
|
||||
<button class="btn-close btn-close-sm" type="button" aria-label="Remove @Model.Aliases[i]" data-remove-alias></button>
|
||||
<input type="hidden" name="Aliases" value="@Model.Aliases[i]" />
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" maxlength="200" placeholder="Add an alias" data-alias-input />
|
||||
<button class="btn btn-outline-secondary" type="button" data-add-alias>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="LocationTypeID" class="form-label">Location type <help-icon key="locations.fields.locationType" mode="inline" /></label>
|
||||
<select asp-for="LocationTypeID" asp-items="Model.LocationTypeOptions" class="form-select">
|
||||
@ -66,4 +83,48 @@
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
<script>
|
||||
(() => {
|
||||
const list = document.querySelector("[data-alias-list]");
|
||||
const input = document.querySelector("[data-alias-input]");
|
||||
const add = document.querySelector("[data-add-alias]");
|
||||
if (!list || !input || !add) return;
|
||||
|
||||
const aliases = () => [...list.querySelectorAll("input[name='Aliases']")].map(x => x.value.toLowerCase());
|
||||
const appendAlias = (value) => {
|
||||
const alias = value.trim();
|
||||
if (!alias || aliases().includes(alias.toLowerCase())) return;
|
||||
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "badge rounded-pill text-bg-light border d-inline-flex align-items-center gap-2";
|
||||
const text = document.createElement("span");
|
||||
text.textContent = alias;
|
||||
const remove = document.createElement("button");
|
||||
remove.className = "btn-close btn-close-sm";
|
||||
remove.type = "button";
|
||||
remove.setAttribute("aria-label", `Remove ${alias}`);
|
||||
remove.dataset.removeAlias = "";
|
||||
const hidden = document.createElement("input");
|
||||
hidden.type = "hidden";
|
||||
hidden.name = "Aliases";
|
||||
hidden.value = alias;
|
||||
chip.append(text, remove, hidden);
|
||||
list.append(chip);
|
||||
input.value = "";
|
||||
};
|
||||
|
||||
add.addEventListener("click", () => appendAlias(input.value));
|
||||
input.addEventListener("keydown", event => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
appendAlias(input.value);
|
||||
}
|
||||
});
|
||||
list.addEventListener("click", event => {
|
||||
if (event.target instanceof HTMLElement && event.target.matches("[data-remove-alias]")) {
|
||||
event.target.closest(".badge")?.remove();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
@ -27,6 +27,23 @@
|
||||
<input asp-for="AssetName" class="form-control" />
|
||||
<span asp-validation-for="AssetName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Aliases</label>
|
||||
<div class="d-flex flex-wrap gap-2 mb-2" data-alias-list>
|
||||
@for (var i = 0; i < Model.Aliases.Count; i++)
|
||||
{
|
||||
<span class="badge rounded-pill text-bg-light border d-inline-flex align-items-center gap-2">
|
||||
<span>@Model.Aliases[i]</span>
|
||||
<button class="btn-close btn-close-sm" type="button" aria-label="Remove @Model.Aliases[i]" data-remove-alias></button>
|
||||
<input type="hidden" name="Aliases" value="@Model.Aliases[i]" />
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" maxlength="200" placeholder="Add an alias" data-alias-input />
|
||||
<button class="btn btn-outline-secondary" type="button" data-add-alias>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="AssetKindID" class="form-label">Kind <help-icon key="storyAssets.fields.kind" mode="inline" /></label>
|
||||
<select asp-for="AssetKindID" asp-items="Model.AssetKindOptions" class="form-select"></select>
|
||||
@ -100,4 +117,48 @@
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
<script>
|
||||
(() => {
|
||||
const list = document.querySelector("[data-alias-list]");
|
||||
const input = document.querySelector("[data-alias-input]");
|
||||
const add = document.querySelector("[data-add-alias]");
|
||||
if (!list || !input || !add) return;
|
||||
|
||||
const aliases = () => [...list.querySelectorAll("input[name='Aliases']")].map(x => x.value.toLowerCase());
|
||||
const appendAlias = (value) => {
|
||||
const alias = value.trim();
|
||||
if (!alias || aliases().includes(alias.toLowerCase())) return;
|
||||
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "badge rounded-pill text-bg-light border d-inline-flex align-items-center gap-2";
|
||||
const text = document.createElement("span");
|
||||
text.textContent = alias;
|
||||
const remove = document.createElement("button");
|
||||
remove.className = "btn-close btn-close-sm";
|
||||
remove.type = "button";
|
||||
remove.setAttribute("aria-label", `Remove ${alias}`);
|
||||
remove.dataset.removeAlias = "";
|
||||
const hidden = document.createElement("input");
|
||||
hidden.type = "hidden";
|
||||
hidden.name = "Aliases";
|
||||
hidden.value = alias;
|
||||
chip.append(text, remove, hidden);
|
||||
list.append(chip);
|
||||
input.value = "";
|
||||
};
|
||||
|
||||
add.addEventListener("click", () => appendAlias(input.value));
|
||||
input.addEventListener("keydown", event => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
appendAlias(input.value);
|
||||
}
|
||||
});
|
||||
list.addEventListener("click", event => {
|
||||
if (event.target instanceof HTMLElement && event.target.matches("[data-remove-alias]")) {
|
||||
event.target.closest(".badge")?.remove();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user