diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/convertor/SortConverter.java b/booklore-api/src/main/java/com/adityachandel/booklore/convertor/SortConverter.java new file mode 100644 index 000000000..353da4237 --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/convertor/SortConverter.java @@ -0,0 +1,30 @@ +package com.adityachandel.booklore.convertor; + +import com.adityachandel.booklore.model.entity.Sort; +import com.adityachandel.booklore.model.enums.SortDirection; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter +public class SortConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(Sort sort) { + if (sort == null) { + return null; + } + return sort.getSortField() + "," + sort.getSortDirection().name(); + } + + @Override + public Sort convertToEntityAttribute(String dbData) { + if (dbData == null || dbData.isEmpty()) { + return null; + } + String[] parts = dbData.split(","); + Sort sort = new Sort(); + sort.setSortField(parts[0]); + sort.setSortDirection(SortDirection.valueOf(parts[1])); + return sort; + } +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/LibraryDTO.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/LibraryDTO.java index 8a1022b05..145201685 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/LibraryDTO.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/LibraryDTO.java @@ -1,5 +1,6 @@ package com.adityachandel.booklore.model.dto; +import com.adityachandel.booklore.model.entity.Sort; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Builder; import lombok.Data; @@ -12,6 +13,7 @@ import java.util.List; public class LibraryDTO { private Long id; private String name; + private Sort sort; private List paths; } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/ShelfDTO.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/ShelfDTO.java index 2d15c39b3..2809a8f72 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/ShelfDTO.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/dto/ShelfDTO.java @@ -1,5 +1,6 @@ package com.adityachandel.booklore.model.dto; +import com.adityachandel.booklore.model.entity.Sort; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Builder; import lombok.Data; @@ -12,4 +13,5 @@ import java.time.Instant; public class ShelfDTO { private Long id; private String name; + private Sort sort; } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/Library.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/Library.java index 8a9c67f75..537fcdb06 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/Library.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/Library.java @@ -1,6 +1,7 @@ package com.adityachandel.booklore.model.entity; import com.adityachandel.booklore.convertor.PathsConverter; +import com.adityachandel.booklore.convertor.SortConverter; import jakarta.persistence.*; import lombok.*; @@ -21,13 +22,13 @@ public class Library { @Column(nullable = false) private String name; + @Convert(converter = SortConverter.class) + private Sort sort; + @Convert(converter = PathsConverter.class) @Column(name = "paths") private List paths; @OneToMany(mappedBy = "library", cascade = CascadeType.ALL, orphanRemoval = true) private List books; - - @Column(name = "initial_processed") - private boolean initialProcessed; } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/Shelf.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/Shelf.java index 48313ecc2..d42568a3a 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/Shelf.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/Shelf.java @@ -1,5 +1,6 @@ package com.adityachandel.booklore.model.entity; +import com.adityachandel.booklore.convertor.SortConverter; import jakarta.persistence.*; import lombok.*; @@ -22,6 +23,9 @@ public class Shelf { @Column(name = "name", nullable = false, unique = true) private String name; + @Convert(converter = SortConverter.class) + private Sort sort; + @ManyToMany(mappedBy = "shelves", fetch = FetchType.LAZY) private Set books = new HashSet<>(); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/Sort.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/Sort.java new file mode 100644 index 000000000..2b386181f --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/entity/Sort.java @@ -0,0 +1,10 @@ +package com.adityachandel.booklore.model.entity; + +import com.adityachandel.booklore.model.enums.SortDirection; +import lombok.Data; + +@Data +public class Sort { + private String sortField; + private SortDirection sortDirection; +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/SortDirection.java b/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/SortDirection.java new file mode 100644 index 000000000..5edb45fbf --- /dev/null +++ b/booklore-api/src/main/java/com/adityachandel/booklore/model/enums/SortDirection.java @@ -0,0 +1,5 @@ +package com.adityachandel.booklore.model.enums; + +public enum SortDirection { + ASCENDING, DESCENDING +} diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/transformer/LibraryTransformer.java b/booklore-api/src/main/java/com/adityachandel/booklore/transformer/LibraryTransformer.java index 1655502ee..f0de13c49 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/transformer/LibraryTransformer.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/transformer/LibraryTransformer.java @@ -10,6 +10,7 @@ public class LibraryTransformer { return LibraryDTO.builder() .id(library.getId()) .name(library.getName()) + .sort(library.getSort()) .paths(library.getPaths()) .build(); } diff --git a/booklore-api/src/main/java/com/adityachandel/booklore/transformer/ShelfTransformer.java b/booklore-api/src/main/java/com/adityachandel/booklore/transformer/ShelfTransformer.java index 54b1725dc..b94dce80f 100644 --- a/booklore-api/src/main/java/com/adityachandel/booklore/transformer/ShelfTransformer.java +++ b/booklore-api/src/main/java/com/adityachandel/booklore/transformer/ShelfTransformer.java @@ -9,6 +9,7 @@ public class ShelfTransformer { return ShelfDTO.builder() .id(shelf.getId()) .name(shelf.getName()) + .sort(shelf.getSort()) .build(); } } diff --git a/booklore-api/src/main/resources/db/migration/V1__Create_Library_and_Book_Tables.sql b/booklore-api/src/main/resources/db/migration/V1__Create_Library_and_Book_Tables.sql index c31f056aa..51192e2d5 100644 --- a/booklore-api/src/main/resources/db/migration/V1__Create_Library_and_Book_Tables.sql +++ b/booklore-api/src/main/resources/db/migration/V1__Create_Library_and_Book_Tables.sql @@ -1,9 +1,9 @@ CREATE TABLE IF NOT EXISTS library ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) UNIQUE NOT NULL, - paths TEXT, - initial_processed BOOLEAN DEFAULT false + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) UNIQUE NOT NULL, + paths TEXT, + sort VARCHAR(255) ); CREATE TABLE IF NOT EXISTS book @@ -84,7 +84,8 @@ CREATE TABLE IF NOT EXISTS book_metadata_author_mapping CREATE TABLE IF NOT EXISTS shelf ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL UNIQUE + name VARCHAR(255) NOT NULL UNIQUE, + sort VARCHAR(255) ); CREATE TABLE IF NOT EXISTS book_shelf_mapping diff --git a/booklore-ui/src/app/book/component/books-browser/books-browser.component.ts b/booklore-ui/src/app/book/component/books-browser/books-browser.component.ts index 80050dbc3..99a5d6d90 100644 --- a/booklore-ui/src/app/book/component/books-browser/books-browser.component.ts +++ b/booklore-ui/src/app/book/component/books-browser/books-browser.component.ts @@ -6,13 +6,13 @@ import {BookService} from '../../service/book.service'; import {map, switchMap} from 'rxjs/operators'; import {Observable, of} from 'rxjs'; import {Book} from '../../model/book.model'; -import {Shelf} from '../../model/book.model'; import {ShelfService} from '../../service/shelf.service'; import {ShelfAssignerComponent} from '../shelf-assigner/shelf-assigner.component'; import {DialogService} from 'primeng/dynamicdialog'; import {SortOption} from '../../model/sort-option.model'; import {SortService} from '../../service/sort.service'; import {Library} from '../../model/library.model'; +import {Shelf} from '../../model/shelf.model'; @Component({ selector: 'app-books-browser', @@ -31,10 +31,10 @@ export class BooksBrowserComponent implements OnInit { entityType: string = ''; selectedSort: SortOption | null = null; sortOptions: SortOption[] = [ - {label: '↑ Title', value: 'ascendingTitle'}, - {label: '↓ Title', value: 'descendingTitle'}, - {label: '↑ Published Date', value: 'ascendingDate'}, - {label: '↓ Published Date', value: 'descendingDate'} + {label: '↑ Title', field: 'title', direction: 'ascending'}, + {label: '↓ Title', field: 'title', direction: 'descending'}, + {label: '↑ Published Date', field: 'publishedDate', direction: 'ascending'}, + {label: '↓ Published Date', field: 'publishedDate', direction: 'descending'} ]; constructor( @@ -75,7 +75,7 @@ export class BooksBrowserComponent implements OnInit { this.entity = entity; }); - routeParam$.subscribe(({ libraryId, shelfId }) => { + routeParam$.subscribe(({libraryId, shelfId}) => { this.handleSortingState(libraryId, shelfId); }); @@ -172,22 +172,21 @@ export class BooksBrowserComponent implements OnInit { this.books$ = this.books$.pipe( map(books => { return books.sort((a, b) => { - const titleA = a.metadata?.title?.toLowerCase() || ''; - const titleB = b.metadata?.title?.toLowerCase() || ''; - const dateA = new Date(a.metadata?.publishedDate || 0); - const dateB = new Date(b.metadata?.publishedDate || 0); - - switch (this.selectedSort?.value) { - case 'ascendingTitle': - return titleA.localeCompare(titleB); - case 'descendingTitle': - return titleB.localeCompare(titleA); - case 'ascendingDate': - return dateA.getTime() - dateB.getTime(); - case 'descendingDate': - return dateB.getTime() - dateA.getTime(); - default: - return 0; + const field = this.selectedSort?.field; + const direction = this.selectedSort?.direction; + let valueA: any, valueB: any; + if (field === 'title') { + valueA = a.metadata?.title?.toLowerCase() || ''; + valueB = b.metadata?.title?.toLowerCase() || ''; + } else if (field === 'publishedDate') { + valueA = new Date(a.metadata?.publishedDate || 0).getTime(); + valueB = new Date(b.metadata?.publishedDate || 0).getTime(); + } + if (valueA === undefined || valueB === undefined) return 0; + if (direction === 'ascending') { + return valueA < valueB ? -1 : valueA > valueB ? 1 : 0; + } else { + return valueA > valueB ? -1 : valueA < valueB ? 1 : 0; } }); }) diff --git a/booklore-ui/src/app/book/component/shelf-assigner/shelf-assigner.component.ts b/booklore-ui/src/app/book/component/shelf-assigner/shelf-assigner.component.ts index e7556c4a4..e5822aacc 100644 --- a/booklore-ui/src/app/book/component/shelf-assigner/shelf-assigner.component.ts +++ b/booklore-ui/src/app/book/component/shelf-assigner/shelf-assigner.component.ts @@ -1,11 +1,12 @@ import {Component, OnInit} from '@angular/core'; import {DynamicDialogConfig, DynamicDialogRef} from 'primeng/dynamicdialog'; -import {Book, Shelf} from '../../model/book.model'; +import {Book} from '../../model/book.model'; import {MessageService} from 'primeng/api'; import {ShelfService} from '../../service/shelf.service'; import {Observable} from 'rxjs'; import {BookService} from '../../service/book.service'; import {map, tap} from 'rxjs/operators'; +import {Shelf} from '../../model/shelf.model'; @Component({ selector: 'app-shelf-assigner', diff --git a/booklore-ui/src/app/book/model/book.model.ts b/booklore-ui/src/app/book/model/book.model.ts index ab47fd2eb..cd2200199 100644 --- a/booklore-ui/src/app/book/model/book.model.ts +++ b/booklore-ui/src/app/book/model/book.model.ts @@ -1,3 +1,5 @@ +import {Shelf} from './shelf.model'; + export interface Book { id: number; libraryId: number; @@ -32,10 +34,6 @@ export interface Category { name: string; } -export interface Shelf { - id?: number; - name: string; -} export interface BookWithNeighborsDTO { currentBook: Book; diff --git a/booklore-ui/src/app/book/model/library.model.ts b/booklore-ui/src/app/book/model/library.model.ts index 23d2e8891..852c7c508 100644 --- a/booklore-ui/src/app/book/model/library.model.ts +++ b/booklore-ui/src/app/book/model/library.model.ts @@ -1,5 +1,8 @@ +import {Sort} from './sort.model'; + export interface Library { id?: number; name: string; + sort?: Sort; paths: string[]; } diff --git a/booklore-ui/src/app/book/model/shelf.model.ts b/booklore-ui/src/app/book/model/shelf.model.ts new file mode 100644 index 000000000..01da8012a --- /dev/null +++ b/booklore-ui/src/app/book/model/shelf.model.ts @@ -0,0 +1,7 @@ +import {Sort} from './sort.model'; + +export interface Shelf { + id?: number; + name: string; + sort?: Sort; +} diff --git a/booklore-ui/src/app/book/model/sort-option.model.ts b/booklore-ui/src/app/book/model/sort-option.model.ts index 2c1dbeaf4..405782ed4 100644 --- a/booklore-ui/src/app/book/model/sort-option.model.ts +++ b/booklore-ui/src/app/book/model/sort-option.model.ts @@ -1,4 +1,5 @@ export interface SortOption { label: string; - value: string; + field: string; + direction: string; } diff --git a/booklore-ui/src/app/book/model/sort.model.ts b/booklore-ui/src/app/book/model/sort.model.ts new file mode 100644 index 000000000..48274ffb3 --- /dev/null +++ b/booklore-ui/src/app/book/model/sort.model.ts @@ -0,0 +1,4 @@ +export interface Sort { + name: string; + direction: string; +} diff --git a/booklore-ui/src/app/book/service/shelf.service.ts b/booklore-ui/src/app/book/service/shelf.service.ts index 7843b23a8..6e02c3ea4 100644 --- a/booklore-ui/src/app/book/service/shelf.service.ts +++ b/booklore-ui/src/app/book/service/shelf.service.ts @@ -1,9 +1,8 @@ import {Injectable} from '@angular/core'; import {BehaviorSubject, Observable, of} from 'rxjs'; -import {Shelf} from '../model/book.model'; import {catchError, map} from 'rxjs/operators'; import {HttpClient} from '@angular/common/http'; -import {Library} from '../model/library.model'; +import {Shelf} from '../model/shelf.model'; @Injectable({ providedIn: 'root'