diff --git a/apps/okreads-e2e/src/integration/reading-list.spec.ts b/apps/okreads-e2e/src/integration/reading-list.spec.ts index dd8668a..289d770 100644 --- a/apps/okreads-e2e/src/integration/reading-list.spec.ts +++ b/apps/okreads-e2e/src/integration/reading-list.spec.ts @@ -11,4 +11,34 @@ describe('When: I use the reading list feature', () => { 'My Reading List' ); }); + + it('Then: I should add book back to reading list which was removed, on undo snackbar action', () => { + cy.get('tmo-root').should('contain.text', 'okreads'); + + cy.get('#searchInput').type('python'); + cy.get('form').submit(); + + cy.get('[data-testing="book-item"]') + .find('button:not(:disabled)') + .its('length') + .should('be.gt', 0) + .then(() => { + cy.get('button[id^="wantToRead-"]:not(:disabled)').first().click(); + + cy.get('[data-testing="toggle-reading-list"]').click(); + cy.get('[data-testing="reading-list-container"]') + .should( + 'contain.text', + 'My Reading List' + ); + + cy.get('button[id^="btnRemove-"]').first().click(); + cy.get('.mat-simple-snackbar-action .mat-button').last().click(); + + cy.get('[data-testing="reading-list-container"]') + .find('.reading-list-item') + .its('length') + .should('be.gt', 0) + }); + }); }); diff --git a/apps/okreads/browser/src/app/app.component.html b/apps/okreads/browser/src/app/app.component.html index 31e82c9..6c67134 100644 --- a/apps/okreads/browser/src/app/app.component.html +++ b/apps/okreads/browser/src/app/app.component.html @@ -7,8 +7,7 @@

okreads

@@ -23,7 +22,10 @@

okreads

My Reading List -

