Backend Bugfixes and Enhanced Selections (#754)

* Updated some signatures to avoid a ToArray() within a loop.

* Use UpdateSeries directly when adding new series, rather than a modified version for new series only.

* Refactored some messages for scanner loop to reduce duplicate code and write messages more clear. Hooked in a RefreshMetadataProgress event (no UI changes).

* Fixed a bug on docker where backup service was using different logic than non-docker, which isn't needed after config change last release.

* Allow user to make more than 1 backup per day

* Implemented a select all checkbox for library access modal
This commit is contained in:
Joseph Milazzo 2021-11-14 10:20:12 -06:00 committed by GitHub
parent 0aff08c9cd
commit f6bfabde4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 171 additions and 101 deletions

View file

@ -16,6 +16,7 @@ export enum EVENTS {
UpdateAvailable = 'UpdateAvailable',
ScanSeries = 'ScanSeries',
RefreshMetadata = 'RefreshMetadata',
RefreshMetadataProgress = 'RefreshMetadataProgress',
SeriesAdded = 'SeriesAdded',
SeriesRemoved = 'SeriesRemoved',
ScanLibraryProgress = 'ScanLibraryProgress',
@ -89,6 +90,13 @@ export class MessageHubService {
this.scanLibrary.emit(resp.body);
});
this.hubConnection.on(EVENTS.RefreshMetadataProgress, resp => {
this.messagesSource.next({
event: EVENTS.RefreshMetadataProgress,
payload: resp.body
});
});
this.hubConnection.on(EVENTS.SeriesAddedToCollection, resp => {
this.messagesSource.next({
event: EVENTS.SeriesAddedToCollection,

View file

@ -6,17 +6,24 @@
</button>
</div>
<div class="modal-body">
<div class="list-group">
<li class="list-group-item" *ngFor="let library of selectedLibraries; let i = index">
<div class="form-check">
<input id="library-{{i}}" type="checkbox" attr.aria-label="Library {{library.data.name}}" class="form-check-input"
[(ngModel)]="library.selected" name="library">
<label attr.for="library-{{i}}" class="form-check-label">{{library.data.name}}</label>
</div>
</li>
<li class="list-group-item" *ngIf="selectedLibraries.length === 0">
There are no libraries setup yet.
</li>
<div class="list-group" *ngIf="!isLoading">
<div class="form-check">
<input id="selectall" type="checkbox" class="form-check-input"
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
<label for="selectall" class="form-check-label">{{selectAll ? 'Deselect' : 'Select'}} All</label>
</div>
<ul>
<li class="list-group-item" *ngFor="let library of allLibraries; let i = index">
<div class="form-check">
<input id="library-{{i}}" type="checkbox" class="form-check-input" attr.aria-label="Library {{library.name}}"
[ngModel]="selections.isSelected(library)" (change)="handleSelection(library)">
<label attr.for="library-{{i}}" class="form-check-label">{{library.name}}</label>
</div>
</li>
<li class="list-group-item" *ngIf="allLibraries.length === 0">
There are no libraries setup yet.
</li>
</ul>
</div>
</div>
<div class="modal-footer">

View file

@ -1,6 +1,7 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { SelectionModel } from 'src/app/typeahead/typeahead.component';
import { Library } from 'src/app/_models/library';
import { Member } from 'src/app/_models/member';
import { LibraryService } from 'src/app/_services/library.service';
@ -15,24 +16,21 @@ export class LibraryAccessModalComponent implements OnInit {
@Input() member: Member | undefined;
allLibraries: Library[] = [];
selectedLibraries: Array<{selected: boolean, data: Library}> = [];
selections!: SelectionModel<Library>;
selectAll: boolean = false;
isLoading: boolean = false;
get hasSomeSelected() {
console.log(this.selections != null && this.selections.hasSomeSelected());
return this.selections != null && this.selections.hasSomeSelected();
}
constructor(public modal: NgbActiveModal, private libraryService: LibraryService, private fb: FormBuilder) { }
ngOnInit(): void {
this.libraryService.getLibraries().subscribe(libs => {
this.allLibraries = libs;
this.selectedLibraries = libs.map(item => {
return {selected: false, data: item};
});
if (this.member !== undefined) {
this.member.libraries.forEach(lib => {
const foundLibrary = this.selectedLibraries.filter(item => item.data.name === lib.name);
if (foundLibrary.length > 0) {
foundLibrary[0].selected = true;
}
});
}
this.setupSelections();
});
}
@ -45,25 +43,41 @@ export class LibraryAccessModalComponent implements OnInit {
return;
}
const selectedLibraries = this.selectedLibraries.filter(item => item.selected).map(item => item.data);
const selectedLibraries = this.selections.selected();
this.libraryService.updateLibrariesForMember(this.member?.username, selectedLibraries).subscribe(() => {
this.modal.close(true);
});
}
reset() {
this.selectedLibraries = this.allLibraries.map(item => {
return {selected: false, data: item};
});
setupSelections() {
this.selections = new SelectionModel<Library>(false, this.allLibraries);
this.isLoading = false;
// If a member is passed in, then auto-select their libraries
if (this.member !== undefined) {
this.member.libraries.forEach(lib => {
const foundLibrary = this.selectedLibraries.filter(item => item.data.name === lib.name);
if (foundLibrary.length > 0) {
foundLibrary[0].selected = true;
}
this.selections.toggle(lib, true, (a, b) => a.name === b.name);
});
this.selectAll = this.selections.selected().length === this.allLibraries.length;
}
}
reset() {
this.setupSelections();
}
toggleAll() {
this.selectAll = !this.selectAll;
this.allLibraries.forEach(s => this.selections.toggle(s, this.selectAll));
}
handleSelection(item: Library) {
this.selections.toggle(item);
const numberOfSelected = this.selections.selected().length;
if (numberOfSelected == 0) {
this.selectAll = false;
} else if (numberOfSelected == this.selectedLibraries.length) {
this.selectAll = true;
}
}

View file

@ -38,7 +38,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
// when a progress event comes in, show it on the UI next to library
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe((event) => {
if (event.event != EVENTS.ScanLibraryProgress) return;
if (event.event !== EVENTS.ScanLibraryProgress) return;
const scanEvent = event.payload as ScanLibraryProgressEvent;
this.scanInProgress[scanEvent.libraryId] = {progress: scanEvent.progress !== 100};
@ -55,6 +55,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
}
});
}
});
}

View file

@ -26,7 +26,7 @@
<h6>Applies to Series</h6>
<div class="form-check">
<input id="selectall" type="checkbox" class="form-check-input"
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="someSelected">
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
<label for="selectall" class="form-check-label">{{selectAll ? 'Deselect' : 'Select'}} All</label>
</div>
<ul>

View file

@ -35,6 +35,11 @@ export class EditCollectionTagsComponent implements OnInit {
imageUrls: Array<string> = [];
selectedCover: string = '';
get hasSomeSelected() {
return this.selections != null && this.selections.hasSomeSelected();
}
constructor(public modal: NgbActiveModal, private seriesService: SeriesService,
private collectionService: CollectionTagService, private toastr: ToastrService,
private confirmSerivce: ConfirmService, private libraryService: LibraryService,
@ -133,11 +138,6 @@ export class EditCollectionTagsComponent implements OnInit {
});
}
get someSelected() {
const selected = this.selections.selected();
return (selected.length !== this.series.length && selected.length !== 0);
}
updateSelectedIndex(index: number) {
this.collectionTagForm.patchValue({
coverImageIndex: index

View file

@ -5,6 +5,8 @@ import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap
import { KEY_CODES } from '../shared/_services/utility.service';
import { TypeaheadSettings } from './typeahead-settings';
export type SelectionCompareFn<T> = (a: T, b: T) => boolean;
/**
* SelectionModel<T> is used for keeping track of multiple selections. Simple interface with ability to toggle.
* @param selectedState Optional state to set selectedOptions to. If not passed, defaults to false.
@ -30,10 +32,16 @@ export class SelectionModel<T> {
/**
* Will toggle if the data item is selected or not. If data option is not tracked, will add it and set state to true.
* @param data Item to toggle
* @param selectedState Force the state
* @param compareFn An optional function to use for the lookup, else will use shallowEqual implementation
*/
toggle(data: T, selectedState?: boolean) {
//const dataItem = this._data.filter(d => d.value == data);
const dataItem = this._data.filter(d => this.shallowEqual(d.value, data));
toggle(data: T, selectedState?: boolean, compareFn?: SelectionCompareFn<T>) {
let lookupMethod = this.shallowEqual;
if (compareFn != undefined || compareFn != null) {
lookupMethod = compareFn;
}
const dataItem = this._data.filter(d => lookupMethod(d.value, data));
if (dataItem.length > 0) {
if (selectedState != undefined) {
dataItem[0].selected = selectedState;
@ -45,6 +53,7 @@ export class SelectionModel<T> {
}
}
/**
* Is the passed item selected
* @param data item to check against
@ -65,6 +74,15 @@ export class SelectionModel<T> {
return false;
}
/**
*
* @returns If some of the items are selected, but not all
*/
hasSomeSelected(): boolean {
const selectedCount = this._data.filter(d => d.selected).length;
return (selectedCount !== this._data.length && selectedCount !== 0)
}
/**
*
* @returns All Selected items