From 784a7381033f42fd38011fdccaa86fb9fa8ecaa0 Mon Sep 17 00:00:00 2001 From: Miguel <141239860+Miguel7373@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:26:36 +0100 Subject: [PATCH] Feature/863 Objektive Abschluss Kommentar anzeigen (#1273) * add backend request for completed with unit tests * display the completed comment in the frontend * update import * backend formatting changes * remove 404 in the completed get request * apply backend formatter * inclued test for the completed comment * Fix naming of endpoint and the corresponding documentation and add better spacing to frontend * Rename service with typo, fix naming of e2e tests and change some logic in the frontend * Try to generalize cypress testing * Fix e2e tests by using correct state for selecting the objectives * Finish abstraction of completeDialog in e2e testing * add unit test for completed * add right role check for get of completed * fix unit test to use right state * fix completed is undefined error by not loading in always * fix takeUntilDestroy * change state enum to no longer hold svg * change backend and add tests for completed services * formatting changes and remove useless methode * change name of completed service file * move exception throwing to business service to prevent error while deleting objectives * formatt backend * update backend tests for get completed logic * formatt backend changes * remove empty test * fix logic so title of completed comment does not get shown if there is no comment written --------- Co-authored-by: Manuel --- .../okr/controller/CompletedController.java | 10 ++++ .../okr/repository/CompletedRepository.java | 3 +- .../CompletedAuthorizationService.java | 6 ++ .../business/CompletedBusinessService.java | 17 ++++++ .../CompletedPersistenceService.java | 2 +- .../okr/controller/CompletedControllerIT.java | 21 +++++++ .../CompletedAuthorizationServiceTest.java | 16 +++++ .../CompletedBusinessServiceTest.java | 27 ++++++++- .../CompletedPersistenceServiceIT.java | 10 +--- frontend/cypress/e2e/objective.cy.ts | 59 +++++-------------- .../dom-helper/dialogs/completeDialog.ts | 25 ++++++++ .../helper/dom-helper/pages/overviewPage.ts | 22 +++++++ .../objective-detail.component.html | 13 +++- .../objective-detail.component.spec.ts | 31 +++++++++- .../objective-detail.component.ts | 27 ++++++++- .../objective-menu-after-actions.spec.ts | 2 +- .../objective/objective-menu-after-actions.ts | 2 +- .../objective/objective.component.html | 2 +- .../objective/objective.component.spec.ts | 2 +- .../objective/objective.component.ts | 4 +- .../app/services/completed.service.spec.ts | 2 +- ...mpleted.servce.ts => completed.service.ts} | 4 ++ .../objective-menu-actions.service.ts | 2 +- frontend/src/app/shared/common.ts | 15 +++++ frontend/src/app/shared/test-data.ts | 35 +++++++---- frontend/src/app/shared/types/enums/state.ts | 9 +-- 26 files changed, 286 insertions(+), 82 deletions(-) create mode 100644 frontend/cypress/support/helper/dom-helper/dialogs/completeDialog.ts rename frontend/src/app/services/{completed.servce.ts => completed.service.ts} (80%) diff --git a/backend/src/main/java/ch/puzzle/okr/controller/CompletedController.java b/backend/src/main/java/ch/puzzle/okr/controller/CompletedController.java index b7cbfcaeb9..78517981f1 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/CompletedController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/CompletedController.java @@ -47,4 +47,14 @@ public ResponseEntity createCompleted(@RequestBody CompletedDto co public void deleteCompletedByObjectiveId(@PathVariable long objectiveId) { completedAuthorizationService.deleteCompletedByObjectiveId(objectiveId); } + + @Operation(summary = "Get Completed by Objective Id", description = "Get Completed from one Objective by objectiveId.") + @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Returned Completed by Objective Id"), + @ApiResponse(responseCode = "401", description = "Not authorized to get Completed Reference", content = @Content), + @ApiResponse(responseCode = "404", description = "Did not find the Completed with requested Objective id") }) + @GetMapping("/{objectiveId}") + public CompletedDto getCompletedByObjectiveId(@PathVariable long objectiveId) { + Completed completedByObjectiveId = completedAuthorizationService.getCompletedByObjectiveId(objectiveId); + return completedMapper.toDto(completedByObjectiveId); + } } diff --git a/backend/src/main/java/ch/puzzle/okr/repository/CompletedRepository.java b/backend/src/main/java/ch/puzzle/okr/repository/CompletedRepository.java index 007f61c3b4..d690ce21b5 100644 --- a/backend/src/main/java/ch/puzzle/okr/repository/CompletedRepository.java +++ b/backend/src/main/java/ch/puzzle/okr/repository/CompletedRepository.java @@ -1,8 +1,9 @@ package ch.puzzle.okr.repository; import ch.puzzle.okr.models.Completed; +import java.util.Optional; import org.springframework.data.repository.CrudRepository; public interface CompletedRepository extends CrudRepository { - Completed findByObjectiveId(Long objectiveId); + Optional findByObjectiveId(Long objectiveId); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/authorization/CompletedAuthorizationService.java b/backend/src/main/java/ch/puzzle/okr/service/authorization/CompletedAuthorizationService.java index 3ea9524532..63cd226752 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/authorization/CompletedAuthorizationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/authorization/CompletedAuthorizationService.java @@ -27,4 +27,10 @@ public void deleteCompletedByObjectiveId(Long objectiveId) { authorizationService.hasRoleDeleteByObjectiveId(objectiveId, authorizationUser); completedBusinessService.deleteCompletedByObjectiveId(objectiveId); } + + public Completed getCompletedByObjectiveId(Long objectiveId) { + AuthorizationUser authorizationUser = authorizationService.updateOrAddAuthorizationUser(); + authorizationService.hasRoleReadByObjectiveId(objectiveId, authorizationUser); + return completedBusinessService.getCompletedByObjectiveId(objectiveId); + } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/CompletedBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/CompletedBusinessService.java index 876a8b8855..3275e7c42d 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/CompletedBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/CompletedBusinessService.java @@ -1,9 +1,14 @@ package ch.puzzle.okr.service.business; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +import ch.puzzle.okr.ErrorKey; +import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.Completed; import ch.puzzle.okr.service.persistence.CompletedPersistenceService; import ch.puzzle.okr.service.validation.CompletedValidationService; import jakarta.transaction.Transactional; +import java.util.List; import org.springframework.stereotype.Service; @Service @@ -31,4 +36,16 @@ public void deleteCompletedByObjectiveId(Long objectiveId) { completedPersistenceService.deleteById(completed.getId()); } } + + public Completed getCompletedByObjectiveId(Long objectiveId) { + Completed completed = completedPersistenceService.getCompletedByObjectiveId(objectiveId); + // Must exist in business service in order to prevent error while deleting + // ongoing objectives + if (completed == null) { + throw new OkrResponseStatusException(NOT_FOUND, + ErrorKey.MODEL_WITH_ID_NOT_FOUND, + List.of(completedPersistenceService.getModelName(), objectiveId)); + } + return completed; + } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/CompletedPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/CompletedPersistenceService.java index 4741bde78e..7ee9c0999c 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/CompletedPersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/CompletedPersistenceService.java @@ -19,6 +19,6 @@ public String getModelName() { } public Completed getCompletedByObjectiveId(Long objectiveId) { - return getRepository().findByObjectiveId(objectiveId); + return getRepository().findByObjectiveId(objectiveId).orElse(null); } } diff --git a/backend/src/test/java/ch/puzzle/okr/controller/CompletedControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/CompletedControllerIT.java index be397fcfd3..184297a9a5 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/CompletedControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/CompletedControllerIT.java @@ -4,6 +4,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doThrow; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -122,4 +123,24 @@ void deleteShouldThrowExceptionWhenCompletedWithIdCantBeFound() throws Exception .perform(delete("/api/v2/completed/1000").with(SecurityMockMvcRequestPostProcessors.csrf())) .andExpect(MockMvcResultMatchers.status().isNotFound()); } + + @DisplayName("get() should get Completed by Objective id") + @Test + void shouldGetMetricCompletedWithId() throws Exception { + mvc + .perform(get("/api/v2/completed/1").with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(MockMvcResultMatchers.status().isOk()); + } + + @DisplayName("get() should throw exception when Completed with id cant be found") + @Test + void getShouldThrowExceptionWhenCompletedWithIdCantBeFound() throws Exception { + doThrow(new ResponseStatusException(HttpStatus.NOT_FOUND, "Completed not found")) + .when(completedAuthorizationService) + .getCompletedByObjectiveId(anyLong()); + + mvc + .perform(get("/api/v2/completed/1000").with(SecurityMockMvcRequestPostProcessors.csrf())) + .andExpect(MockMvcResultMatchers.status().isNotFound()); + } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/CompletedAuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/CompletedAuthorizationServiceTest.java index dda8d7b34f..acb5880b52 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/CompletedAuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/CompletedAuthorizationServiceTest.java @@ -85,4 +85,20 @@ void shouldThrowExceptionWhenNotAuthorizedToDeleteCompletedByObjectiveId() { assertEquals(UNAUTHORIZED, exception.getStatusCode()); assertEquals(reason, exception.getReason()); } + + @DisplayName("Should throw an exception when the user is not authorized to get completed object by objective ID") + @Test + void shouldThrowExceptionWhenNotAuthorizedToGetCompletedByObjectiveId() { + String reason = "junit test reason"; + when(authorizationService.updateOrAddAuthorizationUser()).thenReturn(authorizationUser); + doThrow(new ResponseStatusException(HttpStatus.UNAUTHORIZED, reason)) + .when(authorizationService) + .hasRoleReadByObjectiveId(objectiveId, authorizationUser); + + ResponseStatusException exception = assertThrows(ResponseStatusException.class, + () -> completedAuthorizationService + .getCompletedByObjectiveId(objectiveId)); + assertEquals(UNAUTHORIZED, exception.getStatusCode()); + assertEquals(reason, exception.getReason()); + } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/CompletedBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/CompletedBusinessServiceTest.java index 9785e53507..1cd442d824 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/CompletedBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/CompletedBusinessServiceTest.java @@ -1,9 +1,10 @@ package ch.puzzle.okr.service.business; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.Completed; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.service.persistence.CompletedPersistenceService; @@ -84,7 +85,7 @@ void shouldDeleteKeyResultAndAssociatedCheckIns() { verify(this.completedPersistenceService, times(1)).deleteById(1L); } - @DisplayName("Should do nothing if completed is null") + @DisplayName("Should do nothing if completed to delete is null") @Test void shouldDoNothingIfCompletedIsNull() { when(completedPersistenceService.getCompletedByObjectiveId(anyLong())).thenReturn(null); @@ -93,4 +94,26 @@ void shouldDoNothingIfCompletedIsNull() { verify(validator, never()).validateOnDelete(anyLong()); } + + @DisplayName("Should get completed by objective id") + @Test + void shouldGetCompleted() { + when(completedPersistenceService.getCompletedByObjectiveId(anyLong())).thenReturn(successfulCompleted); + + this.completedBusinessService.getCompletedByObjectiveId(1L); + + verify(this.completedPersistenceService, times(1)).getCompletedByObjectiveId(1L); + } + + @DisplayName("Should throw exception if completed is null") + @Test + void shouldThrowExceptionIfCompletedIsNull() { + when(completedPersistenceService.getCompletedByObjectiveId(-1L)).thenReturn(null); + when(completedPersistenceService.getModelName()).thenCallRealMethod(); + + assertThrows(OkrResponseStatusException.class, () -> completedBusinessService.getCompletedByObjectiveId(-1L)); + + verify(this.completedPersistenceService, times(1)).getCompletedByObjectiveId(-1L); + } + } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/CompletedPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/CompletedPersistenceServiceIT.java index 4d661ab2d0..a641d5b399 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/CompletedPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/CompletedPersistenceServiceIT.java @@ -137,21 +137,15 @@ void deleteByIdShouldDeleteExistingCompletedByObjectiveId() { @DisplayName("Should throw exception on findById() when id does not exist") @Test void deleteCompletedShouldThrowExceptionWhenCompletedNotFound() { - long noExistentId = getNonExistentId(); OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> completedPersistenceService.findById(noExistentId)); + () -> completedPersistenceService.findById(-1L)); List expectedErrors = List - .of(new ErrorDto("MODEL_WITH_ID_NOT_FOUND", List.of(COMPLETED, String.valueOf(noExistentId)))); + .of(new ErrorDto("MODEL_WITH_ID_NOT_FOUND", List.of(COMPLETED, String.valueOf(-1)))); assertEquals(NOT_FOUND, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); } - - private long getNonExistentId() { - long id = completedPersistenceService.findAll().stream().mapToLong(Completed::getId).max().orElse(10L); - return id + 1; - } } diff --git a/frontend/cypress/e2e/objective.cy.ts b/frontend/cypress/e2e/objective.cy.ts index 45c56f8e18..a811370601 100644 --- a/frontend/cypress/e2e/objective.cy.ts +++ b/frontend/cypress/e2e/objective.cy.ts @@ -31,57 +31,30 @@ describe('okr objective', () => { .should('exist'); }); - it('should complete objective with successful', () => { - overviewPage.addObjective() - .fillObjectiveTitle('We want to complete this successful') + it('should complete objective as successful and write successful closing comment', () => { + const title = 'This objective should be successful'; + const comment = 'This objective has been successfully completed. Good work'; + overviewPage.completeObjective(title) + .completeAs(true) + .writeClosingComment(comment) .submit(); - overviewPage - .getObjectiveByNameAndState('We want to complete this successful', 'ongoing') - .findByTestId('three-dot-menu') - .click(); - - overviewPage.selectFromThreeDotMenu('Objective abschliessen'); - - cy.contains('Bewertung'); - cy.contains('Objective erreicht'); - cy.contains('Objective nicht erreicht'); - cy.contains('Kommentar (optional)'); - cy.contains('Objective abschliessen'); - cy.contains('Abbrechen'); - - cy.getByTestId('successful') - .click(); - cy.getByTestId('submit') + overviewPage.getObjectiveByNameAndState(title, 'successful') .click(); - - overviewPage.getObjectiveByNameAndState('We want to complete this successful', 'successful'); + cy.contains(comment); }); - it('should complete objective with not-successful', () => { - overviewPage.addObjective() - .fillObjectiveTitle('A not successful objective') + it('should complete objective as not-successful and write unsuccessful closing comment', () => { + const title = 'This objective should NOT be successful'; + const comment = 'This objective has not been completed successfully. We need to work on this'; + overviewPage.completeObjective(title) + .completeAs(false) + .writeClosingComment(comment) .submit(); - overviewPage - .getObjectiveByNameAndState('A not successful objective', 'ongoing') - .findByTestId('three-dot-menu') + overviewPage.getObjectiveByNameAndState(title, 'not-successful') .click(); - overviewPage.selectFromThreeDotMenu('Objective abschliessen'); - - cy.contains('Bewertung'); - cy.contains('Objective erreicht'); - cy.contains('Objective nicht erreicht'); - cy.contains('Kommentar (optional)'); - cy.contains('Objective abschliessen'); - cy.contains('Abbrechen'); - - cy.getByTestId('not-successful') - .click(); - cy.getByTestId('submit') - .click(); - - overviewPage.getObjectiveByNameAndState('A not successful objective', 'not-successful'); + cy.contains(comment); }); it('should reopen successful objective', () => { diff --git a/frontend/cypress/support/helper/dom-helper/dialogs/completeDialog.ts b/frontend/cypress/support/helper/dom-helper/dialogs/completeDialog.ts new file mode 100644 index 0000000000..b7ba2b1fd5 --- /dev/null +++ b/frontend/cypress/support/helper/dom-helper/dialogs/completeDialog.ts @@ -0,0 +1,25 @@ +import Dialog from './dialog'; + +export default class CompleteDialog extends Dialog { + override submit() { + cy.getByTestId('submit') + .click(); + } + + completeAs(isSuccessful: boolean) { + isSuccessful ? cy.getByTestId('successful') + .click() : cy.getByTestId('not-successful') + .click(); + return this; + } + + writeClosingComment(comment: string) { + cy.getByTestId('completeComment') + .type(comment); + return this; + } + + getPage(): Cypress.Chainable { + return cy.get('app-complete-dialog'); + } +} diff --git a/frontend/cypress/support/helper/dom-helper/pages/overviewPage.ts b/frontend/cypress/support/helper/dom-helper/pages/overviewPage.ts index 68a806b995..dbb9fcc6f4 100644 --- a/frontend/cypress/support/helper/dom-helper/pages/overviewPage.ts +++ b/frontend/cypress/support/helper/dom-helper/pages/overviewPage.ts @@ -3,6 +3,7 @@ import ObjectiveDialog from '../dialogs/objectiveDialog'; import { Page } from './page'; import KeyResultDialog from '../dialogs/keyResultDialog'; import { filterByKeyResultName, getKeyResults } from '../../keyResultHelper'; +import CompleteDialog from '../dialogs/completeDialog'; export default class CyOverviewPage extends Page { elements = { @@ -187,6 +188,27 @@ export default class CyOverviewPage extends Page { return new ObjectiveDialog(); } + completeObjective(title: string) { + this.addObjective() + .fillObjectiveTitle(title) + .submit(); + + this + .getObjectiveByNameAndState(title, 'ongoing') + .findByTestId('three-dot-menu') + .click(); + this.selectFromThreeDotMenu('Objective abschliessen'); + + cy.contains('Bewertung'); + cy.contains('Objective erreicht'); + cy.contains('Objective nicht erreicht'); + cy.contains('Kommentar (optional)'); + cy.contains('Objective abschliessen'); + cy.contains('Abbrechen'); + + return new CompleteDialog(); + } + visitTeamManagement(): void { this.elements.teamManagement() .click(); diff --git a/frontend/src/app/components/objective-detail/objective-detail.component.html b/frontend/src/app/components/objective-detail/objective-detail.component.html index 55ac578e19..80d4f13194 100644 --- a/frontend/src/app/components/objective-detail/objective-detail.component.html +++ b/frontend/src/app/components/objective-detail/objective-detail.component.html @@ -19,7 +19,7 @@

{{ objective.title }}

Beschrieb

-
+

-

Beschrieb >

{{ objective.description }}

- -
+
+ +
+

Abschlusskommentar

+
+

{{ completed.comment }}

+
+
+
diff --git a/frontend/src/app/components/objective-detail/objective-detail.component.spec.ts b/frontend/src/app/components/objective-detail/objective-detail.component.spec.ts index 7e16517083..6dc2027621 100644 --- a/frontend/src/app/components/objective-detail/objective-detail.component.spec.ts +++ b/frontend/src/app/components/objective-detail/objective-detail.component.spec.ts @@ -4,17 +4,27 @@ import { ObjectiveDetailComponent } from './objective-detail.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { By } from '@angular/platform-browser'; import { ObjectiveService } from '../../services/objective.service'; -import { objective, objectiveWriteableFalse } from '../../shared/test-data'; +import { + completed, + notCompleted, + objective, + objectiveWriteableFalse +} from '../../shared/test-data'; import { of } from 'rxjs'; import { MatDialogModule } from '@angular/material/dialog'; import { ActivatedRoute } from '@angular/router'; import { MatIconModule } from '@angular/material/icon'; import { TranslateModule } from '@ngx-translate/core'; +import { CompletedService } from '../../services/completed.service'; const objectiveService = { getFullObjective: jest.fn() }; +const completedService = { + getCompleted: jest.fn() +}; + const activatedRouteMock = { snapshot: { paramMap: { @@ -37,6 +47,8 @@ describe('ObjectiveDetailComponent', () => { ], providers: [{ provide: ObjectiveService, useValue: objectiveService }, + { provide: CompletedService, + useValue: completedService }, { provide: ActivatedRoute, useValue: activatedRouteMock }], declarations: [ObjectiveDetailComponent] @@ -46,6 +58,7 @@ describe('ObjectiveDetailComponent', () => { fixture = TestBed.createComponent(ObjectiveDetailComponent); component = fixture.componentInstance; objectiveService.getFullObjective.mockReturnValue(of(objective)); + completedService.getCompleted.mockReturnValue(of(completed)); activatedRouteMock.snapshot.paramMap.get = jest.fn(); activatedRouteMock.snapshot.paramMap.get.mockReturnValue(of(1)); }); @@ -86,4 +99,20 @@ describe('ObjectiveDetailComponent', () => { expect(button) .toBeFalsy(); }); + + it('should not display Completed comment if objective is completed', async() => { + completedService.getCompleted.mockReturnValue(of(notCompleted)); + fixture.detectChanges(); + const completedComment = fixture.debugElement.query(By.css('[data-testId="completed-comment"]'))?.nativeElement.innerHTML; + expect(completedComment) + .toBeFalsy(); + }); + + it('should display Completed comment if objective is completed', () => { + objectiveService.getFullObjective.mockReturnValue(of(objectiveWriteableFalse)); + fixture.detectChanges(); + const completedComment = fixture.debugElement.query(By.css('[data-testId="completed-comment"]'))?.nativeElement.innerHTML; + expect(completedComment) + .toContain('Abschlusskommentar'); + }); }); diff --git a/frontend/src/app/components/objective-detail/objective-detail.component.ts b/frontend/src/app/components/objective-detail/objective-detail.component.ts index f18946f7e6..c5055db18d 100644 --- a/frontend/src/app/components/objective-detail/objective-detail.component.ts +++ b/frontend/src/app/components/objective-detail/objective-detail.component.ts @@ -1,12 +1,16 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Objective } from '../../shared/types/model/objective'; import { ObjectiveService } from '../../services/objective.service'; -import { BehaviorSubject, catchError, EMPTY } from 'rxjs'; +import { BehaviorSubject, catchError, EMPTY, Observable } from 'rxjs'; import { RefreshDataService } from '../../services/refresh-data.service'; import { KeyResultDialogComponent } from '../key-result-dialog/key-result-dialog.component'; import { ObjectiveFormComponent } from '../../shared/dialog/objective-dialog/objective-form.component'; import { ActivatedRoute, Router } from '@angular/router'; import { DialogService } from '../../services/dialog.service'; +import { CompletedService } from '../../services/completed.service'; +import { Completed } from '../../shared/types/model/completed'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { State } from '../../shared/types/enums/state'; @Component({ selector: 'app-objective-detail', @@ -18,15 +22,26 @@ import { DialogService } from '../../services/dialog.service'; export class ObjectiveDetailComponent implements OnInit { objectiveId!: number; + completed = new Observable(); + objective$: BehaviorSubject = new BehaviorSubject({} as Objective); constructor( private objectiveService: ObjectiveService, + private completedService: CompletedService, private dialogService: DialogService, private refreshDataService: RefreshDataService, private router: Router, private route: ActivatedRoute - ) {} + ) { + this.objective$ + .pipe(takeUntilDestroyed()) + .subscribe((objective) => { + if (objective.state === State.NOTSUCCESSFUL || objective.state === State.SUCCESSFUL) { + this.loadCompleted(this.objectiveId); + } + }); + } ngOnInit(): void { this.objectiveId = this.getIdFromParams(); @@ -48,6 +63,12 @@ export class ObjectiveDetailComponent implements OnInit { .subscribe((objective) => this.objective$.next(objective)); } + loadCompleted(id: number): void { + this.completed = this.completedService + .getCompleted(id) + .pipe(catchError(() => EMPTY)); + } + openAddKeyResultDialog() { this.dialogService .open(KeyResultDialogComponent, { @@ -89,4 +110,6 @@ export class ObjectiveDetailComponent implements OnInit { backToOverview() { this.router.navigate(['']); } + + protected readonly State = State; } diff --git a/frontend/src/app/components/objective/objective-menu-after-actions.spec.ts b/frontend/src/app/components/objective/objective-menu-after-actions.spec.ts index 1cb90509c3..2f5c6c395c 100644 --- a/frontend/src/app/components/objective/objective-menu-after-actions.spec.ts +++ b/frontend/src/app/components/objective/objective-menu-after-actions.spec.ts @@ -1,7 +1,6 @@ import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { ObjectiveService } from '../../services/objective.service'; -import { CompletedService } from '../../services/completed.servce'; import { RefreshDataService } from '../../services/refresh-data.service'; import { ObjectiveMenuAfterActions } from './objective-menu-after-actions'; import { objective, objectiveMin } from '../../shared/test-data'; @@ -10,6 +9,7 @@ import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { State } from '../../shared/types/enums/state'; import { Completed } from '../../shared/types/model/completed'; +import { CompletedService } from '../../services/completed.service'; describe('ObjectiveMenuAfterActions', () => { let objectiveMenuAfterActions: ObjectiveMenuAfterActions; diff --git a/frontend/src/app/components/objective/objective-menu-after-actions.ts b/frontend/src/app/components/objective/objective-menu-after-actions.ts index 2f517fbea3..25ee6c942e 100644 --- a/frontend/src/app/components/objective/objective-menu-after-actions.ts +++ b/frontend/src/app/components/objective/objective-menu-after-actions.ts @@ -4,7 +4,7 @@ import { Completed } from '../../shared/types/model/completed'; import { ObjectiveService } from '../../services/objective.service'; import { RefreshDataService } from '../../services/refresh-data.service'; import { ObjectiveMin } from '../../shared/types/model/objective-min'; -import { CompletedService } from '../../services/completed.servce'; +import { CompletedService } from '../../services/completed.service'; export class ObjectiveMenuAfterActions { constructor(private readonly objectiveService: ObjectiveService, diff --git a/frontend/src/app/components/objective/objective.component.html b/frontend/src/app/components/objective/objective.component.html index 74518045ee..a1a8fad036 100644 --- a/frontend/src/app/components/objective/objective.component.html +++ b/frontend/src/app/components/objective/objective.component.html @@ -12,7 +12,7 @@ The objectives state State[key as keyof typeof State] === value) ?? ''; } + + protected readonly getSvgForState = getSvgForState; } diff --git a/frontend/src/app/services/completed.service.spec.ts b/frontend/src/app/services/completed.service.spec.ts index 995a6f8546..d53429ef7c 100644 --- a/frontend/src/app/services/completed.service.spec.ts +++ b/frontend/src/app/services/completed.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { CompletedService } from './completed.servce'; +import { CompletedService } from './completed.service'; describe('CompletedService', () => { let service: CompletedService; diff --git a/frontend/src/app/services/completed.servce.ts b/frontend/src/app/services/completed.service.ts similarity index 80% rename from frontend/src/app/services/completed.servce.ts rename to frontend/src/app/services/completed.service.ts index bd8c3409f0..6af2c978fb 100644 --- a/frontend/src/app/services/completed.servce.ts +++ b/frontend/src/app/services/completed.service.ts @@ -16,4 +16,8 @@ export class CompletedService { deleteCompleted(objectiveId: number): Observable { return this.httpClient.delete('/api/v2/completed/' + objectiveId); } + + getCompleted(objectiveId: number): Observable { + return this.httpClient.get('/api/v2/completed/' + objectiveId); + } } diff --git a/frontend/src/app/services/objective-menu-actions.service.ts b/frontend/src/app/services/objective-menu-actions.service.ts index bef128ab22..a86eb9651f 100644 --- a/frontend/src/app/services/objective-menu-actions.service.ts +++ b/frontend/src/app/services/objective-menu-actions.service.ts @@ -8,7 +8,7 @@ import { ObjectiveService } from './objective.service'; import { RefreshDataService } from './refresh-data.service'; import { ObjectiveMenuActions } from '../components/objective/objective-menu-actions'; import { GJ_REGEX_PATTERN } from '../shared/constant-library'; -import { CompletedService } from './completed.servce'; +import { CompletedService } from './completed.service'; export type ObjectiveMenuAction = () => MatDialogRef; export type ObjectiveMenuAfterAction = (objective: ObjectiveMin, dialogResult: any) => any; diff --git a/frontend/src/app/shared/common.ts b/frontend/src/app/shared/common.ts index c31af01f17..f5561e48d7 100644 --- a/frontend/src/app/shared/common.ts +++ b/frontend/src/app/shared/common.ts @@ -1,5 +1,6 @@ import { FormGroup } from '@angular/forms'; import { KeyResultMetricMin } from './types/model/key-result-metric-min'; +import { State } from './types/enums/state'; export function getNumberOrNull(str: string | null | undefined): number | null { if (str === null || str === undefined || str.toString() @@ -120,3 +121,17 @@ export function hasFormFieldErrors(formGroup: FormGroup, field: string) { return false; } } + +export function getSvgForState(objectiveState: State) { + const svgMapping = new Map([ + ['ONGOING', + 'ongoing-icon.svg'], + ['NOTSUCCESSFUL', + 'not-successful-icon.svg'], + ['SUCCESSFUL', + 'successful-icon.svg'], + ['DRAFT', + 'draft-icon.svg'] + ]); + return svgMapping.get(objectiveState); +} diff --git a/frontend/src/app/shared/test-data.ts b/frontend/src/app/shared/test-data.ts index dd29496f47..0d591dcb94 100644 --- a/frontend/src/app/shared/test-data.ts +++ b/frontend/src/app/shared/test-data.ts @@ -7,7 +7,6 @@ import { OverviewEntity } from './types/model/overview-entity'; import { KeyResultObjective } from './types/model/key-result-objective'; import { Quarter } from './types/model/quarter'; import { KeyResultOrdinal } from './types/model/key-result-ordinal'; -import { Objective } from './types/model/objective'; import { User } from './types/model/user'; import { KeyResultMetric } from './types/model/key-result-metric'; import { Unit } from './types/enums/unit'; @@ -17,6 +16,8 @@ import { CheckInOrdinal } from './types/model/check-in-ordinal'; import { CheckInMetric } from './types/model/check-in-metric'; import { CheckInOrdinalMin } from './types/model/check-in-ordinal-min'; import { CheckInMetricMin } from './types/model/check-in-metric-min'; +import { Completed } from './types/model/completed'; +import { Objective } from './types/model/objective'; export const teamFormObject = { name: 'newTeamName' @@ -245,41 +246,41 @@ export const objectiveMin: ObjectiveMin = { keyResultOrdinalMin] as KeyResultMin[] } as ObjectiveMin; -export const objectiveResponse1: any = { +export const objectiveResponse1: ObjectiveMin = { id: 101, version: 1, title: 'Increase Environment Engagement', - state: 'ONGOING', + state: State.ONGOING, quarter: quarterMin, keyResults: [keyResultMetricMin, keyResultOrdinalMin] as KeyResultMin[] }; -export const objectiveResponse2: any = { +export const objectiveResponse2: ObjectiveMin = { id: 102, version: 1, title: 'Increase Social Engagement', - state: 'DRAFT', + state: State.DRAFT, quarter: quarterMin, keyResults: [keyResultMetricMin, keyResultOrdinalMin] as KeyResultMin[] }; -export const objectiveResponse3: any = { +export const objectiveResponse3: ObjectiveMin = { id: 103, version: 1, title: 'Increase Member Engagement', - state: 'NOTSUCCESSFUL', + state: State.NOTSUCCESSFUL, quarter: quarterMin, keyResults: [keyResultMetricMin, keyResultOrdinalMin] as KeyResultMin[] }; -export const objectiveResponse4: any = { +export const objectiveResponse4: ObjectiveMin = { id: 104, version: 1, title: 'Increase Company Engagement', - state: 'SUCCESSFUL', + state: State.SUCCESSFUL, quarter: quarterMin, keyResults: [keyResultMetricMin, keyResultOrdinalMin] as KeyResultMin[] @@ -347,7 +348,7 @@ export const objective: Objective = { teamId: 2, quarterId: 2, quarterLabel: 'GJ 22/23-Q2', - state: State.SUCCESSFUL, + state: State.ONGOING, isWriteable: true }; @@ -363,6 +364,20 @@ export const objectiveWriteableFalse: Objective = { isWriteable: false }; +export const completed: Completed = { + id: 1, + version: 1, + objective: objective, + comment: 'This is Completed' +}; + +export const notCompleted: Completed = { + id: 1, + version: 1, + objective: objective, + comment: null +}; + export const firstCheckIn: CheckInMetricMin = { id: 1, version: 1, diff --git a/frontend/src/app/shared/types/enums/state.ts b/frontend/src/app/shared/types/enums/state.ts index 30de3e8ba6..c57f657def 100644 --- a/frontend/src/app/shared/types/enums/state.ts +++ b/frontend/src/app/shared/types/enums/state.ts @@ -1,6 +1,7 @@ export enum State { - ONGOING = 'ongoing-icon.svg', - NOTSUCCESSFUL = 'not-successful-icon.svg', - SUCCESSFUL = 'successful-icon.svg', - DRAFT = 'draft-icon.svg' + ONGOING = 'ONGOING', + NOTSUCCESSFUL = 'NOTSUCCESSFUL', + SUCCESSFUL = 'SUCCESSFUL', + DRAFT = 'DRAFT' } +