Display aliases in the UI, allow adding and removing aliases from the Person Edit modal
Adds forgotten translation string
This commit is contained in:
parent
c36123a58f
commit
d1c5f4a377
21 changed files with 228 additions and 18 deletions
|
|
@ -144,6 +144,45 @@ public class PersonServiceTests: AbstractDbTest
|
||||||
Assert.Equal(chapter.People.First().PersonId, chapter2.People.First().PersonId);
|
Assert.Equal(chapter.People.First().PersonId, chapter2.People.First().PersonId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PersonAddAlias_NoOverlap()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
|
||||||
|
UnitOfWork.PersonRepository.Attach(new PersonBuilder("Jillian Cowan").Build());
|
||||||
|
UnitOfWork.PersonRepository.Attach(new PersonBuilder("Jilly Cowan").WithAlias("Jolly Cowan").Build());
|
||||||
|
await UnitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
var ps = new PersonService(UnitOfWork);
|
||||||
|
|
||||||
|
var person1 = await UnitOfWork.PersonRepository.GetPersonByNameOrAliasAsync("Jillian Cowan");
|
||||||
|
var person2 = await UnitOfWork.PersonRepository.GetPersonByNameOrAliasAsync("Jilly Cowan");
|
||||||
|
Assert.NotNull(person1);
|
||||||
|
Assert.NotNull(person2);
|
||||||
|
|
||||||
|
// Overlap on Name
|
||||||
|
var success = await ps.UpdatePersonAliasesAsync(person1, ["Jilly Cowan"]);
|
||||||
|
Assert.False(success);
|
||||||
|
|
||||||
|
// Overlap on alias
|
||||||
|
success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan"]);
|
||||||
|
Assert.False(success);
|
||||||
|
|
||||||
|
// No overlap
|
||||||
|
success = await ps.UpdatePersonAliasesAsync(person2, ["Jilly Joy Cowan"]);
|
||||||
|
Assert.True(success);
|
||||||
|
|
||||||
|
// Some overlap
|
||||||
|
success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan", "Jilly Joy Cowan"]);
|
||||||
|
Assert.False(success);
|
||||||
|
|
||||||
|
// Some overlap
|
||||||
|
success = await ps.UpdatePersonAliasesAsync(person1, ["Jolly Cowan", "Jilly Joy Cowan"]);
|
||||||
|
Assert.True(success);
|
||||||
|
|
||||||
|
Assert.Single(person2.Aliases);
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task ResetDb()
|
protected override async Task ResetDb()
|
||||||
{
|
{
|
||||||
Context.Person.RemoveRange(Context.Person.ToList());
|
Context.Person.RemoveRange(Context.Person.ToList());
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
|
@ -24,9 +25,10 @@ public class PersonController : BaseApiController
|
||||||
private readonly ICoverDbService _coverDbService;
|
private readonly ICoverDbService _coverDbService;
|
||||||
private readonly IImageService _imageService;
|
private readonly IImageService _imageService;
|
||||||
private readonly IEventHub _eventHub;
|
private readonly IEventHub _eventHub;
|
||||||
|
private readonly IPersonService _personService;
|
||||||
|
|
||||||
public PersonController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper,
|
public PersonController(IUnitOfWork unitOfWork, ILocalizationService localizationService, IMapper mapper,
|
||||||
ICoverDbService coverDbService, IImageService imageService, IEventHub eventHub)
|
ICoverDbService coverDbService, IImageService imageService, IEventHub eventHub, IPersonService personService)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_localizationService = localizationService;
|
_localizationService = localizationService;
|
||||||
|
|
@ -34,6 +36,7 @@ public class PersonController : BaseApiController
|
||||||
_coverDbService = coverDbService;
|
_coverDbService = coverDbService;
|
||||||
_imageService = imageService;
|
_imageService = imageService;
|
||||||
_eventHub = eventHub;
|
_eventHub = eventHub;
|
||||||
|
_personService = personService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -90,6 +93,10 @@ public class PersonController : BaseApiController
|
||||||
return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-name-unique"));
|
return BadRequest(await _localizationService.Translate(User.GetUserId(), "person-name-unique"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var success = await _personService.UpdatePersonAliasesAsync(person, dto.Aliases);
|
||||||
|
if (!success) return BadRequest(_localizationService.Translate(User.GetUserId(), "aliases-have-overlap"));
|
||||||
|
|
||||||
|
|
||||||
person.Name = dto.Name?.Trim();
|
person.Name = dto.Name?.Trim();
|
||||||
person.Description = dto.Description ?? string.Empty;
|
person.Description = dto.Description ?? string.Empty;
|
||||||
person.CoverImageLocked = dto.CoverImageLocked;
|
person.CoverImageLocked = dto.CoverImageLocked;
|
||||||
|
|
@ -173,5 +180,36 @@ public class PersonController : BaseApiController
|
||||||
return Ok(await _unitOfWork.PersonRepository.GetChaptersForPersonByRole(personId, User.GetUserId(), role));
|
return Ok(await _unitOfWork.PersonRepository.GetChaptersForPersonByRole(personId, User.GetUserId(), role));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Merges Persons into one, this action is irreversible
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("merge")]
|
||||||
|
public async Task<ActionResult> MergePersons(PersonMergeDto dto)
|
||||||
|
{
|
||||||
|
var dst = await _unitOfWork.PersonRepository.GetPersonById(dto.DestId);
|
||||||
|
if (dst == null) return BadRequest();
|
||||||
|
|
||||||
|
var src = await _unitOfWork.PersonRepository.GetPersonById(dto.SrcId);
|
||||||
|
if (src == null) return BadRequest();
|
||||||
|
|
||||||
|
await _personService.MergePeopleAsync(dst, src);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("alias/{personId}/{alias}")]
|
||||||
|
public async Task<ActionResult<bool>> IsValidAlias(int personId, string alias)
|
||||||
|
{
|
||||||
|
// Remove and just check against the passed value?
|
||||||
|
var person = await _unitOfWork.PersonRepository.GetPersonById(personId);
|
||||||
|
if (person == null) return NotFound();
|
||||||
|
|
||||||
|
var other = await _unitOfWork.PersonRepository.GetPersonByNameOrAliasAsync(alias);
|
||||||
|
if (other == null) return Ok(true);
|
||||||
|
|
||||||
|
return Ok(other.Id == person.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Runtime.Serialization;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace API.DTOs;
|
namespace API.DTOs;
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
@ -7,6 +7,7 @@ public class PersonDto
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
|
public IList<string> Aliases { get; set; } = [];
|
||||||
|
|
||||||
public bool CoverImageLocked { get; set; }
|
public bool CoverImageLocked { get; set; }
|
||||||
public string? PrimaryColor { get; set; }
|
public string? PrimaryColor { get; set; }
|
||||||
|
|
|
||||||
17
API/DTOs/Person/PersonMergeDto.cs
Normal file
17
API/DTOs/Person/PersonMergeDto.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace API.DTOs;
|
||||||
|
|
||||||
|
public sealed record PersonMergeDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The id of the person being merged into
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int DestId { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// The id of the person being merged. This person will be removed, and become an alias of <see cref="DestId"/>
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public int SrcId { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace API.DTOs;
|
namespace API.DTOs;
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
@ -11,6 +12,7 @@ public sealed record UpdatePersonDto
|
||||||
public bool CoverImageLocked { get; set; }
|
public bool CoverImageLocked { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
public string Name {get; set;}
|
public string Name {get; set;}
|
||||||
|
public IList<string> Aliases { get; set; } = [];
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
public int? AniListId { get; set; }
|
public int? AniListId { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ public interface IPersonRepository
|
||||||
/// <param name="name"></param>
|
/// <param name="name"></param>
|
||||||
/// <param name="includes"></param>
|
/// <param name="includes"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<Person?> GetPersonByNameAsync(string name, PersonIncludes includes = PersonIncludes.Aliases);
|
Task<Person?> GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases);
|
||||||
Task<bool> IsNameUnique(string name);
|
Task<bool> IsNameUnique(string name);
|
||||||
|
|
||||||
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId);
|
Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId);
|
||||||
|
|
@ -235,7 +235,7 @@ public class PersonRepository : IPersonRepository
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Person?> GetPersonByNameAsync(string name, PersonIncludes includes = PersonIncludes.Aliases)
|
public Task<Person?> GetPersonByNameOrAliasAsync(string name, PersonIncludes includes = PersonIncludes.Aliases)
|
||||||
{
|
{
|
||||||
var normalized = name.ToNormalized();
|
var normalized = name.ToNormalized();
|
||||||
return _context.Person
|
return _context.Person
|
||||||
|
|
@ -246,7 +246,10 @@ public class PersonRepository : IPersonRepository
|
||||||
|
|
||||||
public async Task<bool> IsNameUnique(string name)
|
public async Task<bool> IsNameUnique(string name)
|
||||||
{
|
{
|
||||||
return !(await _context.Person.AnyAsync(p => p.Name == name));
|
// Should this use Normalized to check?
|
||||||
|
return !(await _context.Person
|
||||||
|
.Includes(PersonIncludes.Aliases)
|
||||||
|
.AnyAsync(p => p.Name == name || p.Aliases.Any(pa => pa.Alias == name)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId)
|
public async Task<IEnumerable<SeriesDto>> GetSeriesKnownFor(int personId)
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ public static class ApplicationServiceExtensions
|
||||||
services.AddScoped<IMediaConversionService, MediaConversionService>();
|
services.AddScoped<IMediaConversionService, MediaConversionService>();
|
||||||
services.AddScoped<IStreamService, StreamService>();
|
services.AddScoped<IStreamService, StreamService>();
|
||||||
services.AddScoped<IRatingService, RatingService>();
|
services.AddScoped<IRatingService, RatingService>();
|
||||||
|
services.AddScoped<IPersonService, PersonService>();
|
||||||
|
|
||||||
services.AddScoped<IScannerService, ScannerService>();
|
services.AddScoped<IScannerService, ScannerService>();
|
||||||
services.AddScoped<IProcessSeries, ProcessSeries>();
|
services.AddScoped<IProcessSeries, ProcessSeries>();
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,9 @@ public class AutoMapperProfiles : Profile
|
||||||
CreateMap<AppUserCollection, AppUserCollectionDto>()
|
CreateMap<AppUserCollection, AppUserCollectionDto>()
|
||||||
.ForMember(dest => dest.Owner, opt => opt.MapFrom(src => src.AppUser.UserName))
|
.ForMember(dest => dest.Owner, opt => opt.MapFrom(src => src.AppUser.UserName))
|
||||||
.ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count));
|
.ForMember(dest => dest.ItemCount, opt => opt.MapFrom(src => src.Items.Count));
|
||||||
CreateMap<Person, PersonDto>();
|
CreateMap<Person, PersonDto>()
|
||||||
|
.ForMember(dst => dst.Aliases, opt =>
|
||||||
|
opt.MapFrom(src => src.Aliases.Select(pa => pa.Alias).ToList()));
|
||||||
CreateMap<Genre, GenreTagDto>();
|
CreateMap<Genre, GenreTagDto>();
|
||||||
CreateMap<Tag, TagDto>();
|
CreateMap<Tag, TagDto>();
|
||||||
CreateMap<AgeRating, AgeRatingDto>();
|
CreateMap<AgeRating, AgeRatingDto>();
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,7 @@
|
||||||
"user-no-access-library-from-series": "User does not have access to the library this series belongs to",
|
"user-no-access-library-from-series": "User does not have access to the library this series belongs to",
|
||||||
"series-restricted-age-restriction": "User is not allowed to view this series due to age restrictions",
|
"series-restricted-age-restriction": "User is not allowed to view this series due to age restrictions",
|
||||||
"kavitaplus-restricted": "This is restricted to Kavita+ only",
|
"kavitaplus-restricted": "This is restricted to Kavita+ only",
|
||||||
|
"aliases-have-overlap": "One or more of the aliases have overlap with other people, cannot update",
|
||||||
|
|
||||||
"volume-num": "Volume {0}",
|
"volume-num": "Volume {0}",
|
||||||
"book-num": "Book {0}",
|
"book-num": "Book {0}",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Entities.Person;
|
using API.Entities.Person;
|
||||||
|
using API.Extensions;
|
||||||
|
|
||||||
namespace API.Services;
|
namespace API.Services;
|
||||||
|
|
||||||
|
|
@ -13,6 +16,15 @@ public interface IPersonService
|
||||||
/// <param name="src">Merged person</param>
|
/// <param name="src">Merged person</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task MergePeopleAsync(Person dst, Person src);
|
Task MergePeopleAsync(Person dst, Person src);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the alias to the person, requires that the alias is not shared with anyone else
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>This method does NOT commit changes</remarks>
|
||||||
|
/// <param name="person"></param>
|
||||||
|
/// <param name="aliases"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<bool> UpdatePersonAliasesAsync(Person person, IList<string> aliases);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PersonService(IUnitOfWork unitOfWork): IPersonService
|
public class PersonService(IUnitOfWork unitOfWork): IPersonService
|
||||||
|
|
@ -84,4 +96,32 @@ public class PersonService(IUnitOfWork unitOfWork): IPersonService
|
||||||
unitOfWork.PersonRepository.Update(dst);
|
unitOfWork.PersonRepository.Update(dst);
|
||||||
await unitOfWork.CommitAsync();
|
await unitOfWork.CommitAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdatePersonAliasesAsync(Person person, IList<string> aliases)
|
||||||
|
{
|
||||||
|
var normalizedAliases = aliases
|
||||||
|
.Select(a => a.ToNormalized().Trim())
|
||||||
|
.Where(a => !string.IsNullOrWhiteSpace(a))
|
||||||
|
.Where(a => a != person.NormalizedName)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (normalizedAliases.Count == 0)
|
||||||
|
{
|
||||||
|
person.Aliases = [];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var others = await unitOfWork.PersonRepository.GetPeopleByNames(normalizedAliases);
|
||||||
|
others = others.Where(p => p.Id != person.Id).ToList();
|
||||||
|
|
||||||
|
if (others.Count != 0) return false;
|
||||||
|
|
||||||
|
person.Aliases = aliases.Select(a => new PersonAlias
|
||||||
|
{
|
||||||
|
Alias = a.Trim(),
|
||||||
|
NormalizedAlias = a.Trim().ToNormalized()
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export enum PersonRole {
|
||||||
export interface Person extends IHasCover {
|
export interface Person extends IHasCover {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
aliases: string[];
|
||||||
description: string;
|
description: string;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
coverImageLocked: boolean;
|
coverImageLocked: boolean;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {BrowsePerson} from "../_models/person/browse-person";
|
||||||
import {Chapter} from "../_models/chapter";
|
import {Chapter} from "../_models/chapter";
|
||||||
import {StandaloneChapter} from "../_models/standalone-chapter";
|
import {StandaloneChapter} from "../_models/standalone-chapter";
|
||||||
import {TextResonse} from "../_types/text-response";
|
import {TextResonse} from "../_types/text-response";
|
||||||
|
import {al} from "@angular/router/router_module.d-6zbCxc1T";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
|
@ -55,4 +56,9 @@ export class PersonService {
|
||||||
downloadCover(personId: number) {
|
downloadCover(personId: number) {
|
||||||
return this.httpClient.post<string>(this.baseUrl + 'person/fetch-cover?personId=' + personId, {}, TextResonse);
|
return this.httpClient.post<string>(this.baseUrl + 'person/fetch-cover?personId=' + personId, {}, TextResonse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isValidAlias(personId: number, alias: string) {
|
||||||
|
return this.httpClient.get<boolean>(this.baseUrl + `person/alias/${personId}/${alias}`);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -483,7 +483,7 @@ export class EditChapterModalComponent implements OnInit {
|
||||||
};
|
};
|
||||||
|
|
||||||
personSettings.addTransformFn = ((title: string) => {
|
personSettings.addTransformFn = ((title: string) => {
|
||||||
return {id: 0, name: title, role: role, description: '', coverImage: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
|
return {id: 0, name: title, aliases: [], role: role, description: '', coverImage: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
|
||||||
});
|
});
|
||||||
|
|
||||||
personSettings.trackByIdentityFn = (index, value) => value.name + (value.id + '');
|
personSettings.trackByIdentityFn = (index, value) => value.name + (value.id + '');
|
||||||
|
|
|
||||||
|
|
@ -521,7 +521,7 @@ export class EditSeriesModalComponent implements OnInit {
|
||||||
};
|
};
|
||||||
|
|
||||||
personSettings.addTransformFn = ((title: string) => {
|
personSettings.addTransformFn = ((title: string) => {
|
||||||
return {id: 0, name: title, description: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
|
return {id: 0, name: title, aliases: [], description: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
|
||||||
});
|
});
|
||||||
personSettings.trackByIdentityFn = (index, value) => value.name + (value.id + '');
|
personSettings.trackByIdentityFn = (index, value) => value.name + (value.id + '');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,17 @@
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li [ngbNavItem]="TabID.Aliases">
|
||||||
|
<a ngbNavLink>{{t(TabID.Aliases)}}</a>
|
||||||
|
<ng-template ngbNavContent>
|
||||||
|
<app-edit-list [items]="person.aliases"
|
||||||
|
[asyncValidators]="[aliasValidator()]"
|
||||||
|
(updateItems)="person.aliases = $event"
|
||||||
|
[errorMessage]="t('alias-overlap')"
|
||||||
|
[label]="t('aliases-label')"/>
|
||||||
|
</ng-template>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
<li [ngbNavItem]="TabID.CoverImage">
|
<li [ngbNavItem]="TabID.CoverImage">
|
||||||
<a ngbNavLink>{{t(TabID.CoverImage)}}</a>
|
<a ngbNavLink>{{t(TabID.CoverImage)}}</a>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
|
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
|
||||||
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
||||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
import {
|
||||||
|
AbstractControl,
|
||||||
|
AsyncValidatorFn,
|
||||||
|
FormControl,
|
||||||
|
FormGroup,
|
||||||
|
ReactiveFormsModule, ValidationErrors,
|
||||||
|
Validators
|
||||||
|
} from "@angular/forms";
|
||||||
import {Person} from "../../../_models/metadata/person";
|
import {Person} from "../../../_models/metadata/person";
|
||||||
import {
|
import {
|
||||||
NgbActiveModal,
|
NgbActiveModal,
|
||||||
|
|
@ -14,14 +21,17 @@ import {
|
||||||
import {PersonService} from "../../../_services/person.service";
|
import {PersonService} from "../../../_services/person.service";
|
||||||
import {translate, TranslocoDirective} from '@jsverse/transloco';
|
import {translate, TranslocoDirective} from '@jsverse/transloco';
|
||||||
import {CoverImageChooserComponent} from "../../../cards/cover-image-chooser/cover-image-chooser.component";
|
import {CoverImageChooserComponent} from "../../../cards/cover-image-chooser/cover-image-chooser.component";
|
||||||
import {forkJoin} from "rxjs";
|
import {forkJoin, map, of} from "rxjs";
|
||||||
import {UploadService} from "../../../_services/upload.service";
|
import {UploadService} from "../../../_services/upload.service";
|
||||||
import {SettingItemComponent} from "../../../settings/_components/setting-item/setting-item.component";
|
import {SettingItemComponent} from "../../../settings/_components/setting-item/setting-item.component";
|
||||||
import {AccountService} from "../../../_services/account.service";
|
import {AccountService} from "../../../_services/account.service";
|
||||||
import {ToastrService} from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
|
import {EditListComponent} from "../../../shared/edit-list/edit-list.component";
|
||||||
|
import {al} from "@angular/router/router_module.d-6zbCxc1T";
|
||||||
|
|
||||||
enum TabID {
|
enum TabID {
|
||||||
General = 'general-tab',
|
General = 'general-tab',
|
||||||
|
Aliases = 'aliases-tab',
|
||||||
CoverImage = 'cover-image-tab',
|
CoverImage = 'cover-image-tab',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +47,8 @@ enum TabID {
|
||||||
NgbNavOutlet,
|
NgbNavOutlet,
|
||||||
CoverImageChooserComponent,
|
CoverImageChooserComponent,
|
||||||
SettingItemComponent,
|
SettingItemComponent,
|
||||||
NgbNavLink
|
NgbNavLink,
|
||||||
|
EditListComponent
|
||||||
],
|
],
|
||||||
templateUrl: './edit-person-modal.component.html',
|
templateUrl: './edit-person-modal.component.html',
|
||||||
styleUrl: './edit-person-modal.component.scss',
|
styleUrl: './edit-person-modal.component.scss',
|
||||||
|
|
@ -110,6 +121,7 @@ export class EditPersonModalComponent implements OnInit {
|
||||||
id: this.person.id,
|
id: this.person.id,
|
||||||
coverImageLocked: this.person.coverImageLocked,
|
coverImageLocked: this.person.coverImageLocked,
|
||||||
name: this.editForm.get('name')!.value || '',
|
name: this.editForm.get('name')!.value || '',
|
||||||
|
aliases: this.person.aliases,
|
||||||
description: this.editForm.get('description')!.value || '',
|
description: this.editForm.get('description')!.value || '',
|
||||||
asin: this.editForm.get('asin')!.value || '',
|
asin: this.editForm.get('asin')!.value || '',
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -165,4 +177,21 @@ export class EditPersonModalComponent implements OnInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aliasValidator(): AsyncValidatorFn {
|
||||||
|
return (control: AbstractControl) => {
|
||||||
|
const name = control.value;
|
||||||
|
if (!name || name.trim().length === 0) {
|
||||||
|
return of(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.personService.isValidAlias(this.person.id, name).pipe(map(valid => {
|
||||||
|
if (valid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { 'inValidAlias': {'alias': name} } as ValidationErrors;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,11 @@
|
||||||
|
|
||||||
<div class="col-xl-10 col-lg-7 col-md-12 col-xs-12 col-sm-12 mt-2">
|
<div class="col-xl-10 col-lg-7 col-md-12 col-xs-12 col-sm-12 mt-2">
|
||||||
<div class="row g-0 mt-2">
|
<div class="row g-0 mt-2">
|
||||||
<app-read-more [text]="person.description || defaultSummaryText"></app-read-more>
|
@if (person.aliases.length > 0) {
|
||||||
|
<!-- I can't UI, help! -->
|
||||||
|
<span>{{t('aka')}} {{person.aliases.join(", ")}}</span>
|
||||||
|
}
|
||||||
|
<app-read-more [text]="person.description || t('no-info')"></app-read-more>
|
||||||
|
|
||||||
@if (roles$ | async; as roles) {
|
@if (roles$ | async; as roles) {
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,6 @@ export class PersonDetailComponent {
|
||||||
roles$: Observable<PersonRole[]> | null = null;
|
roles$: Observable<PersonRole[]> | null = null;
|
||||||
roles: PersonRole[] | null = null;
|
roles: PersonRole[] | null = null;
|
||||||
works$: Observable<Series[]> | null = null;
|
works$: Observable<Series[]> | null = null;
|
||||||
defaultSummaryText = 'No information about this Person';
|
|
||||||
filter: SeriesFilterV2 | null = null;
|
filter: SeriesFilterV2 | null = null;
|
||||||
personActions: Array<ActionItem<Person>> = this.actionService.getPersonActions(this.handleAction.bind(this));
|
personActions: Array<ActionItem<Person>> = this.actionService.getPersonActions(this.handleAction.bind(this));
|
||||||
chaptersByRole: any = {};
|
chaptersByRole: any = {};
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,13 @@
|
||||||
[formControlName]="i"
|
[formControlName]="i"
|
||||||
id="item--{{i}}"
|
id="item--{{i}}"
|
||||||
>
|
>
|
||||||
|
@if (item.dirty && item.touched && errorMessage) {
|
||||||
|
@if (item.status === "INVALID") {
|
||||||
|
<div id="item--{{i}}-error" class="invalid-feedback" style="display: inline-block">
|
||||||
|
{{errorMessage}}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-2">
|
<div class="col-lg-2">
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
OnInit,
|
OnInit,
|
||||||
Output
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {FormArray, FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
import {AsyncValidatorFn, FormArray, FormControl, FormGroup, ReactiveFormsModule, ValidatorFn} from "@angular/forms";
|
||||||
import {TranslocoDirective} from "@jsverse/transloco";
|
import {TranslocoDirective} from "@jsverse/transloco";
|
||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||||
import {debounceTime, distinctUntilChanged, tap} from "rxjs/operators";
|
import {debounceTime, distinctUntilChanged, tap} from "rxjs/operators";
|
||||||
|
|
@ -28,6 +28,10 @@ export class EditListComponent implements OnInit {
|
||||||
|
|
||||||
@Input({required: true}) items: Array<string> = [];
|
@Input({required: true}) items: Array<string> = [];
|
||||||
@Input({required: true}) label = '';
|
@Input({required: true}) label = '';
|
||||||
|
@Input() validators: ValidatorFn[] = []
|
||||||
|
@Input() asyncValidators: AsyncValidatorFn[] = [];
|
||||||
|
// TODO: Make this more dynamic based on which validator failed
|
||||||
|
@Input() errorMessage: string | null = null;
|
||||||
@Output() updateItems = new EventEmitter<Array<string>>();
|
@Output() updateItems = new EventEmitter<Array<string>>();
|
||||||
|
|
||||||
form: FormGroup = new FormGroup({items: new FormArray([])});
|
form: FormGroup = new FormGroup({items: new FormArray([])});
|
||||||
|
|
@ -51,7 +55,7 @@ export class EditListComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
createItemControl(value: string = ''): FormControl {
|
createItemControl(value: string = ''): FormControl {
|
||||||
return new FormControl(value, []);
|
return new FormControl(value, this.validators, this.asyncValidators);
|
||||||
}
|
}
|
||||||
|
|
||||||
add() {
|
add() {
|
||||||
|
|
|
||||||
|
|
@ -1103,12 +1103,14 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"person-detail": {
|
"person-detail": {
|
||||||
|
"aka": "Also known as ",
|
||||||
"known-for-title": "Known For",
|
"known-for-title": "Known For",
|
||||||
"individual-role-title": "As a {{role}}",
|
"individual-role-title": "As a {{role}}",
|
||||||
"browse-person-title": "All Works of {{name}}",
|
"browse-person-title": "All Works of {{name}}",
|
||||||
"browse-person-by-role-title": "All Works of {{name}} as a {{role}}",
|
"browse-person-by-role-title": "All Works of {{name}} as a {{role}}",
|
||||||
"all-roles": "Roles",
|
"all-roles": "Roles",
|
||||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}"
|
"anilist-url": "{{edit-person-modal.anilist-tooltip}}",
|
||||||
|
"no-info": "No information about this Person"
|
||||||
},
|
},
|
||||||
|
|
||||||
"library-settings-modal": {
|
"library-settings-modal": {
|
||||||
|
|
@ -2246,6 +2248,7 @@
|
||||||
"title": "{{personName}} Details",
|
"title": "{{personName}} Details",
|
||||||
"general-tab": "{{edit-series-modal.general-tab}}",
|
"general-tab": "{{edit-series-modal.general-tab}}",
|
||||||
"cover-image-tab": "{{edit-series-modal.cover-image-tab}}",
|
"cover-image-tab": "{{edit-series-modal.cover-image-tab}}",
|
||||||
|
"aliases-tab": "Aliases",
|
||||||
"loading": "{{common.loading}}",
|
"loading": "{{common.loading}}",
|
||||||
"close": "{{common.close}}",
|
"close": "{{common.close}}",
|
||||||
"name-label": "{{edit-series-modal.name-label}}",
|
"name-label": "{{edit-series-modal.name-label}}",
|
||||||
|
|
@ -2263,7 +2266,9 @@
|
||||||
"cover-image-description": "{{edit-series-modal.cover-image-description}}",
|
"cover-image-description": "{{edit-series-modal.cover-image-description}}",
|
||||||
"cover-image-description-extra": "Alternatively you can download a cover from CoversDB if available.",
|
"cover-image-description-extra": "Alternatively you can download a cover from CoversDB if available.",
|
||||||
"save": "{{common.save}}",
|
"save": "{{common.save}}",
|
||||||
"download-coversdb": "Download from CoversDB"
|
"download-coversdb": "Download from CoversDB",
|
||||||
|
"aliases-label": "Edit aliases",
|
||||||
|
"alias-overlap": "This alias already points towards another person, consider merging them."
|
||||||
},
|
},
|
||||||
|
|
||||||
"day-breakdown": {
|
"day-breakdown": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue