Release Testing Day 1 (#1491)

* Fixed a bug where typeahead wouldn't automatically show results on relationship screen without an additional click.

* Tweaked the code which checks if a modification occured to check on seconds rather than minutes

* Clear cache will now clear temp/ directory as well.

* Fixed an issue where Chrome was caching api responses when it shouldn't had.

* Added a cleanup temp code

* Ensure genres get removed during series scan when removed from metadata.

* Fixed a bug where all epubs with a volume would show as Volume 0 in reading list

* When a scan is in progress, don't let the user delete the library.
This commit is contained in:
Joseph Milazzo 2022-08-29 18:07:39 -05:00 committed by GitHub
parent fb86ce4542
commit d7f2661655
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 112 additions and 32 deletions

View File

@ -16,9 +16,11 @@ using API.Services;
using API.Services.Tasks.Scanner; using API.Services.Tasks.Scanner;
using API.SignalR; using API.SignalR;
using AutoMapper; using AutoMapper;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using TaskScheduler = API.Services.TaskScheduler;
namespace API.Controllers namespace API.Controllers
{ {
@ -133,7 +135,7 @@ namespace API.Controllers
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username); var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(updateLibraryForUserDto.Username);
if (user == null) return BadRequest("Could not validate user"); if (user == null) return BadRequest("Could not validate user");
var libraryString = String.Join(",", updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name)); var libraryString = string.Join(",", updateLibraryForUserDto.SelectedLibraries.Select(x => x.Name));
_logger.LogInformation("Granting user {UserName} access to: {Libraries}", updateLibraryForUserDto.Username, libraryString); _logger.LogInformation("Granting user {UserName} access to: {Libraries}", updateLibraryForUserDto.Username, libraryString);
var allLibraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync(); var allLibraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
@ -242,10 +244,16 @@ namespace API.Controllers
var chapterIds = var chapterIds =
await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(seriesIds); await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(seriesIds);
try try
{ {
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None); var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
if (TaskScheduler.HasScanTaskRunningForLibrary(libraryId))
{
// TODO: Figure out how to cancel a job
_logger.LogInformation("User is attempting to delete a library while a scan is in progress");
return BadRequest(
"You cannot delete a library while a scan is in progress. Please wait for scan to continue then try to delete");
}
_unitOfWork.LibraryRepository.Delete(library); _unitOfWork.LibraryRepository.Delete(library);
await _unitOfWork.CommitAsync(); await _unitOfWork.CommitAsync();

View File

@ -11,6 +11,7 @@ using API.Entities;
using API.Entities.Enums; using API.Entities.Enums;
using API.Extensions; using API.Extensions;
using API.Services; using API.Services;
using API.Services.Tasks;
using Hangfire; using Hangfire;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;

View File

@ -1,11 +1,17 @@
namespace API.DTOs.Reader using System.ComponentModel.DataAnnotations;
namespace API.DTOs.Reader
{ {
public class BookmarkDto public class BookmarkDto
{ {
public int Id { get; set; } public int Id { get; set; }
[Required]
public int Page { get; set; } public int Page { get; set; }
[Required]
public int VolumeId { get; set; } public int VolumeId { get; set; }
[Required]
public int SeriesId { get; set; } public int SeriesId { get; set; }
[Required]
public int ChapterId { get; set; } public int ChapterId { get; set; }
} }
} }

View File

@ -44,6 +44,7 @@ public interface IMetadataService
public class MetadataService : IMetadataService public class MetadataService : IMetadataService
{ {
public const string Name = "MetadataService";
private readonly IUnitOfWork _unitOfWork; private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<MetadataService> _logger; private readonly ILogger<MetadataService> _logger;
private readonly IEventHub _eventHub; private readonly IEventHub _eventHub;

View File

@ -50,6 +50,9 @@ public class TaskScheduler : ITaskScheduler
public const string ScanQueue = "scan"; public const string ScanQueue = "scan";
public const string DefaultQueue = "default"; public const string DefaultQueue = "default";
public static readonly IList<string> ScanTasks = new List<string>()
{"ScannerService", "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"};
private static readonly Random Rnd = new Random(); private static readonly Random Rnd = new Random();
@ -172,7 +175,7 @@ public class TaskScheduler : ITaskScheduler
public void ScanLibraries() public void ScanLibraries()
{ {
if (RunningAnyTasksByMethod(new List<string>() {"ScannerService", "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"}, ScanQueue)) if (RunningAnyTasksByMethod(ScanTasks, ScanQueue))
{ {
_logger.LogInformation("A Scan is already running, rescheduling ScanLibraries in 3 hours"); _logger.LogInformation("A Scan is already running, rescheduling ScanLibraries in 3 hours");
BackgroundJob.Schedule(() => ScanLibraries(), TimeSpan.FromHours(3)); BackgroundJob.Schedule(() => ScanLibraries(), TimeSpan.FromHours(3));
@ -191,7 +194,7 @@ public class TaskScheduler : ITaskScheduler
_logger.LogInformation("A duplicate request to scan library for library occured. Skipping"); _logger.LogInformation("A duplicate request to scan library for library occured. Skipping");
return; return;
} }
if (RunningAnyTasksByMethod(new List<string>() {"ScannerService", "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"}, ScanQueue)) if (RunningAnyTasksByMethod(ScanTasks, ScanQueue))
{ {
_logger.LogInformation("A Scan is already running, rescheduling ScanLibrary in 3 hours"); _logger.LogInformation("A Scan is already running, rescheduling ScanLibrary in 3 hours");
BackgroundJob.Schedule(() => ScanLibrary(libraryId, force), TimeSpan.FromHours(3)); BackgroundJob.Schedule(() => ScanLibrary(libraryId, force), TimeSpan.FromHours(3));
@ -211,7 +214,7 @@ public class TaskScheduler : ITaskScheduler
public void RefreshMetadata(int libraryId, bool forceUpdate = true) public void RefreshMetadata(int libraryId, bool forceUpdate = true)
{ {
var alreadyEnqueued = HasAlreadyEnqueuedTask("MetadataService", "GenerateCoversForLibrary", var alreadyEnqueued = HasAlreadyEnqueuedTask(MetadataService.Name, "GenerateCoversForLibrary",
new object[] {libraryId, true}) || new object[] {libraryId, true}) ||
HasAlreadyEnqueuedTask("MetadataService", "GenerateCoversForLibrary", HasAlreadyEnqueuedTask("MetadataService", "GenerateCoversForLibrary",
new object[] {libraryId, false}); new object[] {libraryId, false});
@ -227,7 +230,7 @@ public class TaskScheduler : ITaskScheduler
public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false) public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false)
{ {
if (HasAlreadyEnqueuedTask("MetadataService","GenerateCoversForSeries", new object[] {libraryId, seriesId, forceUpdate})) if (HasAlreadyEnqueuedTask(MetadataService.Name,"GenerateCoversForSeries", new object[] {libraryId, seriesId, forceUpdate}))
{ {
_logger.LogInformation("A duplicate request to refresh metadata for library occured. Skipping"); _logger.LogInformation("A duplicate request to refresh metadata for library occured. Skipping");
return; return;
@ -244,7 +247,7 @@ public class TaskScheduler : ITaskScheduler
_logger.LogInformation("A duplicate request to scan series occured. Skipping"); _logger.LogInformation("A duplicate request to scan series occured. Skipping");
return; return;
} }
if (RunningAnyTasksByMethod(new List<string>() {ScannerService.Name, "ScanLibrary", "ScanLibraries", "ScanFolder", "ScanSeries"}, ScanQueue)) if (RunningAnyTasksByMethod(ScanTasks, ScanQueue))
{ {
_logger.LogInformation("A Scan is already running, rescheduling ScanSeries in 10 minutes"); _logger.LogInformation("A Scan is already running, rescheduling ScanSeries in 10 minutes");
BackgroundJob.Schedule(() => ScanSeries(libraryId, seriesId, forceUpdate), TimeSpan.FromMinutes(10)); BackgroundJob.Schedule(() => ScanSeries(libraryId, seriesId, forceUpdate), TimeSpan.FromMinutes(10));
@ -282,6 +285,13 @@ public class TaskScheduler : ITaskScheduler
await _versionUpdaterService.PushUpdate(update); await _versionUpdaterService.PushUpdate(update);
} }
public static bool HasScanTaskRunningForLibrary(int libraryId)
{
return
HasAlreadyEnqueuedTask("ScannerService", "ScanLibrary", new object[] {libraryId, true}, ScanQueue) ||
HasAlreadyEnqueuedTask("ScannerService", "ScanLibrary", new object[] {libraryId, false}, ScanQueue);
}
/// <summary> /// <summary>
/// Checks if this same invocation is already enqueued /// Checks if this same invocation is already enqueued
/// </summary> /// </summary>

View File

@ -20,6 +20,7 @@ namespace API.Services.Tasks
Task DeleteChapterCoverImages(); Task DeleteChapterCoverImages();
Task DeleteTagCoverImages(); Task DeleteTagCoverImages();
Task CleanupBackups(); Task CleanupBackups();
void CleanupTemp();
} }
/// <summary> /// <summary>
/// Cleans up after operations on reoccurring basis /// Cleans up after operations on reoccurring basis
@ -127,16 +128,18 @@ namespace API.Services.Tasks
} }
/// <summary> /// <summary>
/// Removes all files and directories in the cache directory /// Removes all files and directories in the cache and temp directory
/// </summary> /// </summary>
public void CleanupCacheDirectory() public void CleanupCacheDirectory()
{ {
_logger.LogInformation("Performing cleanup of Cache directory"); _logger.LogInformation("Performing cleanup of Cache directory");
_directoryService.ExistOrCreate(_directoryService.CacheDirectory); _directoryService.ExistOrCreate(_directoryService.CacheDirectory);
_directoryService.ExistOrCreate(_directoryService.TempDirectory);
try try
{ {
_directoryService.ClearDirectory(_directoryService.CacheDirectory); _directoryService.ClearDirectory(_directoryService.CacheDirectory);
_directoryService.ClearDirectory(_directoryService.TempDirectory);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -175,5 +178,22 @@ namespace API.Services.Tasks
} }
_logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now); _logger.LogInformation("Finished cleanup of Database backups at {Time}", DateTime.Now);
} }
public void CleanupTemp()
{
_logger.LogInformation("Performing cleanup of Temp directory");
_directoryService.ExistOrCreate(_directoryService.TempDirectory);
try
{
_directoryService.ClearDirectory(_directoryService.TempDirectory);
}
catch (Exception ex)
{
_logger.LogError(ex, "There was an issue deleting one or more folders/files during cleanup");
}
_logger.LogInformation("Temp directory purged");
}
} }
} }

View File

@ -289,12 +289,19 @@ namespace API.Services.Tasks.Scanner
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Done", libraryName, ProgressEventType.Ended)); await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, MessageFactory.FileScanProgressEvent("File Scan Done", libraryName, ProgressEventType.Ended));
} }
/// <summary>
/// Checks against all folder paths on file if the last scanned is >= the directory's last write down to the second
/// </summary>
/// <param name="seriesPaths"></param>
/// <param name="normalizedFolder"></param>
/// <param name="forceCheck"></param>
/// <returns></returns>
private bool HasSeriesFolderNotChangedSinceLastScan(IDictionary<string, IList<SeriesModified>> seriesPaths, string normalizedFolder, bool forceCheck = false) private bool HasSeriesFolderNotChangedSinceLastScan(IDictionary<string, IList<SeriesModified>> seriesPaths, string normalizedFolder, bool forceCheck = false)
{ {
if (forceCheck) return false; if (forceCheck) return false;
return seriesPaths.ContainsKey(normalizedFolder) && seriesPaths[normalizedFolder].All(f => f.LastScanned.Truncate(TimeSpan.TicksPerMinute) >= return seriesPaths.ContainsKey(normalizedFolder) && seriesPaths[normalizedFolder].All(f => f.LastScanned.Truncate(TimeSpan.TicksPerSecond) >=
_directoryService.GetLastWriteTime(normalizedFolder).Truncate(TimeSpan.TicksPerMinute)); _directoryService.GetLastWriteTime(normalizedFolder).Truncate(TimeSpan.TicksPerSecond));
} }
/// <summary> /// <summary>

