Bulk Add to Collection (#674)

* Fixed the typeahead not having the same size input box as other inputs

* Implemented the ability to add multiple series to a collection through bulk operations flow. Updated book parser to handle "@import url('...');" syntax as well as @import '...';

* Implemented the ability to create a new Collection tag via bulk operations flow.
This commit is contained in:
Joseph Milazzo 2021-10-14 17:23:21 -07:00 committed by GitHub
parent 908c872f5c
commit 52f8fbe3db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 309 additions and 16 deletions

View file

@ -19,7 +19,8 @@ export enum Action {
Download = 7,
Bookmarks = 8,
IncognitoRead = 9,
AddToReadingList = 10
AddToReadingList = 10,
AddToCollection = 11
}
export interface ActionItem<T> {
@ -90,6 +91,13 @@ export class ActionFactoryService {
requiresAdmin: true
});
this.seriesActions.push({
action: Action.AddToCollection,
title: 'Add to Collection',
callback: this.dummyCallback,
requiresAdmin: true
});
this.seriesActions.push({
action: Action.Edit,
title: 'Edit',
@ -209,7 +217,7 @@ export class ActionFactoryService {
title: 'Add to Reading List',
callback: this.dummyCallback,
requiresAdmin: false
},
}
];
this.volumeActions = [

View file

@ -4,6 +4,7 @@ import { ToastrService } from 'ngx-toastr';
import { Subject } from 'rxjs';
import { take } from 'rxjs/operators';
import { BookmarksModalComponent } from '../cards/_modals/bookmarks-modal/bookmarks-modal.component';
import { BulkAddToCollectionComponent } from '../cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component';
import { AddToListModalComponent, ADD_FLOW } from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component';
import { EditReadingListModalComponent } from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component';
import { ConfirmService } from '../shared/confirm.service';
@ -34,6 +35,7 @@ export class ActionService implements OnDestroy {
private readonly onDestroy = new Subject<void>();
private bookmarkModalRef: NgbModalRef | null = null;
private readingListModalRef: NgbModalRef | null = null;
private collectionModalRef: NgbModalRef | null = null;
constructor(private libraryService: LibraryService, private seriesService: SeriesService,
private readerService: ReaderService, private toastr: ToastrService, private modalService: NgbModal,
@ -358,6 +360,32 @@ export class ActionService implements OnDestroy {
});
}
/**
* Adds a set of series to a collection tag
* @param series
* @param callback
* @returns
*/
addMultipleSeriesToCollectionTag(series: Array<Series>, callback?: VoidActionCallback) {
if (this.collectionModalRef != null) { return; }
this.collectionModalRef = this.modalService.open(BulkAddToCollectionComponent, { scrollable: true, size: 'md' });
this.collectionModalRef.componentInstance.seriesIds = series.map(v => v.id);
this.collectionModalRef.componentInstance.title = 'New Collection';
this.collectionModalRef.closed.pipe(take(1)).subscribe(() => {
this.collectionModalRef = null;
if (callback) {
callback();
}
});
this.collectionModalRef.dismissed.pipe(take(1)).subscribe(() => {
this.collectionModalRef = null;
if (callback) {
callback();
}
});
}
addSeriesToReadingList(series: Series, callback?: SeriesActionCallback) {
if (this.readingListModalRef != null) { return; }
this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md' });

View file

@ -35,4 +35,8 @@ export class CollectionTagService {
updateSeriesForTag(tag: CollectionTag, seriesIdsToRemove: Array<number>) {
return this.httpClient.post(this.baseUrl + 'collection/update-series', {tag, seriesIdsToRemove}, {responseType: 'text' as 'json'});
}
addByMultiple(tagId: number, seriesIds: Array<number>, tagTitle: string = '') {
return this.httpClient.post(this.baseUrl + 'collection/update-for-series', {collectionTagId: tagId, collectionTagTitle: tagTitle, seriesIds}, {responseType: 'text' as 'json'});
}
}

View file

@ -0,0 +1,46 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">Add to Collection</h4>
<button type="button" class="close" aria-label="Close" (click)="close()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form style="width: 100%" [formGroup]="listForm">
<div class="modal-body">
<div class="form-group" *ngIf="lists.length >= 5">
<label for="filter">Filter</label>
<div class="input-group">
<input id="filter" autocomplete="off" class="form-control" formControlName="filterQuery" type="text" aria-describedby="reset-input">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="reset-input" (click)="listForm.get('filterQuery')?.setValue('');">Clear</button>
</div>
</div>
</div>
<ul class="list-group">
<li class="list-group-item clickable" tabindex="0" role="button" *ngFor="let collectionTag of lists | filter: filterList; let i = index" (click)="addToCollection(collectionTag)">
{{collectionTag.title}} <i class="fa fa-angle-double-up" *ngIf="collectionTag.promoted" title="Promoted"></i>
</li>
<li class="list-group-item" *ngIf="lists.length === 0 && !loading">No collections created yet</li>
<li class="list-group-item" *ngIf="loading">
<div class="spinner-border text-secondary" role="status">
<span class="sr-only">Loading...</span>
</div>
</li>
</ul>
</div>
<div class="modal-footer" style="justify-content: normal">
<div style="width: 100%;">
<div class="form-row">
<div class="col-9 col-lg-10">
<label class="sr-only" for="add-rlist">Collection</label>
<input width="100%" #title ngbAutofocus type="text" class="form-control mb-2" id="add-rlist" formControlName="title">
</div>
<div class="col-2">
<button type="submit" class="btn btn-primary" (click)="create()">Create</button>
</div>
</div>
</div>
</div>
</form>

View file

@ -0,0 +1,7 @@
.clickable {
cursor: pointer;
}
.clickable:hover, .clickable:focus {
background-color: lightgreen;
}

View file

@ -0,0 +1,79 @@
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { CollectionTag } from 'src/app/_models/collection-tag';
import { ReadingList } from 'src/app/_models/reading-list';
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
@Component({
selector: 'app-bulk-add-to-collection',
templateUrl: './bulk-add-to-collection.component.html',
styleUrls: ['./bulk-add-to-collection.component.scss']
})
export class BulkAddToCollectionComponent implements OnInit {
@Input() title!: string;
/**
* Series Ids to add to Collection Tag
*/
@Input() seriesIds: Array<number> = [];
/**
* All existing collections sorted by recent use date
*/
lists: Array<CollectionTag> = [];
loading: boolean = false;
listForm: FormGroup = new FormGroup({});
@ViewChild('title') inputElem!: ElementRef<HTMLInputElement>;
constructor(private modal: NgbActiveModal, private collectionService: CollectionTagService, private toastr: ToastrService) { }
ngOnInit(): void {
this.listForm.addControl('title', new FormControl(this.title, []));
this.listForm.addControl('filterQuery', new FormControl('', []));
this.loading = true;
this.collectionService.allTags().subscribe(tags => {
this.lists = tags;
this.loading = false;
});
}
ngAfterViewInit() {
// Shift focus to input
if (this.inputElem) {
this.inputElem.nativeElement.select();
}
}
close() {
this.modal.close();
}
create() {
const tagName = this.listForm.value.title;
this.collectionService.addByMultiple(0, this.seriesIds, tagName).subscribe(() => {
this.toastr.success('Series added to ' + tagName + ' collection');
this.modal.close();
});
}
addToCollection(tag: CollectionTag) {
if (this.seriesIds.length === 0) return;
this.collectionService.addByMultiple(tag.id, this.seriesIds, '').subscribe(() => {
this.toastr.success('Series added to ' + tag.title + ' collection');
this.modal.close();
});
}
filterList = (listItem: ReadingList) => {
return listItem.title.toLowerCase().indexOf((this.listForm.value.filterQuery || '').toLowerCase()) >= 0;
}
}

View file

@ -127,7 +127,7 @@ export class BulkSelectionService {
getActions(callback: (action: Action, data: any) => void) {
// checks if series is present. If so, returns only series actions
// else returns volume/chapter items
const allowedActions = [Action.AddToReadingList, Action.MarkAsRead, Action.MarkAsUnread];
const allowedActions = [Action.AddToReadingList, Action.MarkAsRead, Action.MarkAsUnread, Action.AddToCollection];
if (Object.keys(this.selectedCards).filter(item => item === 'series').length > 0) {
return this.actionFactory.getSeriesActions(callback).filter(item => allowedActions.includes(item.action));
}

View file

@ -16,10 +16,11 @@ import { CardItemComponent } from './card-item/card-item.component';
import { SharedModule } from '../shared/shared.module';
import { RouterModule } from '@angular/router';
import { TypeaheadModule } from '../typeahead/typeahead.module';
import { BrowserModule } from '@angular/platform-browser';
import { CardDetailLayoutComponent } from './card-detail-layout/card-detail-layout.component';
import { CardDetailsModalComponent } from './_modals/card-details-modal/card-details-modal.component';
import { BulkOperationsComponent } from './bulk-operations/bulk-operations.component';
import { BulkAddToCollectionComponent } from './_modals/bulk-add-to-collection/bulk-add-to-collection.component';
import { PipeModule } from '../pipe/pipe.module';
@ -36,11 +37,11 @@ import { BulkOperationsComponent } from './bulk-operations/bulk-operations.compo
CardActionablesComponent,
CardDetailLayoutComponent,
CardDetailsModalComponent,
BulkOperationsComponent
BulkOperationsComponent,
BulkAddToCollectionComponent
],
imports: [
CommonModule,
//BrowserModule,
RouterModule,
ReactiveFormsModule,
FormsModule, // EditCollectionsModal
@ -58,6 +59,7 @@ import { BulkOperationsComponent } from './bulk-operations/bulk-operations.compo
NgbDropdownModule,
NgbProgressbarModule,
NgxFileDropModule, // Cover Chooser
PipeModule // filter for BulkAddToCollectionComponent
],
exports: [
CardItemComponent,

View file

@ -55,6 +55,11 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
this.bulkSelectionService.deselectAll();
});
break;
case Action.AddToCollection:
this.actionService.addMultipleSeriesToCollectionTag(selectedSeries, () => {
this.bulkSelectionService.deselectAll();
});
break;
case Action.MarkAsRead:
this.actionService.markMultipleSeriesAsRead(selectedSeries, () => {
this.loadPage();

View file

@ -46,6 +46,11 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
this.bulkSelectionService.deselectAll();
});
break;
case Action.AddToCollection:
this.actionService.addMultipleSeriesToCollectionTag(selectedSeries, () => {
this.bulkSelectionService.deselectAll();
});
break;
case Action.MarkAsRead:
this.actionService.markMultipleSeriesAsRead(selectedSeries, () => {
this.loadPage();

View file

@ -1,6 +1,5 @@
import { noUndefined } from '@angular/compiler/src/util';
import { AfterViewInit, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { FormControl, FormGroup } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { ReadingList } from 'src/app/_models/reading-list';

View file

@ -10,7 +10,7 @@ input {
.typeahead-input {
border: 1px solid #ccc;
padding: 4px 6px;
padding: 0px 6px;
display: inline-block;
width: 100%;
overflow: hidden;

View file

@ -1,7 +1,7 @@
import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, Renderer2, RendererStyleFlags2, TemplateRef, ViewChild } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Observable, Observer, of, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, last, map, shareReplay, switchMap, take, takeLast, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
import { Observable, of, Subject } from 'rxjs';
import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import { KEY_CODES } from '../shared/_services/utility.service';
import { TypeaheadSettings } from './typeahead-settings';