From 43e424a9df4494da787b19b422201a1a729caaa1 Mon Sep 17 00:00:00 2001 From: Higunio320 Date: Sun, 17 Mar 2024 18:02:14 +0100 Subject: [PATCH 01/18] Added redirection to projects after login --- corn-frontend/src/app/pages/home/home.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corn-frontend/src/app/pages/home/home.component.ts b/corn-frontend/src/app/pages/home/home.component.ts index 071e1671..84e12bd8 100644 --- a/corn-frontend/src/app/pages/home/home.component.ts +++ b/corn-frontend/src/app/pages/home/home.component.ts @@ -36,7 +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(['/projects']); } } From afe1abfca8d31b3636f36e54df3c4ada3d6b1308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20P=C3=B3=C5=82torak?= Date: Sun, 17 Mar 2024 19:37:26 +0100 Subject: [PATCH 02/18] Added redirecting if user is not logged in --- corn-frontend/src/app/pages/boards/boards.component.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/corn-frontend/src/app/pages/boards/boards.component.ts b/corn-frontend/src/app/pages/boards/boards.component.ts index 8c7cf712..c2a85ff4 100644 --- a/corn-frontend/src/app/pages/boards/boards.component.ts +++ b/corn-frontend/src/app/pages/boards/boards.component.ts @@ -65,9 +65,11 @@ export class BoardsComponent implements OnInit { } }); 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]); } } @@ -88,7 +90,8 @@ export class BoardsComponent implements OnInit { } logout(): void { - this.keycloak.logout(); + this.keycloak + .logout(); } } From 9d97952477c502c88dec03b1cde0b3c2cd613d59 Mon Sep 17 00:00:00 2001 From: Higunio320 Date: Mon, 18 Mar 2024 17:32:36 +0100 Subject: [PATCH 03/18] Changed path to value from enum --- corn-frontend/src/app/core/enum/RouterPaths.ts | 3 +++ corn-frontend/src/app/pages/home/home.component.ts | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) 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/pages/home/home.component.ts b/corn-frontend/src/app/pages/home/home.component.ts index 84e12bd8..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(['/projects']); + this.router.navigate([`${RouterPaths.PROJECT_LIST_DIRECT_PATH}`]); } } From f459bec4098009e12e7341f311a7e35295429733 Mon Sep 17 00:00:00 2001 From: Higunio320 Date: Mon, 18 Mar 2024 19:15:00 +0100 Subject: [PATCH 04/18] Added endpoint to retrieve current and future sprints --- .../api/sprint/SprintControllerImpl.java | 8 +++ .../api/sprint/SprintServiceImpl.java | 33 +++++++++- .../api/sprint/constants/SprintMappings.java | 2 + .../sprint/interfaces/SprintController.java | 9 +++ .../api/sprint/interfaces/SprintService.java | 9 +++ .../sprint/interfaces/SprintRepository.java | 12 +++- .../api/sprint/SprintControllerTest.java | 18 ++++++ .../api/sprint/SprintServiceTest.java | 36 +++++++++++ .../sprint/SprintRepositoryTest.java | 63 ++++++++++++++----- .../SprintRepositoryTestDataBuilder.java | 28 +++++++-- .../sprint/data/SprintRepositoryTestData.java | 8 ++- .../test/sprint/data/AddNewSprintData.java | 11 ++++ 12 files changed, 212 insertions(+), 25 deletions(-) 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/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/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/repositories/sprint/SprintRepositoryTest.java b/corn-backend/src/test/java/dev/corn/cornbackend/repositories/sprint/SprintRepositoryTest.java index 8abf3c7e..f8274203 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; @@ -53,7 +55,7 @@ final void test_findAllByProjectIdShouldReturnSprintsWhenGivenUserIsOwnerOfProje //then assertEquals(1L, sprints.getTotalElements(), PAGE_CORRECT_TOTAL_ELEMENTS); - assertTrue(sprints.getContent().contains(TEST_DATA.sprint()), SPRINT_EQUAL); + assertTrue(sprints.getContent().contains(TEST_DATA.currentSprint()), SPRINT_EQUAL); } @Test @@ -68,7 +70,7 @@ final void test_findAllByProjectIdShouldReturnSpritnsWhenGivenUserIsMemberOfProj //then assertEquals(1L, sprints.getTotalElements(), PAGE_CORRECT_TOTAL_ELEMENTS); - assertTrue(sprints.getContent().contains(TEST_DATA.sprint()), SPRINT_EQUAL); + assertTrue(sprints.getContent().contains(TEST_DATA.currentSprint()), SPRINT_EQUAL); } @Test @@ -89,35 +91,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 +144,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 +172,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 +185,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 +212,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 +233,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..fdfa96f9 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 actualSprint = createSampleSprint(1L); + Sprint finishedSprint = createSampleSprint(2L); + Sprint futureSprint = createSampleSprint(3L); + + actualSprint.setSprintStartDate(LocalDate.now().plusDays(3L)); + actualSprint.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); + actualSprint.setProject(project); + finishedSprint.setProject(project); + futureSprint.setProject(project); - testEntityManager.merge(sprint); + testEntityManager.merge(actualSprint); + 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(actualSprint) + .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(); + } + } From 3cd6f80e8d6ae9cfd886a643822549b8be9733e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20P=C3=B3=C5=82torak?= Date: Tue, 19 Mar 2024 11:47:30 +0100 Subject: [PATCH 05/18] Added toolbar do different component --- .../app/pages/boards/boards.component.html | 29 ++-------- .../src/app/pages/boards/boards.component.ts | 11 +--- .../project-list/project-list.component.html | 4 +- .../app/shared/toolbar/toolbar.component.html | 26 +++++++++ .../app/shared/toolbar/toolbar.component.scss | 0 .../shared/toolbar/toolbar.component.spec.ts | 23 ++++++++ .../app/shared/toolbar/toolbar.component.ts | 53 +++++++++++++++++++ 7 files changed, 111 insertions(+), 35 deletions(-) create mode 100644 corn-frontend/src/app/shared/toolbar/toolbar.component.html create mode 100644 corn-frontend/src/app/shared/toolbar/toolbar.component.scss create mode 100644 corn-frontend/src/app/shared/toolbar/toolbar.component.spec.ts create mode 100644 corn-frontend/src/app/shared/toolbar/toolbar.component.ts diff --git a/corn-frontend/src/app/pages/boards/boards.component.html b/corn-frontend/src/app/pages/boards/boards.component.html index 99f34c51..2b7c31ae 100644 --- a/corn-frontend/src/app/pages/boards/boards.component.html +++ b/corn-frontend/src/app/pages/boards/boards.component.html @@ -1,28 +1,7 @@ - -
- - - - - - - - -
- - - - @if (isLoggedIn && userProfile) { - - - } -
+ + diff --git a/corn-frontend/src/app/pages/boards/boards.component.ts b/corn-frontend/src/app/pages/boards/boards.component.ts index 8c7cf712..ced9196a 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', }) @@ -82,13 +84,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/project-list/project-list.component.html b/corn-frontend/src/app/pages/project-list/project-list.component.html index 9374e9a4..9a53642c 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/shared/toolbar/toolbar.component.html b/corn-frontend/src/app/shared/toolbar/toolbar.component.html new file mode 100644 index 00000000..6f9c920b --- /dev/null +++ b/corn-frontend/src/app/shared/toolbar/toolbar.component.html @@ -0,0 +1,26 @@ + +
+ + + + + + + + +
+ + + + @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..e69de29b 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..d535f56f --- /dev/null +++ b/corn-frontend/src/app/shared/toolbar/toolbar.component.ts @@ -0,0 +1,53 @@ +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"; + +@Component({ + selector: 'app-toolbar', + standalone: true, + imports: [ + MatButton, + MatIcon, + MatIconButton, + MatMenu, + MatMenuItem, + MatToolbar, + UserinfoComponent, + MatMenuTrigger + ], + templateUrl: './toolbar.component.html', + styleUrl: './toolbar.component.scss' +}) +export class ToolbarComponent implements OnInit { + @Input() userProfile ?: KeycloakProfile; + @Input() isLoggedIn !: boolean; + @Output() sidebarEvent: EventEmitter = new EventEmitter(); + sidebarShown: boolean = true; + + constructor(private keycloak: KeycloakService) { + } + + async ngOnInit(): Promise { + this.isLoggedIn = this.keycloak.isLoggedIn(); + + if (this.isLoggedIn) { + this.userProfile = await this.keycloak.loadUserProfile(); + console.log(this.userProfile); + } + } + + toggleSidebar(): void { + this.sidebarShown = !this.sidebarShown; + + this.sidebarEvent.emit(this.sidebarShown); + } + + logout(): void { + this.keycloak.logout(); + } +} From 7773d6473a2b0474535da6fa7fe7d032e107f2f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20P=C3=B3=C5=82torak?= Date: Tue, 19 Mar 2024 11:54:52 +0100 Subject: [PATCH 06/18] Cleaned code and added hiding menu button on toolbar --- .../app/pages/boards/boards.component.html | 4 +-- .../src/app/pages/boards/boards.component.ts | 12 ++------ .../project-list/project-list.component.html | 2 +- .../project-list/project-list.component.ts | 4 ++- .../app/shared/toolbar/toolbar.component.html | 28 ++++++++++--------- .../app/shared/toolbar/toolbar.component.ts | 5 ++-- 6 files changed, 25 insertions(+), 30 deletions(-) diff --git a/corn-frontend/src/app/pages/boards/boards.component.html b/corn-frontend/src/app/pages/boards/boards.component.html index 2b7c31ae..175dac48 100644 --- a/corn-frontend/src/app/pages/boards/boards.component.html +++ b/corn-frontend/src/app/pages/boards/boards.component.html @@ -1,6 +1,4 @@ - + diff --git a/corn-frontend/src/app/pages/boards/boards.component.ts b/corn-frontend/src/app/pages/boards/boards.component.ts index ced9196a..49be08d1 100644 --- a/corn-frontend/src/app/pages/boards/boards.component.ts +++ b/corn-frontend/src/app/pages/boards/boards.component.ts @@ -49,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() { 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 { 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 9a53642c..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,4 +1,4 @@ - +
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/shared/toolbar/toolbar.component.html b/corn-frontend/src/app/shared/toolbar/toolbar.component.html index 6f9c920b..52b44acf 100644 --- a/corn-frontend/src/app/shared/toolbar/toolbar.component.html +++ b/corn-frontend/src/app/shared/toolbar/toolbar.component.html @@ -1,19 +1,21 @@ -
- + @if (!isOnProjectRoute) { +
+ - + - - - - -
+ + + + +
+ } diff --git a/corn-frontend/src/app/shared/toolbar/toolbar.component.ts b/corn-frontend/src/app/shared/toolbar/toolbar.component.ts index d535f56f..24b4866c 100644 --- a/corn-frontend/src/app/shared/toolbar/toolbar.component.ts +++ b/corn-frontend/src/app/shared/toolbar/toolbar.component.ts @@ -24,8 +24,9 @@ import { KeycloakService } from "keycloak-angular"; styleUrl: './toolbar.component.scss' }) export class ToolbarComponent implements OnInit { - @Input() userProfile ?: KeycloakProfile; - @Input() isLoggedIn !: boolean; + @Input() isOnProjectRoute: boolean = false; + userProfile ?: KeycloakProfile; + isLoggedIn: boolean = false; @Output() sidebarEvent: EventEmitter = new EventEmitter(); sidebarShown: boolean = true; From 3789d36da1e17b3a22dee90606a223cca1c1a277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20P=C3=B3=C5=82torak?= Date: Tue, 19 Mar 2024 11:57:14 +0100 Subject: [PATCH 07/18] Added changes from DEV-164-DoubleScrollbarFix --- corn-frontend/src/app/pages/boards/boards.component.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/corn-frontend/src/app/pages/boards/boards.component.html b/corn-frontend/src/app/pages/boards/boards.component.html index 175dac48..5288a2b8 100644 --- a/corn-frontend/src/app/pages/boards/boards.component.html +++ b/corn-frontend/src/app/pages/boards/boards.component.html @@ -1,7 +1,7 @@ - +
@@ -16,7 +16,7 @@ [label]="'Backlog'" [selected]="selected == 'backlog'"> + [iconName]="'akarClipboard'" [label]="'Board'"> @@ -29,8 +29,9 @@
+ -
+
From 384b48ec6bf437a538cf6808e1ef2a08da1ec250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20P=C3=B3=C5=82torak?= Date: Tue, 19 Mar 2024 12:00:48 +0100 Subject: [PATCH 08/18] Added changes from devel --- corn-frontend/src/app/shared/toolbar/toolbar.component.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/corn-frontend/src/app/shared/toolbar/toolbar.component.ts b/corn-frontend/src/app/shared/toolbar/toolbar.component.ts index 24b4866c..e5a78b67 100644 --- a/corn-frontend/src/app/shared/toolbar/toolbar.component.ts +++ b/corn-frontend/src/app/shared/toolbar/toolbar.component.ts @@ -6,6 +6,8 @@ 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 } from "@angular/router"; +import { RouterPaths } from "@core/enum/RouterPaths"; @Component({ selector: 'app-toolbar', @@ -30,7 +32,8 @@ export class ToolbarComponent implements OnInit { @Output() sidebarEvent: EventEmitter = new EventEmitter(); sidebarShown: boolean = true; - constructor(private keycloak: KeycloakService) { + constructor(private keycloak: KeycloakService, + private router: Router) { } async ngOnInit(): Promise { @@ -39,6 +42,8 @@ export class ToolbarComponent implements OnInit { if (this.isLoggedIn) { this.userProfile = await this.keycloak.loadUserProfile(); console.log(this.userProfile); + } else { + this.router.navigate([RouterPaths.HOME_DIRECT_PATH]); } } From 4ef77f2126e2d7e1dd1fc051e5e4b9d51749e447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20P=C3=B3=C5=82torak?= Date: Tue, 19 Mar 2024 12:23:14 +0100 Subject: [PATCH 09/18] Added button to return to projects site --- .../src/app/shared/toolbar/toolbar.component.html | 10 ++++++++++ .../src/app/shared/toolbar/toolbar.component.scss | 3 +++ .../src/app/shared/toolbar/toolbar.component.ts | 9 +++++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/corn-frontend/src/app/shared/toolbar/toolbar.component.html b/corn-frontend/src/app/shared/toolbar/toolbar.component.html index 52b44acf..3f83620b 100644 --- a/corn-frontend/src/app/shared/toolbar/toolbar.component.html +++ b/corn-frontend/src/app/shared/toolbar/toolbar.component.html @@ -19,6 +19,16 @@ + @if (!isOnProjectRoute) { + + } + @if (isLoggedIn && userProfile) { Date: Sat, 23 Mar 2024 13:18:49 +0100 Subject: [PATCH 10/18] Rewrote getBySprintId so that it matches getByProjectId (sorting, ordering etc.) --- .../item/BacklogItemControllerImpl.java | 9 +-- .../backlog/item/BacklogItemServiceImpl.java | 68 ++++++++++-------- .../BacklogItemServiceConstants.java | 2 +- .../interfaces/BacklogItemController.java | 4 +- .../item/interfaces/BacklogItemService.java | 4 +- .../interfaces/BacklogItemRepository.java | 4 +- .../item/BacklogItemControllerImplTest.java | 13 ++-- .../item/BacklogItemServiceImplTest.java | 71 ++++++++++++++++--- .../item/BacklogItemRepositoryTest.java | 8 +-- .../sprint/SprintRepositoryTest.java | 14 ++-- .../SprintRepositoryTestDataBuilder.java | 12 ++-- 11 files changed, 136 insertions(+), 73 deletions(-) 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..d3e1c66e 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,8 +18,6 @@ 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; @@ -67,9 +65,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 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..d7996b85 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,8 @@ 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.GETTING_BACKLOG_ITEMS_WITH_SORTING; 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 +132,50 @@ 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); + + return getBySprintId(sprintId, pageable, user); + } + + private BacklogItemResponseList getBySprintId(long sprintId, Pageable pageable, User user) { 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_SORTING, SPRINT, sprint, pageable.getSort()); + 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); + return getByProjectId(projectId, pageable, user); } - private BacklogItemResponseList getByProjectId(long projectId, int pageNumber, BacklogItemSortBy sortBy, - Sort.Direction order, User user) { + private BacklogItemResponseList getByProjectId(long projectId, Pageable pageable, 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_SORTING, PROJECT, project, pageable.getSort()); + 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() @@ -270,4 +265,19 @@ 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) { + 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; + + if(sort == BacklogItemSortBy.ASSIGNEE) { + return getPageableForAssignee(pageNumber, direction); + } + return PageRequest.of(pageNumber, BACKLOG_ITEM_PAGE_SIZE, Sort.by(direction, sort.getValue())); + } } 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..4a3c0331 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,7 @@ 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_SORTING = "Getting backlog items for {}: {}, sort: {}"; 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..cd373a1e 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 */ @@ -56,7 +54,7 @@ public interface BacklogItemController { * @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 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..2080177f 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 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..1dff44e1 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 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..614d04f3 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); } 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..7efc3338 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,7 +38,6 @@ 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; @@ -380,13 +379,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 +397,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, SAMPLE_USER), + 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, pageNumber, value, value, SAMPLE_USER), SHOULD_RETURN_CORRECT_BACKLOG_ITEM_RESPONSE_LIST); } @@ -405,16 +438,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 +500,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 +518,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( 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..73938cc0 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 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 f8274203..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 @@ -47,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); + 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); + 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 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 fdfa96f9..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 @@ -22,12 +22,12 @@ public static SprintRepositoryTestData sprintRepositoryTestData(TestEntityManage User nonProjectMember = createSampleUser(3L, "nonProjectMember"); ProjectMember projectMemberMember = createSampleProjectMember(1L); Project project = createSampleProject(1L, "Project"); - Sprint actualSprint = createSampleSprint(1L); + Sprint currentSprint = createSampleSprint(1L); Sprint finishedSprint = createSampleSprint(2L); Sprint futureSprint = createSampleSprint(3L); - actualSprint.setSprintStartDate(LocalDate.now().plusDays(3L)); - actualSprint.setSprintEndDate(LocalDate.now().plusDays(4L)); + currentSprint.setSprintStartDate(LocalDate.now().plusDays(3L)); + currentSprint.setSprintEndDate(LocalDate.now().plusDays(4L)); finishedSprint.setSprintStartDate(LocalDate.now().plusDays(1L)); finishedSprint.setSprintEndDate(LocalDate.now().plusDays(2L)); @@ -43,11 +43,11 @@ public static SprintRepositoryTestData sprintRepositoryTestData(TestEntityManage testEntityManager.merge(project); - actualSprint.setProject(project); + currentSprint.setProject(project); finishedSprint.setProject(project); futureSprint.setProject(project); - testEntityManager.merge(actualSprint); + testEntityManager.merge(currentSprint); testEntityManager.merge(finishedSprint); testEntityManager.merge(futureSprint); @@ -61,7 +61,7 @@ public static SprintRepositoryTestData sprintRepositoryTestData(TestEntityManage .projectMember(projectMember) .projectOwner(projectOwner) .nonProjectMember(nonProjectMember) - .currentSprint(actualSprint) + .currentSprint(currentSprint) .futureSprint(futureSprint) .finishedSprint(finishedSprint) .project(project) From 568c0dcb8cc691d2bde1a396175e991a112a569c Mon Sep 17 00:00:00 2001 From: Higunio320 Date: Sat, 23 Mar 2024 15:14:45 +0100 Subject: [PATCH 11/18] Integrated backlog view with sprints --- corn-frontend/src/app/core/enum/api-url.ts | 2 + .../core/interfaces/boards/backlog/sprint.ts | 4 +- .../backlog-item/backlog-item.service.ts | 19 +- .../boards/backlog/sprint/sprint.service.ts | 10 +- .../backlog-item-table.component.html | 78 ++++++++ .../backlog-item-table.component.scss | 0 .../backlog-item-table.component.spec.ts | 23 +++ .../backlog-item-table.component.ts | 149 ++++++++++++++ .../boards/backlog/backlog.component.html | 96 ++------- .../pages/boards/backlog/backlog.component.ts | 186 ++++-------------- 10 files changed, 329 insertions(+), 238 deletions(-) create mode 100644 corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.html create mode 100644 corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.scss create mode 100644 corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.spec.ts create mode 100644 corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.ts diff --git a/corn-frontend/src/app/core/enum/api-url.ts b/corn-frontend/src/app/core/enum/api-url.ts index 240e28db..0a78d888 100644 --- a/corn-frontend/src/app/core/enum/api-url.ts +++ b/corn-frontend/src/app/core/enum/api-url.ts @@ -4,11 +4,13 @@ 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', 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..089d42f5 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,7 @@ 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, @@ -26,9 +26,20 @@ export class BacklogItemService { }); } + 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 + } + }); + } + 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,7 +50,7 @@ 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, @@ -55,7 +66,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-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..08a6faca --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.html @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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) { + + } +
+
+ + 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..e69de29b 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..d41e67bd --- /dev/null +++ b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.ts @@ -0,0 +1,149 @@ +import { AfterViewInit, Component, Input, OnDestroy, 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, 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"; + +@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 + ], + 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) { + } + + @Input() sprintId: number = 0; + + dataToDisplay: BacklogItem[] = []; + + + displayedColumns = ['title', 'description', 'status', 'type', 'assignee']; + + @ViewChild(MatSort) sort!: MatSort; + @ViewChild(MatPaginator) paginator!: MatPaginator; + + destroy$: Subject = new Subject(); + + resultsLength: number = 0; + hoveredRow: BacklogItem | null = null; + isLoading: boolean = true; + + statuses: BacklogItemStatus[] = [ + BacklogItemStatus.TODO, + BacklogItemStatus.IN_PROGRESS, + BacklogItemStatus.DONE + ]; + + 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; + }); + } + + 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.getAllBySprintId( + this.sprintId, + 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; + }) + } + + 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(); + } +} 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..d2b737d7 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,18 @@ - - - - - + + + + + {{sprint.sprintName}} + + + {{sprint.sprintStartDate}} - {{sprint.sprintEndDate}} + + + + + - - - - - - - - - - - - - - - - - - - - - - - -
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) { - - } -
-
- - - -@if (isLoading) { -
- -
-} \ No newline at end of file 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..4be60204 100644 --- a/corn-frontend/src/app/pages/boards/backlog/backlog.component.ts +++ b/corn-frontend/src/app/pages/boards/backlog/backlog.component.ts @@ -1,160 +1,54 @@ -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 { - - constructor(public dialog: MatDialog, - private backlogItemService: BacklogItemService) { - } - - @ViewChild(MatSort) sort!: MatSort; - @ViewChild(MatPaginator) paginator!: MatPaginator; - - destroy$: Subject = new Subject(); - - resultsLength: number = 0; - hoveredRow: BacklogItem | null = null; - isLoading: boolean = true; - - 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)); +export class BacklogComponent implements OnInit { - merge(this.sort.sortChange, this.paginator.page) - .pipe( - startWith({}), - switchMap(() => { - this.fetchBacklogItems(); - return of(null); - }) - ).pipe(takeUntil(this.destroy$)).subscribe(); + constructor(private dialog: MatDialog, + private backlogItemService: BacklogItemService, + private sprintService: SprintService) { } - 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; + sprints: Sprint[] = []; - 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; }) } - 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, { @@ -175,19 +69,15 @@ export class BacklogComponent implements AfterViewInit, OnDestroy { 1, //TODO get real projectId from somewhere result.type ).pipe(take(1)).subscribe((newItem) => { - this.fetchBacklogItems(); + for(let backlogItemTableComponent of this.backlogItemTableComponents.toArray()) { + if(!backlogItemTableComponent) { + continue; + } + if(backlogItemTableComponent.sprintId === newItem.sprintId) { + backlogItemTableComponent.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(); - } } From d05e08d82b9496cd1a54b91b3dd13ff9eb7ddbcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20P=C3=B3=C5=82torak?= Date: Sat, 23 Mar 2024 17:23:05 +0100 Subject: [PATCH 12/18] Redesigned backlog item creation dialog --- .../backlog-form/backlog-form.component.html | 298 ++++++++++-------- .../backlog-form/backlog-form.component.scss | 3 + .../pages/boards/backlog/backlog.component.ts | 1 - 3 files changed, 162 insertions(+), 140 deletions(-) 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..ac4ca22f 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,181 @@ -

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.component.ts b/corn-frontend/src/app/pages/boards/backlog/backlog.component.ts index 5e672494..df75606f 100644 --- a/corn-frontend/src/app/pages/boards/backlog/backlog.component.ts +++ b/corn-frontend/src/app/pages/boards/backlog/backlog.component.ts @@ -158,7 +158,6 @@ export class BacklogComponent implements AfterViewInit, OnDestroy { showItemForm(): void { const dialogRef = this.dialog.open(BacklogFormComponent, { - width: '500px', enterAnimationDuration: '300ms', exitAnimationDuration: '100ms', }); From 6668bea6d6b2e23a9680e610469f5c41cec747ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20P=C3=B3=C5=82torak?= Date: Sat, 23 Mar 2024 17:28:37 +0100 Subject: [PATCH 13/18] Fixed the display of a type of backlog item --- .../backlog-form/backlog-form.component.html | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) 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 ac4ca22f..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 @@ -62,23 +62,39 @@ @switch (currentType) { @case (BacklogItemType.BUG) { -
- +
+
+ +
+ +

Bug

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

Story

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

Task

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

Epic

} } @@ -92,6 +108,7 @@
+

Bug

} @@ -100,6 +117,7 @@

Bug

+

Story

} @@ -108,6 +126,7 @@

Story

+

Task

} @@ -116,6 +135,7 @@

