mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
feat(icons): Made icons for libraries and shelves optional with no default icons. (#2599)
* feat(icons): Made icons for libraries and shelves optional with no default icon displayed. * Added tests for making sure the API handles nullable icons for Shelves and Libraries. * Fixed some issues identified during PR review. * Rebased on develop.
This commit is contained in:
@@ -15,7 +15,6 @@ public class MagicShelf {
|
||||
@Size(max = 255, message = "Shelf name must not exceed 255 characters")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "Icon must not be blank")
|
||||
@Size(max = 64, message = "Icon must not exceed 64 characters")
|
||||
private String icon;
|
||||
|
||||
|
||||
@@ -19,10 +19,7 @@ public class CreateLibraryRequest {
|
||||
@NotBlank(message = "Library name must not be empty.")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "Library icon must not be empty.")
|
||||
private String icon;
|
||||
|
||||
@NotNull(message = "Library icon type must not be null.")
|
||||
private IconType iconType;
|
||||
|
||||
@NotEmpty(message = "Library paths must not be empty.")
|
||||
|
||||
@@ -16,10 +16,7 @@ public class ShelfCreateRequest {
|
||||
@NotBlank(message = "Shelf name must not be empty.")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "Shelf icon must not be empty.")
|
||||
private String icon;
|
||||
|
||||
@NotNull(message = "Shelf icon type must not be null.")
|
||||
private IconType iconType;
|
||||
|
||||
private boolean publicShelf;
|
||||
|
||||
@@ -44,9 +44,8 @@ public class LibraryEntity {
|
||||
private String icon;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "icon_type", nullable = false)
|
||||
@Builder.Default
|
||||
private IconType iconType = IconType.PRIME_NG;
|
||||
@Column(name = "icon_type")
|
||||
private IconType iconType;
|
||||
|
||||
@Column(name = "file_naming_pattern")
|
||||
private String fileNamingPattern;
|
||||
@@ -65,10 +64,4 @@ public class LibraryEntity {
|
||||
@Builder.Default
|
||||
private LibraryOrganizationMode organizationMode = LibraryOrganizationMode.AUTO_DETECT;
|
||||
|
||||
@PrePersist
|
||||
public void ensureIconType() {
|
||||
if (this.iconType == null) {
|
||||
this.iconType = IconType.PRIME_NG;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,13 +27,11 @@ public class MagicShelfEntity {
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String icon;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "icon_type", nullable = false)
|
||||
@Builder.Default
|
||||
private IconType iconType = IconType.PRIME_NG;
|
||||
@Column(name = "icon_type")
|
||||
private IconType iconType;
|
||||
|
||||
@Column(name = "filter_json", columnDefinition = "json", nullable = false)
|
||||
private String filterJson;
|
||||
@@ -54,11 +52,4 @@ public class MagicShelfEntity {
|
||||
public void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PrePersist
|
||||
public void ensureIconType() {
|
||||
if (this.iconType == null) {
|
||||
this.iconType = IconType.PRIME_NG;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,8 @@ public class ShelfEntity {
|
||||
private String icon;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "icon_type", nullable = false)
|
||||
@Builder.Default
|
||||
private IconType iconType = IconType.PRIME_NG;
|
||||
@Column(name = "icon_type")
|
||||
private IconType iconType;
|
||||
|
||||
@Column(name = "is_public", nullable = false)
|
||||
@Builder.Default
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.booklore.model.enums.IconType;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -24,6 +25,7 @@ public class UserDefaultsService {
|
||||
.user(user)
|
||||
.name("Favorites")
|
||||
.icon("heart")
|
||||
.iconType(IconType.PRIME_NG)
|
||||
.build();
|
||||
shelfRepository.save(shelf);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE library MODIFY COLUMN icon VARCHAR(64) NULL;
|
||||
ALTER TABLE library MODIFY COLUMN icon_type VARCHAR(255) NULL;
|
||||
|
||||
ALTER TABLE magic_shelf MODIFY COLUMN icon VARCHAR(64) NULL;
|
||||
ALTER TABLE magic_shelf MODIFY COLUMN icon_type VARCHAR(255) NULL;
|
||||
|
||||
ALTER TABLE shelf MODIFY COLUMN icon VARCHAR(64) NULL;
|
||||
ALTER TABLE shelf MODIFY COLUMN icon_type VARCHAR(255) NULL;
|
||||
@@ -0,0 +1,191 @@
|
||||
package org.booklore.service;
|
||||
|
||||
import org.booklore.config.security.service.AuthenticationService;
|
||||
import org.booklore.model.dto.BookLoreUser;
|
||||
import org.booklore.model.dto.MagicShelf;
|
||||
import org.booklore.model.entity.MagicShelfEntity;
|
||||
import org.booklore.model.enums.IconType;
|
||||
import org.booklore.repository.MagicShelfRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MagicShelfServiceTest {
|
||||
|
||||
@Mock
|
||||
private MagicShelfRepository magicShelfRepository;
|
||||
@Mock
|
||||
private AuthenticationService authenticationService;
|
||||
|
||||
@InjectMocks
|
||||
private MagicShelfService magicShelfService;
|
||||
|
||||
private BookLoreUser user;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
BookLoreUser.UserPermissions permissions = new BookLoreUser.UserPermissions();
|
||||
permissions.setAdmin(true);
|
||||
user = BookLoreUser.builder().id(1L).isDefaultPassword(false).permissions(permissions).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createShelf_withNullIcon_shouldPersistNullIconValues() {
|
||||
MagicShelf dto = new MagicShelf();
|
||||
dto.setName("Unread Books");
|
||||
dto.setIcon(null);
|
||||
dto.setIconType(null);
|
||||
dto.setFilterJson("{\"status\": \"unread\"}");
|
||||
dto.setIsPublic(false);
|
||||
|
||||
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||
when(magicShelfRepository.existsByUserIdAndName(1L, "Unread Books")).thenReturn(false);
|
||||
when(magicShelfRepository.save(any(MagicShelfEntity.class))).thenAnswer(invocation -> {
|
||||
MagicShelfEntity entity = invocation.getArgument(0);
|
||||
entity.setId(1L);
|
||||
return entity;
|
||||
});
|
||||
|
||||
MagicShelf result = magicShelfService.createOrUpdateShelf(dto);
|
||||
|
||||
ArgumentCaptor<MagicShelfEntity> captor = ArgumentCaptor.forClass(MagicShelfEntity.class);
|
||||
verify(magicShelfRepository).save(captor.capture());
|
||||
|
||||
MagicShelfEntity saved = captor.getValue();
|
||||
assertNull(saved.getIcon());
|
||||
assertNull(saved.getIconType());
|
||||
assertEquals("Unread Books", saved.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createShelf_withIcon_shouldPersistIconValues() {
|
||||
MagicShelf dto = new MagicShelf();
|
||||
dto.setName("Favorites");
|
||||
dto.setIcon("star");
|
||||
dto.setIconType(IconType.PRIME_NG);
|
||||
dto.setFilterJson("{\"rating\": 5}");
|
||||
dto.setIsPublic(false);
|
||||
|
||||
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||
when(magicShelfRepository.existsByUserIdAndName(1L, "Favorites")).thenReturn(false);
|
||||
when(magicShelfRepository.save(any(MagicShelfEntity.class))).thenAnswer(invocation -> {
|
||||
MagicShelfEntity entity = invocation.getArgument(0);
|
||||
entity.setId(1L);
|
||||
return entity;
|
||||
});
|
||||
|
||||
MagicShelf result = magicShelfService.createOrUpdateShelf(dto);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("star", result.getIcon());
|
||||
assertEquals(IconType.PRIME_NG, result.getIconType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateShelf_withNullIcon_shouldClearIconValues() {
|
||||
MagicShelfEntity existing = MagicShelfEntity.builder()
|
||||
.id(1L)
|
||||
.userId(1L)
|
||||
.name("Old Shelf")
|
||||
.icon("star")
|
||||
.iconType(IconType.PRIME_NG)
|
||||
.filterJson("{\"status\": \"reading\"}")
|
||||
.build();
|
||||
|
||||
MagicShelf dto = new MagicShelf();
|
||||
dto.setId(1L);
|
||||
dto.setName("Updated Shelf");
|
||||
dto.setIcon(null);
|
||||
dto.setIconType(null);
|
||||
dto.setFilterJson("{\"status\": \"updated\"}");
|
||||
dto.setIsPublic(false);
|
||||
|
||||
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||
when(magicShelfRepository.findById(1L)).thenReturn(Optional.of(existing));
|
||||
when(magicShelfRepository.save(any(MagicShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
magicShelfService.createOrUpdateShelf(dto);
|
||||
|
||||
ArgumentCaptor<MagicShelfEntity> captor = ArgumentCaptor.forClass(MagicShelfEntity.class);
|
||||
verify(magicShelfRepository).save(captor.capture());
|
||||
|
||||
MagicShelfEntity saved = captor.getValue();
|
||||
assertNull(saved.getIcon());
|
||||
assertNull(saved.getIconType());
|
||||
assertEquals("Updated Shelf", saved.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateShelf_withIcon_shouldPreserveIconValues() {
|
||||
MagicShelfEntity existing = MagicShelfEntity.builder()
|
||||
.id(1L)
|
||||
.userId(1L)
|
||||
.name("Old Shelf")
|
||||
.icon("star")
|
||||
.iconType(IconType.PRIME_NG)
|
||||
.filterJson("{\"status\": \"reading\"}")
|
||||
.build();
|
||||
|
||||
MagicShelf dto = new MagicShelf();
|
||||
dto.setId(1L);
|
||||
dto.setName("Updated Shelf");
|
||||
dto.setIcon("bookmark");
|
||||
dto.setIconType(IconType.CUSTOM_SVG);
|
||||
dto.setFilterJson("{\"status\": \"updated\"}");
|
||||
dto.setIsPublic(false);
|
||||
|
||||
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||
when(magicShelfRepository.findById(1L)).thenReturn(Optional.of(existing));
|
||||
when(magicShelfRepository.save(any(MagicShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
magicShelfService.createOrUpdateShelf(dto);
|
||||
|
||||
ArgumentCaptor<MagicShelfEntity> captor = ArgumentCaptor.forClass(MagicShelfEntity.class);
|
||||
verify(magicShelfRepository).save(captor.capture());
|
||||
|
||||
MagicShelfEntity saved = captor.getValue();
|
||||
assertEquals("bookmark", saved.getIcon());
|
||||
assertEquals(IconType.CUSTOM_SVG, saved.getIconType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateShelf_fromIconToNull_shouldAllowRemovingIcon() {
|
||||
MagicShelfEntity existing = MagicShelfEntity.builder()
|
||||
.id(1L)
|
||||
.userId(1L)
|
||||
.name("Shelf With Icon")
|
||||
.icon("heart")
|
||||
.iconType(IconType.PRIME_NG)
|
||||
.filterJson("{}")
|
||||
.build();
|
||||
|
||||
MagicShelf dto = new MagicShelf();
|
||||
dto.setId(1L);
|
||||
dto.setName("Shelf With Icon");
|
||||
dto.setIcon(null);
|
||||
dto.setIconType(null);
|
||||
dto.setFilterJson("{}");
|
||||
dto.setIsPublic(false);
|
||||
|
||||
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||
when(magicShelfRepository.findById(1L)).thenReturn(Optional.of(existing));
|
||||
when(magicShelfRepository.save(any(MagicShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
MagicShelf result = magicShelfService.createOrUpdateShelf(dto);
|
||||
|
||||
assertNull(result.getIcon());
|
||||
assertNull(result.getIconType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package org.booklore.service;
|
||||
|
||||
import org.booklore.config.security.service.AuthenticationService;
|
||||
import org.booklore.mapper.ShelfMapper;
|
||||
import org.booklore.model.dto.BookLoreUser;
|
||||
import org.booklore.model.dto.Shelf;
|
||||
import org.booklore.model.dto.request.ShelfCreateRequest;
|
||||
import org.booklore.model.entity.BookLoreUserEntity;
|
||||
import org.booklore.model.entity.ShelfEntity;
|
||||
import org.booklore.model.enums.IconType;
|
||||
import org.booklore.repository.BookRepository;
|
||||
import org.booklore.repository.ShelfRepository;
|
||||
import org.booklore.repository.UserRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ShelfServiceTest {
|
||||
|
||||
@Mock
|
||||
private ShelfRepository shelfRepository;
|
||||
@Mock
|
||||
private BookRepository bookRepository;
|
||||
@Mock
|
||||
private ShelfMapper shelfMapper;
|
||||
@Mock
|
||||
private AuthenticationService authenticationService;
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@InjectMocks
|
||||
private ShelfService shelfService;
|
||||
|
||||
private BookLoreUser user;
|
||||
private BookLoreUserEntity userEntity;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
user = BookLoreUser.builder().id(1L).isDefaultPassword(false).build();
|
||||
userEntity = BookLoreUserEntity.builder().id(1L).username("testuser").build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createShelf_withNullIcon_shouldPersistNullIconValues() {
|
||||
ShelfCreateRequest request = ShelfCreateRequest.builder()
|
||||
.name("My Shelf")
|
||||
.icon(null)
|
||||
.iconType(null)
|
||||
.build();
|
||||
|
||||
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||
when(shelfRepository.existsByUserIdAndName(1L, "My Shelf")).thenReturn(false);
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(userEntity));
|
||||
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(shelfMapper.toShelf(any(ShelfEntity.class))).thenReturn(Shelf.builder().name("My Shelf").build());
|
||||
|
||||
shelfService.createShelf(request);
|
||||
|
||||
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
|
||||
verify(shelfRepository).save(captor.capture());
|
||||
|
||||
ShelfEntity saved = captor.getValue();
|
||||
assertNull(saved.getIcon());
|
||||
assertNull(saved.getIconType());
|
||||
assertEquals("My Shelf", saved.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createShelf_withIcon_shouldPersistIconValues() {
|
||||
ShelfCreateRequest request = ShelfCreateRequest.builder()
|
||||
.name("My Shelf")
|
||||
.icon("heart")
|
||||
.iconType(IconType.PRIME_NG)
|
||||
.build();
|
||||
|
||||
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||
when(shelfRepository.existsByUserIdAndName(1L, "My Shelf")).thenReturn(false);
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(userEntity));
|
||||
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(shelfMapper.toShelf(any(ShelfEntity.class))).thenReturn(
|
||||
Shelf.builder().name("My Shelf").icon("heart").iconType(IconType.PRIME_NG).build());
|
||||
|
||||
shelfService.createShelf(request);
|
||||
|
||||
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
|
||||
verify(shelfRepository).save(captor.capture());
|
||||
|
||||
ShelfEntity saved = captor.getValue();
|
||||
assertEquals("heart", saved.getIcon());
|
||||
assertEquals(IconType.PRIME_NG, saved.getIconType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateShelf_withNullIcon_shouldClearIconValues() {
|
||||
ShelfEntity existingShelf = ShelfEntity.builder()
|
||||
.id(1L)
|
||||
.name("Old Shelf")
|
||||
.icon("star")
|
||||
.iconType(IconType.PRIME_NG)
|
||||
.user(userEntity)
|
||||
.build();
|
||||
|
||||
ShelfCreateRequest request = ShelfCreateRequest.builder()
|
||||
.name("Updated Shelf")
|
||||
.icon(null)
|
||||
.iconType(null)
|
||||
.build();
|
||||
|
||||
when(shelfRepository.findById(1L)).thenReturn(Optional.of(existingShelf));
|
||||
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(shelfMapper.toShelf(any(ShelfEntity.class))).thenReturn(Shelf.builder().name("Updated Shelf").build());
|
||||
|
||||
shelfService.updateShelf(1L, request);
|
||||
|
||||
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
|
||||
verify(shelfRepository).save(captor.capture());
|
||||
|
||||
ShelfEntity saved = captor.getValue();
|
||||
assertNull(saved.getIcon());
|
||||
assertNull(saved.getIconType());
|
||||
assertEquals("Updated Shelf", saved.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateShelf_withIcon_shouldPreserveIconValues() {
|
||||
ShelfEntity existingShelf = ShelfEntity.builder()
|
||||
.id(1L)
|
||||
.name("Old Shelf")
|
||||
.icon("star")
|
||||
.iconType(IconType.PRIME_NG)
|
||||
.user(userEntity)
|
||||
.build();
|
||||
|
||||
ShelfCreateRequest request = ShelfCreateRequest.builder()
|
||||
.name("Updated Shelf")
|
||||
.icon("bookmark")
|
||||
.iconType(IconType.CUSTOM_SVG)
|
||||
.build();
|
||||
|
||||
when(shelfRepository.findById(1L)).thenReturn(Optional.of(existingShelf));
|
||||
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(shelfMapper.toShelf(any(ShelfEntity.class))).thenReturn(
|
||||
Shelf.builder().name("Updated Shelf").icon("bookmark").iconType(IconType.CUSTOM_SVG).build());
|
||||
|
||||
shelfService.updateShelf(1L, request);
|
||||
|
||||
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
|
||||
verify(shelfRepository).save(captor.capture());
|
||||
|
||||
ShelfEntity saved = captor.getValue();
|
||||
assertEquals("bookmark", saved.getIcon());
|
||||
assertEquals(IconType.CUSTOM_SVG, saved.getIconType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createShelf_withCustomSvgIcon_shouldPersistCorrectIconType() {
|
||||
ShelfCreateRequest request = ShelfCreateRequest.builder()
|
||||
.name("SVG Shelf")
|
||||
.icon("<svg>...</svg>")
|
||||
.iconType(IconType.CUSTOM_SVG)
|
||||
.build();
|
||||
|
||||
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||
when(shelfRepository.existsByUserIdAndName(1L, "SVG Shelf")).thenReturn(false);
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(userEntity));
|
||||
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(shelfMapper.toShelf(any(ShelfEntity.class))).thenReturn(
|
||||
Shelf.builder().name("SVG Shelf").icon("<svg>...</svg>").iconType(IconType.CUSTOM_SVG).build());
|
||||
|
||||
shelfService.createShelf(request);
|
||||
|
||||
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
|
||||
verify(shelfRepository).save(captor.capture());
|
||||
|
||||
ShelfEntity saved = captor.getValue();
|
||||
assertEquals("<svg>...</svg>", saved.getIcon());
|
||||
assertEquals(IconType.CUSTOM_SVG, saved.getIconType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package org.booklore.service.library;
|
||||
|
||||
import org.booklore.config.security.service.AuthenticationService;
|
||||
import org.booklore.mapper.LibraryMapper;
|
||||
import org.booklore.model.dto.BookLoreUser;
|
||||
import org.booklore.model.dto.Library;
|
||||
import org.booklore.model.dto.LibraryPath;
|
||||
import org.booklore.model.dto.request.CreateLibraryRequest;
|
||||
import org.booklore.model.entity.LibraryEntity;
|
||||
import org.booklore.model.entity.LibraryPathEntity;
|
||||
import org.booklore.model.enums.IconType;
|
||||
import org.booklore.repository.LibraryRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.booklore.mapper.BookMapper;
|
||||
import org.booklore.repository.BookRepository;
|
||||
import org.booklore.repository.LibraryPathRepository;
|
||||
import org.booklore.repository.UserRepository;
|
||||
import org.booklore.model.entity.BookLoreUserEntity;
|
||||
import org.booklore.service.NotificationService;
|
||||
import org.booklore.service.monitoring.MonitoringService;
|
||||
import org.booklore.util.FileService;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LibraryServiceIconTest {
|
||||
|
||||
@Mock
|
||||
private LibraryRepository libraryRepository;
|
||||
@Mock
|
||||
private LibraryPathRepository libraryPathRepository;
|
||||
@Mock
|
||||
private BookRepository bookRepository;
|
||||
@Mock
|
||||
private LibraryProcessingService libraryProcessingService;
|
||||
@Mock
|
||||
private BookMapper bookMapper;
|
||||
@Mock
|
||||
private LibraryMapper libraryMapper;
|
||||
@Mock
|
||||
private NotificationService notificationService;
|
||||
@Mock
|
||||
private FileService fileService;
|
||||
@Mock
|
||||
private MonitoringService monitoringService;
|
||||
@Mock
|
||||
private AuthenticationService authenticationService;
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@InjectMocks
|
||||
private LibraryService libraryService;
|
||||
|
||||
private BookLoreUser user;
|
||||
private BookLoreUserEntity userEntity;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
user = BookLoreUser.builder().id(1L).isDefaultPassword(false).build();
|
||||
userEntity = BookLoreUserEntity.builder().id(1L).username("testuser").build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateLibrary_withNullIcon_shouldClearIconValues() {
|
||||
LibraryEntity existing = LibraryEntity.builder()
|
||||
.id(1L)
|
||||
.name("My Library")
|
||||
.icon("book")
|
||||
.iconType(IconType.PRIME_NG)
|
||||
.libraryPaths(new ArrayList<>())
|
||||
.watch(false)
|
||||
.build();
|
||||
|
||||
CreateLibraryRequest request = CreateLibraryRequest.builder()
|
||||
.name("Updated Library")
|
||||
.icon(null)
|
||||
.iconType(null)
|
||||
.paths(Collections.emptyList())
|
||||
.watch(false)
|
||||
.build();
|
||||
|
||||
when(libraryRepository.findById(1L)).thenReturn(Optional.of(existing));
|
||||
when(libraryRepository.save(any(LibraryEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(libraryMapper.toLibrary(any(LibraryEntity.class))).thenReturn(Library.builder().name("Updated Library").build());
|
||||
|
||||
libraryService.updateLibrary(request, 1L);
|
||||
|
||||
ArgumentCaptor<LibraryEntity> captor = ArgumentCaptor.forClass(LibraryEntity.class);
|
||||
verify(libraryRepository).save(captor.capture());
|
||||
|
||||
LibraryEntity saved = captor.getValue();
|
||||
assertNull(saved.getIcon());
|
||||
assertNull(saved.getIconType());
|
||||
assertEquals("Updated Library", saved.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateLibrary_withIcon_shouldPreserveIconValues() {
|
||||
LibraryEntity existing = LibraryEntity.builder()
|
||||
.id(1L)
|
||||
.name("My Library")
|
||||
.icon("book")
|
||||
.iconType(IconType.PRIME_NG)
|
||||
.libraryPaths(new ArrayList<>())
|
||||
.watch(false)
|
||||
.build();
|
||||
|
||||
CreateLibraryRequest request = CreateLibraryRequest.builder()
|
||||
.name("Updated Library")
|
||||
.icon("folder")
|
||||
.iconType(IconType.CUSTOM_SVG)
|
||||
.paths(Collections.emptyList())
|
||||
.watch(false)
|
||||
.build();
|
||||
|
||||
when(libraryRepository.findById(1L)).thenReturn(Optional.of(existing));
|
||||
when(libraryRepository.save(any(LibraryEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(libraryMapper.toLibrary(any(LibraryEntity.class))).thenReturn(
|
||||
Library.builder().name("Updated Library").icon("folder").iconType(IconType.CUSTOM_SVG).build());
|
||||
|
||||
libraryService.updateLibrary(request, 1L);
|
||||
|
||||
ArgumentCaptor<LibraryEntity> captor = ArgumentCaptor.forClass(LibraryEntity.class);
|
||||
verify(libraryRepository).save(captor.capture());
|
||||
|
||||
LibraryEntity saved = captor.getValue();
|
||||
assertEquals("folder", saved.getIcon());
|
||||
assertEquals(IconType.CUSTOM_SVG, saved.getIconType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateLibrary_fromIconToNull_shouldAllowRemovingIcon() {
|
||||
LibraryEntity existing = LibraryEntity.builder()
|
||||
.id(1L)
|
||||
.name("Library With Icon")
|
||||
.icon("star")
|
||||
.iconType(IconType.PRIME_NG)
|
||||
.libraryPaths(new ArrayList<>())
|
||||
.watch(false)
|
||||
.build();
|
||||
|
||||
CreateLibraryRequest request = CreateLibraryRequest.builder()
|
||||
.name("Library With Icon")
|
||||
.icon(null)
|
||||
.iconType(null)
|
||||
.paths(Collections.emptyList())
|
||||
.watch(false)
|
||||
.build();
|
||||
|
||||
when(libraryRepository.findById(1L)).thenReturn(Optional.of(existing));
|
||||
when(libraryRepository.save(any(LibraryEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
when(libraryMapper.toLibrary(any(LibraryEntity.class))).thenReturn(Library.builder().name("Library With Icon").build());
|
||||
|
||||
libraryService.updateLibrary(request, 1L);
|
||||
|
||||
ArgumentCaptor<LibraryEntity> captor = ArgumentCaptor.forClass(LibraryEntity.class);
|
||||
verify(libraryRepository).save(captor.capture());
|
||||
|
||||
LibraryEntity saved = captor.getValue();
|
||||
assertNull(saved.getIcon());
|
||||
assertNull(saved.getIconType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createLibrary_withNullIcon_shouldPersistNullIconValues() {
|
||||
CreateLibraryRequest request = CreateLibraryRequest.builder()
|
||||
.name("No Icon Library")
|
||||
.icon(null)
|
||||
.iconType(null)
|
||||
.paths(Collections.emptyList())
|
||||
.watch(false)
|
||||
.build();
|
||||
|
||||
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(userEntity));
|
||||
when(libraryRepository.save(any(LibraryEntity.class))).thenAnswer(invocation -> {
|
||||
LibraryEntity entity = invocation.getArgument(0);
|
||||
entity.setId(1L);
|
||||
return entity;
|
||||
});
|
||||
when(libraryMapper.toLibrary(any(LibraryEntity.class))).thenReturn(Library.builder().name("No Icon Library").build());
|
||||
|
||||
libraryService.createLibrary(request);
|
||||
|
||||
ArgumentCaptor<LibraryEntity> captor = ArgumentCaptor.forClass(LibraryEntity.class);
|
||||
verify(libraryRepository).save(captor.capture());
|
||||
|
||||
LibraryEntity saved = captor.getValue();
|
||||
assertNull(saved.getIcon());
|
||||
assertNull(saved.getIconType());
|
||||
assertEquals("No Icon Library", saved.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createLibrary_withIcon_shouldPersistIconValues() {
|
||||
CreateLibraryRequest request = CreateLibraryRequest.builder()
|
||||
.name("Icon Library")
|
||||
.icon("book")
|
||||
.iconType(IconType.PRIME_NG)
|
||||
.paths(Collections.emptyList())
|
||||
.watch(false)
|
||||
.build();
|
||||
|
||||
when(authenticationService.getAuthenticatedUser()).thenReturn(user);
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(userEntity));
|
||||
when(libraryRepository.save(any(LibraryEntity.class))).thenAnswer(invocation -> {
|
||||
LibraryEntity entity = invocation.getArgument(0);
|
||||
entity.setId(1L);
|
||||
return entity;
|
||||
});
|
||||
when(libraryMapper.toLibrary(any(LibraryEntity.class))).thenReturn(
|
||||
Library.builder().name("Icon Library").icon("book").iconType(IconType.PRIME_NG).build());
|
||||
|
||||
libraryService.createLibrary(request);
|
||||
|
||||
ArgumentCaptor<LibraryEntity> captor = ArgumentCaptor.forClass(LibraryEntity.class);
|
||||
verify(libraryRepository).save(captor.capture());
|
||||
|
||||
LibraryEntity saved = captor.getValue();
|
||||
assertEquals("book", saved.getIcon());
|
||||
assertEquals(IconType.PRIME_NG, saved.getIconType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.booklore.service.user;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.booklore.model.entity.BookLoreUserEntity;
|
||||
import org.booklore.model.entity.ShelfEntity;
|
||||
import org.booklore.model.enums.IconType;
|
||||
import org.booklore.repository.ShelfRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserDefaultsServiceTest {
|
||||
|
||||
@Mock
|
||||
private ShelfRepository shelfRepository;
|
||||
@Mock
|
||||
private ObjectMapper objectMapper;
|
||||
@Mock
|
||||
private DefaultUserSettingsProvider defaultSettingsProvider;
|
||||
|
||||
@InjectMocks
|
||||
private UserDefaultsService userDefaultsService;
|
||||
|
||||
@Test
|
||||
void addDefaultShelves_shouldCreateFavoritesShelfWithIcon() {
|
||||
BookLoreUserEntity user = BookLoreUserEntity.builder()
|
||||
.id(1L)
|
||||
.username("testuser")
|
||||
.build();
|
||||
|
||||
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
userDefaultsService.addDefaultShelves(user);
|
||||
|
||||
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
|
||||
verify(shelfRepository).save(captor.capture());
|
||||
|
||||
ShelfEntity saved = captor.getValue();
|
||||
assertEquals("Favorites", saved.getName());
|
||||
assertEquals("heart", saved.getIcon());
|
||||
assertEquals(IconType.PRIME_NG, saved.getIconType());
|
||||
assertEquals(user, saved.getUser());
|
||||
}
|
||||
|
||||
@Test
|
||||
void addDefaultShelves_shouldSetExplicitIconType() {
|
||||
BookLoreUserEntity user = BookLoreUserEntity.builder()
|
||||
.id(2L)
|
||||
.username("anotheruser")
|
||||
.build();
|
||||
|
||||
when(shelfRepository.save(any(ShelfEntity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
userDefaultsService.addDefaultShelves(user);
|
||||
|
||||
ArgumentCaptor<ShelfEntity> captor = ArgumentCaptor.forClass(ShelfEntity.class);
|
||||
verify(shelfRepository).save(captor.capture());
|
||||
|
||||
ShelfEntity saved = captor.getValue();
|
||||
assertNotNull(saved.getIconType(), "Default shelf must have an explicit iconType since entity no longer has a default");
|
||||
assertNotNull(saved.getIcon(), "Default shelf must have an explicit icon since entity no longer has a default");
|
||||
}
|
||||
}
|
||||
@@ -45,17 +45,18 @@
|
||||
/>
|
||||
</div>
|
||||
<label [for]="'shelf-' + shelf.id" class="shelf-label">
|
||||
<div class="shelf-icon-wrapper">
|
||||
@if (shelf.icon) {
|
||||
<div class="shelf-icon-wrapper">
|
||||
<app-icon-display
|
||||
[icon]="getShelfIcon(shelf)"
|
||||
size="20px"
|
||||
iconClass="shelf-icon"
|
||||
/>
|
||||
} @else {
|
||||
<i class="pi pi-bookmark shelf-icon"></i>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="shelf-icon-wrapper-empty">
|
||||
</div>
|
||||
}
|
||||
<span class="shelf-name">{{ shelf.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -153,12 +153,15 @@
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
|
||||
.shelf-icon-wrapper, .shelf-icon-wrapper-empty {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.shelf-icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: var(--overlay-background);
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
@@ -187,7 +190,7 @@
|
||||
.shelf-label {
|
||||
gap: 0.5rem;
|
||||
|
||||
.shelf-icon-wrapper {
|
||||
.shelf-icon-wrapper, .shelf-icon-wrapper-empty {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
|
||||
@@ -123,10 +123,10 @@ export class ShelfAssignerComponent implements OnInit {
|
||||
}
|
||||
|
||||
getShelfIcon(shelf: Shelf): IconSelection {
|
||||
if (shelf.iconType === 'PRIME_NG') {
|
||||
return {type: 'PRIME_NG', value: `pi pi-${shelf.icon}`};
|
||||
if (shelf.iconType === 'CUSTOM_SVG') {
|
||||
return {type: 'CUSTOM_SVG', value: shelf.icon ?? ""};
|
||||
} else {
|
||||
return {type: 'CUSTOM_SVG', value: shelf.icon};
|
||||
return {type: 'PRIME_NG', value: `pi pi-${shelf.icon}`};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ export class ShelfCreatorComponent {
|
||||
}
|
||||
|
||||
createShelf(): void {
|
||||
const iconValue = this.selectedIcon?.value || 'bookmark';
|
||||
const iconType = this.selectedIcon?.type || 'PRIME_NG';
|
||||
const iconValue = this.selectedIcon?.value ?? null;
|
||||
const iconType = this.selectedIcon?.type ?? null;
|
||||
|
||||
const newShelf: Partial<Shelf> = {
|
||||
name: this.shelfName,
|
||||
|
||||
@@ -47,6 +47,7 @@ export class ShelfEditDialogComponent implements OnInit {
|
||||
if (this.shelf) {
|
||||
this.shelfName = this.shelf.name;
|
||||
this.isPublic = this.shelf.publicShelf ?? false;
|
||||
if (this.shelf.iconType && this.shelf.icon) {
|
||||
if (this.shelf.iconType === 'PRIME_NG') {
|
||||
this.selectedIcon = {type: 'PRIME_NG', value: `pi pi-${this.shelf.icon}`};
|
||||
} else {
|
||||
@@ -54,6 +55,7 @@ export class ShelfEditDialogComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openIconPicker() {
|
||||
this.iconPickerService.open().subscribe(icon => {
|
||||
@@ -68,8 +70,8 @@ export class ShelfEditDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
save() {
|
||||
const iconValue = this.selectedIcon?.value || 'bookmark';
|
||||
const iconType = this.selectedIcon?.type || 'PRIME_NG';
|
||||
const iconValue = this.selectedIcon?.value ?? null;
|
||||
const iconType = this.selectedIcon?.type ?? null;
|
||||
|
||||
const shelf: Shelf = {
|
||||
name: this.shelfName,
|
||||
|
||||
@@ -4,8 +4,8 @@ import {BookType} from './book.model';
|
||||
export interface Library {
|
||||
id?: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
iconType?: 'PRIME_NG' | 'CUSTOM_SVG';
|
||||
icon?: string | null;
|
||||
iconType?: 'PRIME_NG' | 'CUSTOM_SVG' | null;
|
||||
watch: boolean;
|
||||
fileNamingPattern?: string;
|
||||
sort?: SortOption;
|
||||
|
||||
@@ -3,8 +3,8 @@ import {SortOption} from './sort.model';
|
||||
export interface Shelf {
|
||||
id?: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
iconType?: 'PRIME_NG' | 'CUSTOM_SVG';
|
||||
icon?: string | null;
|
||||
iconType?: 'PRIME_NG' | 'CUSTOM_SVG' | null;
|
||||
sort?: SortOption;
|
||||
publicShelf?: boolean;
|
||||
userId?: number;
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
<div class="form-field">
|
||||
<label>
|
||||
Icon <span class="required">*</span>
|
||||
Icon (Optional)
|
||||
</label>
|
||||
@if (!selectedIcon) {
|
||||
<button class="icon-picker-trigger" (click)="openIconPicker()" type="button">
|
||||
|
||||
@@ -70,12 +70,14 @@ export class LibraryCreatorComponent implements OnInit {
|
||||
this.chosenLibraryName = name;
|
||||
this.editModeLibraryName = name;
|
||||
|
||||
if (icon != null && iconType) {
|
||||
if (iconType === 'CUSTOM_SVG') {
|
||||
this.selectedIcon = {type: 'CUSTOM_SVG', value: icon};
|
||||
} else {
|
||||
const value = icon.slice(0, 6) === 'pi pi-' ? icon : `pi pi-${icon}`;
|
||||
this.selectedIcon = {type: 'PRIME_NG', value: value};
|
||||
}
|
||||
}
|
||||
|
||||
this.watch = watch;
|
||||
if (formatPriority && formatPriority.length > 0) {
|
||||
@@ -186,7 +188,7 @@ export class LibraryCreatorComponent implements OnInit {
|
||||
}
|
||||
|
||||
isLibraryDetailsValid(): boolean {
|
||||
return !!this.chosenLibraryName.trim() && !!this.selectedIcon;
|
||||
return !!this.chosenLibraryName.trim();
|
||||
}
|
||||
|
||||
isDirectorySelectionValid(): boolean {
|
||||
@@ -207,8 +209,8 @@ export class LibraryCreatorComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
const iconValue = this.selectedIcon?.value || 'heart';
|
||||
const iconType = this.selectedIcon?.type || 'PRIME_NG';
|
||||
const iconValue = this.selectedIcon?.value ?? null;
|
||||
const iconType = this.selectedIcon?.type ?? null;
|
||||
|
||||
const library: Library = {
|
||||
name: this.chosenLibraryName,
|
||||
|
||||
@@ -29,8 +29,9 @@
|
||||
<div class="icon-picker-section">
|
||||
<div class="icon-picker-wrapper">
|
||||
<label>Shelf Icon</label>
|
||||
<div class="icon-display-container" (click)="openIconPicker()">
|
||||
@if (selectedIcon) {
|
||||
<div class="selected-icon-display">
|
||||
<div class="icon-display-container" (click)="openIconPicker()" (keydown.enter)="openIconPicker()" tabindex="0" role="button">
|
||||
<app-icon-display
|
||||
[icon]="selectedIcon"
|
||||
[size]="'16px'"
|
||||
@@ -38,13 +39,26 @@
|
||||
alt="Selected shelf icon">
|
||||
</app-icon-display>
|
||||
<span class="icon-label">Click to change</span>
|
||||
</div>
|
||||
<p-button
|
||||
icon="pi pi-times"
|
||||
severity="danger"
|
||||
[outlined]="true"
|
||||
[rounded]="true"
|
||||
size="small"
|
||||
(onClick)="clearSelectedIcon()"
|
||||
pTooltip="Remove icon"
|
||||
tooltipPosition="left"
|
||||
/>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="icon-display-container" (click)="openIconPicker()" (keydown.enter)="openIconPicker()" tabindex="0" role="button">
|
||||
<div class="icon-placeholder">
|
||||
<i class="pi pi-plus"></i>
|
||||
<span>Select Icon</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (isAdmin) {
|
||||
|
||||
@@ -74,6 +74,12 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.selected-icon-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.icon-display-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -89,6 +95,11 @@
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.selected-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {IconPickerService, IconSelection} from '../../../shared/service/icon-pic
|
||||
import {CheckboxChangeEvent, CheckboxModule} from "primeng/checkbox";
|
||||
import {UserService} from "../../settings/user-management/user.service";
|
||||
import {IconDisplayComponent} from '../../../shared/components/icon-display/icon-display.component';
|
||||
import {Tooltip} from 'primeng/tooltip';
|
||||
import {BookService} from '../../book/service/book.service';
|
||||
import {ShelfService} from '../../book/service/shelf.service';
|
||||
import {Shelf} from '../../book/model/shelf.model';
|
||||
@@ -167,7 +168,8 @@ const FIELD_CONFIGS: Record<RuleField, FullFieldConfig> = {
|
||||
MultiSelect,
|
||||
AutoComplete,
|
||||
CheckboxModule,
|
||||
IconDisplayComponent
|
||||
IconDisplayComponent,
|
||||
Tooltip
|
||||
]
|
||||
})
|
||||
export class MagicShelfComponent implements OnInit {
|
||||
@@ -271,7 +273,7 @@ export class MagicShelfComponent implements OnInit {
|
||||
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl<string | null>(data?.name ?? null, {nonNullable: true, validators: [Validators.required]}),
|
||||
icon: new FormControl<string | null>(iconValue, {nonNullable: true, validators: [Validators.required]}),
|
||||
icon: new FormControl<string | null>(iconValue),
|
||||
isPublic: new FormControl<boolean>(data?.isPublic ?? false),
|
||||
group: data?.filterJson ? this.buildGroupFromData(JSON.parse(data.filterJson)) : this.createGroup()
|
||||
});
|
||||
@@ -285,7 +287,7 @@ export class MagicShelfComponent implements OnInit {
|
||||
} else {
|
||||
this.form = new FormGroup({
|
||||
name: new FormControl<string | null>(null, {nonNullable: true, validators: [Validators.required]}),
|
||||
icon: new FormControl<string | null>(null, {nonNullable: true, validators: [Validators.required]}),
|
||||
icon: new FormControl<string | null>(null),
|
||||
isPublic: new FormControl<boolean>(false),
|
||||
group: this.createGroup()
|
||||
});
|
||||
@@ -477,6 +479,11 @@ export class MagicShelfComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
clearSelectedIcon(): void {
|
||||
this.selectedIcon = null;
|
||||
this.form.get('icon')?.setValue(null);
|
||||
}
|
||||
|
||||
private hasAtLeastOneValidRule(group: GroupFormGroup): boolean {
|
||||
const rulesArray = group.get('rules') as FormArray;
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ import {GroupRule} from '../component/magic-shelf-component';
|
||||
export interface MagicShelf {
|
||||
id?: number | null;
|
||||
name: string;
|
||||
icon?: string;
|
||||
iconType?: 'PRIME_NG' | 'CUSTOM_SVG';
|
||||
icon?: string | null;
|
||||
iconType?: 'PRIME_NG' | 'CUSTOM_SVG' | null;
|
||||
filterJson: string;
|
||||
isPublic?: boolean;
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export class MagicShelfService {
|
||||
const payload: MagicShelf = {
|
||||
id: data.id,
|
||||
name: data.name ?? '',
|
||||
icon: data.icon ?? 'pi pi-book',
|
||||
icon: data.icon,
|
||||
iconType: data.iconType,
|
||||
filterJson: JSON.stringify(data.group),
|
||||
isPublic: data.isPublic ?? false
|
||||
|
||||
@@ -21,6 +21,10 @@ import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
|
||||
[ngStyle]="getSvgStyle()"
|
||||
></div>
|
||||
}
|
||||
} @else if (displayEmpty) {
|
||||
<div
|
||||
[ngStyle]="getEmptyIconStyle()"
|
||||
></div>
|
||||
}
|
||||
`,
|
||||
styles: [`
|
||||
@@ -50,6 +54,7 @@ export class IconDisplayComponent implements OnInit, OnChanges {
|
||||
@Input() iconStyle: Record<string, string> = {};
|
||||
@Input() size: string = '16px';
|
||||
@Input() alt: string = 'Icon';
|
||||
@Input() displayEmpty: boolean = false;
|
||||
|
||||
private iconCache = inject(IconCacheService);
|
||||
private iconService = inject(IconService);
|
||||
@@ -115,6 +120,13 @@ export class IconDisplayComponent implements OnInit, OnChanges {
|
||||
};
|
||||
}
|
||||
|
||||
getEmptyIconStyle(): Record<string, string> {
|
||||
return {
|
||||
width: this.size,
|
||||
height: this.size,
|
||||
};
|
||||
}
|
||||
|
||||
getPrimeNgStyle(): Record<string, string> {
|
||||
const fontSize = this.size.endsWith('px')
|
||||
? `${parseInt(this.size) * 0.85}px`
|
||||
|
||||
@@ -109,8 +109,8 @@ export class AppMenuComponent implements OnInit {
|
||||
menu: this.libraryShelfMenuService.initializeLibraryMenuItems(library),
|
||||
label: library.name,
|
||||
type: 'Library',
|
||||
icon: library.icon,
|
||||
iconType: (library.iconType || 'PRIME_NG') as 'PRIME_NG' | 'CUSTOM_SVG',
|
||||
icon: library.icon || undefined,
|
||||
iconType: (library.iconType || undefined) as 'PRIME_NG' | 'CUSTOM_SVG' | undefined,
|
||||
routerLink: [`/library/${library.id}/books`],
|
||||
bookCount$: this.libraryService.getBookCount(library.id ?? 0),
|
||||
})),
|
||||
@@ -132,8 +132,8 @@ export class AppMenuComponent implements OnInit {
|
||||
items: sortedShelves.map((shelf) => ({
|
||||
label: shelf.name,
|
||||
type: 'magicShelfItem',
|
||||
icon: shelf.icon || 'pi pi-book',
|
||||
iconType: (shelf.iconType || 'PRIME_NG') as 'PRIME_NG' | 'CUSTOM_SVG',
|
||||
icon: shelf.icon || undefined,
|
||||
iconType: (shelf.iconType || undefined) as 'PRIME_NG' | 'CUSTOM_SVG' | undefined,
|
||||
menu: this.libraryShelfMenuService.initializeMagicShelfMenuItems(shelf),
|
||||
routerLink: [`/magic-shelf/${shelf.id}/books`],
|
||||
bookCount$: this.magicShelfService.getBookCount(shelf.id ?? 0),
|
||||
@@ -158,8 +158,8 @@ export class AppMenuComponent implements OnInit {
|
||||
menu: this.libraryShelfMenuService.initializeShelfMenuItems(shelf),
|
||||
label: shelf.name,
|
||||
type: 'Shelf',
|
||||
icon: shelf.icon,
|
||||
iconType: (shelf.iconType || 'PRIME_NG') as 'PRIME_NG' | 'CUSTOM_SVG',
|
||||
icon: shelf.icon || undefined,
|
||||
iconType: (shelf.iconType || undefined) as 'PRIME_NG' | 'CUSTOM_SVG' | undefined,
|
||||
routerLink: [`/shelf/${shelf.id}/books`],
|
||||
bookCount$: this.shelfService.getBookCount(shelf.id ?? 0),
|
||||
}));
|
||||
@@ -173,13 +173,13 @@ export class AppMenuComponent implements OnInit {
|
||||
bookCount$: this.shelfService.getUnshelvedBookCount?.() ?? of(0),
|
||||
};
|
||||
|
||||
const items = [unshelvedItem];
|
||||
const items: MenuItem[] = [unshelvedItem];
|
||||
if (koboShelf) {
|
||||
items.push({
|
||||
label: koboShelf.name,
|
||||
type: 'Shelf',
|
||||
icon: koboShelf.icon,
|
||||
iconType: (koboShelf.iconType || 'PRIME_NG') as 'PRIME_NG' | 'CUSTOM_SVG',
|
||||
icon: koboShelf.icon || undefined,
|
||||
iconType: (koboShelf.iconType || undefined) as 'PRIME_NG' | 'CUSTOM_SVG' | undefined,
|
||||
routerLink: [`/shelf/${koboShelf.id}/books`],
|
||||
bookCount$: this.shelfService.getBookCount(koboShelf.id ?? 0),
|
||||
});
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
[icon]="getIconSelection()"
|
||||
iconClass="layout-menuitem-icon"
|
||||
size="18px"
|
||||
[displayEmpty]="true"
|
||||
></app-icon-display>
|
||||
<span class="layout-menuitem-text menu-item-text">
|
||||
{{ item.label }}
|
||||
|
||||
Reference in New Issue
Block a user