CBL Import Rework (#1862)

* Fixed a typo in a log

* Invalid XML files now "validate" correctly by sending back a failure.

* Cleaned up messaging on backend and frontend to provide some linking on series name when collision, handle corrupt xml files, etc.

* When reading list conflict occurs, show the reading list name that's conflicting. Started refactoring the code to allow multiple files to be imported at once.

* Started adding new CBL elements for some enhancements I have planned with maintainers.

* Default to empty string for IpAddress to allow to fallback into existing experience

* Tweaked the layout of reading list page (not complete), moved some not used much controls to page extras and reordered the buttons for reading list

* Edit Reading Lists now allows selection of cover image from existing items

* Fixed a bug where cover chooser base64 to image would fail to write webp files.

* Refactored the validate step to now handle multiple files in one go.

* Clean up code

* Don't show CBL name if there were xml errors that prevented showing it

* Don't allow user to go prev step after they perform the import.

* Cleaned up the heading code for accordions

* Fixed a bug with import keeping failed items

* Sort the failures to the bottom of result windows

* CBL import is pretty solid. Need one pass from Robbie on Reading List Page
This commit is contained in:
Joe Milazzo 2023-03-07 15:18:26 -06:00 committed by GitHub
parent c846b36047
commit b55d9e3994
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 609 additions and 249 deletions

View file

@ -5,5 +5,14 @@ export interface CblBookResult {
series: string;
volume: string;
number: string;
/**
* For SeriesCollision
*/
libraryId: number;
/**
* For SeriesCollision
*/
seriesId: number;
readingListName: string;
reason: CblImportReason;
}

View file

@ -7,5 +7,6 @@ export enum CblImportReason {
EmptyFile = 5,
SeriesCollision = 6,
AllChapterMissing = 7,
Success = 8
Success = 8,
InvalidFile = 9
}

View file

@ -9,6 +9,7 @@ export interface CblConflictQuestion {
export interface CblImportSummary {
cblName: string;
fileName: string;
results: Array<CblBookResult>;
success: CblImportResult;
successfulInserts: Array<CblBookResult>;

View file

@ -3,6 +3,7 @@ import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';
import { environment } from 'src/environments/environment';
import { UtilityService } from '../shared/_services/utility.service';
import { Person } from '../_models/metadata/person';
import { PaginatedResult } from '../_models/pagination';
import { ReadingList, ReadingListItem } from '../_models/reading-list';
import { CblImportResult } from '../_models/reading-list/cbl/cbl-import-result.enum';
@ -102,4 +103,8 @@ export class ReadingListService {
importCbl(form: FormData) {
return this.httpClient.post<CblImportSummary>(this.baseUrl + 'cbl/import', form);
}
getCharacters(readingListId: number) {
return this.httpClient.get<Array<Person>>(this.baseUrl + 'readinglist/characters?readingListId=' + readingListId);
}
}

View file

@ -1,4 +1,4 @@
<app-side-nav-companion-bar>
<app-side-nav-companion-bar [hasExtras]="readingList !== undefined" [extraDrawer]="extrasDrawer">
<h2 title>
<span *ngIf="actions.length > 0">
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [attr.labelBy]="readingList?.title"></app-card-actionables>
@ -7,6 +7,34 @@
<span *ngIf="readingList?.promoted" class="ms-1">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
</h2>
<h6 subtitle class="subtitle-with-actionables">{{items.length}} Items</h6>
<ng-template #extrasDrawer let-offcanvas>
<div style="margin-top: 56px" *ngIf="readingList">
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title">Page Settings</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
</div>
<div class="offcanvas-body">
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-3">
<button class="btn btn-danger" (click)="removeRead()" [disabled]="readingList.promoted && !this.isAdmin">
<span>
<i class="fa fa-check"></i>
</span>
<span class="read-btn--text">&nbsp;Remove Read</span>
</button>
<div class="col-auto ms-2 mt-2" *ngIf="!(readingList?.promoted && !this.isAdmin)">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="accessibility-mode" [value]="accessibilityMode" (change)="updateAccesibilityMode()">
<label class="form-check-label" for="accessibility-mode">Order Numbers</label>
</div>
</div>
</div>
</div>
</div>
</div>
</ng-template>
</app-side-nav-companion-bar>
<div class="container-fluid mt-2" *ngIf="readingList">
@ -18,32 +46,40 @@
<div class="row g-0 mb-3">
<div class="col-auto me-2">
<!-- Action row-->
<button class="btn btn-primary" title="Read from beginning" (click)="read()">
<div class="btn-group me-3">
<button type="button" class="btn btn-primary" (click)="continue()">
<span>
<i class="fa fa-book" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;Read</span>
</span>
</button>
<button class="btn btn-primary ms-2" title="Continue from last reading position" (click)="continue()">
<span>
<i class="fa fa-book-open" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;Continue</span>
</span>
</button>
</div>
<div class="col-auto">
<button class="btn btn-secondary" (click)="removeRead()" [disabled]="readingList.promoted && !this.isAdmin">
<span>
<i class="fa fa-check"></i>
</span>
<span class="read-btn--text">&nbsp;Remove Read</span>
</span>
</button>
</div>
<!-- TODO: Move this in companion bar's page actions -->
<div class="col-auto ms-2 mt-2" *ngIf="!(readingList?.promoted && !this.isAdmin)">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="accessibilit-mode" [value]="accessibilityMode" (change)="accessibilityMode = !accessibilityMode">
<label class="form-check-label" for="accessibilit-mode">Order Numbers</label>
<div class="btn-group" ngbDropdown role="group" aria-label="Read options">
<button type="button" class="btn btn-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu" ngbDropdownMenu>
<button ngbDropdownItem (click)="read()">
<span>
<i class="fa fa-book" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;Read</span>
</span>
</button>
<button ngbDropdownItem (click)="continue(true)">
<span>
<i class="fa fa-book-open" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;Continue</span>
(<i class="fa fa-glasses ms-1" aria-hidden="true"></i>)
<span class="visually-hidden">(Incognito)</span>
</span>
</button>
<button ngbDropdownItem (click)="read(true)">
<span>
<i class="fa fa-book" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;Read</span>
(<i class="fa fa-glasses ms-1" aria-hidden="true"></i>)
<span class="visually-hidden">(Incognito)</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
@ -54,6 +90,23 @@
</div>
</div>
<div class="row mb-3">
<ng-container *ngIf="characters$ | async as characters">
<div class="row g-0" *ngIf="characters && characters.length > 0">
<div class="col-md-4">
<h5>Characters</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="characters">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
</ng-container>
</div>
<div class="row mb-3" cdkScrollable>
<div class="mx-auto" style="width: 200px;">
<ng-container *ngIf="items.length === 0 && !isLoading">

View file

@ -1,3 +1,10 @@
.content-container {
width: 100%;
}
.dropdown-toggle-split {
border-top-right-radius: 6px !important;
border-bottom-right-radius: 6px !important;
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
}

View file

@ -13,9 +13,10 @@ import { ActionService } from 'src/app/_services/action.service';
import { ImageService } from 'src/app/_services/image.service';
import { ReadingListService } from 'src/app/_services/reading-list.service';
import { IndexUpdateEvent } from '../draggable-ordered-list/draggable-ordered-list.component';
import { forkJoin } from 'rxjs';
import { forkJoin, Observable } from 'rxjs';
import { ReaderService } from 'src/app/_services/reader.service';
import { LibraryService } from 'src/app/_services/library.service';
import { Person } from 'src/app/_models/metadata/person';
@Component({
selector: 'app-reading-list-detail',
@ -40,6 +41,7 @@ export class ReadingListDetailComponent implements OnInit {
readingListImage: string = '';
libraryTypes: {[key: number]: LibraryType} = {};
characters$!: Observable<Person[]>;
get MangaFormat(): typeof MangaFormat {
return MangaFormat;
@ -59,6 +61,7 @@ export class ReadingListDetailComponent implements OnInit {
return;
}
this.listId = parseInt(listId, 10);
this.characters$ = this.readingListService.getCharacters(this.listId);
this.readingListImage = this.imageService.randomize(this.imageService.getReadingListCoverImage(this.listId));
forkJoin([
@ -115,11 +118,7 @@ export class ReadingListDetailComponent implements OnInit {
}
readChapter(item: ReadingListItem) {
let reader = 'manga';
if (!this.readingList) return;
if (item.seriesFormat === MangaFormat.EPUB) {
reader = 'book;'
}
const params = this.readerService.getQueryParamsObject(false, true, this.readingList.id);
this.router.navigate(this.readerService.getNavigationArray(item.libraryId, item.seriesId, item.chapterId, item.seriesFormat), {queryParams: params});
}
@ -178,13 +177,15 @@ export class ReadingListDetailComponent implements OnInit {
});
}
read() {
read(inconitoMode: boolean = false) {
if (!this.readingList) return;
const firstItem = this.items[0];
this.router.navigate(this.readerService.getNavigationArray(firstItem.libraryId, firstItem.seriesId, firstItem.chapterId, firstItem.seriesFormat), {queryParams: {readingListId: this.readingList.id}});
this.router.navigate(
this.readerService.getNavigationArray(firstItem.libraryId, firstItem.seriesId, firstItem.chapterId, firstItem.seriesFormat),
{queryParams: {readingListId: this.readingList.id, inconitoMode: inconitoMode}});
}
continue() {
continue(inconitoMode: boolean = false) {
// TODO: Can I do this in the backend?
if (!this.readingList) return;
let currentlyReadingChapter = this.items[0];
@ -196,6 +197,13 @@ export class ReadingListDetailComponent implements OnInit {
break;
}
this.router.navigate(this.readerService.getNavigationArray(currentlyReadingChapter.libraryId, currentlyReadingChapter.seriesId, currentlyReadingChapter.chapterId, currentlyReadingChapter.seriesFormat), {queryParams: {readingListId: this.readingList.id}});
this.router.navigate(
this.readerService.getNavigationArray(currentlyReadingChapter.libraryId, currentlyReadingChapter.seriesId, currentlyReadingChapter.chapterId, currentlyReadingChapter.seriesFormat),
{queryParams: {readingListId: this.readingList.id, inconitoMode: inconitoMode}});
}
updateAccesibilityMode() {
this.accessibilityMode = !this.accessibilityMode;
this.cdRef.markForCheck();
}
}

View file

@ -12,18 +12,18 @@
<h5 class="mb-1 pb-0" id="item.id--{{position}}">
{{item.title}}
<div class="float-end">
<button class="btn btn-primary" (click)="readChapter(item)">
<button class="btn btn-danger" (click)="remove.emit(item)">
<span>
<i class="fa fa-trash me-1" aria-hidden="true"></i>
</span>
<span class="d-none d-sm-inline-block">Remove</span>
</button>
<button class="btn btn-primary ms-2" (click)="readChapter(item)">
<span>
<i class="fa fa-book me-1" aria-hidden="true"></i>
</span>
<span class="d-none d-sm-inline-block">Read</span>
</button>
<button class="btn btn-danger ms-2" (click)="remove.emit(item)">
<span>
<i class="fa fa-trash me-1" aria-hidden="true"></i>
</span>
<span class="d-none d-sm-inline-block">Remove</span>
</button>
</div>
</h5>

View file

@ -1,6 +1,6 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">Edit {{readingList.title}} Reading List</h4>
<h4 class="modal-title" id="modal-basic-title">Edit Reading List: {{readingList.title}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
</div>
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">

View file

@ -68,6 +68,13 @@ export class EditReadingListModalComponent implements OnInit, OnDestroy {
).subscribe();
this.imageUrls.push(this.imageService.randomize(this.imageService.getReadingListCoverImage(this.readingList.id)));
if (!this.readingList.items || this.readingList.items.length === 0) {
this.readingListService.getListItems(this.readingList.id).subscribe(items => {
this.imageUrls.push(...(items).map(rli => this.imageService.getChapterCoverImage(rli.chapterId)));
});
} else {
this.imageUrls.push(...(this.readingList.items).map(rli => this.imageService.getChapterCoverImage(rli.chapterId)));
}
}
ngOnDestroy() {

View file

@ -18,46 +18,101 @@
</div>
<ng-container *ngIf="currentStepIndex === Step.Validate">
<p>All files have been validated to see if there are any operations to do on the list. Any lists have have failed will not move to the next step. Fix the CBL files and retry.</p>
<div class="row g-0">
<ng-container *ngIf="validateSummary">
<ng-container *ngIf="validateSummary.results.length > 0; else noValidateIssues">
<h5>There are issues with the CBL that will prevent an import. Correct these issues then try again.</h5>
<ol class="list-group list-group-numbered list-group-flush" >
<li class="list-group-item no-hover" *ngFor="let result of validateSummary.results"
[innerHTML]="result | cblConflictReason | safeHtml">
</li>
</ol>
</ng-container>
<ng-template #noValidateIssues>
No issues found with CBL, press next.
</ng-template>
</ng-container>
<ngb-accordion #a="ngbAccordion">
<ngb-panel *ngFor="let fileToProcess of filesToProcess">
<ng-container *ngIf="fileToProcess.validateSummary as summary">
<ng-template ngbPanelTitle>
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
</ng-template>
<ng-template ngbPanelContent>
<ng-container *ngIf="summary.results.length > 0; else noValidateIssues">
<h5>There are issues with the CBL that will prevent an import. Correct these issues then try again.</h5>
<ol class="list-group list-group-numbered list-group-flush" >
<li class="list-group-item no-hover" *ngFor="let result of summary.results"
[innerHTML]="result | cblConflictReason | safeHtml">
</li>
</ol>
</ng-container>
<ng-template #noValidateIssues>
<div class="justify-content-center col">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<i class="fa-solid fa-circle-check" style="font-size: 24px" aria-hidden="true"></i>
</div>
<div class="flex-grow-1 ms-3">
Looks good
</div>
</div>
No issues found with CBL, press next.
</div>
</ng-template>
</ng-template>
</ng-container>
</ngb-panel>
</ngb-accordion>
</div>
</ng-container>
<ng-container *ngIf="currentStepIndex === Step.DryRun && dryRunSummary">
<ng-container *ngIf="currentStepIndex === Step.DryRun">
<div class="row g-0">
<h5>This is a dry run and shows what will happen if you press Next</h5>
<h6>The import was a {{dryRunSummary.success | cblImportResult}}</h6>
<ul class="list-group list-group-flush">
<li class="list-group-item no-hover" *ngFor="let result of dryRunResults"
innerHTML="{{result.order + 1}}. {{result | cblConflictReason | safeHtml}}"></li>
</ul>
<p>This is a dry run and shows what will happen if you press Next and perform the import. All Failures will not be imported.</p>
<ngb-accordion #a="ngbAccordion">
<ngb-panel *ngFor="let fileToProcess of filesToProcess">
<ng-container *ngIf="fileToProcess.dryRunSummary as summary">
<ng-template ngbPanelTitle>
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
</ng-template>
<ng-template ngbPanelContent>
<ng-container [ngTemplateOutlet]="resultsList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
</ng-template>
</ng-container>
</ngb-panel>
</ngb-accordion>
</div>
</ng-container>
<ng-container *ngIf="currentStepIndex === Step.Finalize && finalizeSummary && dryRunSummary">
<ng-container *ngIf="currentStepIndex === Step.Finalize">
<div class="row g-0">
<h5>{{finalizeSummary.success | cblImportResult }} on {{dryRunSummary.cblName}} Import</h5>
<ul class="list-group list-group-flush">
<li class="list-group-item no-hover" *ngFor="let result of finalizeResults"
innerHTML="{{result.order + 1}}. {{result | cblConflictReason | safeHtml}}">
</li>
</ul>
<ngb-accordion #a="ngbAccordion">
<ngb-panel *ngFor="let fileToProcess of filesToProcess">
<ng-container *ngIf="fileToProcess.finalizeSummary as summary">
<ng-template ngbPanelTitle>
<ng-container [ngTemplateOutlet]="heading" [ngTemplateOutletContext]="{ summary: summary, filename: fileToProcess.fileName }"></ng-container>
</ng-template>
<ng-template ngbPanelContent>
<ng-container [ngTemplateOutlet]="resultsList" [ngTemplateOutletContext]="{ summary: summary }"></ng-container>
</ng-template>
</ng-container>
</ngb-panel>
</ngb-accordion>
</div>
</ng-container>
</div>
<ng-template #resultsList let-summary="summary">
<ul class="list-group list-group-flush">
<li class="list-group-item no-hover" *ngFor="let result of summary.results"
innerHTML="{{result.order + 1}}. {{result | cblConflictReason | safeHtml}}"></li>
</ul>
</ng-template>
<ng-template #heading let-filename="filename" let-summary="summary">
<ng-container *ngIf="summary.success | cblImportResult as success">
<ng-container [ngSwitch]="summary.success">
<span *ngSwitchCase="CblImportResult.Success" class="badge bg-primary me-1">{{success}}</span>
<span *ngSwitchCase="CblImportResult.Fail" class="badge bg-danger me-1">{{success}}</span>
<span *ngSwitchCase="CblImportResult.Partial" class="badge bg-warning me-1">{{success}}</span>
</ng-container>
</ng-container>
<span>{{filename}}<span *ngIf="summary.cblName">: ({{summary.cblName}})</span></span>
</ng-template>
</div>

View file

@ -3,13 +3,20 @@ import { FormControl, FormGroup } from '@angular/forms';
import { FileUploadValidators } from '@iplab/ngx-file-upload';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { forkJoin } from 'rxjs';
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
import { CblBookResult } from 'src/app/_models/reading-list/cbl/cbl-book-result';
import { CblImportResult } from 'src/app/_models/reading-list/cbl/cbl-import-result.enum';
import { CblImportSummary } from 'src/app/_models/reading-list/cbl/cbl-import-summary';
import { ReadingListService } from 'src/app/_services/reading-list.service';
import { TimelineStep } from '../../_components/step-tracker/step-tracker.component';
interface FileStep {
fileName: string;
validateSummary: CblImportSummary | undefined;
dryRunSummary: CblImportSummary | undefined;
finalizeSummary: CblImportSummary | undefined;
}
enum Step {
Import = 0,
Validate = 1,
@ -28,7 +35,6 @@ export class ImportCblModalComponent {
@ViewChild('fileUpload') fileUpload!: ElementRef<HTMLInputElement>;
fileUploadControl = new FormControl<undefined | Array<File>>(undefined, [
FileUploadValidators.filesLimit(1),
FileUploadValidators.accept(['.cbl']),
]);
@ -36,25 +42,28 @@ export class ImportCblModalComponent {
files: this.fileUploadControl
});
importSummaries: Array<CblImportSummary> = [];
validateSummary: CblImportSummary | undefined;
dryRunSummary: CblImportSummary | undefined;
dryRunResults: Array<CblBookResult> = [];
finalizeSummary: CblImportSummary | undefined;
finalizeResults: Array<CblBookResult> = [];
isLoading: boolean = false;
steps: Array<TimelineStep> = [
{title: 'Import CBL', index: Step.Import, active: true, icon: 'fa-solid fa-file-arrow-up'},
{title: 'Validate File', index: Step.Validate, active: false, icon: 'fa-solid fa-spell-check'},
{title: 'Import CBLs', index: Step.Import, active: true, icon: 'fa-solid fa-file-arrow-up'},
{title: 'Validate CBL', index: Step.Validate, active: false, icon: 'fa-solid fa-spell-check'},
{title: 'Dry Run', index: Step.DryRun, active: false, icon: 'fa-solid fa-gears'},
{title: 'Final Import', index: Step.Finalize, active: false, icon: 'fa-solid fa-floppy-disk'},
];
currentStepIndex = this.steps[0].index;
filesToProcess: Array<FileStep> = [];
failedFiles: Array<FileStep> = [];
get Breakpoint() { return Breakpoint; }
get Step() { return Step; }
get CblImportResult() { return CblImportResult; }
get FileCount() {
const files = this.uploadForm.get('files')?.value;
if (!files) return 0;
return files.length;
}
get NextButtonLabel() {
switch(this.currentStepIndex) {
@ -77,29 +86,59 @@ export class ImportCblModalComponent {
nextStep() {
if (this.currentStepIndex === Step.Import && !this.isFileSelected()) return;
if (this.currentStepIndex === Step.Validate && this.validateSummary && this.validateSummary.results.length > 0) return;
//if (this.currentStepIndex === Step.Validate && this.validateSummary && this.validateSummary.results.length > 0) return;
this.isLoading = true;
switch (this.currentStepIndex) {
case Step.Import:
this.importFile();
const files = this.uploadForm.get('files')?.value;
if (!files) {
this.toastr.error('You need to select files to move forward');
return;
}
// Load each file into filesToProcess and group their data
let pages = [];
for (let i = 0; i < files.length; i++) {
const formData = new FormData();
formData.append('cbl', files[i]);
formData.append('dryRun', true + '');
pages.push(this.readingListService.validateCbl(formData));
}
forkJoin(pages).subscribe(results => {
this.filesToProcess = [];
results.forEach(cblImport => {
this.filesToProcess.push({
fileName: cblImport.fileName,
validateSummary: cblImport,
dryRunSummary: undefined,
finalizeSummary: undefined
});
});
this.filesToProcess = this.filesToProcess.sort((a, b) => b.validateSummary!.success - a.validateSummary!.success);
this.currentStepIndex++;
this.isLoading = false;
this.cdRef.markForCheck();
});
break;
case Step.Validate:
this.import(true);
this.failedFiles = this.filesToProcess.filter(item => item.validateSummary?.success === CblImportResult.Fail);
this.filesToProcess = this.filesToProcess.filter(item => item.validateSummary?.success != CblImportResult.Fail);
this.dryRun();
break;
case Step.DryRun:
this.import(false);
this.failedFiles.push(...this.filesToProcess.filter(item => item.dryRunSummary?.success === CblImportResult.Fail));
this.filesToProcess = this.filesToProcess.filter(item => item.dryRunSummary?.success != CblImportResult.Fail);
this.import();
break;
case Step.Finalize:
// Clear the models and allow user to do another import
this.uploadForm.get('files')?.setValue(undefined);
this.currentStepIndex = Step.Import;
this.validateSummary = undefined;
this.dryRunSummary = undefined;
this.dryRunResults = [];
this.finalizeSummary = undefined;
this.finalizeResults = [];
this.isLoading = false;
this.filesToProcess = [];
this.failedFiles = [];
this.cdRef.markForCheck();
break;
@ -116,9 +155,9 @@ export class ImportCblModalComponent {
case Step.Import:
return this.isFileSelected();
case Step.Validate:
return this.validateSummary && this.validateSummary.results.length === 0;
return this.filesToProcess.filter(item => item.validateSummary?.success != CblImportResult.Fail).length > 0;
case Step.DryRun:
return this.dryRunSummary?.success != CblImportResult.Fail;
return this.filesToProcess.filter(item => item.dryRunSummary?.success != CblImportResult.Fail).length > 0;
case Step.Finalize:
return true;
default:
@ -129,6 +168,7 @@ export class ImportCblModalComponent {
canMoveToPrevStep() {
switch (this.currentStepIndex) {
case Step.Import:
case Step.Finalize:
return false;
default:
return true;
@ -142,45 +182,52 @@ export class ImportCblModalComponent {
return false;
}
importFile() {
const files = this.uploadForm.get('files')?.value;
if (!files) return;
this.cdRef.markForCheck();
dryRun() {
const filenamesAllowedToProcess = this.filesToProcess.map(p => p.fileName);
const files = (this.uploadForm.get('files')?.value || []).filter(f => filenamesAllowedToProcess.includes(f.name));
let pages = [];
for (let i = 0; i < files.length; i++) {
const formData = new FormData();
formData.append('cbl', files[i]);
formData.append('dryRun', 'true');
pages.push(this.readingListService.importCbl(formData));
}
forkJoin(pages).subscribe(results => {
results.forEach(cblImport => {
const index = this.filesToProcess.findIndex(p => p.fileName === cblImport.fileName);
this.filesToProcess[index].dryRunSummary = cblImport;
});
this.filesToProcess = this.filesToProcess.sort((a, b) => b.dryRunSummary!.success - a.dryRunSummary!.success);
const formData = new FormData();
formData.append('cbl', files[0]);
this.readingListService.validateCbl(formData).subscribe(res => {
if (this.currentStepIndex === Step.Import) {
this.validateSummary = res;
}
this.importSummaries.push(res);
this.currentStepIndex++;
this.isLoading = false;
this.currentStepIndex++;
this.cdRef.markForCheck();
});
}
import(dryRun: boolean = false) {
const files = this.uploadForm.get('files')?.value;
if (!files) return;
import() {
const filenamesAllowedToProcess = this.filesToProcess.map(p => p.fileName);
const files = (this.uploadForm.get('files')?.value || []).filter(f => filenamesAllowedToProcess.includes(f.name));
const formData = new FormData();
formData.append('cbl', files[0]);
formData.append('dryRun', dryRun + '');
this.readingListService.importCbl(formData).subscribe(res => {
// Our step when calling is always one behind
if (dryRun) {
this.dryRunSummary = res;
this.dryRunResults = [...res.successfulInserts, ...res.results].sort((a, b) => a.order - b.order);
} else {
this.finalizeSummary = res;
this.finalizeResults = [...res.successfulInserts, ...res.results].sort((a, b) => a.order - b.order);
this.toastr.success('Reading List imported');
}
let pages = [];
for (let i = 0; i < files.length; i++) {
const formData = new FormData();
formData.append('cbl', files[i]);
formData.append('dryRun', 'false');
pages.push(this.readingListService.importCbl(formData));
}
forkJoin(pages).subscribe(results => {
results.forEach(cblImport => {
const index = this.filesToProcess.findIndex(p => p.fileName === cblImport.fileName);
this.filesToProcess[index].finalizeSummary = cblImport;
});
this.isLoading = false;
this.currentStepIndex++;
this.toastr.success('Reading List imported');
this.cdRef.markForCheck();
});
}

View file

@ -19,9 +19,9 @@ export class CblConflictReasonPipe implements PipeTransform {
case CblImportReason.EmptyFile:
return failIcon + 'The cbl file is empty, nothing to be done.';
case CblImportReason.NameConflict:
return failIcon + 'A reading list already exists on your account that matches the cbl file.';
return failIcon + 'A reading list (' + result.readingListName + ') already exists on your account that matches the cbl file.';
case CblImportReason.SeriesCollision:
return failIcon + 'The series, ' + result.series + ', collides with another series of the same name in another library.';
return failIcon + 'The series, ' + `<a href="/library/${result.libraryId}/series/${result.seriesId}" target="_blank">${result.series}</a>` + ', collides with another series of the same name in another library.';
case CblImportReason.SeriesMissing:
return failIcon + 'The series, ' + result.series + ', is missing from Kavita or your account does not have permission. All items with this series will be skipped from import.';
case CblImportReason.VolumeMissing:
@ -29,7 +29,9 @@ export class CblConflictReasonPipe implements PipeTransform {
case CblImportReason.AllChapterMissing:
return failIcon + 'All chapters cannot be matched to Chapters in Kavita.';
case CblImportReason.Success:
return successIcon + result.series + ' volume ' + result.volume + ' number ' + result.number + ' mapped successfully';
return successIcon + result.series + ' volume ' + result.volume + ' number ' + result.number + ' mapped successfully.';
case CblImportReason.InvalidFile:
return failIcon + 'The file is corrupted or not matching the expected tags/spec.';
}
}

View file

@ -11,7 +11,7 @@ export class CblImportResultPipe implements PipeTransform {
case CblImportResult.Success:
return 'Success';
case CblImportResult.Partial:
return 'Partial Success';
return 'Partial';
case CblImportResult.Fail:
return 'Failure';
}

View file

@ -8,7 +8,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { EditReadingListModalComponent } from './_modals/edit-reading-list-modal/edit-reading-list-modal.component';
import { PipeModule } from '../pipe/pipe.module';
import { SharedModule } from '../shared/shared.module';
import { NgbAccordionModule, NgbNavModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { NgbAccordionModule, NgbDropdownModule, NgbNavModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module';
import { ReadingListDetailComponent } from './_components/reading-list-detail/reading-list-detail.component';
import { ReadingListItemComponent } from './_components/reading-list-item/reading-list-item.component';
@ -39,6 +39,7 @@ import { CblImportResultPipe } from './_pipes/cbl-import-result.pipe';
NgbNavModule,
NgbProgressbarModule,
NgbTooltipModule,
NgbDropdownModule,
PipeModule,
SharedModule,