Task

+

Epic

} From 6a57629dbe1c6c248943518d5fd9c10a1acd2c49 Mon Sep 17 00:00:00 2001 From: Higunio320 Date: Mon, 25 Mar 2024 17:33:41 +0100 Subject: [PATCH 14/18] Added ability to retrieve backlogitems that are not assigned to any sprint from backend and improved some tests --- .../item/BacklogItemControllerImpl.java | 10 ++ .../backlog/item/BacklogItemServiceImpl.java | 59 +++++++--- .../item/constants/BacklogItemMappings.java | 2 + .../BacklogItemServiceConstants.java | 3 +- .../interfaces/BacklogItemController.java | 21 +++- .../item/interfaces/BacklogItemService.java | 2 + .../entities/backlog/item/BacklogItem.java | 1 - .../interfaces/BacklogItemRepository.java | 8 ++ .../item/BacklogItemControllerImplTest.java | 17 +++ .../item/BacklogItemServiceImplTest.java | 106 ++++++++++++++++++ .../backlog/item/BacklogItemTest.java | 6 - ...gItemCommentRepositoryTestDataBuilder.java | 10 +- .../item/BacklogItemRepositoryTest.java | 18 ++- .../BacklogItemRepositoryTestDataBuilder.java | 18 ++- .../data/BacklogItemRepositoryTestData.java | 1 + .../ProjectRepositoryTestDataBuilder.java | 10 +- 16 files changed, 249 insertions(+), 43 deletions(-) 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 d3e1c66e..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 @@ -21,6 +21,7 @@ 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; @@ -89,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 d7996b85..eaf0c035 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,7 +41,8 @@ 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.GETTING_BACKLOG_ITEMS_WITH_SORTING; +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.PROJECT; import static dev.corn.cornbackend.api.backlog.item.constants.BacklogItemServiceConstants.PROJECT_MEMBER; @@ -135,16 +136,12 @@ public BacklogItemResponse create(BacklogItemRequest backlogItemRequest, User us public BacklogItemResponseList getBySprintId(long sprintId, int pageNumber, String sortBy, String order, User user) { Pageable pageable = createPageableForBacklogItems(pageNumber, sortBy, order); - return getBySprintId(sprintId, pageable, user); - } - - private BacklogItemResponseList getBySprintId(long sprintId, Pageable pageable, User user) { 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_WITH_SORTING, SPRINT, sprint, pageable.getSort()); + log.info(GETTING_BACKLOG_ITEMS_WITH_PAGEABLE, SPRINT, sprint, pageable); Page items = backlogItemRepository.getBySprint(sprint, pageable); log.info(RETURNING_BACKLOG_ITEMS_OF_QUANTITY, items.getNumberOfElements()); @@ -162,17 +159,13 @@ public BacklogItemResponseList getByProjectId(long projectId, int pageNumber, St String order, User user) { Pageable pageable = createPageableForBacklogItems(pageNumber, sortBy, order); - return getByProjectId(projectId, pageable, user); - } - - private BacklogItemResponseList getByProjectId(long projectId, Pageable pageable, User user) { 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_WITH_SORTING, PROJECT, project, pageable.getSort()); + log.info(GETTING_BACKLOG_ITEMS_WITH_PAGEABLE, PROJECT, project, pageable); Page items = backlogItemRepository.getByProject(project, pageable); log.info(RETURNING_BACKLOG_ITEMS_OF_QUANTITY, items.getNumberOfElements()); @@ -197,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,10 +228,14 @@ private BacklogItemBuilderDto prepareDataForBacklogItemCreation(BacklogItemReque ProjectMember assignee = projectMemberRepository.findByProjectMemberIdAndProject(backlogItemRequest.projectMemberId(), project) .orElseThrow(() -> new ProjectMemberDoesNotExistException(PROJECT_MEMBER_NOT_FOUND_MESSAGE)); - log.info(GETTING_BY_ID, SPRINT, backlogItemRequest.sprintId()); + Sprint sprint = null; - Sprint sprint = sprintRepository.findBySprintIdAndProject(backlogItemRequest.sprintId(), project) - .orElseThrow(() -> new SprintNotFoundException(SPRINT_NOT_FOUND_MESSAGE)); + 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); } @@ -267,6 +288,7 @@ private Pageable getPageableForAssignee(int pageNumber, Sort.Direction direction } private Pageable createPageableForBacklogItems(int pageNumber, String sortBy, String order) { + log.info(CREATING_PAGEABLE_FOR, pageNumber, sortBy, order); if(pageNumber < 0) { throw new WrongPageNumberException(pageNumber); } @@ -274,10 +296,15 @@ private Pageable createPageableForBacklogItems(int pageNumber, String sortBy, St 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) { - return getPageableForAssignee(pageNumber, direction); + pageable = getPageableForAssignee(pageNumber, direction); + } else { + pageable = PageRequest.of(pageNumber, BACKLOG_ITEM_PAGE_SIZE, Sort.by(direction, sort.getValue())); } - return 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 4a3c0331..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_BACKLOG_ITEMS_WITH_SORTING = "Getting backlog items for {}: {}, sort: {}"; + 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 cd373a1e..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 @@ -48,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 */ 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 */ @@ -74,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 2080177f..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 @@ -73,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/entities/backlog/item/BacklogItem.java b/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/BacklogItem.java index de2a1b85..b1cad6e9 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 @@ -70,7 +70,6 @@ public class BacklogItem implements Jsonable { 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/BacklogItemRepository.java b/corn-backend/src/main/java/dev/corn/cornbackend/entities/backlog/item/interfaces/BacklogItemRepository.java index 1dff44e1..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 @@ -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/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 614d04f3..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 @@ -152,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 7efc3338..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 @@ -41,6 +41,10 @@ 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; @@ -590,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/entities/backlog/item/BacklogItemTest.java b/corn-backend/src/test/java/dev/corn/cornbackend/entities/backlog/item/BacklogItemTest.java index e032ce76..cb0efc2e 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 @@ -234,11 +233,6 @@ final void test_shouldReturnNullElementViolationOnNullElementOnNotNullFields() { 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 73938cc0..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 @@ -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(); } } From 245e5efb060e0d35be95a28861a4ee6445818c55 Mon Sep 17 00:00:00 2001 From: Higunio320 Date: Mon, 25 Mar 2024 17:59:40 +0100 Subject: [PATCH 15/18] Added example backlog items without sprints data and removed notnull from assignee --- .../cornbackend/config/PlaceholderData.java | 69 +++++++++++++++++++ .../entities/backlog/item/BacklogItem.java | 1 - .../backlog/item/BacklogItemTest.java | 5 -- 3 files changed, 69 insertions(+), 6 deletions(-) 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..ee19a5d7 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,61 @@ 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 b1cad6e9..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,7 +66,6 @@ public class BacklogItem implements Jsonable { private List comments; @ManyToOne - @NotNull(message = BacklogItemConstants.BACKLOG_ITEM_ASSIGNEE_NULL_MSG) private ProjectMember assignee; @ManyToOne 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 cb0efc2e..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 @@ -228,11 +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_PROJECT_FIELD_NAME, From 3175b204e989a2a0acc814fb9f8b955f39f1bd5c Mon Sep 17 00:00:00 2001 From: Higunio320 Date: Mon, 25 Mar 2024 20:32:22 +0100 Subject: [PATCH 16/18] Added backlog items without sprints to backlog view and ability to move items between sprints and backlog --- .../backlog/item/BacklogItemServiceImpl.java | 10 ++- .../cornbackend/config/PlaceholderData.java | 2 - .../item/interfaces/BacklogItemMapper.java | 2 +- corn-frontend/src/app/core/enum/api-url.ts | 1 + .../backlog-item/backlog-item.service.ts | 13 +++- .../backlog-item-table.component.html | 11 ++-- .../backlog-item-table.component.scss | 8 +++ .../backlog-item-table.component.ts | 65 +++++++++++++++---- .../boards/backlog/backlog.component.html | 7 +- .../boards/backlog/backlog.component.scss | 4 ++ .../pages/boards/backlog/backlog.component.ts | 17 +++-- .../user-avatar/user-avatar.component.html | 15 +++-- .../user-avatar/user-avatar.component.ts | 14 ++-- 13 files changed, 128 insertions(+), 41 deletions(-) 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 eaf0c035..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 @@ -223,10 +223,14 @@ 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()); + + assignee = projectMemberRepository.findByProjectMemberIdAndProject(backlogItemRequest.projectMemberId(), project) + .orElseThrow(() -> new ProjectMemberDoesNotExistException(PROJECT_MEMBER_NOT_FOUND_MESSAGE)); + } Sprint sprint = null; 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 ee19a5d7..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 @@ -281,6 +281,4 @@ private T drawRandom(List list) { {"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/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-frontend/src/app/core/enum/api-url.ts b/corn-frontend/src/app/core/enum/api-url.ts index 0a78d888..a06a2f10 100644 --- a/corn-frontend/src/app/core/enum/api-url.ts +++ b/corn-frontend/src/app/core/enum/api-url.ts @@ -5,6 +5,7 @@ export enum ApiUrl { 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', 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 089d42f5..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 @@ -37,6 +37,17 @@ export class BacklogItemService { }); } + 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, + sortBy: sortBy, + order: order + } + }); + } + createNewBacklogItem(title: string, description: string, projectMemberId: number, sprintId: number, projectId: number, itemType: BacklogItemType): Observable { return this.http.post(ApiUrl.CREATE_BACKLOG_ITEM, { @@ -53,7 +64,7 @@ export class BacklogItemService { 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(), 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 index 08a6faca..028b3934 100644 --- 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 @@ -1,11 +1,13 @@ - +
+ (mouseover)="hoveredRow = row" (mouseleave)="hoveredRow = null" cdkDrag> +
Status
@for (status of statuses; track status) { - {{ status.replace('_', ' ')}} } @@ -57,7 +59,7 @@
- +
@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 index e69de29b..ce2f0428 100644 --- 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 @@ -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.ts b/corn-frontend/src/app/pages/boards/backlog/backlog-item-table/backlog-item-table.component.ts index d41e67bd..d0ae19b5 100644 --- 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 @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, Input, OnDestroy, ViewChild } from '@angular/core'; +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 { @@ -13,7 +13,7 @@ import { import { MatOption, MatSelect } from "@angular/material/select"; import { BacklogItemStatus } from "@core/enum/BacklogItemStatus"; import { MatPaginator } from "@angular/material/paginator"; -import { catchError, merge, of, startWith, Subject, switchMap, take, takeUntil } from "rxjs"; +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"; @@ -26,6 +26,17 @@ 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, + CdkDragPreview, + CdkDropList, + moveItemInArray, + transferArrayItem +} from "@angular/cdk/drag-drop"; +import { MatTab } from "@angular/material/tabs"; +import { BacklogComponent } from "@pages/boards/backlog/backlog.component"; @Component({ selector: 'app-backlog-item-table', @@ -50,18 +61,23 @@ import { MatTooltip } from "@angular/material/tooltip"; MatRow, MatHeaderRowDef, MatRowDef, - MatSortHeader + MatSortHeader, + CdkDropList, + CdkDrag, + CdkDragPreview ], 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 { +export class BacklogItemTableComponent implements AfterViewInit, OnDestroy{ - constructor(private backlogItemService: BacklogItemService) { + constructor(private backlogItemService: BacklogItemService, + private backlogComponent: BacklogComponent) { } @Input() sprintId: number = 0; + @Input() sprintIds: string[] = []; dataToDisplay: BacklogItem[] = []; @@ -70,6 +86,7 @@ export class BacklogItemTableComponent implements AfterViewInit, OnDestroy { @ViewChild(MatSort) sort!: MatSort; @ViewChild(MatPaginator) paginator!: MatPaginator; + @ViewChild(MatTable) table!: MatTable; destroy$: Subject = new Subject(); @@ -113,12 +130,18 @@ export class BacklogItemTableComponent implements AfterViewInit, OnDestroy { fetchBacklogItems(): void { this.isLoading = true; let active: string = this.sort.active === 'type' ? 'itemType' : this.sort.active; - this.backlogItemService.getAllBySprintId( - this.sprintId, - this.paginator.pageIndex, - active, - this.sort.direction.toUpperCase()) - .pipe( + + 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; @@ -133,15 +156,33 @@ export class BacklogItemTableComponent implements AfterViewInit, OnDestroy { takeUntil(this.destroy$) ).subscribe(data => { this.dataToDisplay = data; + }) } - updateStatus(item: BacklogItem): void { + 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.component.html b/corn-frontend/src/app/pages/boards/backlog/backlog.component.html index d2b737d7..8c42da56 100644 --- a/corn-frontend/src/app/pages/boards/backlog/backlog.component.html +++ b/corn-frontend/src/app/pages/boards/backlog/backlog.component.html @@ -8,10 +8,15 @@ {{sprint.sprintStartDate}} - {{sprint.sprintEndDate}} - + +
+ +
+ + 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 e1921a10..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,110 +2,13 @@ $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; -} - -.cdk-drag-animating { - transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } \ No newline at end of file