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.WritingStartDate = model.WritingStartDate;
|
||||||
createModel.TargetCompletionDate = model.TargetCompletionDate;
|
createModel.TargetCompletionDate = model.TargetCompletionDate;
|
||||||
createModel.PublicationDate = model.PublicationDate;
|
createModel.PublicationDate = model.PublicationDate;
|
||||||
|
createModel.UsesExplicitScenes = model.UsesExplicitScenes;
|
||||||
await books.PopulateEditContextAsync(createModel);
|
await books.PopulateEditContextAsync(createModel);
|
||||||
return View("Edit", createModel);
|
return View("Edit", createModel);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,7 +118,7 @@ public sealed class CharactersController(ICharacterService characters) : Control
|
|||||||
private static void CopyCharacterFormValues(CharacterEditViewModel source, CharacterEditViewModel target)
|
private static void CopyCharacterFormValues(CharacterEditViewModel source, CharacterEditViewModel target)
|
||||||
{
|
{
|
||||||
target.CharacterName = source.CharacterName;
|
target.CharacterName = source.CharacterName;
|
||||||
target.ShortName = source.ShortName;
|
target.Aliases = source.Aliases;
|
||||||
target.SexValueID = source.SexValueID;
|
target.SexValueID = source.SexValueID;
|
||||||
target.Sex = source.Sex;
|
target.Sex = source.Sex;
|
||||||
target.CustomSex = source.CustomSex;
|
target.CustomSex = source.CustomSex;
|
||||||
|
|||||||
@ -38,7 +38,21 @@ public sealed class LocationsController(ILocationService locations) : Controller
|
|||||||
{
|
{
|
||||||
if (!ModelState.IsValid)
|
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);
|
var locationId = await locations.SaveAsync(model);
|
||||||
|
|||||||
@ -88,7 +88,16 @@ public sealed class ScenesController(
|
|||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> Archive(int id, int chapterId)
|
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.";
|
TempData["ArchiveMessage"] = "Scene archived. You can restore it from Archived Items.";
|
||||||
if (TryGetLocalReturnUrl(out var returnUrl))
|
if (TryGetLocalReturnUrl(out var returnUrl))
|
||||||
{
|
{
|
||||||
|
|||||||
@ -110,6 +110,7 @@ public sealed class StoryAssetsController(IAssetService assets) : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
model.AssetName = posted.AssetName;
|
model.AssetName = posted.AssetName;
|
||||||
|
model.Aliases = posted.Aliases;
|
||||||
model.AssetKindID = posted.AssetKindID;
|
model.AssetKindID = posted.AssetKindID;
|
||||||
model.Description = posted.Description;
|
model.Description = posted.Description;
|
||||||
model.Importance = posted.Importance;
|
model.Importance = posted.Importance;
|
||||||
|
|||||||
@ -287,15 +287,31 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|
|||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
chaptersCreated++;
|
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 primaryLocationId = ResolveId(locationMap, scene.LocationName, package.Aliases.Locations);
|
||||||
var povCharacterId = ResolveId(characterMap, scene.PovCharacterName, package.Aliases.Characters);
|
var povCharacterId = ResolveId(characterMap, scene.PovCharacterName, package.Aliases.Characters);
|
||||||
var sceneId = await connection.QuerySingleAsync<int>(
|
var sceneId = await connection.QuerySingleAsync<int>(
|
||||||
"dbo.Scene_Save",
|
"dbo.Scene_Save",
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
SceneID = 0,
|
SceneID = sceneIndex == 0 ? defaultSceneId : 0,
|
||||||
ChapterID = chapterId,
|
ChapterID = chapterId,
|
||||||
scene.SceneNumber,
|
scene.SceneNumber,
|
||||||
SceneTitle = Clean(scene.Title),
|
SceneTitle = Clean(scene.Title),
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
using PlotLine.Models;
|
using PlotLine.Models;
|
||||||
|
|
||||||
namespace PlotLine.Data;
|
namespace PlotLine.Data;
|
||||||
@ -159,6 +160,10 @@ public interface IAssetRepository
|
|||||||
Task<IReadOnlyList<StoryAsset>> ListAssetsAsync(int projectId);
|
Task<IReadOnlyList<StoryAsset>> ListAssetsAsync(int projectId);
|
||||||
Task<StoryAsset?> GetAssetAsync(int storyAssetId);
|
Task<StoryAsset?> GetAssetAsync(int storyAssetId);
|
||||||
Task<int> SaveAssetAsync(StoryAsset asset);
|
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 UpdateAssetImageAsync(int storyAssetId, string? imagePath, string? thumbnailPath);
|
||||||
Task ArchiveAssetAsync(int storyAssetId);
|
Task ArchiveAssetAsync(int storyAssetId);
|
||||||
Task<IReadOnlyList<AssetEvent>> ListEventsBySceneAsync(int sceneId);
|
Task<IReadOnlyList<AssetEvent>> ListEventsBySceneAsync(int sceneId);
|
||||||
@ -183,6 +188,10 @@ public interface ICharacterRepository
|
|||||||
Task<IReadOnlyList<Character>> ListCharactersAsync(int projectId);
|
Task<IReadOnlyList<Character>> ListCharactersAsync(int projectId);
|
||||||
Task<Character?> GetCharacterAsync(int characterId);
|
Task<Character?> GetCharacterAsync(int characterId);
|
||||||
Task<int> SaveCharacterAsync(Character character);
|
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<IReadOnlyList<CharacterSexValue>> ListSexValuesAsync(int ownerUserId);
|
||||||
Task<CharacterSexValue?> GetSexValueAsync(int characterSexValueId, int ownerUserId);
|
Task<CharacterSexValue?> GetSexValueAsync(int characterSexValueId, int ownerUserId);
|
||||||
Task<int> GetOrCreateSexValueAsync(int ownerUserId, string sexName);
|
Task<int> GetOrCreateSexValueAsync(int ownerUserId, string sexName);
|
||||||
@ -223,6 +232,10 @@ public interface ILocationRepository
|
|||||||
Task<IReadOnlyList<LocationItem>> ListByProjectAsync(int projectId);
|
Task<IReadOnlyList<LocationItem>> ListByProjectAsync(int projectId);
|
||||||
Task<LocationItem?> GetAsync(int locationId);
|
Task<LocationItem?> GetAsync(int locationId);
|
||||||
Task<int> SaveAsync(LocationItem location);
|
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 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);
|
||||||
@ -477,6 +490,43 @@ public sealed class LocationRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
commandType: CommandType.StoredProcedure);
|
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)
|
public async Task ArchiveAsync(int locationId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
@ -2178,7 +2228,8 @@ public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IB
|
|||||||
book.TargetWordCount,
|
book.TargetWordCount,
|
||||||
book.WritingStartDate,
|
book.WritingStartDate,
|
||||||
book.TargetCompletionDate,
|
book.TargetCompletionDate,
|
||||||
book.PublicationDate
|
book.PublicationDate,
|
||||||
|
book.UsesExplicitScenes
|
||||||
},
|
},
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
@ -2337,7 +2388,14 @@ public sealed class SceneRepository(ISqlConnectionFactory connectionFactory) : I
|
|||||||
public async Task ArchiveAsync(int sceneId)
|
public async Task ArchiveAsync(int sceneId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
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)
|
public async Task MoveAsync(int sceneId, string direction)
|
||||||
@ -2977,6 +3035,43 @@ public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : I
|
|||||||
commandType: CommandType.StoredProcedure);
|
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)
|
public async Task UpdateAssetImageAsync(int storyAssetId, string? imagePath, string? thumbnailPath)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
@ -3228,6 +3323,43 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
commandType: CommandType.StoredProcedure);
|
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)
|
public async Task<IReadOnlyList<CharacterSexValue>> ListSexValuesAsync(int ownerUserId)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
|||||||
@ -238,6 +238,7 @@ public sealed class Book
|
|||||||
public DateTime? WritingStartDate { get; set; }
|
public DateTime? WritingStartDate { get; set; }
|
||||||
public DateTime? TargetCompletionDate { get; set; }
|
public DateTime? TargetCompletionDate { get; set; }
|
||||||
public DateTime? PublicationDate { get; set; }
|
public DateTime? PublicationDate { get; set; }
|
||||||
|
public bool UsesExplicitScenes { get; set; }
|
||||||
public string? CoverThumbnailPath { get; set; }
|
public string? CoverThumbnailPath { get; set; }
|
||||||
public string? CoverStandardPath { get; set; }
|
public string? CoverStandardPath { get; set; }
|
||||||
public int CurrentWordCount { get; set; }
|
public int CurrentWordCount { get; set; }
|
||||||
@ -254,6 +255,33 @@ public sealed class Book
|
|||||||
: null;
|
: 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 static class BookStatuses
|
||||||
{
|
{
|
||||||
public const string Planning = "Planning";
|
public const string Planning = "Planning";
|
||||||
|
|||||||
@ -10,6 +10,17 @@ using PlotLine.ViewModels;
|
|||||||
|
|
||||||
namespace PlotLine.Services;
|
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
|
public interface IProjectService
|
||||||
{
|
{
|
||||||
Task<ProjectIndexViewModel> ListAsync();
|
Task<ProjectIndexViewModel> ListAsync();
|
||||||
@ -1181,6 +1192,7 @@ public sealed class BookService(
|
|||||||
WritingStartDate = book.WritingStartDate,
|
WritingStartDate = book.WritingStartDate,
|
||||||
TargetCompletionDate = book.TargetCompletionDate,
|
TargetCompletionDate = book.TargetCompletionDate,
|
||||||
PublicationDate = book.PublicationDate,
|
PublicationDate = book.PublicationDate,
|
||||||
|
UsesExplicitScenes = book.UsesExplicitScenes,
|
||||||
CoverThumbnailPath = book.CoverThumbnailPath ?? BookCoverService.PlaceholderPath,
|
CoverThumbnailPath = book.CoverThumbnailPath ?? BookCoverService.PlaceholderPath,
|
||||||
CoverStandardPath = book.CoverStandardPath,
|
CoverStandardPath = book.CoverStandardPath,
|
||||||
Project = await projects.GetAsync(book.ProjectID),
|
Project = await projects.GetAsync(book.ProjectID),
|
||||||
@ -1227,7 +1239,8 @@ public sealed class BookService(
|
|||||||
TargetWordCount = model.TargetWordCount,
|
TargetWordCount = model.TargetWordCount,
|
||||||
WritingStartDate = model.WritingStartDate,
|
WritingStartDate = model.WritingStartDate,
|
||||||
TargetCompletionDate = model.TargetCompletionDate,
|
TargetCompletionDate = model.TargetCompletionDate,
|
||||||
PublicationDate = model.PublicationDate
|
PublicationDate = model.PublicationDate,
|
||||||
|
UsesExplicitScenes = model.UsesExplicitScenes
|
||||||
});
|
});
|
||||||
await activity.RecordAsync(model.ProjectID, isNew ? "Created" : "Updated", "Book", bookId, model.BookTitle);
|
await activity.RecordAsync(model.ProjectID, isNew ? "Created" : "Updated", "Book", bookId, model.BookTitle);
|
||||||
|
|
||||||
@ -7479,6 +7492,7 @@ public sealed class AssetService(
|
|||||||
StoryAssetID = asset.StoryAssetID,
|
StoryAssetID = asset.StoryAssetID,
|
||||||
ProjectID = asset.ProjectID,
|
ProjectID = asset.ProjectID,
|
||||||
AssetName = asset.AssetName,
|
AssetName = asset.AssetName,
|
||||||
|
Aliases = (await assets.ListAliasesAsync(asset.StoryAssetID)).Select(x => x.Alias).ToList(),
|
||||||
AssetKindID = asset.AssetKindID,
|
AssetKindID = asset.AssetKindID,
|
||||||
Description = asset.Description,
|
Description = asset.Description,
|
||||||
Importance = asset.Importance,
|
Importance = asset.Importance,
|
||||||
@ -7549,6 +7563,7 @@ public sealed class AssetService(
|
|||||||
ImagePath = model.ImagePath ?? existing?.ImagePath,
|
ImagePath = model.ImagePath ?? existing?.ImagePath,
|
||||||
ThumbnailPath = model.ThumbnailPath ?? existing?.ThumbnailPath
|
ThumbnailPath = model.ThumbnailPath ?? existing?.ThumbnailPath
|
||||||
});
|
});
|
||||||
|
await SyncAssetAliasesAsync(assetId, model.Aliases);
|
||||||
var imageHost = existing ?? new StoryAsset { StoryAssetID = assetId, ProjectID = model.ProjectID };
|
var imageHost = existing ?? new StoryAsset { StoryAssetID = assetId, ProjectID = model.ProjectID };
|
||||||
imageHost.StoryAssetID = assetId;
|
imageHost.StoryAssetID = assetId;
|
||||||
imageHost.ProjectID = model.ProjectID;
|
imageHost.ProjectID = model.ProjectID;
|
||||||
@ -7664,6 +7679,33 @@ public sealed class AssetService(
|
|||||||
return model;
|
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)
|
private static IReadOnlyList<SelectListItem> OptionalStateList(IEnumerable<AssetState> states)
|
||||||
=> states.Select(x => new SelectListItem(x.StateName, x.AssetStateID.ToString())).ToList();
|
=> 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,
|
ProjectID = location.ProjectID,
|
||||||
ParentLocationID = location.ParentLocationID,
|
ParentLocationID = location.ParentLocationID,
|
||||||
LocationName = location.LocationName,
|
LocationName = location.LocationName,
|
||||||
|
Aliases = (await locations.ListAliasesAsync(location.LocationID)).Select(x => x.Alias).ToList(),
|
||||||
LocationTypeID = location.LocationTypeID,
|
LocationTypeID = location.LocationTypeID,
|
||||||
Description = location.Description,
|
Description = location.Description,
|
||||||
ShowInQuickAddBar = location.ShowInQuickAddBar,
|
ShowInQuickAddBar = location.ShowInQuickAddBar,
|
||||||
@ -7768,6 +7811,7 @@ public sealed class LocationService(IProjectRepository projects, ILocationReposi
|
|||||||
Description = model.Description,
|
Description = model.Description,
|
||||||
ShowInQuickAddBar = model.ShowInQuickAddBar
|
ShowInQuickAddBar = model.ShowInQuickAddBar
|
||||||
});
|
});
|
||||||
|
await SyncLocationAliasesAsync(locationId, model.Aliases);
|
||||||
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;
|
||||||
}
|
}
|
||||||
@ -7828,6 +7872,33 @@ public sealed class LocationService(IProjectRepository projects, ILocationReposi
|
|||||||
model.LocationTypeOptions = ChapterService.ToSelectList(lookups.LocationTypes, x => x.LocationTypeID, x => x.TypeName);
|
model.LocationTypeOptions = ChapterService.ToSelectList(lookups.LocationTypes, x => x.LocationTypeID, x => x.TypeName);
|
||||||
return model;
|
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(
|
public sealed class FloorPlanService(
|
||||||
@ -8880,7 +8951,7 @@ public sealed class CharacterService(
|
|||||||
CharacterID = character.CharacterID,
|
CharacterID = character.CharacterID,
|
||||||
ProjectID = character.ProjectID,
|
ProjectID = character.ProjectID,
|
||||||
CharacterName = character.CharacterName,
|
CharacterName = character.CharacterName,
|
||||||
ShortName = character.ShortName,
|
Aliases = (await characters.ListAliasesAsync(character.CharacterID)).Select(x => x.Alias).ToList(),
|
||||||
SexValueID = character.SexValueID,
|
SexValueID = character.SexValueID,
|
||||||
Sex = character.Sex,
|
Sex = character.Sex,
|
||||||
BirthDate = character.BirthDate,
|
BirthDate = character.BirthDate,
|
||||||
@ -8928,6 +8999,7 @@ public sealed class CharacterService(
|
|||||||
{
|
{
|
||||||
Project = project,
|
Project = project,
|
||||||
Character = character,
|
Character = character,
|
||||||
|
Aliases = await characters.ListAliasesAsync(character.CharacterID),
|
||||||
ProjectCharacters = projectCharacters,
|
ProjectCharacters = projectCharacters,
|
||||||
Images = await characters.ListCharacterImagesAsync(character.CharacterID),
|
Images = await characters.ListCharacterImagesAsync(character.CharacterID),
|
||||||
DisplayAge = DisplayAge(character),
|
DisplayAge = DisplayAge(character),
|
||||||
@ -9009,7 +9081,7 @@ public sealed class CharacterService(
|
|||||||
CharacterID = model.CharacterID,
|
CharacterID = model.CharacterID,
|
||||||
ProjectID = model.ProjectID,
|
ProjectID = model.ProjectID,
|
||||||
CharacterName = model.CharacterName,
|
CharacterName = model.CharacterName,
|
||||||
ShortName = model.ShortName,
|
ShortName = existing?.ShortName,
|
||||||
SexValueID = sex.SexValueID,
|
SexValueID = sex.SexValueID,
|
||||||
Sex = sex.SexName,
|
Sex = sex.SexName,
|
||||||
BirthDate = model.BirthDate,
|
BirthDate = model.BirthDate,
|
||||||
@ -9022,6 +9094,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);
|
||||||
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;
|
||||||
@ -9044,6 +9117,33 @@ public sealed class CharacterService(
|
|||||||
return new CharacterSaveResult(characterId, uploadedCharacterImageId);
|
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)
|
private async Task<CharacterEditViewModel> PopulateCharacterEditOptionsAsync(CharacterEditViewModel model)
|
||||||
{
|
{
|
||||||
var ownerUserId = await collaboration.GetOwnerUserIdAsync(model.ProjectID);
|
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")]
|
[Display(Name = "Publication date")]
|
||||||
public DateTime? PublicationDate { get; set; }
|
public DateTime? PublicationDate { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Uses explicit scenes")]
|
||||||
|
public bool UsesExplicitScenes { get; set; }
|
||||||
|
|
||||||
[Display(Name = "Cover image")]
|
[Display(Name = "Cover image")]
|
||||||
public IFormFile? CoverUpload { get; set; }
|
public IFormFile? CoverUpload { get; set; }
|
||||||
|
|
||||||
@ -1475,6 +1478,8 @@ public sealed class StoryAssetEditViewModel
|
|||||||
[Display(Name = "Asset name")]
|
[Display(Name = "Asset name")]
|
||||||
public string AssetName { get; set; } = string.Empty;
|
public string AssetName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<string> Aliases { get; set; } = [];
|
||||||
|
|
||||||
[Display(Name = "Kind")]
|
[Display(Name = "Kind")]
|
||||||
public int AssetKindID { get; set; }
|
public int AssetKindID { get; set; }
|
||||||
|
|
||||||
@ -1602,11 +1607,10 @@ public sealed class CharacterEditViewModel
|
|||||||
public int ProjectID { get; set; }
|
public int ProjectID { get; set; }
|
||||||
|
|
||||||
[Required, StringLength(200)]
|
[Required, StringLength(200)]
|
||||||
[Display(Name = "Character name")]
|
[Display(Name = "Display name")]
|
||||||
public string CharacterName { get; set; } = string.Empty;
|
public string CharacterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Display(Name = "Short name")]
|
public List<string> Aliases { get; set; } = [];
|
||||||
public string? ShortName { get; set; }
|
|
||||||
|
|
||||||
[Display(Name = "Sex")]
|
[Display(Name = "Sex")]
|
||||||
public int? SexValueID { get; set; }
|
public int? SexValueID { get; set; }
|
||||||
@ -1666,6 +1670,7 @@ public sealed class CharacterDetailViewModel
|
|||||||
{
|
{
|
||||||
public Project Project { get; set; } = new();
|
public Project Project { get; set; } = new();
|
||||||
public Character Character { get; set; } = new();
|
public Character Character { get; set; } = new();
|
||||||
|
public IReadOnlyList<CharacterAlias> Aliases { get; set; } = [];
|
||||||
public IReadOnlyList<Character> ProjectCharacters { get; set; } = [];
|
public IReadOnlyList<Character> ProjectCharacters { get; set; } = [];
|
||||||
public IReadOnlyList<CharacterImage> Images { get; set; } = [];
|
public IReadOnlyList<CharacterImage> Images { get; set; } = [];
|
||||||
public string DisplayAge { get; set; } = "Unknown";
|
public string DisplayAge { get; set; } = "Unknown";
|
||||||
@ -1915,6 +1920,8 @@ public sealed class LocationEditViewModel
|
|||||||
[Display(Name = "Location name")]
|
[Display(Name = "Location name")]
|
||||||
public string LocationName { get; set; } = string.Empty;
|
public string LocationName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<string> Aliases { get; set; } = [];
|
||||||
|
|
||||||
[Display(Name = "Location type")]
|
[Display(Name = "Location type")]
|
||||||
public int? LocationTypeID { get; set; }
|
public int? LocationTypeID { get; set; }
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,12 @@
|
|||||||
<select asp-for="Status" asp-items="Model.StatusOptions" class="form-select"></select>
|
<select asp-for="Status" asp-items="Model.StatusOptions" class="form-select"></select>
|
||||||
<span asp-validation-for="Status" class="text-danger"></span>
|
<span asp-validation-for="Status" class="text-danger"></span>
|
||||||
</div>
|
</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">
|
<div class="col-md-4">
|
||||||
<label asp-for="TargetWordCount" class="form-label"></label>
|
<label asp-for="TargetWordCount" class="form-label"></label>
|
||||||
<input asp-for="TargetWordCount" class="form-control" min="1" />
|
<input asp-for="TargetWordCount" class="form-control" min="1" />
|
||||||
|
|||||||
@ -76,7 +76,7 @@
|
|||||||
<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>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>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>
|
||||||
@ -508,4 +508,3 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,9 +30,22 @@
|
|||||||
<input asp-for="CharacterName" class="form-control" />
|
<input asp-for="CharacterName" class="form-control" />
|
||||||
<span asp-validation-for="CharacterName" class="text-danger"></span>
|
<span asp-validation-for="CharacterName" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-12">
|
||||||
<label asp-for="ShortName" class="form-label"></label>
|
<label class="form-label">Aliases</label>
|
||||||
<input asp-for="ShortName" class="form-control" />
|
<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>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label asp-for="SexValueID" class="form-label"></label>
|
<label asp-for="SexValueID" class="form-label"></label>
|
||||||
@ -130,5 +143,52 @@
|
|||||||
select.addEventListener("change", syncCustomSex);
|
select.addEventListener("change", syncCustomSex);
|
||||||
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>
|
</script>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,10 +42,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="asset-card-footer">
|
<div class="asset-card-footer">
|
||||||
@if (!string.IsNullOrWhiteSpace(character.ShortName))
|
|
||||||
{
|
|
||||||
<span class="purpose-chip">@character.ShortName</span>
|
|
||||||
}
|
|
||||||
@if (character.AgeAtSeriesStart.HasValue)
|
@if (character.AgeAtSeriesStart.HasValue)
|
||||||
{
|
{
|
||||||
<span class="status-pill">Age @character.AgeAtSeriesStart</span>
|
<span class="status-pill">Age @character.AgeAtSeriesStart</span>
|
||||||
|
|||||||
@ -33,6 +33,23 @@
|
|||||||
<input asp-for="LocationName" class="form-control" />
|
<input asp-for="LocationName" class="form-control" />
|
||||||
<span asp-validation-for="LocationName" class="text-danger"></span>
|
<span asp-validation-for="LocationName" class="text-danger"></span>
|
||||||
</div>
|
</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">
|
<div class="col-md-4">
|
||||||
<label asp-for="LocationTypeID" class="form-label">Location type <help-icon key="locations.fields.locationType" mode="inline" /></label>
|
<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">
|
<select asp-for="LocationTypeID" asp-items="Model.LocationTypeOptions" class="form-select">
|
||||||
@ -66,4 +83,48 @@
|
|||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<partial name="_ValidationScriptsPartial" />
|
<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" />
|
<input asp-for="AssetName" class="form-control" />
|
||||||
<span asp-validation-for="AssetName" class="text-danger"></span>
|
<span asp-validation-for="AssetName" class="text-danger"></span>
|
||||||
</div>
|
</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">
|
<div class="col-md-4">
|
||||||
<label asp-for="AssetKindID" class="form-label">Kind <help-icon key="storyAssets.fields.kind" mode="inline" /></label>
|
<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>
|
<select asp-for="AssetKindID" asp-items="Model.AssetKindOptions" class="form-select"></select>
|
||||||
@ -100,4 +117,48 @@
|
|||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<partial name="_ValidationScriptsPartial" />
|
<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