View File

@ -375,6 +375,13 @@ public class ProcessSeries : IProcessSeries
} }
} }
var genres = chapters.SelectMany(c => c.Genres).ToList();
GenreHelper.KeepOnlySameGenreBetweenLists(series.Metadata.Genres.ToList(), genres, genre =>
{
if (series.Metadata.GenresLocked) return;
series.Metadata.Genres.Remove(genre);
});
// NOTE: The issue here is that people is just from chapter, but series metadata might already have some people on it // NOTE: The issue here is that people is just from chapter, but series metadata might already have some people on it
// I might be able to filter out people that are in locked fields? // I might be able to filter out people that are in locked fields?
var people = chapters.SelectMany(c => c.People).ToList(); var people = chapters.SelectMany(c => c.People).ToList();

View File

@ -273,13 +273,14 @@ namespace API
app.Use(async (context, next) => app.Use(async (context, next) =>
{ {
context.Response.GetTypedHeaders().CacheControl = // Note: I removed this as I caught Chrome caching api responses when it shouldn't have
new Microsoft.Net.Http.Headers.CacheControlHeaderValue() // context.Response.GetTypedHeaders().CacheControl =
{ // new CacheControlHeaderValue()
Public = false, // {
MaxAge = TimeSpan.FromSeconds(10), // Public = false,
}; // MaxAge = TimeSpan.FromSeconds(10),
context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] = // };
context.Response.Headers[HeaderNames.Vary] =
new[] { "Accept-Encoding" }; new[] { "Accept-Encoding" };
// Don't let the site be iframed outside the same origin (clickjacking) // Don't let the site be iframed outside the same origin (clickjacking)

View File

@ -12,7 +12,7 @@
<form> <form>
<div class="row g-0 mb-3" *ngFor="let relation of relations; let idx = index; let isLast = last;"> <div class="row g-0 mb-3" *ngFor="let relation of relations; let idx = index; let isLast = last;">
<div class="col-sm-12 col-md-7"> <div class="col-sm-12 col-md-7">
<app-typeahead (selectedData)="updateSeries($event, relation)" [settings]="relation.typeaheadSettings" id="relation--{{idx}}"> <app-typeahead (selectedData)="updateSeries($event, relation)" [settings]="relation.typeaheadSettings" id="relation--{{idx}}" [focus]="focusTypeahead">
<ng-template #badgeItem let-item let-position="idx"> <ng-template #badgeItem let-item let-position="idx">
{{item.name}} ({{libraryNames[item.libraryId]}}) {{item.name}} ({{libraryNames[item.libraryId]}})
</ng-template> </ng-template>

View File

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { UntypedFormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { map, Subject, Observable, of, firstValueFrom, takeUntil, ReplaySubject } from 'rxjs'; import { map, Subject, Observable, of, firstValueFrom, takeUntil, ReplaySubject } from 'rxjs';
import { UtilityService } from 'src/app/shared/_services/utility.service'; import { UtilityService } from 'src/app/shared/_services/utility.service';
import { TypeaheadSettings } from 'src/app/typeahead/typeahead-settings'; import { TypeaheadSettings } from 'src/app/typeahead/typeahead-settings';
@ -13,7 +13,7 @@ import { SeriesService } from 'src/app/_services/series.service';
interface RelationControl { interface RelationControl {
series: {id: number, name: string} | undefined; // Will add type as well series: {id: number, name: string} | undefined; // Will add type as well
typeaheadSettings: TypeaheadSettings<SearchResult>; typeaheadSettings: TypeaheadSettings<SearchResult>;
formControl: UntypedFormControl; formControl: FormControl;
} }
@Component({ @Component({
@ -37,6 +37,8 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
seriesSettings: TypeaheadSettings<SearchResult> = new TypeaheadSettings(); seriesSettings: TypeaheadSettings<SearchResult> = new TypeaheadSettings();
libraryNames: {[key:number]: string} = {}; libraryNames: {[key:number]: string} = {};
focusTypeahead = new EventEmitter();
get RelationKind() { get RelationKind() {
return RelationKind; return RelationKind;
} }
@ -79,9 +81,9 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
} }
setupRelationRows(relations: Array<Series>, kind: RelationKind) { setupRelationRows(relations: Array<Series>, kind: RelationKind) {
relations.map(async item => { relations.map(async (item, indx) => {
const settings = await firstValueFrom(this.createSeriesTypeahead(item, kind)); const settings = await firstValueFrom(this.createSeriesTypeahead(item, kind, indx));
const form = new UntypedFormControl(kind, []); const form = new FormControl(kind, []);
if (kind === RelationKind.Parent) { if (kind === RelationKind.Parent) {
form.disable(); form.disable();
} }
@ -93,15 +95,13 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
} }
async addNewRelation() { async addNewRelation() {
this.relations.push({series: undefined, formControl: new UntypedFormControl(RelationKind.Adaptation, []), typeaheadSettings: await firstValueFrom(this.createSeriesTypeahead(undefined, RelationKind.Adaptation))}); this.relations.push({series: undefined, formControl: new FormControl(RelationKind.Adaptation, []), typeaheadSettings: await firstValueFrom(this.createSeriesTypeahead(undefined, RelationKind.Adaptation, this.relations.length))});
this.cdRef.markForCheck(); this.cdRef.markForCheck();
// Focus on the new typeahead // Focus on the new typeahead
setTimeout(() => { setTimeout(() => {
const typeahead = document.querySelector(`#relation--${this.relations.length - 1} .typeahead-input input`) as HTMLInputElement; this.focusTypeahead.emit(`relation--${this.relations.length - 1}`);
if (typeahead) typeahead.focus();
}, 10); }, 10);
} }
removeRelation(index: number) { removeRelation(index: number) {
@ -120,11 +120,11 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
createSeriesTypeahead(series: Series | undefined, relationship: RelationKind): Observable<TypeaheadSettings<SearchResult>> { createSeriesTypeahead(series: Series | undefined, relationship: RelationKind, index: number): Observable<TypeaheadSettings<SearchResult>> {
const seriesSettings = new TypeaheadSettings<SearchResult>(); const seriesSettings = new TypeaheadSettings<SearchResult>();
seriesSettings.minCharacters = 0; seriesSettings.minCharacters = 0;
seriesSettings.multiple = false; seriesSettings.multiple = false;
seriesSettings.id = 'format'; seriesSettings.id = 'relation--' + index;
seriesSettings.unique = true; seriesSettings.unique = true;
seriesSettings.addIfNonExisting = false; seriesSettings.addIfNonExisting = false;
seriesSettings.fetchFn = (searchFilter: string) => this.libraryService.search(searchFilter).pipe( seriesSettings.fetchFn = (searchFilter: string) => this.libraryService.search(searchFilter).pipe(
@ -165,7 +165,7 @@ export class EditSeriesRelationComponent implements OnInit, OnDestroy {
const alternativeVersions = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.AlternativeVersion && item.series !== undefined).map(item => item.series!.id); const alternativeVersions = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.AlternativeVersion && item.series !== undefined).map(item => item.series!.id);
const doujinshis = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Doujinshi && item.series !== undefined).map(item => item.series!.id); const doujinshis = this.relations.filter(item => (parseInt(item.formControl.value, 10) as RelationKind) === RelationKind.Doujinshi && item.series !== undefined).map(item => item.series!.id);
// TODO: We can actually emit this onto an observable and in main parent, use mergeMap into the forkJoin // NOTE: We can actually emit this onto an observable and in main parent, use mergeMap into the forkJoin
this.seriesService.updateRelationships(this.series.id, adaptations, characters, contains, others, prequels, sequels, sideStories, spinOffs, alternativeSettings, alternativeVersions, doujinshis).subscribe(() => {}); this.seriesService.updateRelationships(this.series.id, adaptations, characters, contains, others, prequels, sequels, sideStories, spinOffs, alternativeSettings, alternativeVersions, doujinshis).subscribe(() => {});
} }

View File

@ -42,7 +42,12 @@ export class ReadingListItemComponent implements OnInit {
} }
if (item.seriesFormat === MangaFormat.EPUB) { if (item.seriesFormat === MangaFormat.EPUB) {
this.title = 'Volume ' + this.utilityService.cleanSpecialTitle(item.chapterNumber); const specialTitle = this.utilityService.cleanSpecialTitle(item.chapterNumber);
if (specialTitle === '0') {
this.title = 'Volume ' + this.utilityService.cleanSpecialTitle(item.volumeNumber);
} else {
this.title = 'Volume ' + specialTitle;
}
} }
let chapterNum = item.chapterNumber; let chapterNum = item.chapterNumber;

View File

@ -504,6 +504,9 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
if (this.relations.length > 0) { if (this.relations.length > 0) {
this.hasRelations = true; this.hasRelations = true;
this.changeDetectionRef.markForCheck(); this.changeDetectionRef.markForCheck();
} else {
this.hasRelations = false;
this.changeDetectionRef.markForCheck();
} }
}); });

View File

@ -168,6 +168,10 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
* If disabled, a user will not be able to interact with the typeahead * If disabled, a user will not be able to interact with the typeahead
*/ */
@Input() disabled: boolean = false; @Input() disabled: boolean = false;
/**
* When triggered, will focus the input if the passed string matches the id
*/
@Input() focus: EventEmitter<string> | undefined;
@Output() selectedData = new EventEmitter<any[] | any>(); @Output() selectedData = new EventEmitter<any[] | any>();
@Output() newItemAdded = new EventEmitter<any[] | any>(); @Output() newItemAdded = new EventEmitter<any[] | any>();
@Output() onUnlock = new EventEmitter<void>(); @Output() onUnlock = new EventEmitter<void>();
@ -203,6 +207,13 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
this.init(); this.init();
}); });
if (this.focus) {
this.focus.pipe(takeUntil(this.onDestroy)).subscribe((id: string) => {
if (this.settings.id !== id) return;
this.onInputFocus();
});
}
this.init(); this.init();
} }