Skip to content

Commit

Permalink
feat: modal to add favourite activity (#1794)
Browse files Browse the repository at this point in the history
Co-authored-by: Xiao Peng <[email protected]>
  • Loading branch information
xiaopeng0202 and Xiao Peng authored Jan 21, 2025

Verified

This commit was signed with the committer’s verified signature. The key has expired.
hperl Henning Perl
1 parent ad64aaa commit 4bc965b
Showing 19 changed files with 1,062 additions and 263 deletions.
Original file line number Diff line number Diff line change
@@ -44,29 +44,29 @@ public class FavouriteActivityEndpoint {
* Creates to the logged user a {@link FavouriteActivityEntity} record based on the activity or
* page title.
*
* @param createDto a {@link FavouriteActivityCreateDto} with the activity title
* @param createDtos a {@link FavouriteActivityCreateDto} with the activity title
* @return a {@link FavouriteActivityEntity} created
*/
@PostMapping(consumes = "application/json", produces = "application/json")
@Operation(
summary = "Creates a Favourite Activity",
summary = "Creates a Favourite Activities in bulk",
description =
"""
Creates a Favourite Activity to the logged user based on the activity
title or page name, with an optional isConsep flag.
Creates Favourite Activities for the logged user in bulk based on an array
of activity titles or page names, with optional isConsep flags.
""")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "The Favourite Activity entity was successfully created",
description = "The Favourite Activities were successfully created",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = FavouriteActivityEntity.class))),
@ApiResponse(
responseCode = "400",
description = "The activity doesn't exists or is already defined to that user",
description = "One or more activities failed validation or already exist",
content =
@Content(
mediaType = "application/json",
@@ -82,16 +82,17 @@ public class FavouriteActivityEndpoint {
content = @Content(schema = @Schema(implementation = Void.class)))
})
@RoleAccessConfig({"SPAR_TSC_ADMIN", "SPAR_MINISTRY_ORCHARD", "SPAR_NONMINISTRY_ORCHARD"})
public ResponseEntity<FavouriteActivityEntity> createUserActivity(
public ResponseEntity<List<FavouriteActivityEntity>> createUserActivities(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Body containing the activity name that will be created",
required = true,
content =
@Content(schema = @Schema(implementation = FavouriteActivityCreateDto.class)))
@Valid
@RequestBody
FavouriteActivityCreateDto createDto) {
FavouriteActivityEntity entity = favouriteActivityService.createUserActivity(createDto);
List<FavouriteActivityCreateDto> createDtos) {
List<FavouriteActivityEntity> entity =
favouriteActivityService.createUserActivities(createDtos);
return ResponseEntity.status(HttpStatus.CREATED).body(entity);
}

Original file line number Diff line number Diff line change
@@ -14,6 +14,8 @@ public interface FavouriteActivityRepository extends CrudRepository<FavouriteAct

Optional<FavouriteActivityEntity> findByActivity(String activity);

boolean existsByUserIdAndActivity(String userId, String activity);

@Modifying
@Query("update FavouriteActivityEntity set highlighted = false where userId = ?1")
void removeAllHighlightedByUser(String userId);
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
import ca.bc.gov.backendstartapi.repository.FavouriteActivityRepository;
import ca.bc.gov.backendstartapi.security.LoggedUserService;
import jakarta.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -36,35 +37,59 @@ public FavouriteActivityService(
this.favouriteActivityRepository = favouriteActivityRepository;
}

/**
* Validates the activity input.
*
* @param activityDto a {@link FavouriteActivityCreateDto} containing the activity title
*/
private void validateActivityInput(FavouriteActivityCreateDto activityDto) {
if (Objects.isNull(activityDto.activity()) || activityDto.activity().isBlank()) {
throw new InvalidActivityException();
}
}

/**
* Builds a FavouriteActivityEntity.
*
* @param userId a {@link String} containing the user id
* @param dto a {@link FavouriteActivityCreateDto} containing the activity title
* @return a {@link FavouriteActivityEntity} instance
*/
private FavouriteActivityEntity buildFavouriteActivityEntity(
String userId, FavouriteActivityCreateDto dto) {
FavouriteActivityEntity entity = new FavouriteActivityEntity();
entity.setUserId(userId);
entity.setActivity(dto.activity());
entity.setIsConsep(Optional.ofNullable(dto.isConsep()).orElse(false));
return entity;
}

