Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: modal to add favourite activity #1794

Draft
wants to merge 1 commit into
base: feat/1580-list-favorite-activity-in-consep
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,24 @@ public class FavouriteActivityEndpoint {
*/
@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",
Expand All @@ -82,16 +82,16 @@ 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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
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;
Expand Down Expand Up @@ -36,35 +38,51 @@ public FavouriteActivityService(
this.favouriteActivityRepository = favouriteActivityRepository;
}

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

/**
* Builds a FavouriteActivityEntity.
*/
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
* @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;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api-service/favouriteActivitiesAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
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.
121 changes: 121 additions & 0 deletions frontend/src/components/Card/FavouriteCard/FavouriteConsepCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';

import {
Tile, OverflowMenu, OverflowMenuItem, Column
} from '@carbon/react';
import * as Icons from '@carbon/icons-react';
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { FavActivityType } from '../../../types/FavActivityTypes';
import { patchFavAct, deleteFavAct } from '../../../api-service/favouriteActivitiesAPI';
import useWindowSize from '../../../hooks/UseWindowSize';
import { MEDIUM_SCREEN_WIDTH } from '../../../shared-constants/shared-constants';

import SmallCard from '../SmallCard';

import './styles.scss';

type FavouriteCardProps = {
favObject: FavActivityType
}

const FavouriteCard = ({
favObject
}: FavouriteCardProps) => {
const Icon = Icons[favObject.image];
const navigate = useNavigate();
const favActQueryKey = ['favourite-activities'];
const queryClient = useQueryClient();

const windowSize = useWindowSize();

const highlightFavAct = useMutation({
mutationFn: () => patchFavAct('highlighted', favObject),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: favActQueryKey });
}
});

const removeFavAct = useMutation({
mutationFn: () => deleteFavAct(favObject.id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: favActQueryKey });
}
});

const ActionBtn = (
<OverflowMenu
className="fav-card-overflow"
menuOptionsClass="fav-card-menu-options"
aria-label={`${favObject.header} options`}
flipped
iconDescription="More actions"
// Need to stop bubbling here so it won't trigger the
// the tile onKeyDown event
onKeyDown={(e: React.KeyboardEvent<HTMLElement>) => {
e.stopPropagation();
}}
onClick={(e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
}}
>
<OverflowMenuItem
itemText={favObject.highlighted ? 'Dehighlight shortcut' : 'Highlight shortcut'}
onClick={(e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
highlightFavAct.mutate();
}}
/>
<OverflowMenuItem
itemText="Delete shortcut"
onClick={(e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
removeFavAct.mutate();
}}
/>
</OverflowMenu>
);

if (windowSize.innerWidth < MEDIUM_SCREEN_WIDTH) {
return (
<SmallCard
header={favObject.header}
actionBtn={ActionBtn}
path={favObject.link}
image={favObject.image}
isIcon
favClassName={favObject.highlighted ? 'fav-card-main-highlighted' : 'fav-card-main'}
/>
);
}

return (
<Column key={favObject.type} lg={4} md={4} sm={2}>
{' '}
<Tile
className={favObject.highlighted ? 'consep-fav-card-highlighted' : 'consep-fav-card'}
onClick={() => navigate(favObject.link)}
tabIndex={0}
aria-label={`Go to ${favObject.header}`}
onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter' || e.key === ' ') {
navigate(favObject.link);
}
}}
>
<div className="consep-fav-card-header">
<Icon className="fav-card-icon" style={{ textAlign: 'right' }} />
{ActionBtn}
</div>
<div className="consep-fav-card-content">
<p>{favObject.header}</p>
</div>

</Tile>
</Column>

);
};

export default FavouriteCard;
Loading
Loading