diff --git a/libs/books/feature/src/lib/book-search/book-search.component.spec.ts b/libs/books/feature/src/lib/book-search/book-search.component.spec.ts index 292a608..606141e 100644 --- a/libs/books/feature/src/lib/book-search/book-search.component.spec.ts +++ b/libs/books/feature/src/lib/book-search/book-search.component.spec.ts @@ -12,6 +12,7 @@ import { getBooksError, getBooksLoaded, searchBooks, + removeFromReadingList, } from '@tmo/books/data-access'; import { BooksFeatureModule } from '../books-feature.module'; @@ -23,6 +24,8 @@ Object.defineProperty(window, 'matchMedia', { matches: false, media: query, onchange: null, + addListener: jest.fn(), // deprecated + removeListener: jest.fn(), // deprecated addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), @@ -166,4 +169,30 @@ describe('BookSearch Component', () => { expect(store.dispatch).not.toHaveBeenCalledWith(clearSearch()); }); + + it('should remove book from readinglist on undo action of snackbar', () => { + const bookToRead = { ...createBook('9U5I_tskq9MC'), isAdded: false }; + + store.overrideSelector(getAllBooks, [ + { ...bookToRead }, + { ...createBook('qU3rAgAAQBAJ'), isAdded: false, publishedDate: null }, + { ...createBook('PXa2bby0oQ0C'), isAdded: false } + ]); + + const searchCtrl = fixture.debugElement.query(By.css('#searchInput')); + searchCtrl.nativeElement.value = 'javascript'; + searchCtrl.nativeElement.dispatchEvent(new Event('input')); + store.refreshState(); + fixture.detectChanges(); + + const btnWantToRead = fixture.debugElement.query(By.css('#wantToRead-9U5I_tskq9MC')); + btnWantToRead.nativeElement.click(); + + const btnUndoAddToReadingList = (document.querySelector('.mat-simple-snackbar-action .mat-button')); + btnUndoAddToReadingList.click(); + + expect(store.dispatch).toHaveBeenCalledWith( + removeFromReadingList({ item: { bookId: bookToRead.id, ...bookToRead } }) + ); + }); }); diff --git a/libs/books/feature/src/lib/book-search/book-search.component.ts b/libs/books/feature/src/lib/book-search/book-search.component.ts index 0605ecf..101a4fa 100644 --- a/libs/books/feature/src/lib/book-search/book-search.component.ts +++ b/libs/books/feature/src/lib/book-search/book-search.component.ts @@ -1,7 +1,10 @@ import { Component } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; + import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; import { Store } from '@ngrx/store'; @@ -12,9 +15,10 @@ import { ReadingListBook, searchBooks, getBooksError, - getBooksLoaded + getBooksLoaded, + removeFromReadingList } from '@tmo/books/data-access'; -import { Book } from '@tmo/shared/models'; +import { Book, ReadingListItem } from '@tmo/shared/models'; @Component({ selector: 'tmo-book-search', @@ -36,8 +40,9 @@ export class BookSearchComponent { constructor( private readonly store: Store, - private readonly fb: FormBuilder - ) {} + private readonly fb: FormBuilder, + private readonly snackBar: MatSnackBar + ) { } get searchTerm(): string { return this.searchForm.value.term; @@ -51,6 +56,21 @@ export class BookSearchComponent { addBookToReadingList(book: Book) { this.store.dispatch(addToReadingList({ book })); + + const snackBarUndoAdd = this.snackBar.open( + `${book.title} - is added to your reading list`, + 'Undo', + { duration: 10000 } + ); + snackBarUndoAdd.onAction().pipe(take(1)).subscribe(() => { + const item: ReadingListItem = { + ...book, + bookId: book.id + }; + + this.store.dispatch(removeFromReadingList({ item })); + this.snackBar.dismiss(); + }); } searchExample() { diff --git a/libs/books/feature/src/lib/books-feature.module.ts b/libs/books/feature/src/lib/books-feature.module.ts index 513e02e..8649cbb 100644 --- a/libs/books/feature/src/lib/books-feature.module.ts +++ b/libs/books/feature/src/lib/books-feature.module.ts @@ -11,6 +11,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatIconModule } from '@angular/material/icon'; import { MatBadgeModule } from '@angular/material/badge'; import { MatSnackBarModule } from '@angular/material/snack-bar'; + import { BooksDataAccessModule } from '@tmo/books/data-access'; import { BookSearchComponent } from './book-search/book-search.component'; diff --git a/libs/books/feature/src/lib/reading-list/reading-list.component.spec.ts b/libs/books/feature/src/lib/reading-list/reading-list.component.spec.ts index 20cd221..81ba61c 100644 --- a/libs/books/feature/src/lib/reading-list/reading-list.component.spec.ts +++ b/libs/books/feature/src/lib/reading-list/reading-list.component.spec.ts @@ -1,12 +1,12 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { provideMockStore, MockStore } from '@ngrx/store/testing'; import { createReadingListItem, SharedTestingModule } from '@tmo/shared/testing'; import { BooksFeatureModule } from '@tmo/books/feature'; -import { getReadingList, removeFromReadingList } from '@tmo/books/data-access'; - +import { addToReadingList, getReadingList, removeFromReadingList } from '@tmo/books/data-access'; import { ReadingListComponent } from './reading-list.component'; describe('ReadingList Component', () => { @@ -16,7 +16,7 @@ describe('ReadingList Component', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [BooksFeatureModule, SharedTestingModule], + imports: [BooksFeatureModule, SharedTestingModule, NoopAnimationsModule], providers: [provideMockStore()] }).compileComponents(); })); @@ -46,4 +46,18 @@ describe('ReadingList Component', () => { expect(store.dispatch).toHaveBeenCalledWith(removeFromReadingList({ item: readingListItem })); }); + + it('should add book back to readinglist on undo snackbar action', () => { + const readingListItem = createReadingListItem('9U5I_tskq9MC'); + fixture.detectChanges(); + const btnRemove = fixture.debugElement.query(By.css('#btnRemove-9U5I_tskq9MC')); + btnRemove.nativeElement.click(); + + const btnUndoRemoveFromList = (document.querySelector('.mat-simple-snackbar-action .mat-button')); + btnUndoRemoveFromList.click(); + + expect(store.dispatch).toHaveBeenCalledWith( + addToReadingList({ book: { id: readingListItem.bookId, ...readingListItem } }) + ); + }); }); diff --git a/libs/books/feature/src/lib/reading-list/reading-list.component.ts b/libs/books/feature/src/lib/reading-list/reading-list.component.ts index 0174904..7c3135e 100644 --- a/libs/books/feature/src/lib/reading-list/reading-list.component.ts +++ b/libs/books/feature/src/lib/reading-list/reading-list.component.ts @@ -1,11 +1,18 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; + +import { MatSnackBar } from '@angular/material/snack-bar'; import { Store } from '@ngrx/store'; -import { getReadingList, removeFromReadingList } from '@tmo/books/data-access'; -import { ReadingListItem } from '@tmo/shared/models'; +import { + getReadingList, + removeFromReadingList, + addToReadingList +} from '@tmo/books/data-access'; +import { Book, ReadingListItem } from '@tmo/shared/models'; @Component({ selector: 'tmo-reading-list', @@ -16,9 +23,28 @@ import { ReadingListItem } from '@tmo/shared/models'; export class ReadingListComponent { readingList$: Observable = this.store.select(getReadingList); - constructor(private readonly store: Store) { } + constructor( + private readonly store: Store, + private readonly snackBar: MatSnackBar + ) { } removeFromReadingList(item: ReadingListItem) { this.store.dispatch(removeFromReadingList({ item })); + + const snackBarUndoRemove = this.snackBar.open( + `${item.title} - is removed from your reading list`, + 'Undo', + { duration: 10000 } + ); + + snackBarUndoRemove.onAction().pipe(take(1)).subscribe(() => { + const book: Book = { + ...item, + id: item.bookId + }; + + this.store.dispatch(addToReadingList({ book })); + this.snackBar.dismiss(); + }); } } diff --git a/libs/shared/styles/src/lib/base.scss b/libs/shared/styles/src/lib/base.scss index 64a86db..db9e348 100644 --- a/libs/shared/styles/src/lib/base.scss +++ b/libs/shared/styles/src/lib/base.scss @@ -30,3 +30,13 @@ a { color: $pink-dark; } } + +.mat-simple-snackbar span { + color:#ffffff; +} +.mat-simple-snackbar-action .mat-button span { + color:$pink-accent; +} +.mat-snack-bar-container { + background-color: #171b14; +} \ No newline at end of file