/**
* Create a user's activity in the database.
*
* @param activityDto a {@link FavouriteActivityCreateDto} containing the activity title
* @param activityDtos a {@link FavouriteActivityCreateDto} containing the activity title
* @return the {@link FavouriteActivityEntity} created
*/
public FavouriteActivityEntity createUserActivity(FavouriteActivityCreateDto activityDto) {
public List<FavouriteActivityEntity> createUserActivities(
List<FavouriteActivityCreateDto> activityDtos) {
String userId = loggedUserService.getLoggedUserId();
SparLog.info("Creating activity {} for user {}", activityDto.activity(), userId);

if (Objects.isNull(activityDto.activity()) || activityDto.activity().isBlank()) {
throw new InvalidActivityException();
SparLog.info("Creating activities for user {}", userId);

List<FavouriteActivityEntity> createdActivities = new ArrayList<>();

for (FavouriteActivityCreateDto dto : activityDtos) {
try {
validateActivityInput(dto);
if (favouriteActivityRepository.existsByUserIdAndActivity(userId, dto.activity())) {
continue;
}
FavouriteActivityEntity entity = buildFavouriteActivityEntity(userId, dto);
createdActivities.add(favouriteActivityRepository.save(entity));
} catch (InvalidActivityException | FavoriteActivityExistsToUser e) {
SparLog.error("Error creating activity: {}", e.getMessage());
}
}

List<FavouriteActivityEntity> userFavList = favouriteActivityRepository.findAllByUserId(userId);
if (userFavList.stream().anyMatch(ac -> ac.getActivity().equals(activityDto.activity()))) {
SparLog.info("Activity {} already exists for user {}!", activityDto.activity(), userId);
throw new FavoriteActivityExistsToUser();
}

FavouriteActivityEntity activityEntity = new FavouriteActivityEntity();
activityEntity.setUserId(userId);
activityEntity.setActivity(activityDto.activity());

activityEntity.setIsConsep(activityDto.isConsep() != null ? activityDto.isConsep() : false);

FavouriteActivityEntity activityEntitySaved = favouriteActivityRepository.save(activityEntity);
SparLog.info("Activity {} created for user {}", activityDto.activity(), userId);
return activityEntitySaved;
return createdActivities;
}

/**
Original file line number Diff line number Diff line change
@@ -59,26 +59,27 @@ private FavouriteActivityEntity createEntity(String activity) {
@Test
@DisplayName("createFavoriteActivitySuccessTest")
void createFavoriteActivitySuccessTest() throws Exception {
String content = "[{\"activity\":\"CREATE_A_CLASS_SEEDLOT\"}]";
FavouriteActivityEntity activityEntity = createEntity("CREATE_A_CLASS_SEEDLOT");
when(favouriteActivityService.createUserActivity(any())).thenReturn(activityEntity);
when(favouriteActivityService.createUserActivities(any())).thenReturn(List.of(activityEntity));

mockMvc
.perform(
post(API_PATH)
.with(csrf().asHeader())
.header(CONTENT_HEADER, JSON)
.accept(MediaType.APPLICATION_JSON)
.content(stringifyCreate("CREATE_A_CLASS_SEEDLOT")))
.content(content))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.activity").value("CREATE_A_CLASS_SEEDLOT"))
.andExpect(jsonPath("$.highlighted").value("false"))
.andExpect(jsonPath("$[0].activity").value("CREATE_A_CLASS_SEEDLOT"))
.andExpect(jsonPath("$[0].highlighted").value("false"))
.andReturn();
}

@Test
@DisplayName("createFavoriteActivityNotFoundTest")
void createFavoriteActivityNotFoundTest() throws Exception {
when(favouriteActivityService.createUserActivity(any()))
when(favouriteActivityService.createUserActivities(any()))
.thenThrow(new InvalidActivityException());

mockMvc
@@ -95,23 +96,23 @@ void createFavoriteActivityNotFoundTest() throws Exception {
@Test
@DisplayName("createFavoriteActivityDuplicatedTest")
void createFavoriteActivityDuplicatedTest() throws Exception {
String contentString = stringifyCreate("CREATE_A_CLASS_SEEDLOT");
String content = "[{\"activity\":\"CREATE_A_CLASS_SEEDLOT\"}]";
FavouriteActivityEntity activityEntity = createEntity("CREATE_A_CLASS_SEEDLOT");
when(favouriteActivityService.createUserActivity(any())).thenReturn(activityEntity);
when(favouriteActivityService.createUserActivities(any())).thenReturn(List.of(activityEntity));

mockMvc
.perform(
post(API_PATH)
.with(csrf().asHeader())
.header(CONTENT_HEADER, JSON)
.accept(MediaType.APPLICATION_JSON)
.content(contentString))
.content(content))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.activity").value("CREATE_A_CLASS_SEEDLOT"))
.andExpect(jsonPath("$.highlighted").value("false"))
.andExpect(jsonPath("$[0].activity").value("CREATE_A_CLASS_SEEDLOT"))
.andExpect(jsonPath("$[0].highlighted").value("false"))
.andReturn();

when(favouriteActivityService.createUserActivity(any()))
when(favouriteActivityService.createUserActivities(any()))
.thenThrow(new FavoriteActivityExistsToUser());

mockMvc
@@ -120,7 +121,7 @@ void createFavoriteActivityDuplicatedTest() throws Exception {
.with(csrf().asHeader())
.header(CONTENT_HEADER, JSON)
.accept(MediaType.APPLICATION_JSON)
.content(contentString))
.content(content))
.andExpect(status().isBadRequest())
.andReturn();
}
@@ -153,20 +154,21 @@ void getAllUsersActivityTest() throws Exception {
@Test
@DisplayName("updateUserFavoriteActivity")
void updateUserFavoriteActivity() throws Exception {
String content = "[{\"activity\":\"EXISTING_SEEDLOTS\"}]";
FavouriteActivityEntity activityEntity = createEntity("EXISTING_SEEDLOTS");
activityEntity.setId(10000L);
when(favouriteActivityService.createUserActivity(any())).thenReturn(activityEntity);
when(favouriteActivityService.createUserActivities(any())).thenReturn(List.of(activityEntity));

mockMvc
.perform(
post(API_PATH)
.with(csrf().asHeader())
.header(CONTENT_HEADER, JSON)
.accept(MediaType.APPLICATION_JSON)
.content(stringifyCreate("EXISTING_SEEDLOTS")))
.content(content))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.activity").value("EXISTING_SEEDLOTS"))
.andExpect(jsonPath("$.highlighted").value("false"))
.andExpect(jsonPath("$[0].activity").value("EXISTING_SEEDLOTS"))
.andExpect(jsonPath("$[0].highlighted").value("false"))
.andReturn();

activityEntity.setHighlighted(true);
@@ -191,21 +193,22 @@ void updateUserFavoriteActivity() throws Exception {
@Test
@DisplayName("deleteUserFavoriteActivity")
void deleteUserFavoriteActivity() throws Exception {
String content = "[{\"activity\":\"EXISTING_SEEDLOTS\"}]";
FavouriteActivityEntity activityEntity = createEntity("EXISTING_SEEDLOTS");
activityEntity.setId(10000L);

when(favouriteActivityService.createUserActivity(any())).thenReturn(activityEntity);
when(favouriteActivityService.createUserActivities(any())).thenReturn(List.of(activityEntity));

mockMvc
.perform(
post(API_PATH)
.with(csrf().asHeader())
.header(CONTENT_HEADER, JSON)
.accept(MediaType.APPLICATION_JSON)
.content(stringifyCreate("EXISTING_SEEDLOTS")))
.content(content))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.activity").value("EXISTING_SEEDLOTS"))
.andExpect(jsonPath("$.highlighted").value("false"))
.andExpect(jsonPath("$[0].activity").value("EXISTING_SEEDLOTS"))
.andExpect(jsonPath("$[0].highlighted").value("false"))
.andReturn();

activityEntity.setHighlighted(true);
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@
import ca.bc.gov.backendstartapi.dto.FavouriteActivityCreateDto;
import ca.bc.gov.backendstartapi.dto.FavouriteActivityUpdateDto;
import ca.bc.gov.backendstartapi.entity.FavouriteActivityEntity;
import ca.bc.gov.backendstartapi.exception.FavoriteActivityExistsToUser;
import ca.bc.gov.backendstartapi.exception.InvalidActivityException;
import ca.bc.gov.backendstartapi.repository.FavouriteActivityRepository;
import ca.bc.gov.backendstartapi.security.LoggedUserService;
@@ -51,49 +50,15 @@ void createUserActivityTest() {

FavouriteActivityCreateDto createDto =
new FavouriteActivityCreateDto("CREATE_A_CLASS_SEEDLOT", false);
FavouriteActivityEntity entitySaved = favouriteActivityService.createUserActivity(createDto);
List<FavouriteActivityEntity> entitiesSaved =
favouriteActivityService.createUserActivities(List.of(createDto));
FavouriteActivityEntity entitySaved = entitiesSaved.get(0);

Assertions.assertNotNull(entitySaved);
Assertions.assertEquals("CREATE_A_CLASS_SEEDLOT", entitySaved.getActivity());
Assertions.assertFalse(entitySaved.getHighlighted());
}

@Test
@DisplayName("createUserActivityExceptionTest")
void createUserActivityExceptionTest() {
when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID);

FavouriteActivityEntity entity = new FavouriteActivityEntity();
entity.setActivity("CREATE_A_CLASS_SEEDLOT");
entity.setHighlighted(Boolean.FALSE);
when(favouriteActivityRepository.save(any())).thenReturn(entity);

FavouriteActivityCreateDto createDto = new FavouriteActivityCreateDto(null, false);

Exception notFoundExc =
Assertions.assertThrows(
InvalidActivityException.class,
() -> favouriteActivityService.createUserActivity(createDto));

Assertions.assertEquals(
"404 NOT_FOUND \"Invalid or not found activity id!\"", notFoundExc.getMessage());

List<FavouriteActivityEntity> userFavList = List.of(entity);
when(favouriteActivityRepository.findAllByUserId(any())).thenReturn(userFavList);

FavouriteActivityCreateDto createAnotherDto =
new FavouriteActivityCreateDto("CREATE_A_CLASS_SEEDLOT", false);

Exception activityExists =
Assertions.assertThrows(
FavoriteActivityExistsToUser.class,
() -> favouriteActivityService.createUserActivity(createAnotherDto));

Assertions.assertEquals(
"400 BAD_REQUEST \"Activity already registered to this user!\"",
activityExists.getMessage());
}

@Test
@DisplayName("getAllUserFavoriteActivitiesTest")
void getAllUserFavoriteActivitiesTest() {
4 changes: 2 additions & 2 deletions frontend/src/api-service/favouriteActivitiesAPI.ts
Original file line number Diff line number Diff line change
@@ -36,9 +36,9 @@ export const getFavAct = () => {
});
};

export const postFavAct = (newAct: FavActivityPostType) => {
export const postFavAct = (newActs: FavActivityPostType[]) => {
const url = ApiConfig.favouriteActivities;
return api.post(url, newAct);
return api.post(url, newActs);
};

export const patchFavAct = (field: string, activity: FavActivityType) => {
4 changes: 4 additions & 0 deletions frontend/src/assets/img/fav-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 6 additions & 5 deletions frontend/src/components/Card/FavouriteCard/styles.scss
Original file line number Diff line number Diff line change
@@ -91,21 +91,23 @@ p.fav-card-title-large {
.fav-card-main-highlighted .fav-card-icon,
.fav-card-main-highlighted .fav-card-content > p,
.fav-card-main-highlighted .fav-card-header > p {
color: var(--#{vars.$bcgov-prefix}-text-on-color)
color: var(--#{vars.$bcgov-prefix}-text-on-color);
}

.fav-card-main-highlighted .fav-card-overflow > svg {
fill: var(--#{vars.$bcgov-prefix}-icon-on-color);
}

.#{vars.$bcgov-prefix}--overflow-menu.#{vars.$bcgov-prefix}--overflow-menu--open >svg {
.#{vars.$bcgov-prefix}--overflow-menu.#{vars.$bcgov-prefix}--overflow-menu--open
> svg {
fill: var(--#{vars.$bcgov-prefix}-icon-primary);
}

.fav-card-main-highlighted:hover,
.fav-card-main-highlighted:focus {
border-color: var(--#{vars.$bcgov-prefix}-icon-on-color);
box-shadow: inset 0 0 0 1px var(--#{vars.$bcgov-prefix}-icon-on-color), 0 0 0 3px var(--#{vars.$bcgov-prefix}-button-primary);
box-shadow: inset 0 0 0 1px var(--#{vars.$bcgov-prefix}-icon-on-color),
0 0 0 3px var(--#{vars.$bcgov-prefix}-button-primary);
cursor: pointer;
outline: 0;
outline-offset: 0;
@@ -142,9 +144,8 @@ p.fav-card-title-large {
justify-content: flex-start;
align-items: center;
}

}

.fav-card-menu-options{
.fav-card-menu-options {
width: fit-content;
}
Loading

0 comments on commit 4bc965b

Please sign in to comment.