diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/BacklogItemControllerImpl.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/BacklogItemControllerImpl.java index 1f3ef620..20a27677 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/BacklogItemControllerImpl.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/BacklogItemControllerImpl.java @@ -18,11 +18,10 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemMappings.BACKLOG_ITEM_ADD_MAPPING; import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemMappings.BACKLOG_ITEM_API_MAPPING; import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemMappings.BACKLOG_ITEM_DELETE_MAPPING; +import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemMappings.BACKLOG_ITEM_GET_ALL_WITHOUT_SPRINT_MAPPING; import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemMappings.BACKLOG_ITEM_GET_BY_PROJECT_MAPPING; import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemMappings.BACKLOG_ITEM_GET_BY_SPRINT_MAPPING; import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemMappings.BACKLOG_ITEM_GET_DETAILS_MAPPING; @@ -67,9 +66,12 @@ public final BacklogItemResponse create(@RequestBody BacklogItemRequest backlogI @Override @GetMapping(BACKLOG_ITEM_GET_BY_SPRINT_MAPPING) - public final List getBySprintId(@RequestParam long sprintId, + public final BacklogItemResponseList getBySprintId(@RequestParam long sprintId, + @RequestParam int pageNumber, + @RequestParam String sortBy, + @RequestParam String order, @JwtAuthed User user) { - return backlogItemService.getBySprintId(sprintId, user); + return backlogItemService.getBySprintId(sprintId, pageNumber, sortBy, order, user); } @Override @@ -88,4 +90,13 @@ public final BacklogItemDetails getDetailsById(@RequestParam long id, @JwtAuthed User user) { return backlogItemService.getDetailsById(id, user); } + @Override + @GetMapping(BACKLOG_ITEM_GET_ALL_WITHOUT_SPRINT_MAPPING) + public final BacklogItemResponseList getAllWithoutSprint(@RequestParam long projectId, + @RequestParam int pageNumber, + @RequestParam String sortBy, + @RequestParam String order, + @JwtAuthed User user) { + return backlogItemService.getAllWithoutSprint(projectId, pageNumber, sortBy, order, user); + } } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/BacklogItemServiceImpl.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/BacklogItemServiceImpl.java index 646e8301..2ed51954 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/BacklogItemServiceImpl.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/BacklogItemServiceImpl.java @@ -41,8 +41,9 @@ import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemServiceConstants.BACKLOG_ITEM; import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemServiceConstants.BACKLOG_ITEM_NOT_FOUND_MESSAGE; import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemServiceConstants.BACKLOG_ITEM_PAGE_SIZE; +import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemServiceConstants.CREATING_PAGEABLE_FOR; +import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemServiceConstants.GETTING_BACKLOG_ITEMS_WITH_PAGEABLE; import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemServiceConstants.GETTING_BY_ID; -import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemServiceConstants.GETTING_BY_PROJECT; import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemServiceConstants.PROJECT; import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemServiceConstants.PROJECT_MEMBER; import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemServiceConstants.PROJECT_MEMBER_NOT_FOUND_MESSAGE; @@ -132,55 +133,42 @@ public BacklogItemResponse create(BacklogItemRequest backlogItemRequest, User us } @Override - public List getBySprintId(long sprintId, User user) { + public BacklogItemResponseList getBySprintId(long sprintId, int pageNumber, String sortBy, String order, User user) { + Pageable pageable = createPageableForBacklogItems(pageNumber, sortBy, order); + log.info(GETTING_BY_ID, SPRINT, sprintId); Sprint sprint = sprintRepository.findByIdWithProjectMember(sprintId, user) .orElseThrow(() -> new SprintNotFoundException(SPRINT_NOT_FOUND_MESSAGE)); - log.info("Getting backlog items for sprint: {}", sprint); - - List items = backlogItemRepository.getBySprint(sprint); + log.info(GETTING_BACKLOG_ITEMS_WITH_PAGEABLE, SPRINT, sprint, pageable); + Page items = backlogItemRepository.getBySprint(sprint, pageable); - log.info(RETURNING_BACKLOG_ITEMS_OF_QUANTITY, items.size()); + log.info(RETURNING_BACKLOG_ITEMS_OF_QUANTITY, items.getNumberOfElements()); - return items.stream() - .map(backlogItemMapper::backlogItemToBacklogItemResponse) - .toList(); + return BacklogItemResponseList.builder() + .backlogItemResponseList(items.stream() + .map(backlogItemMapper::backlogItemToBacklogItemResponse) + .toList()) + .totalNumber(items.getTotalElements()) + .build(); } @Override public BacklogItemResponseList getByProjectId(long projectId, int pageNumber, String sortBy, - String order, User user) { - if(pageNumber < 0) { - throw new WrongPageNumberException(pageNumber); - } + String order, User user) { + Pageable pageable = createPageableForBacklogItems(pageNumber, sortBy, order); - BacklogItemSortBy sort = BacklogItemSortBy.of(sortBy); - Sort.Direction direction = Sort.Direction.DESC.name().equalsIgnoreCase(order) ? - Sort.Direction.DESC : Sort.DEFAULT_DIRECTION; - - return getByProjectId(projectId, pageNumber, sort, direction, user); - } - - private BacklogItemResponseList getByProjectId(long projectId, int pageNumber, BacklogItemSortBy sortBy, - Sort.Direction order, User user) { log.info(GETTING_BY_ID, PROJECT, projectId); Project project = projectRepository.findByIdWithProjectMember(projectId, user) .orElseThrow(() -> new ProjectDoesNotExistException(PROJECT_NOT_FOUND_MESSAGE)); - Pageable pageRequest; - if(sortBy == BacklogItemSortBy.ASSIGNEE) { - pageRequest = getPageableForAssignee(pageNumber, order); - } else { - pageRequest = PageRequest.of(pageNumber, BACKLOG_ITEM_PAGE_SIZE, Sort.by(order, - sortBy.getValue())); - } + log.info(GETTING_BACKLOG_ITEMS_WITH_PAGEABLE, PROJECT, project, pageable); + Page items = backlogItemRepository.getByProject(project, pageable); - log.info(GETTING_BY_PROJECT, project, sortBy.getValue(), order); - Page items = backlogItemRepository.getByProject(project, pageRequest); + log.info(RETURNING_BACKLOG_ITEMS_OF_QUANTITY, items.getNumberOfElements()); return BacklogItemResponseList.builder() .backlogItemResponseList(items.stream() @@ -202,6 +190,30 @@ public BacklogItemDetails getDetailsById(long id, User user) { return backlogItemMapper.backlogItemToBacklogItemDetails(backlogItem); } + @Override + public BacklogItemResponseList getAllWithoutSprint(long projectId, int pageNumber, String sortBy, + String order, User user) { + Pageable pageable = createPageableForBacklogItems(pageNumber, sortBy, order); + + log.info(GETTING_BY_ID, PROJECT, projectId); + + Project project = projectRepository.findByIdWithProjectMember(projectId, user) + .orElseThrow(() -> new ProjectDoesNotExistException(PROJECT_NOT_FOUND_MESSAGE)); + + log.info("Getting backlog items that aren't assigned to any sprint for project: {}, pageable: {}", + project, pageable); + Page items = backlogItemRepository.findByProjectAndSprintIsNull(project, pageable); + + log.info(RETURNING_BACKLOG_ITEMS_OF_QUANTITY, items.getNumberOfElements()); + + return BacklogItemResponseList.builder() + .backlogItemResponseList(items.stream() + .map(backlogItemMapper::backlogItemToBacklogItemResponse) + .toList()) + .totalNumber(items.getTotalElements()) + .build(); + } + private record BacklogItemBuilderDto(Sprint sprint, Project project, ProjectMember assignee) { } @@ -211,15 +223,23 @@ private BacklogItemBuilderDto prepareDataForBacklogItemCreation(BacklogItemReque Project project = projectRepository.findByIdWithProjectMember(backlogItemRequest.projectId(), user) .orElseThrow(() -> new ProjectDoesNotExistException(PROJECT_NOT_FOUND_MESSAGE)); - log.info(GETTING_BY_ID, PROJECT_MEMBER, backlogItemRequest.projectMemberId()); + ProjectMember assignee = null; - ProjectMember assignee = projectMemberRepository.findByProjectMemberIdAndProject(backlogItemRequest.projectMemberId(), project) - .orElseThrow(() -> new ProjectMemberDoesNotExistException(PROJECT_MEMBER_NOT_FOUND_MESSAGE)); + if(backlogItemRequest.projectMemberId() != -1L) { + log.info(GETTING_BY_ID, PROJECT_MEMBER, backlogItemRequest.projectMemberId()); - log.info(GETTING_BY_ID, SPRINT, backlogItemRequest.sprintId()); + assignee = projectMemberRepository.findByProjectMemberIdAndProject(backlogItemRequest.projectMemberId(), project) + .orElseThrow(() -> new ProjectMemberDoesNotExistException(PROJECT_MEMBER_NOT_FOUND_MESSAGE)); + } - Sprint sprint = sprintRepository.findBySprintIdAndProject(backlogItemRequest.sprintId(), project) - .orElseThrow(() -> new SprintNotFoundException(SPRINT_NOT_FOUND_MESSAGE)); + Sprint sprint = null; + + if(backlogItemRequest.sprintId() != -1L) { + log.info(GETTING_BY_ID, SPRINT, backlogItemRequest.sprintId()); + + sprint = sprintRepository.findBySprintIdAndProject(backlogItemRequest.sprintId(), project) + .orElseThrow(() -> new SprintNotFoundException(SPRINT_NOT_FOUND_MESSAGE)); + } return new BacklogItemBuilderDto(sprint, project, assignee); } @@ -270,4 +290,25 @@ private Pageable getPageableForAssignee(int pageNumber, Sort.Direction direction return PageRequest.of(pageNumber, BACKLOG_ITEM_PAGE_SIZE, sorting); } + + private Pageable createPageableForBacklogItems(int pageNumber, String sortBy, String order) { + log.info(CREATING_PAGEABLE_FOR, pageNumber, sortBy, order); + if(pageNumber < 0) { + throw new WrongPageNumberException(pageNumber); + } + + BacklogItemSortBy sort = BacklogItemSortBy.of(sortBy); + Sort.Direction direction = Sort.Direction.DESC.name().equalsIgnoreCase(order) ? + Sort.Direction.DESC : Sort.DEFAULT_DIRECTION; + Pageable pageable; + + if(sort == BacklogItemSortBy.ASSIGNEE) { + pageable = getPageableForAssignee(pageNumber, direction); + } else { + pageable = PageRequest.of(pageNumber, BACKLOG_ITEM_PAGE_SIZE, Sort.by(direction, sort.getValue())); + } + + log.info("Returning pageable: {}", pageable); + return pageable; + } } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/constants/BacklogItemMappings.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/constants/BacklogItemMappings.java index cd2dc175..8cd641a1 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/constants/BacklogItemMappings.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/constants/BacklogItemMappings.java @@ -14,6 +14,8 @@ public class BacklogItemMappings { public static final String BACKLOG_ITEM_GET_DETAILS_MAPPING = "/getDetails"; + public static final String BACKLOG_ITEM_GET_ALL_WITHOUT_SPRINT_MAPPING = "/getAllWithoutSprint"; + private BacklogItemMappings() { } } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/constants/BacklogItemServiceConstants.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/constants/BacklogItemServiceConstants.java index 356b2907..d946f303 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/constants/BacklogItemServiceConstants.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/constants/BacklogItemServiceConstants.java @@ -14,7 +14,8 @@ public final class BacklogItemServiceConstants { public static final String PROJECT = "Project"; public static final String RETURNING_BACKLOG_ITEMS_OF_QUANTITY = "Returning backlog items of quantity: {}"; public static final String SAVING_AND_RETURNING_RESPONSE_OF = "Saving and returning response of: {}"; - public static final String GETTING_BY_PROJECT = "Getting backlog items for project: {}, sorting by: {}, order: {}"; + public static final String GETTING_BACKLOG_ITEMS_WITH_PAGEABLE = "Getting backlog items for {}: {}, pageable: {}"; + public static final String CREATING_PAGEABLE_FOR = "Creating pageable for pageNumber: {}, sortBy: {}, order{}"; public static final int BACKLOG_ITEM_PAGE_SIZE = 30; diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/interfaces/BacklogItemController.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/interfaces/BacklogItemController.java index d151df2b..6941b01b 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/interfaces/BacklogItemController.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/interfaces/BacklogItemController.java @@ -6,8 +6,6 @@ import dev.corn.cornbackend.api.backlog.item.data.BacklogItemResponseList; import dev.corn.cornbackend.entities.user.User; -import java.util.List; - /** * Interface for the BacklogItemController */ @@ -50,19 +48,24 @@ public interface BacklogItemController { BacklogItemResponse create(BacklogItemRequest backlogItemRequest, User user); /** - * Get backlog items by sprint id on given page + * Get backlog items by sprint id on given page and sort by given field in given order * * @param sprintId id of the sprint + * @param pageNumber number of page + * @param sortBy name of field to sort backlog items by + * @param order order of sort ("ASC" or "DESC") * @param user user to get the backlog items for * @return the backlog items */ - List getBySprintId(long sprintId, User user); + BacklogItemResponseList getBySprintId(long sprintId, int pageNumber, String sortBy, String order, User user); /** - * Get backlog items by project id + * Get backlog items by project id on given page and sort by given field in given order * * @param projectId id of the project * @param pageNumber number of page + * @param sortBy name of field to sort backlog items by + * @param order order of sort ("ASC" or "DESC") * @param user user to get the backlog items for * @return the backlog items */ @@ -76,4 +79,16 @@ public interface BacklogItemController { * @return the backlog item details */ BacklogItemDetails getDetailsById(long id, User user); + + /** + * Get all backlog items that aren't assigned to any sprint + * @param projectId id of the project + * @param pageNumber number of page + * @param sortBy name of field to sort backlog items by + * @param order order of sort ("ASC" or "DESC") + * @param user user to get the backlog items for + * @return the backlog items + */ + + BacklogItemResponseList getAllWithoutSprint(long projectId, int pageNumber, String sortBy, String order, User user); } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/interfaces/BacklogItemService.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/interfaces/BacklogItemService.java index 3895f1a2..afa9075d 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/interfaces/BacklogItemService.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/backlog/item/interfaces/BacklogItemService.java @@ -6,8 +6,6 @@ import dev.corn.cornbackend.api.backlog.item.data.BacklogItemResponseList; import dev.corn.cornbackend.entities.user.User; -import java.util.List; - /** * Service for backlog items */ @@ -56,7 +54,7 @@ public interface BacklogItemService { * @param sprintId id of the sprint * @return response with the list of backlog items */ - List getBySprintId(long sprintId, User user); + BacklogItemResponseList getBySprintId(long sprintId, int pageNumber, String sortBy, String order, User user); /** * Get all backlog items by project id @@ -75,4 +73,6 @@ public interface BacklogItemService { * @return response with the backlog item details */ BacklogItemDetails getDetailsById(long id, User user); + + BacklogItemResponseList getAllWithoutSprint(long projectId, int pageNumber, String sortBy, String order, User user); } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintControllerImpl.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintControllerImpl.java index 770971b6..283ab48e 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintControllerImpl.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintControllerImpl.java @@ -20,6 +20,7 @@ import java.util.List; import static dev.corn.cornbackend.api.sprint.constants.SprintMappings.ADD_SPRINT; +import static dev.corn.cornbackend.api.sprint.constants.SprintMappings.CURRENT_AND_FUTURE_SPRINTS; import static dev.corn.cornbackend.api.sprint.constants.SprintMappings.DELETE_SPRINT; import static dev.corn.cornbackend.api.sprint.constants.SprintMappings.GET_SPRINTS_ON_PAGE; import static dev.corn.cornbackend.api.sprint.constants.SprintMappings.GET_SPRINT_BY_ID; @@ -90,4 +91,11 @@ public final SprintResponse updateSprintsEndDate(@RequestParam LocalDate endDate public final SprintResponse deleteSprint(@RequestParam long sprintId, @JwtAuthed User user) { return sprintService.deleteSprint(sprintId, user); } + + @Override + @GetMapping(value = CURRENT_AND_FUTURE_SPRINTS) + public List getCurrentAndFutureSprints(@RequestParam long projectId, + @JwtAuthed User user) { + return sprintService.getCurrentAndFutureSprints(projectId, user); + } } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintServiceImpl.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintServiceImpl.java index ef198116..d19ea848 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintServiceImpl.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/SprintServiceImpl.java @@ -17,18 +17,24 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import java.time.LocalDate; import java.util.List; +import static dev.corn.cornbackend.entities.sprint.constants.SprintConstants.SPRINT_START_DATE_FIELD_NAME; + @Slf4j @Service @RequiredArgsConstructor public class SprintServiceImpl implements SprintService { public static final int SPRINTS_PER_PAGE = 20; + public static final int FUTURE_SPRINTS_PER_PAGE = 5; private static final String UPDATED_SPRINT = "Updated sprint: {}"; private static final String FOUND_SPRINT_TO_UPDATE = "Found sprint to update: {}"; + private static final String PROJECT_NOT_FOUND = "Project with projectId: %d does not exist"; + private static final String SPRINTS_ON_PAGE = "Sprints found on page : {}"; private final SprintRepository sprintRepository; private final ProjectRepository projectRepository; private final SprintMapper sprintMapper; @@ -42,7 +48,7 @@ public final SprintResponse addNewSprint(SprintRequest sprintRequest, User user) Project project = projectRepository.findByProjectIdAndOwner(sprintRequest.projectId(), user) .orElseThrow(() -> new ProjectDoesNotExistException( - String.format("Project with projectId: %d does not exist", sprintRequest.projectId())) + String.format(PROJECT_NOT_FOUND, sprintRequest.projectId())) ); Sprint sprint = Sprint .builder() @@ -81,7 +87,7 @@ public final List getSprintsOnPage(int page, long projectId, Use Page sprints = sprintRepository.findAllByProjectId(projectId, user, pageable); - log.info("Sprints found on page : {}", sprints.getNumberOfElements()); + log.info(SPRINTS_ON_PAGE, sprints.getNumberOfElements()); return sprints.map(sprintMapper::toSprintResponse).toList(); } @@ -180,4 +186,27 @@ public final SprintResponse deleteSprint(long sprintId, User user) { return sprintMapper.toSprintResponse(sprintToDelete); } + @Override + public List getCurrentAndFutureSprints(long projectId, User user) { + log.info("Getting current and future sprints for project with id: {}", projectId); + + Project project = projectRepository.findByIdWithProjectMember(projectId, user) + .orElseThrow(() -> new ProjectDoesNotExistException( + String.format(PROJECT_NOT_FOUND, projectId) + )); + + log.info("Found project with id: {}", project); + + Pageable pageable = PageRequest.of(0, FUTURE_SPRINTS_PER_PAGE, Sort.by( + Sort.Direction.ASC, SPRINT_START_DATE_FIELD_NAME) + ); + + Page sprints = sprintRepository.findAllByProjectAndSprintEndDateAfter(project, + LocalDate.now(), pageable); + + log.info(SPRINTS_ON_PAGE, sprints.getNumberOfElements()); + + return sprints.map(sprintMapper::toSprintResponse).toList(); + } + } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/constants/SprintMappings.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/constants/SprintMappings.java index 2c2ece73..9a84bee3 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/constants/SprintMappings.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/constants/SprintMappings.java @@ -19,6 +19,8 @@ public final class SprintMappings { public static final String DELETE_SPRINT = "/deleteSprint"; + public static final String CURRENT_AND_FUTURE_SPRINTS = "/currentAndFuture"; + private SprintMappings() { } } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintController.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintController.java index 391736f7..2a0dc672 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintController.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintController.java @@ -89,4 +89,13 @@ public interface SprintController { * @return The details of the deleted sprint. */ SprintResponse deleteSprint(long sprintId, User user); + + /** + * Gets current and future sprints for given project + * + * @param projectId id of project to retrieve sprints from + * @param user User requesting access + * @return List of sprints + */ + List getCurrentAndFutureSprints(long projectId, User user); } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintService.java b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintService.java index 090cf77d..ab87443d 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintService.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/api/sprint/interfaces/SprintService.java @@ -89,4 +89,13 @@ public interface SprintService { * @return The details of the deleted sprint. */ SprintResponse deleteSprint(long sprintId, User user); + + /** + * Gets current and future sprints for given project + * + * @param projectId id of project to retrieve sprints from + * @param user User requesting access + * @return List of sprints + */ + List getCurrentAndFutureSprints(long projectId, User user); } \ No newline at end of file diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/config/PlaceholderData.java b/corn-backend/src/main/java/dev/corn/cornbackend/config/PlaceholderData.java index 23be0264..bd513620 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/config/PlaceholderData.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/config/PlaceholderData.java @@ -120,6 +120,18 @@ public void run(String... args) { ), commenter); } } + + Arrays.stream(SAMPLE_BACKLOG_ITEMS_WITHOUT_SPRINT) + .map(item -> new BacklogItem(0, + item[0], item[1], + ItemStatus.TODO, + null, + Collections.emptyList(), + null, + null, + project, + drawRandom(typesPool))) + .forEach(backlogItemRepository::save); } private T drawRandom(List list) { @@ -214,4 +226,59 @@ private T drawRandom(List list) { "Database access refactor success metrics written." ); + private final String[][] SAMPLE_BACKLOG_ITEMS_WITHOUT_SPRINT = { + {"Implement Feature Z", "Develop and integrate Feature Z to improve application functionality."}, + {"Resolve Performance Bottlenecks", "Identify and address performance bottlenecks to enhance application speed."}, + {"Refactor Legacy Codebase", "Restructure legacy codebase to improve maintainability and readability."}, + {"Design New Landing Page", "Create a captivating landing page to attract more visitors to the application."}, + {"Implement Offline Mode", "Develop offline mode functionality to allow users to access key features without internet connectivity."}, + {"Enhance Data Visualization Tools", "Upgrade data visualization tools to provide more insights to users."}, + {"Upgrade Hosting Infrastructure", "Migrate to a more robust hosting infrastructure for better reliability."}, + {"Create Interactive User Guides", "Develop interactive user guides to assist users in navigating the application."}, + {"Implement Real-Time Collaboration", "Introduce real-time collaboration features for enhanced teamwork."}, + {"Optimize Frontend Performance", "Optimize frontend performance to reduce load times and improve user experience."}, + {"Integrate Voice Search", "Incorporate voice search functionality for hands-free navigation."}, + {"Enhance Data Security Measures", "Implement additional security measures to protect user data from threats."}, + {"Upgrade User Feedback Mechanism", "Enhance the system for collecting and analyzing user feedback."}, + {"Implement AI-driven Recommendations", "Integrate AI algorithms to provide personalized recommendations to users."}, + {"Optimize Database Schema", "Optimize database schema for improved data organization and retrieval."}, + {"Enhance Mobile App Navigation", "Improve mobile app navigation for easier access to features."}, + {"Implement Cross-Platform Syncing", "Enable syncing of data across different platforms used by the same user."}, + {"Upgrade Email Notification Templates", "Revise and update email notification templates for better communication."}, + {"Create Interactive User Polls", "Set up interactive user polls to gather opinions and preferences."}, + {"Implement Progressive Image Loading", "Load images progressively to improve page loading times."}, + {"Enhance Content Filtering Options", "Expand content filtering options to provide more tailored results to users."}, + {"Integrate Machine Learning Algorithms", "Incorporate machine learning algorithms for predictive analytics."}, + {"Optimize Database Replication", "Optimize database replication for improved data redundancy and availability."}, + {"Implement Smart Search Suggestions", "Provide smart search suggestions to assist users in finding relevant content."}, + {"Upgrade API Rate Limiting", "Enhance API rate limiting mechanisms to prevent abuse and ensure fair usage."}, + {"Create User Engagement Analytics", "Track user engagement metrics to understand user behavior."}, + {"Implement Customizable Themes", "Allow users to customize application themes according to their preferences."}, + {"Enhance Data Import/Export Tools", "Improve tools for importing and exporting data to and from the application."}, + {"Integrate Automated Data Cleansing", "Automate data cleansing processes to ensure data accuracy."}, + {"Upgrade Content Recommendation Engine", "Enhance the content recommendation engine for better accuracy and relevance."}, + {"Optimize SQL Queries", "Optimize SQL queries for faster database retrieval."}, + {"Enhance User Feedback Mechanism", "Improve the system for collecting and processing user feedback."}, + {"Implement Browser Push Notifications", "Enable browser-based push notifications for desktop users."}, + {"Upgrade Firewall Security", "Enhance firewall security to protect against cyber threats."}, + {"Create Community Forums", "Establish community forums for users to interact and share experiences."}, + {"Integrate Voice Recognition", "Incorporate voice recognition for hands-free interaction."}, + {"Improve Mobile App Accessibility", "Enhance accessibility features for the mobile app version."}, + {"Implement Social Login", "Allow users to log in using their social media accounts."}, + {"Upgrade Data Storage Solution", "Upgrade data storage solution for increased reliability and scalability."}, + {"Create Interactive Onboarding Process", "Develop an interactive onboarding process for new users."}, + {"Implement Augmented Reality Features", "Introduce augmented reality features for immersive experiences."}, + {"Upgrade CDN for Content Delivery", "Upgrade content delivery network for faster content distribution."}, + {"Enhance Error Reporting", "Improve error reporting mechanisms for faster issue resolution."}, + {"Implement User Rewards System", "Create a rewards system to incentivize user engagement."}, + {"Upgrade Cross-Platform Compatibility", "Ensure compatibility across various operating systems and devices."}, + {"Optimize Mobile App UI", "Optimize the user interface for better usability on mobile devices."}, + {"Implement Data Anonymization", "Anonymize user data to enhance privacy protection."}, + {"Upgrade API Documentation", "Improve documentation for developers integrating with the application's API."}, + {"Create Knowledge Base", "Establish a knowledge base for users to find answers to common questions."}, + {"Implement Load Balancing", "Introduce load balancing for better distribution of server resources."}, + {"Enhance Email Marketing Tools", "Improve tools for managing and analyzing email marketing campaigns."}, + {"Upgrade CAPTCHA Security", "Enhance CAPTCHA security to prevent spam and abuse."}, + {"Implement Continuous Integration", "Introduce continuous integration for automated code testing and deployment."}, + }; } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/BacklogItem.java b/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/BacklogItem.java index de2a1b85..36c6870a 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/BacklogItem.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/BacklogItem.java @@ -66,11 +66,9 @@ public class BacklogItem implements Jsonable { private List comments; @ManyToOne - @NotNull(message = BacklogItemConstants.BACKLOG_ITEM_ASSIGNEE_NULL_MSG) private ProjectMember assignee; @ManyToOne - @NotNull(message = BacklogItemConstants.BACKLOG_ITEM_SPRINT_NULL_MSG) private Sprint sprint; @ManyToOne diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/interfaces/BacklogItemMapper.java b/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/interfaces/BacklogItemMapper.java index 37fdcde4..1504ff2c 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/interfaces/BacklogItemMapper.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/interfaces/BacklogItemMapper.java @@ -23,7 +23,7 @@ public interface BacklogItemMapper { * @return The mapped BacklogItemResponse. */ @Mapping(target="projectId", expression="java(backlogItem.getProject().getProjectId())") - @Mapping(target="sprintId", expression="java(backlogItem.getSprint().getSprintId())") + @Mapping(target="sprintId", expression="java(backlogItem.getSprint() != null ? backlogItem.getSprint().getSprintId() : -1)") BacklogItemResponse backlogItemToBacklogItemResponse(BacklogItem backlogItem); /** diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/interfaces/BacklogItemRepository.java b/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/interfaces/BacklogItemRepository.java index 50d084bf..ae37fa92 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/interfaces/BacklogItemRepository.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/interfaces/BacklogItemRepository.java @@ -11,7 +11,6 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.List; import java.util.Optional; /** @@ -23,9 +22,10 @@ public interface BacklogItemRepository extends JpaRepository * Finds all BacklogItems associated with a Sprint * * @param sprint Sprint to find BacklogItems for + * @param pageable Pageable that pages and sorts the data * @return List of BacklogItems associated with the Sprint */ - List getBySprint(Sprint sprint); + Page getBySprint(Sprint sprint, Pageable pageable); /** * Finds all BacklogItems associated with a Project @@ -45,4 +45,12 @@ public interface BacklogItemRepository extends JpaRepository */ @Query("SELECT b FROM BacklogItem b JOIN b.project p WHERE b.backlogItemId = :id AND (p.owner = :user OR :user IN (SELECT pm.user FROM ProjectMember pm WHERE pm.project = p))") Optional findByIdWithProjectMember(@Param("id") long id, @Param("user") User user); + + /** + * Finds all BacklogItems for given project that aren't assigned to any sprint + * @param project Project to find BacklogItems for + * @param pageable Pageable that pages and sorts the data + * @return List of BacklogItems associated with the Project + */ + Page findByProjectAndSprintIsNull(Project project, Pageable pageable); } diff --git a/corn-backend/src/main/java/dev/corn/cornbackend/entities/sprint/interfaces/SprintRepository.java b/corn-backend/src/main/java/dev/corn/cornbackend/entities/sprint/interfaces/SprintRepository.java index 174c8637..c4d2e7ab 100644 --- a/corn-backend/src/main/java/dev/corn/cornbackend/entities/sprint/interfaces/SprintRepository.java +++ b/corn-backend/src/main/java/dev/corn/cornbackend/entities/sprint/interfaces/SprintRepository.java @@ -10,6 +10,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDate; import java.util.Optional; /** @@ -19,7 +20,7 @@ public interface SprintRepository extends JpaRepository { /** - * Finds all Sprints associated with Project of given projectId and checks if the user is a assignee + * Finds all Sprints associated with Project of given projectId and checks if the user is an assignee * or the owner of the project * @param projectId id of Project * @param user user requesting access @@ -56,4 +57,13 @@ public interface SprintRepository extends JpaRepository { @Query("SELECT s FROM Sprint s WHERE s.sprintId = :id AND s.project.owner = :user") Optional findByIdWithProjectOwner(@Param("id") long id, @Param("user") User user); + /** + * Finds all sprints with given project that have end date after specified date and pages them + * @param project project to find sprints associated with + * @param date date after which found sprints should end + * @param pageable pageable to page and sort sprints + * @return Page of found sprints + */ + Page findAllByProjectAndSprintEndDateAfter(Project project, LocalDate date, Pageable pageable); + } diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/api/backlog/item/BacklogItemControllerImplTest.java b/corn-backend/src/test/java/dev/corn/cornbackend/api/backlog/item/BacklogItemControllerImplTest.java index 7752d3d7..e87daade 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/api/backlog/item/BacklogItemControllerImplTest.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/api/backlog/item/BacklogItemControllerImplTest.java @@ -16,8 +16,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.List; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @@ -104,15 +102,18 @@ final void test_deleteByIdShouldReturnCorrectBacklogItemResponse() { final void test_getBySprintIdShouldReturnCorrectBacklogItemResponse() { //given long sprintId = 1L; + int pageNumber = 0; + String sortBy = ""; + String orderBy = ""; //when - when(backlogItemService.getBySprintId(sprintId, SAMPLE_USER)) - .thenReturn(BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponses()); + when(backlogItemService.getBySprintId(sprintId, pageNumber, sortBy, orderBy, SAMPLE_USER)) + .thenReturn(BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponseList()); - List expected = BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponses(); + BacklogItemResponseList expected = BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponseList(); //then - assertEquals(expected, backlogItemController.getBySprintId(sprintId, SAMPLE_USER), + assertEquals(expected, backlogItemController.getBySprintId(sprintId, pageNumber, sortBy, orderBy, SAMPLE_USER), SHOULD_RETURN_CORRECT_BACKLOG_ITEM_RESPONSE); } @@ -151,6 +152,23 @@ final void test_getDetailsByIdShouldReturnCorrectBacklogItemDetails() { SHOULD_RETURN_CORRECT_BACKLOG_ITEM_RESPONSE); } + @Test + final void test_getAllWithoutSprintShouldReturnCorrectBacklogItemResponse() { + //given + long projectId = 1L; + int pageNumber = 0; + String sortBy = ""; + String orderBy = ""; + + //when + when(backlogItemService.getAllWithoutSprint(projectId, pageNumber, sortBy, orderBy, SAMPLE_USER)) + .thenReturn(BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponseList()); + BacklogItemResponseList expected = BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponseList(); + + //then + assertEquals(expected, backlogItemController.getAllWithoutSprint(projectId, pageNumber, sortBy, orderBy, SAMPLE_USER), + SHOULD_RETURN_CORRECT_BACKLOG_ITEM_RESPONSE); + } } \ No newline at end of file diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/api/backlog/item/BacklogItemServiceImplTest.java b/corn-backend/src/test/java/dev/corn/cornbackend/api/backlog/item/BacklogItemServiceImplTest.java index 10b3f490..e969d760 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/api/backlog/item/BacklogItemServiceImplTest.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/api/backlog/item/BacklogItemServiceImplTest.java @@ -38,10 +38,13 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import java.util.List; import java.util.Optional; import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemServiceConstants.BACKLOG_ITEM_PAGE_SIZE; +import static dev.corn.cornbackend.entities.backlog.item.constants.BacklogItemConstants.BACKLOG_ITEM_ASSIGNEE_FIELD_NAME; +import static dev.corn.cornbackend.entities.project.member.constants.ProjectMemberConstants.PROJECT_MEMBER_USER_FIELD_NAME; +import static dev.corn.cornbackend.entities.user.constants.UserConstants.USER_NAME_FIELD_NAME; +import static dev.corn.cornbackend.entities.user.constants.UserConstants.USER_SURNAME_FIELD_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -380,13 +383,17 @@ final void deleteById_shouldThrowBacklogItemNotFoundExceptionOnIncorrectId() { final void getBySprintId_shouldReturnCorrectBacklogItemListResponseOnCorrectId() { //given long id = 1L; + int pageNumber = 0; + String sortBy = "status"; + String order = "ASC"; + Pageable pageable = PageRequest.of(pageNumber, 30, Sort.Direction.ASC, sortBy); //when when(sprintRepository.findByIdWithProjectMember(id, SAMPLE_USER)) .thenReturn(Optional.of(ENTITY_DATA.sprint())); - when(backlogItemRepository.getBySprint(ENTITY_DATA.sprint())) - .thenReturn(BACKLOG_ITEM_LIST_TEST_DATA.backlogItems()); + when(backlogItemRepository.getBySprint(ENTITY_DATA.sprint(), pageable)) + .thenReturn(new PageImpl<>(BACKLOG_ITEM_LIST_TEST_DATA.backlogItems())); when(backlogItemMapper.backlogItemToBacklogItemResponse(BACKLOG_ITEM_LIST_TEST_DATA.backlogItems().get(0))) .thenReturn(BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponses().get(0)); @@ -394,10 +401,40 @@ final void getBySprintId_shouldReturnCorrectBacklogItemListResponseOnCorrectId() when(backlogItemMapper.backlogItemToBacklogItemResponse(BACKLOG_ITEM_LIST_TEST_DATA.backlogItems().get(1))) .thenReturn(BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponses().get(1)); - List expected = BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponses(); + BacklogItemResponseList expected = BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponseList(); + + //then + assertEquals(expected, backlogItemServiceImpl.getBySprintId(id, pageNumber, sortBy, order, SAMPLE_USER), + SHOULD_RETURN_CORRECT_BACKLOG_ITEM_RESPONSE_LIST); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"abab"}) + final void getBySprintId_shouldCallDatabaseWithDefaultValuesAndReturnCorrectBacklogItemsWhenGivenSortByOrOrderIsNullOrIncorrect(String value) { + //given + long id = 1L; + int pageNumber = 0; + Pageable pageable = PageRequest.of(pageNumber, BACKLOG_ITEM_PAGE_SIZE, + Sort.by(Sort.DEFAULT_DIRECTION, BacklogItemSortBy.of(value).getValue())); + + //when + when(sprintRepository.findByIdWithProjectMember(id, SAMPLE_USER)) + .thenReturn(Optional.of(ENTITY_DATA.sprint())); + + when(backlogItemRepository.getBySprint(ENTITY_DATA.sprint(), pageable)) + .thenReturn(new PageImpl<>(BACKLOG_ITEM_LIST_TEST_DATA.backlogItems())); + + when(backlogItemMapper.backlogItemToBacklogItemResponse(BACKLOG_ITEM_LIST_TEST_DATA.backlogItems().get(0))) + .thenReturn(BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponses().get(0)); + + when(backlogItemMapper.backlogItemToBacklogItemResponse(BACKLOG_ITEM_LIST_TEST_DATA.backlogItems().get(1))) + .thenReturn(BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponses().get(1)); + + BacklogItemResponseList expected = BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponseList(); //then - assertEquals(expected, backlogItemServiceImpl.getBySprintId(id, SAMPLE_USER), + assertEquals(expected, backlogItemServiceImpl.getBySprintId(id, pageNumber, value, value, SAMPLE_USER), SHOULD_RETURN_CORRECT_BACKLOG_ITEM_RESPONSE_LIST); } @@ -405,16 +442,34 @@ final void getBySprintId_shouldReturnCorrectBacklogItemListResponseOnCorrectId() final void getBySprintId_shouldThrowSprintNotFoundExceptionOnIncorrectId() { //given long id = -1L; + int pageNumber = 0; + String sortBy = "status"; + String order = "ASC"; //when when(sprintRepository.findByIdWithProjectMember(id, SAMPLE_USER)) .thenReturn(Optional.empty()); //then - assertThrows(SprintNotFoundException.class, () -> backlogItemServiceImpl.getBySprintId(id, SAMPLE_USER), + assertThrows(SprintNotFoundException.class, () -> backlogItemServiceImpl.getBySprintId(id, + pageNumber, sortBy, order, SAMPLE_USER), String.format(SHOULD_THROW, SprintNotFoundException.class.getSimpleName())); } + @Test + final void getBySprintId_shouldThrowExceptionWhenGivenPageNumberIsNegative() { + //given + long id = 1L; + int pageNumber = -1; + String sortBy = "status"; + String order = "ASC"; + + //then + assertThrows(WrongPageNumberException.class, () -> backlogItemServiceImpl.getBySprintId( + id, pageNumber, sortBy, order, SAMPLE_USER), + String.format(SHOULD_THROW, WrongPageNumberException.class.getSimpleName())); + } + @Test final void getByProjectId_shouldReturnCorrectBacklogItemListResponseOnCorrectId() { //given @@ -449,8 +504,8 @@ final void getByProjectId_shouldThrowProjectNotFoundExceptionOnIncorrectId() { //given long id = -1L; int pageNumber = 0; - String sortBy = ""; - String order = ""; + String sortBy = "status"; + String order = "ASC"; //when when(projectRepository.findByIdWithProjectMember(id, SAMPLE_USER)) @@ -467,8 +522,8 @@ final void getByProjectId_shouldThrowExceptionWhenGivenPageNumberIsNegative() { //given long id = 1L; int pageNumber = -1; - String sortBy = ""; - String order = ""; + String sortBy = "status"; + String order = "ASC"; //then assertThrows(WrongPageNumberException.class, () -> backlogItemServiceImpl.getByProjectId( @@ -539,4 +594,106 @@ final void getDetailsById_shouldThrowBacklogItemNotFoundExceptionOnIncorrectId() String.format(SHOULD_THROW, BacklogItemNotFoundException.class.getSimpleName())); } + @Test + final void getAllWithoutSprint_shouldReturnCorrectBacklogItemListResponseOnCorrectId() { + //given + long id = 1L; + int pageNumber = 0; + String sortBy = "assignee"; + String order = "DESC"; + Pageable pageable = PageRequest.of(pageNumber, 30, Sort.by( + new Sort.Order(Sort.Direction.DESC, String.format("%s.%s.%s", + BACKLOG_ITEM_ASSIGNEE_FIELD_NAME, + PROJECT_MEMBER_USER_FIELD_NAME, + USER_SURNAME_FIELD_NAME)), + new Sort.Order(Sort.Direction.DESC, String.format("%s.%s.%s", + BACKLOG_ITEM_ASSIGNEE_FIELD_NAME, + PROJECT_MEMBER_USER_FIELD_NAME, + USER_NAME_FIELD_NAME)) + )); + + //when + when(projectRepository.findByIdWithProjectMember(id, SAMPLE_USER)) + .thenReturn(Optional.of(ENTITY_DATA.project())); + + when(backlogItemRepository.findByProjectAndSprintIsNull(ENTITY_DATA.project(), pageable)) + .thenReturn(new PageImpl<>(BACKLOG_ITEM_LIST_TEST_DATA.backlogItems())); + + when(backlogItemMapper.backlogItemToBacklogItemResponse(BACKLOG_ITEM_LIST_TEST_DATA.backlogItems().get(0))) + .thenReturn(BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponses().get(0)); + + when(backlogItemMapper.backlogItemToBacklogItemResponse(BACKLOG_ITEM_LIST_TEST_DATA.backlogItems().get(1))) + .thenReturn(BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponses().get(1)); + + BacklogItemResponseList expected = BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponseList(); + + //then + assertEquals(expected, backlogItemServiceImpl.getAllWithoutSprint(id, pageNumber, sortBy, order, SAMPLE_USER), + SHOULD_RETURN_CORRECT_BACKLOG_ITEM_RESPONSE_LIST); + } + + @Test + final void getAllWithoutSprint_shouldThrowProjectNotFoundExceptionOnIncorrectId() { + //given + long id = -1L; + int pageNumber = 0; + String sortBy = "status"; + String order = "ASC"; + + //when + when(projectRepository.findByIdWithProjectMember(id, SAMPLE_USER)) + .thenReturn(Optional.empty()); + + //then + assertThrows(ProjectDoesNotExistException.class, () -> backlogItemServiceImpl.getAllWithoutSprint(id, pageNumber, + sortBy, order, SAMPLE_USER), + String.format(SHOULD_THROW, ProjectDoesNotExistException.class.getSimpleName())); + } + + @Test + final void getAllWithoutSprint_shouldThrowExceptionWhenGivenPageNumberIsNegative() { + //given + long id = 1L; + int pageNumber = -1; + String sortBy = "status"; + String order = "ASC"; + + //then + assertThrows(WrongPageNumberException.class, () -> backlogItemServiceImpl.getAllWithoutSprint( + id, pageNumber, sortBy, order, SAMPLE_USER), + String.format(SHOULD_THROW, WrongPageNumberException.class.getSimpleName())); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"abab"}) + final void getAllWithoutSprint_shouldCallDatabaseWithDefaultValuesAndReturnCorrectBacklogItemsWhenGivenSortByOrOrderIsNullOrIncorrect(String value) { + //given + long id = 1L; + int pageNumber = 0; + Pageable pageable = PageRequest.of(pageNumber, BACKLOG_ITEM_PAGE_SIZE, + Sort.by(Sort.DEFAULT_DIRECTION, BacklogItemSortBy.of(value).getValue())); + + //when + when(projectRepository.findByIdWithProjectMember(id, SAMPLE_USER)) + .thenReturn(Optional.of(ENTITY_DATA.project())); + + when(backlogItemRepository.findByProjectAndSprintIsNull(ENTITY_DATA.project(), pageable)) + .thenReturn(new PageImpl<>(BACKLOG_ITEM_LIST_TEST_DATA.backlogItems())); + + when(backlogItemMapper.backlogItemToBacklogItemResponse(BACKLOG_ITEM_LIST_TEST_DATA.backlogItems().get(0))) + .thenReturn(BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponses().get(0)); + + when(backlogItemMapper.backlogItemToBacklogItemResponse(BACKLOG_ITEM_LIST_TEST_DATA.backlogItems().get(1))) + .thenReturn(BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponses().get(1)); + + BacklogItemResponseList expected = BACKLOG_ITEM_LIST_TEST_DATA.backlogItemResponseList(); + + //then + assertEquals(expected, backlogItemServiceImpl.getAllWithoutSprint(id, pageNumber, value, value, SAMPLE_USER), + SHOULD_RETURN_CORRECT_BACKLOG_ITEM_RESPONSE_LIST); + } + + + } \ No newline at end of file diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/api/sprint/SprintControllerTest.java b/corn-backend/src/test/java/dev/corn/cornbackend/api/sprint/SprintControllerTest.java index fb8f6e01..786f2b7f 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/api/sprint/SprintControllerTest.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/api/sprint/SprintControllerTest.java @@ -162,4 +162,22 @@ final void test_deleteSprint_shouldDeleteSprint() { // then assertEquals(expected, actual, SPRINT_RESPONSE_SHOULD_BE_EQUAL_TO_EXPECTED); } + + @Test + final void test_getCurrentAndFutureSprints_shouldReturnListOfSprints() { + // given + List expected = List.of(MAPPER.toSprintResponse(ADD_SPRINT_DATA.asSprint())); + long projectId = 1L; + + // when + when(sprintService.getCurrentAndFutureSprints(projectId, + ADD_SPRINT_DATA.project().getOwner())) + .thenReturn(expected); + + List actual = sprintService.getCurrentAndFutureSprints(projectId, + ADD_SPRINT_DATA.project().getOwner()); + + // then + assertEquals(expected, actual, SPRINT_RESPONSE_SHOULD_BE_EQUAL_TO_EXPECTED); + } } \ No newline at end of file diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/api/sprint/SprintServiceTest.java b/corn-backend/src/test/java/dev/corn/cornbackend/api/sprint/SprintServiceTest.java index 0dcbeebb..4c39ee9f 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/api/sprint/SprintServiceTest.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/api/sprint/SprintServiceTest.java @@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -383,4 +384,39 @@ final void test_deleteSprint_shouldDeleteSprint() { // then assertEquals(expected, actual, SPRINT_RESPONSE_SHOULD_BE_EQUAL_TO_EXPECTED); } + + @Test + final void test_getCurrentAndFutureSprints_shouldReturnSprintResponseList() { + // given + long projectId = 1L; + User user = ADD_SPRINT_DATA.project().getOwner(); + + // when + when(projectRepository.findByIdWithProjectMember(projectId, user)) + .thenReturn(Optional.of(ADD_SPRINT_DATA.project())); + when(sprintRepository.findAllByProjectAndSprintEndDateAfter( + any(),any(), any())) + .thenReturn(new PageImpl<>(List.of(ADD_SPRINT_DATA.asSprint()))); + when(MAPPER.toSprintResponse(ADD_SPRINT_DATA.asSprint())) + .thenReturn(ADD_SPRINT_DATA.asSprintResponse()); + + List expected = List.of(MAPPER.toSprintResponse(ADD_SPRINT_DATA.asSprint())); + // then + assertEquals(expected, sprintService.getCurrentAndFutureSprints(projectId, user)); + } + + @Test + final void test_getCurrentAndFutureSprints_shouldThrowProjectDoesNotExistExceptionOnIncorrectProjectId() { + // given + long projectId = -1L; + User user = ADD_SPRINT_DATA.project().getOwner(); + + // when + when(projectRepository.findByIdWithProjectMember(projectId, user)) + .thenReturn(Optional.empty()); + + // then + assertThrows(ProjectDoesNotExistException.class, () -> + sprintService.getCurrentAndFutureSprints(projectId, user)); + } } diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/entities/backlog/item/BacklogItemTest.java b/corn-backend/src/test/java/dev/corn/cornbackend/entities/backlog/item/BacklogItemTest.java index e032ce76..2c53ff81 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/entities/backlog/item/BacklogItemTest.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/entities/backlog/item/BacklogItemTest.java @@ -218,7 +218,6 @@ final void test_shouldReturnNullElementViolationOnNullElementOnNotNullFields() { BacklogItem backlogItem = new BacklogItem(); backlogItem.setStatus(null); backlogItem.setAssignee(null); - backlogItem.setSprint(null); backlogItem.setProject(null); // when @@ -229,16 +228,6 @@ final void test_shouldReturnNullElementViolationOnNullElementOnNotNullFields() { BacklogItemConstants.BACKLOG_ITEM_STATUS_FIELD_NAME, BacklogItemConstants.BACKLOG_ITEM_STATUS_NULL_MSG), "Should return null status violation"); - assertTrue(validateField( - backlogItem, - BacklogItemConstants.BACKLOG_ITEM_ASSIGNEE_FIELD_NAME, - BacklogItemConstants.BACKLOG_ITEM_ASSIGNEE_NULL_MSG), - "Should return null assignee violation"); - assertTrue(validateField( - backlogItem, - BacklogItemConstants.BACKLOG_ITEM_SPRINT_FIELD_NAME, - BacklogItemConstants.BACKLOG_ITEM_SPRINT_NULL_MSG), - "Should return null sprint violation"); assertTrue(validateField( backlogItem, BacklogItemConstants.BACKLOG_ITEM_PROJECT_FIELD_NAME, diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/comment/BacklogItemCommentRepositoryTestDataBuilder.java b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/comment/BacklogItemCommentRepositoryTestDataBuilder.java index 25376491..c0be548e 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/comment/BacklogItemCommentRepositoryTestDataBuilder.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/comment/BacklogItemCommentRepositoryTestDataBuilder.java @@ -63,11 +63,11 @@ public static BacklogItemCommentRepositoryTestData backlogItemCommentRepositoryT testEntityManager.merge(comment); return BacklogItemCommentRepositoryTestData.builder() - .owner(testEntityManager.find(User.class, owner.getUserId())) - .commentOwner(testEntityManager.find(User.class, commentOwner.getUserId())) - .nonCommentOwner(testEntityManager.find(User.class, nonCommentOwner.getUserId())) - .nonProjectMember(testEntityManager.find(User.class, nonProjectMember.getUserId())) - .comment(testEntityManager.find(BacklogItemComment.class, comment.getBacklogItemCommentId())) + .owner(owner) + .commentOwner(commentOwner) + .nonCommentOwner(nonCommentOwner) + .nonProjectMember(nonProjectMember) + .comment(comment) .build(); } } diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/item/BacklogItemRepositoryTest.java b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/item/BacklogItemRepositoryTest.java index da89c272..a0ec5be3 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/item/BacklogItemRepositoryTest.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/item/BacklogItemRepositoryTest.java @@ -14,7 +14,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; -import java.util.List; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -85,13 +84,14 @@ final void test_findByIdWithUserNotInProjectShouldReturnEmptyOptional() { final void test_getBySprintShouldReturnCorrectBacklogItems() { //given Sprint sprint = TEST_DATA.sprint(); + PageRequest pageRequest = PageRequest.of(0, 1); //when - List backlogItems = backlogItemRepository.getBySprint(sprint); + Page backlogItems = backlogItemRepository.getBySprint(sprint, pageRequest); //then - assertEquals(1, backlogItems.size(), LIST_CORRECT_SIZE); - assertEquals(TEST_DATA.backlogItem(), backlogItems.get(0), BACKLOG_ITEM_EQUAL); + assertEquals(1, backlogItems.getTotalElements(), LIST_CORRECT_SIZE); + assertEquals(TEST_DATA.backlogItem(), backlogItems.toList().get(0), BACKLOG_ITEM_EQUAL); } @Test @@ -102,8 +102,24 @@ final void test_getByProjectShouldReturnCorrectBacklogItems() { //when Page backlogItems = backlogItemRepository.getByProject(project, pageRequest); + //then - assertEquals(1L, backlogItems.getTotalElements(), LIST_CORRECT_SIZE); + assertEquals(1, backlogItems.getNumberOfElements(), LIST_CORRECT_SIZE); assertEquals(TEST_DATA.backlogItem(), backlogItems.toList().get(0), BACKLOG_ITEM_EQUAL); } + + @Test + final void test_findByProjectAndSprintIsNullShouldReturnCorrectBacklogItems() { + //given + Project project = TEST_DATA.project(); + PageRequest pageRequest = PageRequest.of(0, 1); + + //when + Page backlogItems = backlogItemRepository.findByProjectAndSprintIsNull(project, pageRequest); + + //then + assertEquals(1, backlogItems.getNumberOfElements(), LIST_CORRECT_SIZE); + assertEquals(TEST_DATA.backlogItemWithoutSprint(), backlogItems.toList().get(0), BACKLOG_ITEM_EQUAL); + + } } diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/item/BacklogItemRepositoryTestDataBuilder.java b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/item/BacklogItemRepositoryTestDataBuilder.java index 67b86266..cc45279a 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/item/BacklogItemRepositoryTestDataBuilder.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/item/BacklogItemRepositoryTestDataBuilder.java @@ -23,6 +23,7 @@ public static BacklogItemRepositoryTestData backlogItemRepositoryTestData(TestEn Project project = createSampleProject(1L, "project"); ProjectMember projectMemberMember = createSampleProjectMember(1L); BacklogItem backlogItem = createSampleBacklogItem(1L); + BacklogItem backlogItemWithoutSprint = createSampleBacklogItem(2L); Sprint sprint = createSampleSprint(1L); testEntityManager.merge(owner); @@ -46,15 +47,20 @@ public static BacklogItemRepositoryTestData backlogItemRepositoryTestData(TestEn backlogItem.setProject(project); backlogItem.setSprint(sprint); + backlogItemWithoutSprint.setAssignee(projectMemberMember); + backlogItemWithoutSprint.setProject(project); + testEntityManager.merge(backlogItem); + testEntityManager.merge(backlogItemWithoutSprint); return BacklogItemRepositoryTestData.builder() - .backlogItem(testEntityManager.find(BacklogItem.class, backlogItem.getBacklogItemId())) - .owner(testEntityManager.find(User.class, owner.getUserId())) - .projectMember(testEntityManager.find(User.class, projectMember.getUserId())) - .nonProjectMember(testEntityManager.find(User.class, nonProjectMember.getUserId())) - .project(testEntityManager.find(Project.class, project.getProjectId())) - .sprint(testEntityManager.find(Sprint.class, sprint.getSprintId())) + .backlogItem(backlogItem) + .backlogItemWithoutSprint(backlogItemWithoutSprint) + .owner(owner) + .projectMember(projectMember) + .nonProjectMember(nonProjectMember) + .project(project) + .sprint(sprint) .build(); } } diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/item/data/BacklogItemRepositoryTestData.java b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/item/data/BacklogItemRepositoryTestData.java index eb8e52f0..d0b35c60 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/item/data/BacklogItemRepositoryTestData.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/backlog/item/data/BacklogItemRepositoryTestData.java @@ -9,6 +9,7 @@ @Builder public record BacklogItemRepositoryTestData( BacklogItem backlogItem, + BacklogItem backlogItemWithoutSprint, User owner, User projectMember, User nonProjectMember, diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/project/ProjectRepositoryTestDataBuilder.java b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/project/ProjectRepositoryTestDataBuilder.java index 24a807e5..c9d006ba 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/project/ProjectRepositoryTestDataBuilder.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/project/ProjectRepositoryTestDataBuilder.java @@ -36,11 +36,11 @@ public static ProjectRepositoryTestData projectRepositoryTestData(TestEntityMana testEntityManager.merge(project1MemberMember); return ProjectRepositoryTestData.builder() - .project1(testEntityManager.find(Project.class, project1.getProjectId())) - .project2(testEntityManager.find(Project.class, project2.getProjectId())) - .project1And2Owner(testEntityManager.find(User.class, project1And2Owner.getUserId())) - .project1Member(testEntityManager.find(User.class, project1Member.getUserId())) - .nonProjectMember(testEntityManager.find(User.class, nonProjectMember.getUserId())) + .project1(project1) + .project2(project2) + .project1And2Owner(project1And2Owner) + .project1Member(project1Member) + .nonProjectMember(nonProjectMember) .build(); } } diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/sprint/SprintRepositoryTest.java b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/sprint/SprintRepositoryTest.java index 8abf3c7e..5f3f12a2 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/sprint/SprintRepositoryTest.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/sprint/SprintRepositoryTest.java @@ -14,6 +14,8 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import java.time.LocalDate; +import java.util.List; import java.util.Optional; import static dev.corn.cornbackend.repositories.SampleEntitiesBuilder.createSampleProject; @@ -45,30 +47,34 @@ public final void setUp() { final void test_findAllByProjectIdShouldReturnSprintsWhenGivenUserIsOwnerOfProject() { //given User owner = TEST_DATA.projectOwner(); - Pageable pageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 3); long projectId = TEST_DATA.projectId(); //when Page sprints = sprintRepository.findAllByProjectId(projectId, owner, pageable); //then - assertEquals(1L, sprints.getTotalElements(), PAGE_CORRECT_TOTAL_ELEMENTS); - assertTrue(sprints.getContent().contains(TEST_DATA.sprint()), SPRINT_EQUAL); + assertEquals(3, sprints.getNumberOfElements(), PAGE_CORRECT_TOTAL_ELEMENTS); + assertTrue(sprints.getContent().contains(TEST_DATA.currentSprint()), SPRINT_EQUAL); + assertTrue(sprints.getContent().contains(TEST_DATA.futureSprint()), SPRINT_EQUAL); + assertTrue(sprints.getContent().contains(TEST_DATA.finishedSprint()), SPRINT_EQUAL); } @Test - final void test_findAllByProjectIdShouldReturnSpritnsWhenGivenUserIsMemberOfProject() { + final void test_findAllByProjectIdShouldReturnSprintsWhenGivenUserIsMemberOfProject() { //given User member = TEST_DATA.projectMember(); - Pageable pageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 3); long projectId = TEST_DATA.projectId(); //when Page sprints = sprintRepository.findAllByProjectId(projectId, member, pageable); //then - assertEquals(1L, sprints.getTotalElements(), PAGE_CORRECT_TOTAL_ELEMENTS); - assertTrue(sprints.getContent().contains(TEST_DATA.sprint()), SPRINT_EQUAL); + assertEquals(3, sprints.getNumberOfElements(), PAGE_CORRECT_TOTAL_ELEMENTS); + assertTrue(sprints.getContent().contains(TEST_DATA.currentSprint()), SPRINT_EQUAL); + assertTrue(sprints.getContent().contains(TEST_DATA.futureSprint()), SPRINT_EQUAL); + assertTrue(sprints.getContent().contains(TEST_DATA.finishedSprint()), SPRINT_EQUAL); } @Test @@ -89,35 +95,35 @@ final void test_findAllByProjectIdShouldReturnEmptyPageWhenGivenUserIsNotAMember final void test_findByIdWithProjectMemberShouldReturnCorrectSprintWhenUserIsOwnerOfProject() { //given User owner = TEST_DATA.projectOwner(); - long sprintId = TEST_DATA.sprint().getSprintId(); + long sprintId = TEST_DATA.currentSprint().getSprintId(); //when Optional sprint = sprintRepository.findByIdWithProjectMember(sprintId, owner); //then assertTrue(sprint.isPresent(), OPTIONAL_PRESENT); - assertEquals(TEST_DATA.sprint(), sprint.get(), SPRINT_EQUAL); + assertEquals(TEST_DATA.currentSprint(), sprint.get(), SPRINT_EQUAL); } @Test final void test_findByIdWithProjectMemberShouldReturnCorrectSprintWhenUserIsMemberOfProject() { //given User member = TEST_DATA.projectMember(); - long sprintId = TEST_DATA.sprint().getSprintId(); + long sprintId = TEST_DATA.currentSprint().getSprintId(); //when Optional sprint = sprintRepository.findByIdWithProjectMember(sprintId, member); //then assertTrue(sprint.isPresent(), OPTIONAL_PRESENT); - assertEquals(TEST_DATA.sprint(), sprint.get(), SPRINT_EQUAL); + assertEquals(TEST_DATA.currentSprint(), sprint.get(), SPRINT_EQUAL); } @Test final void test_findByIdWithProjectMemberShouldReturnEmptyOptionalWhenUserIsNotOwnerOrMemberOfProject() { //given User nonMember = TEST_DATA.nonProjectMember(); - long sprintId = TEST_DATA.sprint().getSprintId(); + long sprintId = TEST_DATA.currentSprint().getSprintId(); //when Optional sprint = sprintRepository.findByIdWithProjectMember(sprintId, nonMember); @@ -142,21 +148,21 @@ final void test_findByIdWithProjectMemberShouldReturnEmptyOptionalWhenGivenIdIsI @Test final void test_findBySprintIdAndProjectShouldReturnCorrectSprint() { //given - Project project = TEST_DATA.sprint().getProject(); - long sprintId = TEST_DATA.sprint().getSprintId(); + Project project = TEST_DATA.currentSprint().getProject(); + long sprintId = TEST_DATA.currentSprint().getSprintId(); //when Optional sprint = sprintRepository.findBySprintIdAndProject(sprintId, project); //then assertTrue(sprint.isPresent(), OPTIONAL_PRESENT); - assertEquals(TEST_DATA.sprint(), sprint.get(), SPRINT_EQUAL); + assertEquals(TEST_DATA.currentSprint(), sprint.get(), SPRINT_EQUAL); } @Test final void test_findBySprintIdAndProjectShouldReturnEmptyOptionalOnIncorrectId() { //given - Project project = TEST_DATA.sprint().getProject(); + Project project = TEST_DATA.currentSprint().getProject(); long sprintId = -1L; //when @@ -170,7 +176,7 @@ final void test_findBySprintIdAndProjectShouldReturnEmptyOptionalOnIncorrectId() final void test_findBySprintIdAndProjectShouldReturnEmptyOptionalOnIncorrectProject() { //given Project project = createSampleProject(2L, "Incorrect project"); - long sprintId = TEST_DATA.sprint().getSprintId(); + long sprintId = TEST_DATA.currentSprint().getSprintId(); //when Optional sprint = sprintRepository.findBySprintIdAndProject(sprintId, project); @@ -183,21 +189,21 @@ final void test_findBySprintIdAndProjectShouldReturnEmptyOptionalOnIncorrectProj final void test_findByIdWithProjectOwnerShouldReturnSprintOnOwner() { //given User owner = TEST_DATA.projectOwner(); - long sprintId = TEST_DATA.sprint().getSprintId(); + long sprintId = TEST_DATA.currentSprint().getSprintId(); //when Optional sprint = sprintRepository.findByIdWithProjectOwner(sprintId, owner); //then assertTrue(sprint.isPresent(), OPTIONAL_PRESENT); - assertEquals(TEST_DATA.sprint(), sprint.get(), SPRINT_EQUAL); + assertEquals(TEST_DATA.currentSprint(), sprint.get(), SPRINT_EQUAL); } @Test final void test_findByIdWithProjectOwnerShouldReturnEmptyOptionalOnProjectMember() { //given User member = TEST_DATA.projectMember(); - long sprintId = TEST_DATA.sprint().getSprintId(); + long sprintId = TEST_DATA.currentSprint().getSprintId(); //when Optional sprint = sprintRepository.findByIdWithProjectOwner(sprintId, member); @@ -210,7 +216,7 @@ final void test_findByIdWithProjectOwnerShouldReturnEmptyOptionalOnProjectMember final void test_findByIdWithProjectOwnerShouldReturnEmptyOptionalOnNonProjectMember() { //given User nonMember = TEST_DATA.nonProjectMember(); - long sprintId = TEST_DATA.sprint().getSprintId(); + long sprintId = TEST_DATA.currentSprint().getSprintId(); //when Optional sprint = sprintRepository.findByIdWithProjectOwner(sprintId, nonMember); @@ -231,4 +237,33 @@ final void test_findByIdWithProjectOwnerShouldReturnEmptyOptionalOnIncorrectId() //then assertTrue(sprint.isEmpty(), OPTIONAL_EMPTY); } + + @Test + final void test_findAllByProjectAndSprintEndDateAfterShouldReturnCorrectSprints() { + //given + Project project = TEST_DATA.project(); + LocalDate date = TEST_DATA.currentSprint().getSprintStartDate(); + Pageable pageable = PageRequest.of(0, 3); + + //when + Page sprints = sprintRepository.findAllByProjectAndSprintEndDateAfter(project, date, pageable); + + //then + assertEquals(2, sprints.getNumberOfElements()); + assertTrue(sprints.getContent().containsAll(List.of(TEST_DATA.currentSprint(), TEST_DATA.futureSprint()))); + } + + @Test + final void test_findAllByProjectAndSprintEndDateAfterShouldReturnEmptyPageWhenNoneOfSprintsEndsAfterDate() { + //given + Project project = TEST_DATA.project(); + LocalDate date = TEST_DATA.futureSprint().getSprintEndDate().plusDays(1L); + Pageable pageable = PageRequest.of(0, 3); + + //when + Page sprints = sprintRepository.findAllByProjectAndSprintEndDateAfter(project, date, pageable); + + //then + assertEquals(0, sprints.getNumberOfElements()); + } } diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/sprint/SprintRepositoryTestDataBuilder.java b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/sprint/SprintRepositoryTestDataBuilder.java index 06c4d945..b07a0cb4 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/sprint/SprintRepositoryTestDataBuilder.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/sprint/SprintRepositoryTestDataBuilder.java @@ -7,6 +7,8 @@ import dev.corn.cornbackend.repositories.sprint.data.SprintRepositoryTestData; import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import java.time.LocalDate; + import static dev.corn.cornbackend.repositories.SampleEntitiesBuilder.createSampleProject; import static dev.corn.cornbackend.repositories.SampleEntitiesBuilder.createSampleProjectMember; import static dev.corn.cornbackend.repositories.SampleEntitiesBuilder.createSampleSprint; @@ -20,7 +22,18 @@ public static SprintRepositoryTestData sprintRepositoryTestData(TestEntityManage User nonProjectMember = createSampleUser(3L, "nonProjectMember"); ProjectMember projectMemberMember = createSampleProjectMember(1L); Project project = createSampleProject(1L, "Project"); - Sprint sprint = createSampleSprint(1L); + Sprint currentSprint = createSampleSprint(1L); + Sprint finishedSprint = createSampleSprint(2L); + Sprint futureSprint = createSampleSprint(3L); + + currentSprint.setSprintStartDate(LocalDate.now().plusDays(3L)); + currentSprint.setSprintEndDate(LocalDate.now().plusDays(4L)); + + finishedSprint.setSprintStartDate(LocalDate.now().plusDays(1L)); + finishedSprint.setSprintEndDate(LocalDate.now().plusDays(2L)); + + futureSprint.setSprintStartDate(LocalDate.now().plusDays(5L)); + futureSprint.setSprintEndDate(LocalDate.now().plusDays(6L)); testEntityManager.merge(projectOwner); testEntityManager.merge(projectMember); @@ -30,9 +43,13 @@ public static SprintRepositoryTestData sprintRepositoryTestData(TestEntityManage testEntityManager.merge(project); - sprint.setProject(project); + currentSprint.setProject(project); + finishedSprint.setProject(project); + futureSprint.setProject(project); - testEntityManager.merge(sprint); + testEntityManager.merge(currentSprint); + testEntityManager.merge(finishedSprint); + testEntityManager.merge(futureSprint); projectMemberMember.setUser(projectMember); projectMemberMember.setProject(project); @@ -44,7 +61,10 @@ public static SprintRepositoryTestData sprintRepositoryTestData(TestEntityManage .projectMember(projectMember) .projectOwner(projectOwner) .nonProjectMember(nonProjectMember) - .sprint(sprint) + .currentSprint(currentSprint) + .futureSprint(futureSprint) + .finishedSprint(finishedSprint) + .project(project) .build(); } } diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/sprint/data/SprintRepositoryTestData.java b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/sprint/data/SprintRepositoryTestData.java index e469fbca..7edbfc7c 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/repositories/sprint/data/SprintRepositoryTestData.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/sprint/data/SprintRepositoryTestData.java @@ -1,15 +1,19 @@ package dev.corn.cornbackend.repositories.sprint.data; +import dev.corn.cornbackend.entities.project.Project; import dev.corn.cornbackend.entities.sprint.Sprint; import dev.corn.cornbackend.entities.user.User; import lombok.Builder; @Builder public record SprintRepositoryTestData( - Sprint sprint, + Sprint currentSprint, long projectId, User projectOwner, User projectMember, - User nonProjectMember + User nonProjectMember, + Sprint futureSprint, + Sprint finishedSprint, + Project project ) { } diff --git a/corn-backend/src/test/java/dev/corn/cornbackend/test/sprint/data/AddNewSprintData.java b/corn-backend/src/test/java/dev/corn/cornbackend/test/sprint/data/AddNewSprintData.java index 04f12d01..3f473d79 100644 --- a/corn-backend/src/test/java/dev/corn/cornbackend/test/sprint/data/AddNewSprintData.java +++ b/corn-backend/src/test/java/dev/corn/cornbackend/test/sprint/data/AddNewSprintData.java @@ -1,6 +1,7 @@ package dev.corn.cornbackend.test.sprint.data; import dev.corn.cornbackend.api.sprint.data.SprintRequest; +import dev.corn.cornbackend.api.sprint.data.SprintResponse; import dev.corn.cornbackend.entities.project.Project; import dev.corn.cornbackend.entities.sprint.Sprint; @@ -22,4 +23,14 @@ public SprintRequest asSprintRequest() { .build(); } + public SprintResponse asSprintResponse() { + return SprintResponse.builder() + .sprintName(name) + .projectId(project.getProjectId()) + .sprintDescription(description) + .sprintStartDate(startDate) + .sprintEndDate(endDate) + .build(); + } + } diff --git a/corn-frontend/src/app/core/enum/RouterPaths.ts b/corn-frontend/src/app/core/enum/RouterPaths.ts index 21c1ee0c..cabd99fd 100644 --- a/corn-frontend/src/app/core/enum/RouterPaths.ts +++ b/corn-frontend/src/app/core/enum/RouterPaths.ts @@ -5,6 +5,9 @@ export enum RouterPaths { HOME_DIRECT_PATH = "/home", BOARDS_PATH = "boards", + PROJECT_LIST_PATH = "projects", + PROJECT_LIST_DIRECT_PATH = "/projects", + UNKNOWN_PATH = "**" } diff --git a/corn-frontend/src/app/core/enum/api-url.ts b/corn-frontend/src/app/core/enum/api-url.ts index 240e28db..a06a2f10 100644 --- a/corn-frontend/src/app/core/enum/api-url.ts +++ b/corn-frontend/src/app/core/enum/api-url.ts @@ -4,11 +4,14 @@ export enum ApiUrl { PROJECT_MEMBER_API_URL = '/api/v1/project/assignee', GET_BACKLOG_ITEMS_BY_PROJECT_ID = BACKLOG_ITEM_API_URL + '/getByProject', + GET_BACKLOG_ITEMS_BY_SPRINT_ID = BACKLOG_ITEM_API_URL + '/getBySprint', + GET_BACKLOG_ITEMS_WITHOUT_SPRINT = BACKLOG_ITEM_API_URL + '/getAllWithoutSprint', CREATE_BACKLOG_ITEM = BACKLOG_ITEM_API_URL + '/add', UPDATE_BACKLOG_ITEM = BACKLOG_ITEM_API_URL + '/update', DELETE_BACKLOG_ITEM = BACKLOG_ITEM_API_URL + '/delete', GET_SPRINTS_ON_PAGE = SPRINT_API_URL + '/getSprintsOnPage', + GET_CURRENT_AND_FUTURE_SPRINTS = SPRINT_API_URL + '/currentAndFuture', GET_PROJECT_MEMBERS = PROJECT_MEMBER_API_URL + '/getMembers' } diff --git a/corn-frontend/src/app/core/interfaces/boards/backlog/sprint.ts b/corn-frontend/src/app/core/interfaces/boards/backlog/sprint.ts index 201c05f7..73d3c706 100644 --- a/corn-frontend/src/app/core/interfaces/boards/backlog/sprint.ts +++ b/corn-frontend/src/app/core/interfaces/boards/backlog/sprint.ts @@ -3,6 +3,6 @@ export interface Sprint { projectId: number, sprintName: string, sprintDescription: string, - startDate: Date, - endDate: Date + sprintStartDate: Date, + sprintEndDate: Date } \ No newline at end of file diff --git a/corn-frontend/src/app/core/services/boards/backlog/backlog-item/backlog-item.service.ts b/corn-frontend/src/app/core/services/boards/backlog/backlog-item/backlog-item.service.ts index fad92431..37d14646 100644 --- a/corn-frontend/src/app/core/services/boards/backlog/backlog-item/backlog-item.service.ts +++ b/corn-frontend/src/app/core/services/boards/backlog/backlog-item/backlog-item.service.ts @@ -16,7 +16,29 @@ export class BacklogItemService { } getAllByProjectId(projectId: number, pageNumber: number, sortBy: string, order: string): Observable { - return this.http.get(`${ environment.httpBackend }${ ApiUrl.GET_BACKLOG_ITEMS_BY_PROJECT_ID }`, { + return this.http.get(ApiUrl.GET_BACKLOG_ITEMS_BY_PROJECT_ID, { + params: { + projectId: projectId, + pageNumber: pageNumber, + sortBy: sortBy, + order: order + } + }); + } + + getAllBySprintId(sprintId: number, pageNumber: number, sortBy: string, order: string): Observable { + return this.http.get(ApiUrl.GET_BACKLOG_ITEMS_BY_SPRINT_ID, { + params: { + sprintId: sprintId, + pageNumber: pageNumber, + sortBy: sortBy, + order: order + } + }); + } + + getAllWithoutSprint(projectId: number, pageNumber: number, sortBy: string, order: string): Observable { + return this.http.get(ApiUrl.GET_BACKLOG_ITEMS_WITHOUT_SPRINT, { params: { projectId: projectId, pageNumber: pageNumber, @@ -28,7 +50,7 @@ export class BacklogItemService { createNewBacklogItem(title: string, description: string, projectMemberId: number, sprintId: number, projectId: number, itemType: BacklogItemType): Observable { - return this.http.post(`${ ApiUrl.CREATE_BACKLOG_ITEM }`, { + return this.http.post(ApiUrl.CREATE_BACKLOG_ITEM, { title: title, description: description, projectMemberId: projectMemberId, @@ -39,10 +61,10 @@ export class BacklogItemService { } updateBacklogItem(item: BacklogItem): Observable { - return this.http.put(`${ApiUrl.UPDATE_BACKLOG_ITEM}`, { + return this.http.put(ApiUrl.UPDATE_BACKLOG_ITEM, { title: item.title, description: item.description, - projectMemberId: item.assignee.userId, + projectMemberId: item.assignee ? item.assignee.userId : -1, sprintId: item.sprintId, projectId: item.projectId, itemType: item.itemType.toString(), @@ -55,7 +77,7 @@ export class BacklogItemService { } deleteBacklogItem(item: BacklogItem): Observable { - return this.http.delete(`${ApiUrl.DELETE_BACKLOG_ITEM}`, { + return this.http.delete(ApiUrl.DELETE_BACKLOG_ITEM, { params: { id: item.backlogItemId } diff --git a/corn-frontend/src/app/core/services/boards/backlog/sprint/sprint.service.ts b/corn-frontend/src/app/core/services/boards/backlog/sprint/sprint.service.ts index f5cc482c..9fb56fa5 100644 --- a/corn-frontend/src/app/core/services/boards/backlog/sprint/sprint.service.ts +++ b/corn-frontend/src/app/core/services/boards/backlog/sprint/sprint.service.ts @@ -14,11 +14,19 @@ export class SprintService { } getSprintsOnPageForProject(projectId: number, pageNumber: number): Observable { - return this.http.get(`${ ApiUrl.GET_SPRINTS_ON_PAGE }`, { + return this.http.get(ApiUrl.GET_SPRINTS_ON_PAGE, { params: { page: pageNumber, projectId: projectId } }); } + + getCurrentAndFutureSprints(projectId: number): Observable { + return this.http.get(ApiUrl.GET_CURRENT_AND_FUTURE_SPRINTS, { + params: { + projectId: projectId + } + }); + } } \ No newline at end of file diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-form/backlog-form.component.html b/corn-frontend/src/app/pages/boards/backlog/backlog-form/backlog-form.component.html index bf0e4059..c809fdc5 100644 --- a/corn-frontend/src/app/pages/boards/backlog/backlog-form/backlog-form.component.html +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-form/backlog-form.component.html @@ -1,161 +1,201 @@ -

Add Backlog Item

- -
- - Title - - - @if(itemForm.controls['title'].hasError('required')) { - Title is required - } @else if (itemForm.controls['title'].hasError('maxlength')) { - Title cannot be more than 100 characters - } @else if (itemForm.controls['title'].hasError('notWhitespace')) { - Title cannot be only whitespace - } - - - - Description - - {{textArea.textLength}} / {{textArea.maxLength}} - - @if(itemForm.controls['description'].hasError('required')) { - Description is required - } @else if (itemForm.controls['description'].hasError('maxlength')) { - Description cannot be more than 500 characters - } @else if (itemForm.controls['description'].hasError('notWhitespace')) { - Description cannot be only whitespace - } - - - - -
-
-

Type

- - - - @switch(currentType) { - @case (BacklogItemType.BUG) { -
- -
- } +
+
+ Add Backlog Item +
- @case (BacklogItemType.STORY) { -
- -
- } + + + + Title + + + @if (itemForm.controls['title'].hasError('required')) { + Title is required + } @else if (itemForm.controls['title'].hasError('maxlength')) { + Title cannot be more than 100 characters + } @else if (itemForm.controls['title'].hasError('notWhitespace')) { + Title cannot be only whitespace + } + - @case (BacklogItemType.TASK) { -
- -
- } + + Description + - @case (BacklogItemType.EPIC) { -
- -
- } - } - - @for (type of types; track type) { - - @switch (type) { + {{ textArea.textLength }} / {{ textArea.maxLength }} + + @if (itemForm.controls['description'].hasError('required')) { + Description is required + } @else if (itemForm.controls['description'].hasError('maxlength')) { + Description cannot be more than 500 characters + } @else if (itemForm.controls['description'].hasError('notWhitespace')) { + Description cannot be only whitespace + } +
+ + + Sprint + + + @for (sprint of sprints; track sprint) { + + {{ sprint.sprintName }} + + } + + + @if (itemForm.controls['sprint'].hasError('required')) { + Sprint is required + } + + + +
+
+ + Type + + + + @switch (currentType) { @case (BacklogItemType.BUG) { -
+
+

Bug

} - @case (BacklogItemType.STORY) { -
+
+

Story

} - @case (BacklogItemType.TASK) { -
+
+

Task

} - @case (BacklogItemType.EPIC) { -
+
+

Epic

} } - + + + @for (type of types; track type) { + + @switch (type) { + @case (BacklogItemType.BUG) { +
+
+ +
+ +

Bug

+
+ } + @case (BacklogItemType.STORY) { +
+
+ +
+ +

Story

+
+ } + @case (BacklogItemType.TASK) { +
+
+ +
+ +

Task

+
+ } + @case (BacklogItemType.EPIC) { +
+
+ +
+ +

Epic

+
+ } + } +
+ } + + + @if (itemForm.controls['type'].hasError('required')) { + Type is required } - + +
- @if(itemForm.controls['type'].hasError('required')) { - Type is required - } - -
+
+ + Assignee -
-

Assignee

- - - -
- -
-
- @for (user of users; track user) { - -
- -
-
- } -
+ + + @if (currentUser !== undefined) { +
+ - @if(itemForm.controls['assignee'].hasError('required')) { - Assignee is required - } - -
+ {{ currentUser.name }} {{ currentUser.surname }} +
+ } + - - Sprint - - @for (sprint of sprints; track sprint) { - - {{sprint.sprintName}} - - } - + @for (user of users; track user) { + +
+ - @if(itemForm.controls['sprint'].hasError('required')) { - Sprint is required - } - -
- - - - -
- - -
-
\ No newline at end of file + {{ user.name }} {{ user.surname }} +
+ + } + + + @if (itemForm.controls['assignee'].hasError('required')) { + Assignee is required + } + +
+
+ + + + + + + + +
diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-form/backlog-form.component.scss b/corn-frontend/src/app/pages/boards/backlog/backlog-form/backlog-form.component.scss index e69de29b..3332dc15 100644 --- a/corn-frontend/src/app/pages/boards/backlog/backlog-form/backlog-form.component.scss +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-form/backlog-form.component.scss @@ -0,0 +1,3 @@ +.button-bar { + @apply flex justify-between p-3; +} diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-drag/backlog-drag.component.html b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-drag/backlog-drag.component.html new file mode 100644 index 00000000..0eb15355 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-drag/backlog-drag.component.html @@ -0,0 +1,9 @@ +
+

{{backlogItem.title}}

+

{{backlogItem.description}}

+ + +
+ +
+
\ No newline at end of file diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-drag/backlog-drag.component.scss b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-drag/backlog-drag.component.scss new file mode 100644 index 00000000..5aad4627 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-drag/backlog-drag.component.scss @@ -0,0 +1,4 @@ +.avatar-container { + width: 2.5em; + height: 2.5em; +} \ No newline at end of file diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-drag/backlog-drag.component.spec.ts b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-drag/backlog-drag.component.spec.ts new file mode 100644 index 00000000..8ecdb366 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-drag/backlog-drag.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BacklogDragComponent } from './backlog-drag.component'; + +describe('BacklogDragComponent', () => { + let component: BacklogDragComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BacklogDragComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(BacklogDragComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-drag/backlog-drag.component.ts b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-drag/backlog-drag.component.ts new file mode 100644 index 00000000..b3fc5c12 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-drag/backlog-drag.component.ts @@ -0,0 +1,22 @@ +import { Component, Input } from '@angular/core'; +import { BacklogItem } from "@interfaces/boards/backlog/backlog.item"; +import { StatusSelectComponent } from "@pages/boards/backlog/backlog-item-table/status-select/status-select.component"; +import { BacklogTypeComponent } from "@pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component"; +import { UserAvatarComponent } from "@pages/utils/user-avatar/user-avatar.component"; + +@Component({ + selector: 'app-backlog-drag', + standalone: true, + imports: [ + StatusSelectComponent, + BacklogTypeComponent, + UserAvatarComponent + ], + templateUrl: './backlog-drag.component.html', + styleUrl: './backlog-drag.component.scss' +}) +export class BacklogDragComponent { + + @Input() backlogItem!: BacklogItem; + +} diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.html b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.html new file mode 100644 index 00000000..0745927f --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
Status + + Title{{ backlogItem.title }}Description{{ backlogItem.description }}Type + + Assignee +
+
+ +
+ @if (backlogItem == hoveredRow) { + + } +
+
+ + diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.scss b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.scss new file mode 100644 index 00000000..ce2f0428 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.scss @@ -0,0 +1,8 @@ +.cdk-drop-list-dragging .cdk-drag { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} + + +.cdk-drag-animating { + transition: transform 300ms cubic-bezier(0, 0, 0.2, 1); +} \ No newline at end of file diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.spec.ts b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.spec.ts new file mode 100644 index 00000000..e7cf2c47 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BacklogItemTableComponent } from './backlog-item-table.component'; + +describe('BacklogItemTableComponent', () => { + let component: BacklogItemTableComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BacklogItemTableComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(BacklogItemTableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.ts b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.ts new file mode 100644 index 00000000..ad34cf2a --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.ts @@ -0,0 +1,188 @@ +import { AfterViewInit, ChangeDetectorRef, Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { BacklogItem } from "@interfaces/boards/backlog/backlog.item"; +import { MatSort, MatSortHeader } from "@angular/material/sort"; +import { + MatCell, + MatCellDef, + MatColumnDef, + MatHeaderCell, + MatHeaderCellDef, + MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, + MatTable +} from "@angular/material/table"; +import { MatOption, MatSelect } from "@angular/material/select"; +import { BacklogItemStatus } from "@core/enum/BacklogItemStatus"; +import { MatPaginator } from "@angular/material/paginator"; +import { catchError, merge, Observable, of, startWith, Subject, switchMap, take, takeUntil } from "rxjs"; +import { NgClass } from "@angular/common"; +import { map } from "rxjs/operators"; +import { BacklogItemService } from "@core/services/boards/backlog/backlog-item/backlog-item.service"; +import { BacklogItemType } from "@core/enum/BacklogItemType"; +import { NgIcon, provideIcons } from "@ng-icons/core"; +import { bootstrapBugFill } from "@ng-icons/bootstrap-icons"; +import { featherBook } from "@ng-icons/feather-icons"; +import { matDelete, matTask } from "@ng-icons/material-icons/baseline"; +import { octContainer } from "@ng-icons/octicons"; +import { UserAvatarComponent } from "@pages/utils/user-avatar/user-avatar.component"; +import { MatFabButton } from "@angular/material/button"; +import { MatTooltip } from "@angular/material/tooltip"; +import { BacklogItemList } from "@interfaces/boards/backlog/backlog.item.list"; +import { + CdkDrag, + CdkDragDrop, CdkDragPlaceholder, + CdkDragPreview, + CdkDropList, + moveItemInArray, + transferArrayItem +} from "@angular/cdk/drag-drop"; +import { MatTab } from "@angular/material/tabs"; +import { BacklogComponent } from "@pages/boards/backlog/backlog.component"; +import { StatusSelectComponent } from "@pages/boards/backlog/backlog-item-table/status-select/status-select.component"; +import { BacklogTypeComponent } from "@pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component"; +import { BacklogDragComponent } from "@pages/boards/backlog/backlog-item-table/backlog-drag/backlog-drag.component"; + +@Component({ + selector: 'app-backlog-item-table', + standalone: true, + imports: [ + MatSort, + MatColumnDef, + MatTable, + MatHeaderCellDef, + MatSelect, + MatOption, + NgClass, + NgIcon, + UserAvatarComponent, + MatCell, + MatHeaderCell, + MatFabButton, + MatPaginator, + MatCellDef, + MatTooltip, + MatHeaderRow, + MatRow, + MatHeaderRowDef, + MatRowDef, + MatSortHeader, + CdkDropList, + CdkDrag, + CdkDragPreview, + StatusSelectComponent, + BacklogTypeComponent, + BacklogDragComponent, + CdkDragPlaceholder + ], + templateUrl: './backlog-item-table.component.html', + styleUrl: './backlog-item-table.component.scss', + providers: [provideIcons({ bootstrapBugFill, featherBook, matTask, octContainer, matDelete })], +}) +export class BacklogItemTableComponent implements AfterViewInit, OnDestroy{ + + constructor(private backlogItemService: BacklogItemService, + private backlogComponent: BacklogComponent) { + } + + @Input() sprintId: number = 0; + @Input() sprintIds: string[] = []; + + dataToDisplay: BacklogItem[] = []; + + + displayedColumns = ['title', 'description', 'status', 'type', 'assignee']; + + @ViewChild(MatSort) sort!: MatSort; + @ViewChild(MatPaginator) paginator!: MatPaginator; + @ViewChild(MatTable) table!: MatTable; + + destroy$: Subject = new Subject(); + + resultsLength: number = 0; + hoveredRow: BacklogItem | null = null; + isLoading: boolean = true; + + deleteItem(item: BacklogItem): void { + this.backlogItemService.deleteBacklogItem(item).pipe(take(1)).subscribe((deletedItem: BacklogItem) => { + this.dataToDisplay = this.dataToDisplay.filter((i) => i !== item); + this.resultsLength -= 1; + }); + } + + protected readonly BacklogItemType = BacklogItemType; + + ngAfterViewInit(): void { + this.sort.sortChange.pipe(takeUntil(this.destroy$)).subscribe( + () => (this.paginator.pageIndex = 0)); + + merge(this.sort.sortChange, this.paginator.page) + .pipe( + startWith({}), + switchMap(() => { + this.fetchBacklogItems(); + return of(null); + }) + ).pipe(takeUntil(this.destroy$)).subscribe(); + } + + fetchBacklogItems(): void { + this.isLoading = true; + let active: string = this.sort.active === 'type' ? 'itemType' : this.sort.active; + + let source: Observable; + + if(this.sprintId === -1) { + //TODO get real projectId from somewhere + source = this.backlogItemService.getAllWithoutSprint(1, this.paginator.pageIndex, active, this.sort.direction.toUpperCase()); + } else { + source = this.backlogItemService.getAllBySprintId(this.sprintId, this.paginator.pageIndex, active, this.sort.direction.toUpperCase()); + } + + + source.pipe( + catchError(() => of(null)), + map(data => { + this.isLoading = false; + + if (!data) { + return []; + } + + this.resultsLength = data.totalNumber; + return data.backlogItemResponseList; + }), + takeUntil(this.destroy$) + ).subscribe(data => { + this.dataToDisplay = data; + + }) + } + + updateBacklogItem(item: BacklogItem): void { + this.backlogItemService.updateBacklogItem(item).pipe(take(1)).subscribe((newItem) => { + this.dataToDisplay[this.dataToDisplay.indexOf(item)] = newItem; + }) + } + + drop(event: CdkDragDrop) { + + if (event.previousContainer === event.container) { + moveItemInArray(event.container.data, event.previousIndex, event.currentIndex); + } else { + transferArrayItem(event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex); + + const previousTable = this.backlogComponent.findBacklogItemTableById(event.previousContainer.id); + if(previousTable) { + previousTable.table.renderRows(); + } + event.container.data[event.currentIndex].sprintId = this.sprintId; + this.updateBacklogItem(event.container.data[event.currentIndex]); + } + + this.table.renderRows(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component.html b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component.html new file mode 100644 index 00000000..631ba684 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component.html @@ -0,0 +1,22 @@ +@switch (backlogItem.itemType) { + @case (BacklogItemType.BUG) { +
+ +
+ } + @case (BacklogItemType.STORY) { +
+ +
+ } + @case (BacklogItemType.TASK) { +
+ +
+ } + @case (BacklogItemType.EPIC) { +
+ +
+ } +} diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component.scss b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component.scss new file mode 100644 index 00000000..9842f633 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component.scss @@ -0,0 +1,8 @@ +.type-icon { + height: 2em; + width: 2em; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component.spec.ts b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component.spec.ts new file mode 100644 index 00000000..d60b8b8d --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BacklogTypeComponent } from './backlog-type.component'; + +describe('BacklogTypeComponent', () => { + let component: BacklogTypeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BacklogTypeComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(BacklogTypeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component.ts b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component.ts new file mode 100644 index 00000000..3b1238eb --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-type/backlog-type.component.ts @@ -0,0 +1,21 @@ +import { Component, Input } from '@angular/core'; +import { BacklogItem } from "@interfaces/boards/backlog/backlog.item"; +import { BacklogItemType } from "@core/enum/BacklogItemType"; +import { MatTooltip } from "@angular/material/tooltip"; +import { NgIcon } from "@ng-icons/core"; + +@Component({ + selector: 'app-backlog-type', + standalone: true, + imports: [ + MatTooltip, + NgIcon + ], + templateUrl: './backlog-type.component.html', + styleUrl: './backlog-type.component.scss' +}) +export class BacklogTypeComponent { + + @Input() backlogItem!: BacklogItem; + protected readonly BacklogItemType = BacklogItemType; +} diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/status-select/status-select.component.html b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/status-select/status-select.component.html new file mode 100644 index 00000000..9f4381b2 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/status-select/status-select.component.html @@ -0,0 +1,9 @@ +
+ + @for (status of statuses; track status) { + {{ status.replace('_', ' ')}} + + } + +
\ No newline at end of file diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/status-select/status-select.component.scss b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/status-select/status-select.component.scss new file mode 100644 index 00000000..543efba0 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/status-select/status-select.component.scss @@ -0,0 +1,71 @@ +mat-select { + width: max-content; + margin-left: 0.5em; + margin-right: 0.5em; +} + +.status-background { + border-radius: 5px; + width: max-content; + height: 2em; + display: flex; + align-items: center; + justify-content: center; +} + +.status-background:hover { + cursor: pointer; +} + +.status-background.TODO { + background-color: #464444; +} + +.status-background.TODO:hover { + background-color: lighten(#464444, 10%); +} + +.status-background.IN_PROGRESS { + background-color: rgba(67,183,239,0.35); +} + +.status-background.IN_PROGRESS:hover { + background-color: lighten(rgba(67,183,239,0.35), 10%); +} + +.status-background.DONE { + background-color: rgba(14,168,4,0.35); +} + +.status-background.DONE:hover { + background-color: lighten(rgba(14,168,4,0.35), 10%); +} + +.status-panel { + font-size: 0.5em; + width: 3em; +} + +.status-panel-TODO { + background-color: #464444!important; +} + +.status-panel-TODO:hover { + background-color: lighten(#464444, 10%)!important; +} + +.status-panel-IN_PROGRESS { + background-color: rgba(67,183,239,0.35)!important; +} + +.status-panel-IN_PROGRESS:hover { + background-color: lighten(rgba(67,183,239,0.35), 10%)!important; +} + +.status-panel-DONE { + background-color: rgba(14,168,4, 0.55)!important; +} + +.status-panel-DONE:hover { + background-color: lighten(rgba(14,168,4, 0.55), 10%)!important; +} \ No newline at end of file diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/status-select/status-select.component.spec.ts b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/status-select/status-select.component.spec.ts new file mode 100644 index 00000000..42e8ba42 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/status-select/status-select.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StatusSelectComponent } from './status-select.component'; + +describe('StatusSelectComponent', () => { + let component: StatusSelectComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StatusSelectComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(StatusSelectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/status-select/status-select.component.ts b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/status-select/status-select.component.ts new file mode 100644 index 00000000..1b2b84c0 --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/status-select/status-select.component.ts @@ -0,0 +1,40 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatOption, MatSelect } from "@angular/material/select"; +import { NgClass } from "@angular/common"; +import { BacklogItem } from "@interfaces/boards/backlog/backlog.item"; +import { BacklogItemStatus } from "@core/enum/BacklogItemStatus"; +import { BacklogItemService } from "@core/services/boards/backlog/backlog-item/backlog-item.service"; + +@Component({ + selector: 'app-status-select', + standalone: true, + imports: [ + MatSelect, + NgClass, + MatSelect, + MatOption + ], + templateUrl: './status-select.component.html', + styleUrl: './status-select.component.scss' +}) +export class StatusSelectComponent { + + @Input() backlogItem!: BacklogItem; + + @Output() statusChange = new EventEmitter + + statuses: BacklogItemStatus[] = [ + BacklogItemStatus.TODO, + BacklogItemStatus.IN_PROGRESS, + BacklogItemStatus.DONE + ]; + + updateBacklogItem(): void { + this.statusChange.emit(this.backlogItem); + } + + getStatusClass(status: BacklogItemStatus): string { + return status.replace(' ', '_'); + } + +} diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog.component.html b/corn-frontend/src/app/pages/boards/backlog/backlog.component.html index 2dc64f10..86655c61 100644 --- a/corn-frontend/src/app/pages/boards/backlog/backlog.component.html +++ b/corn-frontend/src/app/pages/boards/backlog/backlog.component.html @@ -1,88 +1,24 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Status -
- - @for (status of statuses; track status) { - {{ status.replace('_', ' ')}} - - } - -
-
Title{{ backlogItem.title }}Description{{ backlogItem.description }}Type - @switch (backlogItem.itemType) { - @case (BacklogItemType.BUG) { -
- -
- } - @case (BacklogItemType.STORY) { -
- -
- } - @case (BacklogItemType.TASK) { -
- -
- } - @case (BacklogItemType.EPIC) { -
- -
- } - } -
Assignee -
-
- -
- @if (backlogItem == hoveredRow) { - - } -
-
- - + + @for (sprint of sprints; track sprint.sprintId) { + + + + {{sprint.sprintName}} + + + {{sprint.sprintStartDate}} - {{sprint.sprintEndDate}} + + + + + } + + + +
+ +
- -@if (isLoading) { -
- -
-} \ No newline at end of file diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog.component.scss b/corn-frontend/src/app/pages/boards/backlog/backlog.component.scss index 65781724..383692e2 100644 --- a/corn-frontend/src/app/pages/boards/backlog/backlog.component.scss +++ b/corn-frontend/src/app/pages/boards/backlog/backlog.component.scss @@ -2,105 +2,12 @@ $background-color: map-get($dark-color-settings, container-background-color); $background-color-hover: lighten($background-color, 5%); -.todo { - background-color: #737373; -} - -.in-progress { - background-color: #0089c5; -} - -.done { - background-color: #029b40; -} - table .table-row:hover { cursor: pointer; background-color: $background-color-hover; outline: 1.5px solid rgba(194, 194, 194, 0.8); } -.status-background { - border-radius: 5px; - width: max-content; - height: 2em; - display: flex; - align-items: center; - justify-content: center; -} - -.status-background:hover { - cursor: pointer; -} - -.status-background.TODO { - background-color: #464444; -} - -.status-background.TODO:hover { - background-color: lighten(#464444, 10%); -} - -.status-background.IN_PROGRESS { - background-color: rgba(67,183,239,0.35); -} - -.status-background.IN_PROGRESS:hover { - background-color: lighten(rgba(67,183,239,0.35), 10%); -} - -.status-background.DONE { - background-color: rgba(14,168,4,0.35); -} - -.status-background.DONE:hover { - background-color: lighten(rgba(14,168,4,0.35), 10%); -} - -.status-panel { - font-size: 0.5em; - width: 3em; -} - -.status-panel-TODO { - background-color: #464444!important; -} - -.status-panel-TODO:hover { - background-color: lighten(#464444, 10%)!important; -} - -.status-panel-IN_PROGRESS { - background-color: rgba(67,183,239,0.35)!important; -} - -.status-panel-IN_PROGRESS:hover { - background-color: lighten(rgba(67,183,239,0.35), 10%)!important; -} - -.status-panel-DONE { - background-color: rgba(14,168,4, 0.55)!important; -} - -.status-panel-DONE:hover { - background-color: lighten(rgba(14,168,4, 0.55), 10%)!important; -} - -.type-icon { - height: 2em; - width: 2em; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; -} - -mat-select { - width: max-content; - margin-left: 0.5em; - margin-right: 0.5em; -} - .avatar-container { width: 2.5em; height: 2.5em; diff --git a/corn-frontend/src/app/pages/boards/backlog/backlog.component.ts b/corn-frontend/src/app/pages/boards/backlog/backlog.component.ts index 5e672494..ae163e01 100644 --- a/corn-frontend/src/app/pages/boards/backlog/backlog.component.ts +++ b/corn-frontend/src/app/pages/boards/backlog/backlog.component.ts @@ -1,164 +1,60 @@ -import { AfterViewInit, Component, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core'; -import { - MatCell, - MatCellDef, - MatColumnDef, - MatHeaderCell, - MatHeaderCellDef, - MatHeaderRow, - MatHeaderRowDef, - MatRow, - MatRowDef, - MatTable, -} from "@angular/material/table"; -import { BacklogItem } from '@interfaces/boards/backlog/backlog.item'; -import { NgIcon, provideIcons } from "@ng-icons/core"; -import { matDelete, matTask } from "@ng-icons/material-icons/baseline"; -import { BacklogItemStatus } from "@core/enum/BacklogItemStatus"; -import { BacklogItemType } from "@core/enum/BacklogItemType"; -import { MatFormField, MatLabel, MatOption, MatSelect } from "@angular/material/select"; -import { NgClass, NgForOf } from "@angular/common"; -import { UserAvatarComponent } from "@pages/utils/user-avatar/user-avatar.component"; -import { bootstrapBugFill } from "@ng-icons/bootstrap-icons"; -import { MatTooltip } from "@angular/material/tooltip"; -import { featherBook } from "@ng-icons/feather-icons"; -import { octContainer } from "@ng-icons/octicons"; -import { MatSort, MatSortHeader } from "@angular/material/sort"; -import { MatButton, MatButtonModule, MatFabButton, MatIconButton } from "@angular/material/button"; -import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { MatInput } from "@angular/material/input"; -import { MatFormFieldModule } from "@angular/material/form-field"; +import { Component, OnInit, QueryList, ViewChildren, ViewEncapsulation } from '@angular/core'; +import { MatButton } from "@angular/material/button"; import { MatDialog } from "@angular/material/dialog"; import { BacklogFormComponent } from "@pages/boards/backlog/backlog-form/backlog-form.component"; -import { MatProgressSpinner } from "@angular/material/progress-spinner"; -import { MatPaginator } from "@angular/material/paginator"; -import { catchError, merge, of, startWith, Subject, switchMap, take, takeUntil } from "rxjs"; +import { take } from "rxjs"; import { BacklogItemService } from "@core/services/boards/backlog/backlog-item/backlog-item.service"; -import { map } from "rxjs/operators"; -import { MatIcon } from "@angular/material/icon"; +import { Sprint } from "@interfaces/boards/backlog/sprint"; +import { BacklogItemTableComponent } from "@pages/boards/backlog/backlog-item-table/backlog-item-table.component"; +import { + MatAccordion, + MatExpansionPanel, + MatExpansionPanelDescription, MatExpansionPanelHeader, + MatExpansionPanelTitle, +} from "@angular/material/expansion"; +import { SprintService } from "@core/services/boards/backlog/sprint/sprint.service"; +import { NgForOf } from "@angular/common"; @Component({ selector: 'app-backlog', standalone: true, imports: [ - MatTable, - MatColumnDef, - MatHeaderCell, - MatCell, - MatCellDef, - MatHeaderCellDef, - MatHeaderRow, - MatHeaderRowDef, - MatRow, - MatRowDef, - NgIcon, - MatSelect, - MatOption, - NgClass, - NgForOf, - UserAvatarComponent, - MatTooltip, - MatSortHeader, - MatSort, MatButton, - MatFabButton, - FormsModule, - ReactiveFormsModule, - MatLabel, - MatInput, - MatFormField, - MatFormFieldModule, - MatProgressSpinner, - MatPaginator, - MatIcon, - MatIconButton, - MatButtonModule + MatAccordion, + MatExpansionPanel, + MatExpansionPanelTitle, + MatExpansionPanelDescription, + MatExpansionPanelHeader, + NgForOf, + BacklogItemTableComponent ], templateUrl: './backlog.component.html', styleUrl: './backlog.component.scss', - providers: [provideIcons({ bootstrapBugFill, featherBook, matTask, octContainer, matDelete })], encapsulation: ViewEncapsulation.None }) -export class BacklogComponent implements AfterViewInit, OnDestroy { +export class BacklogComponent implements OnInit { - constructor(public dialog: MatDialog, - private backlogItemService: BacklogItemService) { + constructor(private dialog: MatDialog, + private backlogItemService: BacklogItemService, + private sprintService: SprintService) { } - @ViewChild(MatSort) sort!: MatSort; - @ViewChild(MatPaginator) paginator!: MatPaginator; - - destroy$: Subject = new Subject(); - - resultsLength: number = 0; - hoveredRow: BacklogItem | null = null; - isLoading: boolean = true; + sprints: Sprint[] = []; + sprintIds: string[] = []; - statuses: BacklogItemStatus[] = [ - BacklogItemStatus.TODO, - BacklogItemStatus.IN_PROGRESS, - BacklogItemStatus.DONE - ]; - - dataToDisplay: BacklogItem[] = []; - displayedColumns = ['title', 'description', 'status', 'type', 'assignee']; - - protected readonly BacklogItemType = BacklogItemType; - - ngAfterViewInit(): void { - this.sort.sortChange.pipe(takeUntil(this.destroy$)).subscribe( - () => (this.paginator.pageIndex = 0)); - - merge(this.sort.sortChange, this.paginator.page) - .pipe( - startWith({}), - switchMap(() => { - this.fetchBacklogItems(); - return of(null); - }) - ).pipe(takeUntil(this.destroy$)).subscribe(); - } - - fetchBacklogItems(): void { - this.isLoading = true; - let active: string = this.sort.active === 'type' ? 'itemType' : this.sort.active; - this.backlogItemService.getAllByProjectId( - 1, //TODO get real projectId from somewhere - this.paginator.pageIndex, - active, - this.sort.direction.toUpperCase()) - .pipe( - catchError(() => of(null)), - map(data => { - this.isLoading = false; - - if (!data) { - return []; - } - - this.resultsLength = data.totalNumber; - return data.backlogItemResponseList; - }), - takeUntil(this.destroy$) - ).subscribe(data => { - this.dataToDisplay = data; + ngOnInit(): void { + //TODO get real projectId from somewhere + this.sprintService.getCurrentAndFutureSprints(1).pipe(take(1)).subscribe((sprints) => { + this.sprints = sprints; + this.sprintIds = sprints.map(sprint => sprint.sprintId.toString()); + this.sprintIds.push('-1') }) } - getStatusClass(status: BacklogItemStatus): string { - return status.replace(' ', '_'); - } - - deleteItem(item: BacklogItem): void { - this.backlogItemService.deleteBacklogItem(item).pipe(take(1)).subscribe((deletedItem: BacklogItem) => { - this.dataToDisplay = this.dataToDisplay.filter((i) => i !== item); - this.resultsLength -= 1; - }); - } + @ViewChildren(BacklogItemTableComponent) backlogItemTableComponents!: QueryList; showItemForm(): void { const dialogRef = this.dialog.open(BacklogFormComponent, { - width: '500px', enterAnimationDuration: '300ms', exitAnimationDuration: '100ms', }); @@ -175,19 +71,15 @@ export class BacklogComponent implements AfterViewInit, OnDestroy { 1, //TODO get real projectId from somewhere result.type ).pipe(take(1)).subscribe((newItem) => { - this.fetchBacklogItems(); + const table: BacklogItemTableComponent | undefined = this.findBacklogItemTableById(newItem.sprintId.toString()); + if(table) { + table.fetchBacklogItems(); + } }) }) } - updateStatus(item: BacklogItem): void { - this.backlogItemService.updateBacklogItem(item).pipe(take(1)).subscribe((newItem) => { - this.dataToDisplay[this.dataToDisplay.indexOf(item)] = newItem; - }) - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + findBacklogItemTableById(id: string): BacklogItemTableComponent | undefined { + return this.backlogItemTableComponents.find(table => table.sprintId.toString() === id); } } diff --git a/corn-frontend/src/app/pages/boards/boards.component.html b/corn-frontend/src/app/pages/boards/boards.component.html index 99f34c51..5288a2b8 100644 --- a/corn-frontend/src/app/pages/boards/boards.component.html +++ b/corn-frontend/src/app/pages/boards/boards.component.html @@ -1,30 +1,7 @@ - -
- - - - - - - - -
- - - - @if (isLoggedIn && userProfile) { - - - } -
- - + + + +
@@ -39,7 +16,7 @@ [label]="'Backlog'" [selected]="selected == 'backlog'"> + [iconName]="'akarClipboard'" [label]="'Board'"> @@ -52,8 +29,9 @@
+ -
+
diff --git a/corn-frontend/src/app/pages/boards/boards.component.ts b/corn-frontend/src/app/pages/boards/boards.component.ts index 8c7cf712..d083eb13 100644 --- a/corn-frontend/src/app/pages/boards/boards.component.ts +++ b/corn-frontend/src/app/pages/boards/boards.component.ts @@ -16,6 +16,7 @@ import { KeycloakProfile } from 'keycloak-js'; import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu'; import { RouterPaths } from '@core/enum/RouterPaths'; import { BoardsPaths } from '@core/enum/BoardsPaths'; +import { ToolbarComponent } from "@shared/toolbar/toolbar.component"; @Component({ selector: 'app-boards', @@ -37,6 +38,7 @@ import { BoardsPaths } from '@core/enum/BoardsPaths'; MatMenuTrigger, MatMenuModule, CommonModule, + ToolbarComponent, ], templateUrl: './boards.component.html', }) @@ -47,28 +49,20 @@ export class BoardsComponent implements OnInit { selected: string = ''; - isLoggedIn: boolean = false; - userProfile?: KeycloakProfile; - constructor( protected readonly router: Router, - protected readonly location: Location, - protected readonly keycloak: KeycloakService, + protected readonly location: Location ) { } - async ngOnInit() { + async ngOnInit(): Promise { this.selected = this.location.path().split('/').pop() || ''; + this.router.events.subscribe((val) => { if (val instanceof NavigationEnd) { this.selected = val.url.split('/').pop() || ''; } }); - this.isLoggedIn = this.keycloak.isLoggedIn(); - if (this.isLoggedIn) { - this.userProfile = await this.keycloak.loadUserProfile(); - console.log(this.userProfile); - } } navigateToBacklog(): void { @@ -82,13 +76,4 @@ export class BoardsComponent implements OnInit { navigateToBoard(): void { this.router.navigate([`/${RouterPaths.BOARDS_PATH}/${BoardsPaths.BOARD}`]); } - - toggleSidebar(): void { - this.sidebarShown = !this.sidebarShown; - } - - logout(): void { - this.keycloak.logout(); - } - } diff --git a/corn-frontend/src/app/pages/home/home.component.ts b/corn-frontend/src/app/pages/home/home.component.ts index 071e1671..45305e81 100644 --- a/corn-frontend/src/app/pages/home/home.component.ts +++ b/corn-frontend/src/app/pages/home/home.component.ts @@ -8,6 +8,7 @@ import { Feature } from "@core/interfaces/home/feature.interface"; import { KeycloakService } from 'keycloak-angular'; import { CommonModule, NgOptimizedImage } from '@angular/common'; import { Router } from '@angular/router'; +import { RouterPaths } from "@core/enum/RouterPaths"; @Component({ selector: 'app-home', @@ -35,8 +36,7 @@ export class HomeComponent implements OnInit { ngOnInit() { if (this.keycloak.isLoggedIn()) { - //TODO implement proper auth guard - this.router.navigate(['/boards/backlog']); + this.router.navigate([`${RouterPaths.PROJECT_LIST_DIRECT_PATH}`]); } } diff --git a/corn-frontend/src/app/pages/project-list/project-list.component.html b/corn-frontend/src/app/pages/project-list/project-list.component.html index 9374e9a4..964d2bf5 100644 --- a/corn-frontend/src/app/pages/project-list/project-list.component.html +++ b/corn-frontend/src/app/pages/project-list/project-list.component.html @@ -1,3 +1,5 @@ + +
-
\ No newline at end of file +
diff --git a/corn-frontend/src/app/pages/project-list/project-list.component.ts b/corn-frontend/src/app/pages/project-list/project-list.component.ts index d0d40915..4c22d343 100644 --- a/corn-frontend/src/app/pages/project-list/project-list.component.ts +++ b/corn-frontend/src/app/pages/project-list/project-list.component.ts @@ -2,6 +2,7 @@ import { Component, HostListener } from '@angular/core'; import { ProjectComponent } from "@pages/project-list/project/project.component"; import { MatGridList, MatGridTile } from "@angular/material/grid-list"; import { User } from "@core/interfaces/boards/user"; +import { ToolbarComponent } from "@shared/toolbar/toolbar.component"; @Component({ selector: 'app-project-list', @@ -9,7 +10,8 @@ import { User } from "@core/interfaces/boards/user"; imports: [ ProjectComponent, MatGridList, - MatGridTile + MatGridTile, + ToolbarComponent ], templateUrl: './project-list.component.html', styleUrl: './project-list.component.scss' diff --git a/corn-frontend/src/app/pages/utils/user-avatar/user-avatar.component.html b/corn-frontend/src/app/pages/utils/user-avatar/user-avatar.component.html index 324e679a..2ad40291 100644 --- a/corn-frontend/src/app/pages/utils/user-avatar/user-avatar.component.html +++ b/corn-frontend/src/app/pages/utils/user-avatar/user-avatar.component.html @@ -1,14 +1,19 @@ -@if (user !== undefined) { -
-

+@if (user) { +
+

{{getInitials()}}

+} @else if (isEmpty) { +
+ +
} @else { -
-

+
+

{{getInitials()}}

} + diff --git a/corn-frontend/src/app/pages/utils/user-avatar/user-avatar.component.ts b/corn-frontend/src/app/pages/utils/user-avatar/user-avatar.component.ts index 9b421cb2..6089e692 100644 --- a/corn-frontend/src/app/pages/utils/user-avatar/user-avatar.component.ts +++ b/corn-frontend/src/app/pages/utils/user-avatar/user-avatar.component.ts @@ -1,26 +1,30 @@ import { Component, Input } from '@angular/core'; import { User } from "@core/interfaces/boards/user"; import { MatTooltip } from "@angular/material/tooltip"; +import { NgIcon, provideIcons } from "@ng-icons/core"; +import { heroUser } from "@ng-icons/heroicons/outline"; @Component({ selector: 'app-user-avatar', standalone: true, imports: [ - MatTooltip + MatTooltip, + NgIcon ], templateUrl: './user-avatar.component.html', - styleUrl: './user-avatar.component.scss' + styleUrl: './user-avatar.component.scss', + providers: [provideIcons({heroUser})] }) export class UserAvatarComponent { @Input() user: User | undefined; @Input() usersLeft: number = 0; + @Input() isEmpty: boolean = false; getInitials(): string { - if (this.user !== undefined) { + if (this.user) { return this.user.name.charAt(0) + this.user.surname.charAt(0); - } else { - return '+' + this.usersLeft; } + return '+' + this.usersLeft; } } diff --git a/corn-frontend/src/app/shared/toolbar/toolbar.component.html b/corn-frontend/src/app/shared/toolbar/toolbar.component.html new file mode 100644 index 00000000..3f83620b --- /dev/null +++ b/corn-frontend/src/app/shared/toolbar/toolbar.component.html @@ -0,0 +1,38 @@ + + @if (!isOnProjectRoute) { +
+ + + + + + + + +
+ } + + + + @if (!isOnProjectRoute) { + + } + + @if (isLoggedIn && userProfile) { + + + } +
diff --git a/corn-frontend/src/app/shared/toolbar/toolbar.component.scss b/corn-frontend/src/app/shared/toolbar/toolbar.component.scss new file mode 100644 index 00000000..00baeafa --- /dev/null +++ b/corn-frontend/src/app/shared/toolbar/toolbar.component.scss @@ -0,0 +1,3 @@ +.center { + @apply flex items-center justify-center; +} \ No newline at end of file diff --git a/corn-frontend/src/app/shared/toolbar/toolbar.component.spec.ts b/corn-frontend/src/app/shared/toolbar/toolbar.component.spec.ts new file mode 100644 index 00000000..e02b6b35 --- /dev/null +++ b/corn-frontend/src/app/shared/toolbar/toolbar.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ToolbarComponent } from './toolbar.component'; + +describe('ToolbarComponent', () => { + let component: ToolbarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ToolbarComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ToolbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/corn-frontend/src/app/shared/toolbar/toolbar.component.ts b/corn-frontend/src/app/shared/toolbar/toolbar.component.ts new file mode 100644 index 00000000..1333fd9b --- /dev/null +++ b/corn-frontend/src/app/shared/toolbar/toolbar.component.ts @@ -0,0 +1,64 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { MatButton, MatIconButton } from "@angular/material/button"; +import { MatIcon } from "@angular/material/icon"; +import { MatMenu, MatMenuItem, MatMenuTrigger } from "@angular/material/menu"; +import { MatToolbar } from "@angular/material/toolbar"; +import { KeycloakProfile } from "keycloak-js"; +import { UserinfoComponent } from "@pages/boards/userinfo/userinfo.component"; +import { KeycloakService } from "keycloak-angular"; +import { Router, RouterLink } from "@angular/router"; +import { RouterPaths } from "@core/enum/RouterPaths"; +import { MatTooltip } from "@angular/material/tooltip"; + +@Component({ + selector: 'app-toolbar', + standalone: true, + imports: [ + MatButton, + MatIcon, + MatIconButton, + MatMenu, + MatMenuItem, + MatToolbar, + UserinfoComponent, + MatMenuTrigger, + MatTooltip, + RouterLink + ], + templateUrl: './toolbar.component.html', + styleUrl: './toolbar.component.scss' +}) +export class ToolbarComponent implements OnInit { + @Input() isOnProjectRoute: boolean = false; + userProfile ?: KeycloakProfile; + isLoggedIn: boolean = false; + @Output() sidebarEvent: EventEmitter = new EventEmitter(); + sidebarShown: boolean = true; + + constructor(private keycloak: KeycloakService, + private router: Router) { + } + + async ngOnInit(): Promise { + this.isLoggedIn = this.keycloak.isLoggedIn(); + + if (this.isLoggedIn) { + this.userProfile = await this.keycloak.loadUserProfile(); + console.log(this.userProfile); + } else { + this.router.navigate([RouterPaths.HOME_DIRECT_PATH]); + } + } + + toggleSidebar(): void { + this.sidebarShown = !this.sidebarShown; + + this.sidebarEvent.emit(this.sidebarShown); + } + + logout(): void { + this.keycloak.logout(); + } + + protected readonly RouterPaths = RouterPaths; +}