From 06c57516099cdd01c95e4150b6759d08a6b7c488 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 12 Feb 2024 09:20:33 +0100 Subject: [PATCH 001/119] Finish backend implementation --- .../src/main/java/ch/puzzle/okr/ErrorKey.java | 2 +- .../okr/controller/ObjectiveController.java | 16 +++ .../okr/dto/ObjectiveAlignmentsDto.java | 9 ++ .../java/ch/puzzle/okr/dto/ObjectiveDto.java | 3 +- .../dto/keyresult/KeyResultAlignmentsDto.java | 4 + .../ch/puzzle/okr/mapper/ObjectiveMapper.java | 5 +- .../java/ch/puzzle/okr/models/Objective.java | 18 ++- .../okr/models/alignment/Alignment.java | 4 + .../okr/repository/AlignmentRepository.java | 2 +- .../okr/repository/ObjectiveRepository.java | 2 + .../ObjectiveAuthorizationService.java | 7 ++ .../business/AlignmentBusinessService.java | 112 ++++++++++++++++++ .../business/KeyResultBusinessService.java | 5 +- .../business/ObjectiveBusinessService.java | 46 ++++++- .../AlignmentPersistenceService.java | 13 +- .../ObjectivePersistenceService.java | 4 + .../AlignmentValidationService.java | 79 ++++++++++++ .../okr/controller/ObjectiveControllerIT.java | 10 +- .../AlignmentPersistenceServiceIT.java | 8 +- .../types/model/AlignmentPossibility.ts | 8 ++ 20 files changed, 336 insertions(+), 21 deletions(-) create mode 100644 backend/src/main/java/ch/puzzle/okr/dto/ObjectiveAlignmentsDto.java create mode 100644 backend/src/main/java/ch/puzzle/okr/dto/keyresult/KeyResultAlignmentsDto.java create mode 100644 backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java create mode 100644 backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java create mode 100644 frontend/src/app/shared/types/model/AlignmentPossibility.ts diff --git a/backend/src/main/java/ch/puzzle/okr/ErrorKey.java b/backend/src/main/java/ch/puzzle/okr/ErrorKey.java index 415a76da20..b9fdf4c89a 100644 --- a/backend/src/main/java/ch/puzzle/okr/ErrorKey.java +++ b/backend/src/main/java/ch/puzzle/okr/ErrorKey.java @@ -4,5 +4,5 @@ public enum ErrorKey { ATTRIBUTE_NULL, ATTRIBUTE_CHANGED, ATTRIBUTE_SET_FORBIDDEN, ATTRIBUTE_NOT_SET, ATTRIBUTE_CANNOT_CHANGE, ATTRIBUTE_MUST_BE_DRAFT, KEY_RESULT_CONVERSION, ALREADY_EXISTS_SAME_NAME, CONVERT_TOKEN, DATA_HAS_BEEN_UPDATED, MODEL_NULL, MODEL_WITH_ID_NOT_FOUND, NOT_AUTHORIZED_TO_READ, NOT_AUTHORIZED_TO_WRITE, NOT_AUTHORIZED_TO_DELETE, - TOKEN_NULL + TOKEN_NULL, NOT_LINK_YOURSELF, ALIGNMENT_ALREADY_EXISTS } diff --git a/backend/src/main/java/ch/puzzle/okr/controller/ObjectiveController.java b/backend/src/main/java/ch/puzzle/okr/controller/ObjectiveController.java index 61e0f2cab8..f608d87770 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/ObjectiveController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/ObjectiveController.java @@ -1,5 +1,6 @@ package ch.puzzle.okr.controller; +import ch.puzzle.okr.dto.ObjectiveAlignmentsDto; import ch.puzzle.okr.dto.ObjectiveDto; import ch.puzzle.okr.mapper.ObjectiveMapper; import ch.puzzle.okr.models.Objective; @@ -14,6 +15,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.List; + import static org.springframework.http.HttpStatus.IM_USED; import static org.springframework.http.HttpStatus.OK; @@ -42,6 +45,19 @@ public ResponseEntity getObjective( .body(objectiveMapper.toDto(objectiveAuthorizationService.getEntityById(id))); } + @Operation(summary = "Get Alignment possibilities", description = "Get all possibilities to create an Alignment") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Returned all Alignment possibilities for an Objective", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ObjectiveAlignmentsDto.class)) }), + @ApiResponse(responseCode = "401", description = "Not authorized to get Alignment possibilities", content = @Content), + @ApiResponse(responseCode = "404", description = "Did not find any possibilities to create an Alignment", content = @Content) }) + @GetMapping("/alignmentPossibilities/{quarterId}") + public ResponseEntity> getAlignmentPossibilities( + @Parameter(description = "The Quarter ID for getting Alignment possibilities.", required = true) @PathVariable Long quarterId) { + return ResponseEntity.status(HttpStatus.OK) + .body(objectiveAuthorizationService.getAlignmentPossibilities(quarterId)); + } + @Operation(summary = "Delete Objective by ID", description = "Delete Objective by ID") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Deleted Objective by ID"), @ApiResponse(responseCode = "401", description = "Not authorized to delete an Objective", content = @Content), diff --git a/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveAlignmentsDto.java b/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveAlignmentsDto.java new file mode 100644 index 0000000000..7c67d85a6a --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveAlignmentsDto.java @@ -0,0 +1,9 @@ +package ch.puzzle.okr.dto; + +import ch.puzzle.okr.dto.keyresult.KeyResultAlignmentsDto; + +import java.util.List; + +public record ObjectiveAlignmentsDto(Long objectiveId, String objectiveTitle, + List keyResultAlignmentsDtos) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveDto.java b/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveDto.java index bf230fdcc8..559ab6336e 100644 --- a/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveDto.java +++ b/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveDto.java @@ -5,5 +5,6 @@ import java.time.LocalDateTime; public record ObjectiveDto(Long id, int version, String title, Long teamId, Long quarterId, String quarterLabel, - String description, State state, LocalDateTime createdOn, LocalDateTime modifiedOn, boolean writeable) { + String description, State state, LocalDateTime createdOn, LocalDateTime modifiedOn, boolean writeable, + String alignedEntityId) { } diff --git a/backend/src/main/java/ch/puzzle/okr/dto/keyresult/KeyResultAlignmentsDto.java b/backend/src/main/java/ch/puzzle/okr/dto/keyresult/KeyResultAlignmentsDto.java new file mode 100644 index 0000000000..6d3980c702 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/keyresult/KeyResultAlignmentsDto.java @@ -0,0 +1,4 @@ +package ch.puzzle.okr.dto.keyresult; + +public record KeyResultAlignmentsDto(Long keyResultId, String keyResultTitle) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java index a65574c408..c72994f56c 100644 --- a/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java +++ b/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java @@ -23,7 +23,7 @@ public ObjectiveDto toDto(Objective objective) { return new ObjectiveDto(objective.getId(), objective.getVersion(), objective.getTitle(), objective.getTeam().getId(), objective.getQuarter().getId(), objective.getQuarter().getLabel(), objective.getDescription(), objective.getState(), objective.getCreatedOn(), objective.getModifiedOn(), - objective.isWriteable()); + objective.isWriteable(), objective.getAlignedEntityId()); } public Objective toObjective(ObjectiveDto objectiveDto) { @@ -31,6 +31,7 @@ public Objective toObjective(ObjectiveDto objectiveDto) { .withTitle(objectiveDto.title()).withTeam(teamBusinessService.getTeamById(objectiveDto.teamId())) .withDescription(objectiveDto.description()).withModifiedOn(LocalDateTime.now()) .withState(objectiveDto.state()).withCreatedOn(objectiveDto.createdOn()) - .withQuarter(quarterBusinessService.getQuarterById(objectiveDto.quarterId())).build(); + .withQuarter(quarterBusinessService.getQuarterById(objectiveDto.quarterId())) + .withAlignedEntityId(objectiveDto.alignedEntityId()).build(); } } diff --git a/backend/src/main/java/ch/puzzle/okr/models/Objective.java b/backend/src/main/java/ch/puzzle/okr/models/Objective.java index 85a96b59dd..1a174a73e4 100644 --- a/backend/src/main/java/ch/puzzle/okr/models/Objective.java +++ b/backend/src/main/java/ch/puzzle/okr/models/Objective.java @@ -52,6 +52,7 @@ public class Objective implements WriteableInterface { private User modifiedBy; private transient boolean writeable; + private transient String alignedEntityId; public Objective() { } @@ -68,6 +69,7 @@ private Objective(Builder builder) { setState(builder.state); setCreatedOn(builder.createdOn); setModifiedBy(builder.modifiedBy); + setAlignedEntityId(builder.alignedEntityId); } public Long getId() { @@ -160,12 +162,20 @@ public void setWriteable(boolean writeable) { this.writeable = writeable; } + public String getAlignedEntityId() { + return alignedEntityId; + } + + public void setAlignedEntityId(String alignedEntityId) { + this.alignedEntityId = alignedEntityId; + } + @Override public String toString() { return "Objective{" + "id=" + id + ", version=" + version + ", title='" + title + '\'' + ", createdBy=" + createdBy + ", team=" + team + ", quarter=" + quarter + ", description='" + description + '\'' + ", modifiedOn=" + modifiedOn + ", state=" + state + ", createdOn=" + createdOn + ", modifiedBy=" - + modifiedBy + ", writeable=" + writeable + '\'' + '}'; + + modifiedBy + ", writeable=" + writeable + ", alignedEntityId=" + alignedEntityId + '\'' + '}'; } @Override @@ -201,6 +211,7 @@ public static final class Builder { private State state; private LocalDateTime createdOn; private User modifiedBy; + private String alignedEntityId; private Builder() { } @@ -264,6 +275,11 @@ public Builder withModifiedBy(User modifiedBy) { return this; } + public Builder withAlignedEntityId(String alignedEntityId) { + this.alignedEntityId = alignedEntityId; + return this; + } + public Objective build() { return new Objective(this); } diff --git a/backend/src/main/java/ch/puzzle/okr/models/alignment/Alignment.java b/backend/src/main/java/ch/puzzle/okr/models/alignment/Alignment.java index cf3a64f7fc..9cacab5dfb 100644 --- a/backend/src/main/java/ch/puzzle/okr/models/alignment/Alignment.java +++ b/backend/src/main/java/ch/puzzle/okr/models/alignment/Alignment.java @@ -34,6 +34,10 @@ public Long getId() { return id; } + public void setId(Long id) { + this.id = id; + } + public int getVersion() { return version; } diff --git a/backend/src/main/java/ch/puzzle/okr/repository/AlignmentRepository.java b/backend/src/main/java/ch/puzzle/okr/repository/AlignmentRepository.java index d6ebcc70a7..16e8210932 100644 --- a/backend/src/main/java/ch/puzzle/okr/repository/AlignmentRepository.java +++ b/backend/src/main/java/ch/puzzle/okr/repository/AlignmentRepository.java @@ -11,7 +11,7 @@ public interface AlignmentRepository extends CrudRepository { - List findByAlignedObjectiveId(Long alignedObjectiveId); + Alignment findByAlignedObjectiveId(Long alignedObjectiveId); @Query(value = "from KeyResultAlignment where targetKeyResult.id = :keyResultId") List findByKeyResultAlignmentId(@Param("keyResultId") Long keyResultId); diff --git a/backend/src/main/java/ch/puzzle/okr/repository/ObjectiveRepository.java b/backend/src/main/java/ch/puzzle/okr/repository/ObjectiveRepository.java index 009b34ac96..ccbc0aaa8d 100644 --- a/backend/src/main/java/ch/puzzle/okr/repository/ObjectiveRepository.java +++ b/backend/src/main/java/ch/puzzle/okr/repository/ObjectiveRepository.java @@ -14,4 +14,6 @@ public interface ObjectiveRepository extends CrudRepository { Integer countByTeamAndQuarter(Team team, Quarter quarter); List findObjectivesByTeamId(Long id); + + List findObjectivesByQuarterId(Long id); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationService.java b/backend/src/main/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationService.java index bb213bf2d8..d5d7cdf105 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationService.java @@ -1,10 +1,13 @@ package ch.puzzle.okr.service.authorization; +import ch.puzzle.okr.dto.ObjectiveAlignmentsDto; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.service.business.ObjectiveBusinessService; import org.springframework.stereotype.Service; +import java.util.List; + @Service public class ObjectiveAuthorizationService extends AuthorizationServiceBase { @@ -19,6 +22,10 @@ public Objective duplicateEntity(Long id, Objective objective) { return getBusinessService().duplicateObjective(id, objective, authorizationUser); } + public List getAlignmentPossibilities(Long quarterId) { + return getBusinessService().getAlignmentPossibilities(quarterId); + } + @Override protected void hasRoleReadById(Long id, AuthorizationUser authorizationUser) { getAuthorizationService().hasRoleReadByObjectiveId(id, authorizationUser); diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java new file mode 100644 index 0000000000..7950a5b6ec --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -0,0 +1,112 @@ +package ch.puzzle.okr.service.business; + +import ch.puzzle.okr.ErrorKey; +import ch.puzzle.okr.exception.OkrResponseStatusException; +import ch.puzzle.okr.models.Objective; +import ch.puzzle.okr.models.alignment.Alignment; +import ch.puzzle.okr.models.alignment.KeyResultAlignment; +import ch.puzzle.okr.models.alignment.ObjectiveAlignment; +import ch.puzzle.okr.models.keyresult.KeyResult; +import ch.puzzle.okr.service.persistence.AlignmentPersistenceService; +import ch.puzzle.okr.service.persistence.KeyResultPersistenceService; +import ch.puzzle.okr.service.persistence.ObjectivePersistenceService; +import ch.puzzle.okr.service.validation.AlignmentValidationService; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class AlignmentBusinessService { + + private final AlignmentPersistenceService alignmentPersistenceService; + private final AlignmentValidationService alignmentValidationService; + private final ObjectivePersistenceService objectivePersistenceService; + private final KeyResultPersistenceService keyResultPersistenceService; + + public AlignmentBusinessService(AlignmentPersistenceService alignmentPersistenceService, + AlignmentValidationService alignmentValidationService, + ObjectivePersistenceService objectivePersistenceService, + KeyResultPersistenceService keyResultPersistenceService) { + this.alignmentPersistenceService = alignmentPersistenceService; + this.alignmentValidationService = alignmentValidationService; + this.objectivePersistenceService = objectivePersistenceService; + this.keyResultPersistenceService = keyResultPersistenceService; + } + + public String getTargetIdByAlignedObjectiveId(Long alignedObjectiveId) { + alignmentValidationService.validateOnGet(alignedObjectiveId); + Alignment alignment = alignmentPersistenceService.findByAlignedObjectiveId(alignedObjectiveId); + if (alignment instanceof KeyResultAlignment keyResultAlignment) { + return "K" + keyResultAlignment.getAlignmentTarget().getId(); + } else if (alignment instanceof ObjectiveAlignment objectiveAlignment) { + return "O" + objectiveAlignment.getAlignmentTarget().getId(); + } else { + return null; + } + } + + public void createEntity(Objective alignedObjective) { + Alignment alignment = buildAlignmentModel(alignedObjective); + alignmentValidationService.validateOnCreate(alignment); + alignmentPersistenceService.save(alignment); + } + + public void updateEntity(Long objectiveId, Objective objective) { + Alignment savedAlignment = alignmentPersistenceService.findByAlignedObjectiveId(objectiveId); + + if (savedAlignment == null) { + Alignment alignment = buildAlignmentModel(objective); + alignmentValidationService.validateOnCreate(alignment); + alignmentPersistenceService.save(alignment); + } else { + if (objective.getAlignedEntityId() == null) { + alignmentValidationService.validateOnDelete(savedAlignment.getId()); + alignmentPersistenceService.deleteById(savedAlignment.getId()); + } else { + Alignment alignment = buildAlignmentModel(objective); + alignment.setId(savedAlignment.getId()); + alignmentValidationService.validateOnUpdate(savedAlignment.getId(), alignment); + if (isAlignmentTypeChange(alignment, savedAlignment)) { + alignmentPersistenceService.recreateEntity(savedAlignment.getId(), alignment); + } else { + alignmentPersistenceService.save(alignment); + } + } + } + } + + private Alignment buildAlignmentModel(Objective alignedObjective) { + if (alignedObjective.getAlignedEntityId().startsWith("O")) { + Objective targetObjective = objectivePersistenceService + .findById(Long.valueOf(alignedObjective.getAlignedEntityId().replace("O", ""))); + return ObjectiveAlignment.Builder.builder().withAlignedObjective(alignedObjective) + .withTargetObjective(targetObjective).build(); + } else if (alignedObjective.getAlignedEntityId().startsWith("K")) { + KeyResult targetKeyResult = keyResultPersistenceService + .findById(Long.valueOf(alignedObjective.getAlignedEntityId().replace("K", ""))); + + return KeyResultAlignment.Builder.builder().withAlignedObjective(alignedObjective) + .withTargetKeyResult(targetKeyResult).build(); + } else { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NOT_SET, + List.of("alignedEntityId", alignedObjective.getAlignedEntityId())); + } + } + + private boolean isAlignmentTypeChange(Alignment alignment, Alignment savedAlignment) { + if (alignment instanceof ObjectiveAlignment && savedAlignment instanceof KeyResultAlignment) { + return true; + } else + return alignment instanceof KeyResultAlignment && savedAlignment instanceof ObjectiveAlignment; + } + + public void updateKeyResultIdOnIdChange(Long oldId, KeyResult keyResult) { + List alignments = alignmentPersistenceService.findByKeyResultAlignmentId(oldId); + alignments.forEach(alignment -> { + alignment.setAlignmentTarget(keyResult); + alignmentValidationService.validateOnUpdate(alignment.getId(), alignment); + alignmentPersistenceService.save(alignment); + }); + } +} diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java index 309a09d524..5f0c8d278e 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java @@ -24,14 +24,16 @@ public class KeyResultBusinessService implements BusinessServiceInterface acti action.resetId(); action.setKeyResult(recreatedEntity); }); + alignmentBusinessService.updateKeyResultIdOnIdChange(id, recreatedEntity); return recreatedEntity; } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java index b532aba932..08986ea39e 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java @@ -1,5 +1,7 @@ package ch.puzzle.okr.service.business; +import ch.puzzle.okr.dto.ObjectiveAlignmentsDto; +import ch.puzzle.okr.dto.keyresult.KeyResultAlignmentsDto; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.models.keyresult.KeyResult; @@ -14,6 +16,7 @@ import org.springframework.stereotype.Service; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -26,21 +29,49 @@ public class ObjectiveBusinessService implements BusinessServiceInterface getAlignmentPossibilities(Long quarterId) { + validator.validateOnGet(quarterId); + + List objectivesByQuarter = objectivePersistenceService.findObjectiveByQuarterId(quarterId); + List objectiveAlignmentsDtos = new ArrayList<>(); + + objectivesByQuarter.forEach(objective -> { + List keyResults = keyResultBusinessService.getAllKeyResultsByObjective(objective.getId()); + List keyResultAlignmentsDtos = new ArrayList<>(); + keyResults.forEach(keyResult -> { + KeyResultAlignmentsDto keyResultAlignmentsDto = new KeyResultAlignmentsDto(keyResult.getId(), + "K - " + keyResult.getTitle()); + keyResultAlignmentsDtos.add(keyResultAlignmentsDto); + }); + ObjectiveAlignmentsDto objectiveAlignmentsDto = new ObjectiveAlignmentsDto(objective.getId(), + "O - " + objective.getTitle(), keyResultAlignmentsDtos); + objectiveAlignmentsDtos.add(objectiveAlignmentsDto); + }); + + return objectiveAlignmentsDtos; } public List getEntitiesByTeamId(Long id) { @@ -62,7 +93,10 @@ public Objective updateEntity(Long id, Objective objective, AuthorizationUser au } logger.debug("quarter has changed and is{}changeable, {}", not, objective); validator.validateOnUpdate(id, objective); - return objectivePersistenceService.save(objective); + savedObjective = objectivePersistenceService.save(objective); + savedObjective.setAlignedEntityId(objective.getAlignedEntityId()); + alignmentBusinessService.updateEntity(id, savedObjective); + return savedObjective; } public boolean isImUsed(Objective objective) { @@ -88,7 +122,11 @@ public Objective createEntity(Objective objective, AuthorizationUser authorizati objective.setCreatedBy(authorizationUser.user()); objective.setCreatedOn(LocalDateTime.now()); validator.validateOnCreate(objective); - return objectivePersistenceService.save(objective); + Objective savedObjective = objectivePersistenceService.save(objective); + if (objective.getAlignedEntityId() != null) { + alignmentBusinessService.createEntity(savedObjective); + } + return savedObjective; } @Transactional diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java index 03bb2ad444..8f3c8ffaec 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java @@ -4,6 +4,7 @@ import ch.puzzle.okr.models.alignment.KeyResultAlignment; import ch.puzzle.okr.models.alignment.ObjectiveAlignment; import ch.puzzle.okr.repository.AlignmentRepository; +import jakarta.transaction.Transactional; import org.springframework.stereotype.Service; import java.util.List; @@ -22,7 +23,17 @@ public String getModelName() { return ALIGNMENT; } - public List findByAlignedObjectiveId(Long alignedObjectiveId) { + @Transactional + public void recreateEntity(Long id, Alignment alignment) { + System.out.println(alignment.toString()); + System.out.println("*".repeat(30)); + // delete entity in order to prevent duplicates in case of changed keyResultType + deleteById(id); + System.out.printf("reached delete entity with %d", id); + save(alignment); + } + + public Alignment findByAlignedObjectiveId(Long alignedObjectiveId) { return getRepository().findByAlignedObjectiveId(alignedObjectiveId); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceService.java index 675d8ec249..e444a1c085 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceService.java @@ -49,6 +49,10 @@ public Objective findObjectiveById(Long objectiveId, AuthorizationUser authoriza return findByAnyId(objectiveId, authorizationUser, SELECT_OBJECTIVE_BY_ID, noResultException); } + public List findObjectiveByQuarterId(Long quarterId) { + return getRepository().findObjectivesByQuarterId(quarterId); + } + public List findObjectiveByTeamId(Long teamId) { return getRepository().findObjectivesByTeamId(teamId); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java new file mode 100644 index 0000000000..740d2a772b --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java @@ -0,0 +1,79 @@ +package ch.puzzle.okr.service.validation; + +import ch.puzzle.okr.ErrorKey; +import ch.puzzle.okr.exception.OkrResponseStatusException; +import ch.puzzle.okr.models.alignment.Alignment; +import ch.puzzle.okr.models.alignment.KeyResultAlignment; +import ch.puzzle.okr.models.alignment.ObjectiveAlignment; +import ch.puzzle.okr.repository.AlignmentRepository; +import ch.puzzle.okr.service.persistence.AlignmentPersistenceService; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Objects; + +@Service +public class AlignmentValidationService + extends ValidationBase { + + private final AlignmentPersistenceService alignmentPersistenceService; + + public AlignmentValidationService(AlignmentPersistenceService alignmentPersistenceService) { + super(alignmentPersistenceService); + this.alignmentPersistenceService = alignmentPersistenceService; + } + + @Override + public void validateOnCreate(Alignment model) { + throwExceptionWhenModelIsNull(model); + throwExceptionWhenIdIsNotNull(model.getId()); + throwExceptionWhenAlignedObjectIsNull(model); + throwExceptionWhenAlignedIdIsSameAsTargetId(model); + throwExceptionWhenAlignedObjectiveAlreadyExists(model); + validate(model); + } + + @Override + public void validateOnUpdate(Long id, Alignment model) { + throwExceptionWhenModelIsNull(model); + throwExceptionWhenIdIsNull(model.getId()); + throwExceptionWhenAlignedObjectIsNull(model); + throwExceptionWhenAlignedIdIsSameAsTargetId(model); + validate(model); + } + + private static void throwExceptionWhenAlignedObjectIsNull(Alignment model) { + if (model.getAlignedObjective() == null) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, + List.of("alignedObjectiveId", model.getAlignedObjective().getId())); + } else if (model instanceof ObjectiveAlignment objectiveAlignment) { + if (objectiveAlignment.getAlignmentTarget() == null) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, + List.of("targetObjectiveId", objectiveAlignment.getAlignmentTarget().getId())); + } + } else if (model instanceof KeyResultAlignment keyResultAlignment) { + if (keyResultAlignment.getAlignmentTarget() == null) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, + List.of("targetKeyResultId", keyResultAlignment.getAlignmentTarget().getId())); + } + } + } + + private static void throwExceptionWhenAlignedIdIsSameAsTargetId(Alignment model) { + if (model instanceof ObjectiveAlignment objectiveAlignment) { + if (Objects.equals(objectiveAlignment.getAlignedObjective().getId(), + objectiveAlignment.getAlignmentTarget().getId())) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.NOT_LINK_YOURSELF, + List.of("targetObjectiveId", objectiveAlignment.getAlignmentTarget().getId())); + } + } + } + + private void throwExceptionWhenAlignedObjectiveAlreadyExists(Alignment model) { + if (this.alignmentPersistenceService.findByAlignedObjectiveId(model.getAlignedObjective().getId()) != null) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ALIGNMENT_ALREADY_EXISTS, + List.of("alignedObjectiveId", model.getAlignedObjective().getId())); + } + } +} diff --git a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java index f4fbc5d68a..7768bdfcdf 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java @@ -79,9 +79,9 @@ class ObjectiveControllerIT { .withCreatedBy(user).withTeam(team).withQuarter(quarter).withDescription(DESCRIPTION) .withModifiedOn(LocalDateTime.MAX).build(); private static final ObjectiveDto objective1Dto = new ObjectiveDto(5L, 1, OBJECTIVE_TITLE_1, 1L, 1L, "GJ 22/23-Q2", - DESCRIPTION, State.DRAFT, LocalDateTime.MAX, LocalDateTime.MAX, true); + DESCRIPTION, State.DRAFT, LocalDateTime.MAX, LocalDateTime.MAX, true, null); private static final ObjectiveDto objective2Dto = new ObjectiveDto(7L, 1, OBJECTIVE_TITLE_2, 1L, 1L, "GJ 22/23-Q2", - DESCRIPTION, State.DRAFT, LocalDateTime.MIN, LocalDateTime.MIN, true); + DESCRIPTION, State.DRAFT, LocalDateTime.MIN, LocalDateTime.MIN, true, "O5"); @Autowired private MockMvc mvc; @@ -119,7 +119,7 @@ void getObjectiveByIdFail() throws Exception { @Test void shouldReturnObjectiveWhenCreatingNewObjective() throws Exception { ObjectiveDto testObjective = new ObjectiveDto(null, 1, "Program Faster", 1L, 1L, "GJ 22/23-Q2", - "Just be faster", State.DRAFT, null, null, true); + "Just be faster", State.DRAFT, null, null, true, null); BDDMockito.given(objectiveMapper.toDto(any())).willReturn(testObjective); BDDMockito.given(objectiveAuthorizationService.createEntity(any())).willReturn(fullObjective); @@ -144,7 +144,7 @@ void shouldReturnResponseStatusExceptionWhenCreatingObjectiveWithNullValues() th @Test void shouldReturnUpdatedObjective() throws Exception { ObjectiveDto testObjective = new ObjectiveDto(1L, 1, TITLE, 1L, 1L, "GJ 22/23-Q2", EVERYTHING_FINE_DESCRIPTION, - State.NOTSUCCESSFUL, LocalDateTime.MIN, LocalDateTime.MAX, true); + State.NOTSUCCESSFUL, LocalDateTime.MIN, LocalDateTime.MAX, true, null); Objective objective = Objective.Builder.builder().withId(1L).withDescription(EVERYTHING_FINE_DESCRIPTION) .withTitle(TITLE).build(); @@ -162,7 +162,7 @@ void shouldReturnUpdatedObjective() throws Exception { @Test void shouldReturnImUsed() throws Exception { ObjectiveDto testObjectiveDto = new ObjectiveDto(1L, 1, TITLE, 1L, 1L, "GJ 22/23-Q2", - EVERYTHING_FINE_DESCRIPTION, State.SUCCESSFUL, LocalDateTime.MAX, LocalDateTime.MAX, true); + EVERYTHING_FINE_DESCRIPTION, State.SUCCESSFUL, LocalDateTime.MAX, LocalDateTime.MAX, true, null); Objective objectiveImUsed = Objective.Builder.builder().withId(1L).withDescription(EVERYTHING_FINE_DESCRIPTION) .withQuarter(Quarter.Builder.builder().withId(1L).withLabel("GJ 22/23-Q2").build()).withTitle(TITLE) .build(); diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java index bbbe9749f3..6eff1fb0b3 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java @@ -107,11 +107,11 @@ void updateAlignmentShouldThrowExceptionWhenAlreadyUpdated() { } @Test - void findByAlignedObjectiveIdShouldReturnListOfAlignments() { - List alignments = alignmentPersistenceService.findByAlignedObjectiveId(4L); + void findByAlignedObjectiveIdShouldReturnAlignmentModel() { + Alignment alignment = alignmentPersistenceService.findByAlignedObjectiveId(4L); - assertEquals(2, alignments.size()); - alignments.forEach(this::assertAlignment); + assertNotNull(alignment); + assertEquals(alignment.getAlignedObjective().getId(), 4); } @Test diff --git a/frontend/src/app/shared/types/model/AlignmentPossibility.ts b/frontend/src/app/shared/types/model/AlignmentPossibility.ts new file mode 100644 index 0000000000..9961a689af --- /dev/null +++ b/frontend/src/app/shared/types/model/AlignmentPossibility.ts @@ -0,0 +1,8 @@ +export interface AlignmentPossibility { + objectiveId: number | null; + objectiveTitle: string; + keyResultAlignmentsDtos: { + keyResultId: number; + keyResultTitle: string; + }[] +} From c883425efa1a5afb4f731227c41071a0e3e4fbdc Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 12 Feb 2024 12:04:25 +0100 Subject: [PATCH 002/119] Finish frontend implementation --- .../objective-form.component.html | 42 ++++++++++++++----- .../objective-form.component.scss | 22 +++------- .../objective-form.component.ts | 19 ++++++++- .../app/shared/services/objective.service.ts | 5 +++ .../src/app/shared/types/model/Objective.ts | 1 + 5 files changed, 61 insertions(+), 28 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index 28e461b3bf..b0997dcacb 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -53,7 +53,7 @@
- -

Key Results im Anschluss erfassen

-
+ +
+ + +
+

{{ this.objectiveForm.value.alignment }}

+ +

Key Results im Anschluss erfassen

+
diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss index 28e560f6d2..d068aca146 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss @@ -10,8 +10,11 @@ .add-keyresult { // Select is aligned with label and it's not easy to align checkbox to select - margin-top: -6px; - margin-left: 1.2rem; + margin-left: -10px; +} + +.add-keyresult-text { + margin: 0; } @media only screen and (min-width: 820px) { @@ -22,21 +25,6 @@ } } -@media only screen and (max-width: 770px) { - .add-keyresult { - margin-left: -10px; - margin-top: 1rem; - } - - .add-keyresult-text { - margin: 0; - } -} - -.add-keyresult-text { - margin-left: -10px; -} - .dialog-content { max-height: 35vh; } diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 2b967cabd8..07ec3ad1d8 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -15,6 +15,7 @@ import { formInputCheck, getQuarterLabel, getValueFromQuery, hasFormFieldErrors, import { ActivatedRoute } from '@angular/router'; import { CONFIRM_DIALOG_WIDTH, GJ_REGEX_PATTERN } from '../../constantLibary'; import { TranslateService } from '@ngx-translate/core'; +import { AlignmentPossibility } from '../../types/model/AlignmentPossibility'; @Component({ selector: 'app-objective-form', @@ -28,12 +29,13 @@ export class ObjectiveFormComponent implements OnInit { description: new FormControl('', [Validators.maxLength(4096)]), quarter: new FormControl(0, [Validators.required]), team: new FormControl({ value: 0, disabled: true }, [Validators.required]), - relation: new FormControl({ value: 0, disabled: true }), + alignment: new FormControl(''), createKeyResults: new FormControl(false), }); quarters$: Observable = of([]); quarters: Quarter[] = []; teams$: Observable = of([]); + alignmentPossibilities$: Observable = of([]); currentTeam: Subject = new Subject(); state: string | null = null; version!: number; @@ -70,6 +72,7 @@ export class ObjectiveFormComponent implements OnInit { title: value.title, teamId: value.team, state: state, + alignedEntityId: value.alignment == 'Onull' ? null : value.alignment, } as unknown as Objective; const submitFunction = this.getSubmitFunction(objectiveDTO.id, objectiveDTO); @@ -101,12 +104,26 @@ export class ObjectiveFormComponent implements OnInit { this.teams$.subscribe((value) => { this.currentTeam.next(value.filter((team) => team.id == teamId)[0]); }); + this.alignmentPossibilities$ = this.objectiveService.getAlignmentPossibilities(quarterId); + this.alignmentPossibilities$.subscribe((value) => { + if (objective.id) { + value = value.filter((item) => !(item.objectiveId == objective.id)); + } + let noSelectOption: AlignmentPossibility = { + objectiveId: null, + objectiveTitle: 'Bitte wählen', + keyResultAlignmentsDtos: [], + }; + value.unshift(noSelectOption); + this.alignmentPossibilities$ = of(value); + }); this.objectiveForm.patchValue({ title: objective.title, description: objective.description, team: teamId, quarter: quarterId, + alignment: objective.alignedEntityId ? objective.alignedEntityId : 'Onull', }); }); } diff --git a/frontend/src/app/shared/services/objective.service.ts b/frontend/src/app/shared/services/objective.service.ts index 04988bf6cd..388abce455 100644 --- a/frontend/src/app/shared/services/objective.service.ts +++ b/frontend/src/app/shared/services/objective.service.ts @@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { Objective } from '../types/model/Objective'; import { Observable } from 'rxjs'; import { Completed } from '../types/model/Completed'; +import { AlignmentPossibility } from '../types/model/AlignmentPossibility'; @Injectable({ providedIn: 'root', @@ -14,6 +15,10 @@ export class ObjectiveService { return this.httpClient.get('/api/v2/objectives/' + id); } + getAlignmentPossibilities(quarterId: number): Observable { + return this.httpClient.get('/api/v2/objectives/alignmentPossibilities/' + quarterId); + } + createObjective(objectiveDTO: Objective): Observable { return this.httpClient.post('/api/v2/objectives', objectiveDTO); } diff --git a/frontend/src/app/shared/types/model/Objective.ts b/frontend/src/app/shared/types/model/Objective.ts index 4126c61fd5..eaef2c04bc 100644 --- a/frontend/src/app/shared/types/model/Objective.ts +++ b/frontend/src/app/shared/types/model/Objective.ts @@ -14,4 +14,5 @@ export interface Objective { modifiedOn?: Date; createdBy?: User; writeable: boolean; + alignedEntityId: string; } From 6d1849db478c6ad2d58bbf37e6fe4966566f1881 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 12 Feb 2024 12:59:59 +0100 Subject: [PATCH 003/119] Delete alignment on kr or ob delete --- .../business/AlignmentBusinessService.java | 20 +++++++++++ .../business/KeyResultBusinessService.java | 1 + .../business/ObjectiveBusinessService.java | 1 + .../objective-form.component.html | 1 + .../objective-form.component.ts | 36 ++++++++++++------- .../types/model/AlignmentPossibility.ts | 2 +- 6 files changed, 47 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index 7950a5b6ec..1726f7013b 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -109,4 +109,24 @@ public void updateKeyResultIdOnIdChange(Long oldId, KeyResult keyResult) { alignmentPersistenceService.save(alignment); }); } + + public void deleteAlignmentByObjectiveId(Long objectiveId) { + Alignment alignment = alignmentPersistenceService.findByAlignedObjectiveId(objectiveId); + alignmentValidationService.validateOnDelete(alignment.getId()); + alignmentPersistenceService.deleteById(alignment.getId()); + + List alignmentList = alignmentPersistenceService.findByObjectiveAlignmentId(objectiveId); + alignmentList.forEach(objectiveAlignment -> { + alignmentValidationService.validateOnDelete(objectiveAlignment.getId()); + alignmentPersistenceService.deleteById(objectiveAlignment.getId()); + }); + } + + public void deleteAlignmentByKeyResultId(Long keyResultId) { + List alignmentList = alignmentPersistenceService.findByKeyResultAlignmentId(keyResultId); + alignmentList.forEach(keyResultAlignment -> { + alignmentValidationService.validateOnDelete(keyResultAlignment.getId()); + alignmentPersistenceService.deleteById(keyResultAlignment.getId()); + }); + } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java index 5f0c8d278e..2835f440e7 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/KeyResultBusinessService.java @@ -105,6 +105,7 @@ public void deleteEntityById(Long id) { .forEach(checkIn -> checkInBusinessService.deleteEntityById(checkIn.getId())); actionBusinessService.getActionsByKeyResultId(id) .forEach(action -> actionBusinessService.deleteEntityById(action.getId())); + alignmentBusinessService.deleteAlignmentByKeyResultId(id); keyResultPersistenceService.deleteById(id); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java index 08986ea39e..e2b4baa942 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java @@ -157,6 +157,7 @@ public void deleteEntityById(Long id) { completedBusinessService.deleteCompletedByObjectiveId(id); keyResultBusinessService.getAllKeyResultsByObjective(id) .forEach(keyResult -> keyResultBusinessService.deleteEntityById(keyResult.getId())); + alignmentBusinessService.deleteAlignmentByObjectiveId(id); objectivePersistenceService.deleteById(id); } } diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index b0997dcacb..bf39188c51 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -56,6 +56,7 @@ class="custom-select bg-white w-75" formControlName="quarter" id="quarter" + (change)="updateAlignments()" [attr.data-testId]="'quarterSelect'" > diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 07ec3ad1d8..e75de9e003 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -34,6 +34,7 @@ export class ObjectiveFormComponent implements OnInit { }); quarters$: Observable = of([]); quarters: Quarter[] = []; + objective: Objective | null = null; teams$: Observable = of([]); alignmentPossibilities$: Observable = of([]); currentTeam: Subject = new Subject(); @@ -91,6 +92,7 @@ export class ObjectiveFormComponent implements OnInit { forkJoin([objective$, this.quarters$]).subscribe(([objective, quarters]) => { this.quarters = quarters; + this.objective = objective; const teamId = isCreating ? objective.teamId : this.data.objective.teamId; let quarterId = getValueFromQuery(this.route.snapshot.queryParams['quarter'], quarters[1].id)[0]; @@ -104,19 +106,7 @@ export class ObjectiveFormComponent implements OnInit { this.teams$.subscribe((value) => { this.currentTeam.next(value.filter((team) => team.id == teamId)[0]); }); - this.alignmentPossibilities$ = this.objectiveService.getAlignmentPossibilities(quarterId); - this.alignmentPossibilities$.subscribe((value) => { - if (objective.id) { - value = value.filter((item) => !(item.objectiveId == objective.id)); - } - let noSelectOption: AlignmentPossibility = { - objectiveId: null, - objectiveTitle: 'Bitte wählen', - keyResultAlignmentsDtos: [], - }; - value.unshift(noSelectOption); - this.alignmentPossibilities$ = of(value); - }); + this.generateAlignmentPossibilities(quarterId); this.objectiveForm.patchValue({ title: objective.title, @@ -247,5 +237,25 @@ export class ObjectiveFormComponent implements OnInit { return GJ_REGEX_PATTERN.test(label); } + generateAlignmentPossibilities(quarterId: number) { + this.alignmentPossibilities$ = this.objectiveService.getAlignmentPossibilities(quarterId); + this.alignmentPossibilities$.subscribe((value) => { + if (this.objective?.id) { + value = value.filter((item) => !(item.objectiveId == this.objective!.id)); + } + let noSelectOption: AlignmentPossibility = { + objectiveId: null, + objectiveTitle: 'Bitte wählen', + keyResultAlignmentsDtos: [], + }; + value.unshift(noSelectOption); + this.alignmentPossibilities$ = of(value); + }); + } + + updateAlignments() { + this.generateAlignmentPossibilities(this.objectiveForm.value.quarter!); + } + protected readonly getQuarterLabel = getQuarterLabel; } diff --git a/frontend/src/app/shared/types/model/AlignmentPossibility.ts b/frontend/src/app/shared/types/model/AlignmentPossibility.ts index 9961a689af..e2b3e69673 100644 --- a/frontend/src/app/shared/types/model/AlignmentPossibility.ts +++ b/frontend/src/app/shared/types/model/AlignmentPossibility.ts @@ -4,5 +4,5 @@ export interface AlignmentPossibility { keyResultAlignmentsDtos: { keyResultId: number; keyResultTitle: string; - }[] + }[]; } From 4d34225dd2ddb5544abce257fe6cd6c9ffbf9a43 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 12 Feb 2024 13:38:05 +0100 Subject: [PATCH 004/119] Write objectivecontroller tests --- .../okr/controller/ObjectiveControllerIT.java | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java index 7768bdfcdf..120a38fbdb 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java @@ -1,6 +1,8 @@ package ch.puzzle.okr.controller; +import ch.puzzle.okr.dto.ObjectiveAlignmentsDto; import ch.puzzle.okr.dto.ObjectiveDto; +import ch.puzzle.okr.dto.keyresult.KeyResultAlignmentsDto; import ch.puzzle.okr.mapper.ObjectiveMapper; import ch.puzzle.okr.models.*; import ch.puzzle.okr.service.authorization.AuthorizationService; @@ -25,6 +27,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.List; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -65,10 +68,12 @@ class ObjectiveControllerIT { } """; private static final String RESPONSE_NEW_OBJECTIVE = """ - {"id":null,"version":1,"title":"Program Faster","teamId":1,"quarterId":1,"quarterLabel":"GJ 22/23-Q2","description":"Just be faster","state":"DRAFT","createdOn":null,"modifiedOn":null,"writeable":true}"""; + {"id":null,"version":1,"title":"Program Faster","teamId":1,"quarterId":1,"quarterLabel":"GJ 22/23-Q2","description":"Just be faster","state":"DRAFT","createdOn":null,"modifiedOn":null,"writeable":true,"alignedEntityId":null}"""; private static final String JSON_PATH_TITLE = "$.title"; private static final Objective objective1 = Objective.Builder.builder().withId(5L).withTitle(OBJECTIVE_TITLE_1) .build(); + private static final Objective objectiveAlignment = Objective.Builder.builder().withId(9L) + .withTitle("Objective Alignment").withAlignedEntityId("O42").build(); private static final Objective objective2 = Objective.Builder.builder().withId(7L).withTitle(OBJECTIVE_TITLE_2) .build(); private static final User user = User.Builder.builder().withId(1L).withFirstname("Bob").withLastname("Kaufmann") @@ -82,6 +87,12 @@ class ObjectiveControllerIT { DESCRIPTION, State.DRAFT, LocalDateTime.MAX, LocalDateTime.MAX, true, null); private static final ObjectiveDto objective2Dto = new ObjectiveDto(7L, 1, OBJECTIVE_TITLE_2, 1L, 1L, "GJ 22/23-Q2", DESCRIPTION, State.DRAFT, LocalDateTime.MIN, LocalDateTime.MIN, true, "O5"); + private static final ObjectiveDto objectiveAlignmentDto = new ObjectiveDto(9L, 1, "Objective Alignment", 1L, 1L, + "GJ 22/23-Q2", DESCRIPTION, State.DRAFT, LocalDateTime.MAX, LocalDateTime.MAX, true, "O42"); + private static final KeyResultAlignmentsDto keyResultAlignmentsDto1 = new KeyResultAlignmentsDto(3L, "KR Title 1"); + private static final KeyResultAlignmentsDto keyResultAlignmentsDto2 = new KeyResultAlignmentsDto(6L, "KR Title 2"); + private static final ObjectiveAlignmentsDto alignmentPossibilities = new ObjectiveAlignmentsDto(1L, + "This is my objective title", List.of(keyResultAlignmentsDto1, keyResultAlignmentsDto2)); @Autowired private MockMvc mvc; @@ -96,6 +107,7 @@ class ObjectiveControllerIT { void setUp() { BDDMockito.given(objectiveMapper.toDto(objective1)).willReturn(objective1Dto); BDDMockito.given(objectiveMapper.toDto(objective2)).willReturn(objective2Dto); + BDDMockito.given(objectiveMapper.toDto(objectiveAlignment)).willReturn(objectiveAlignmentDto); } @Test @@ -107,6 +119,16 @@ void getObjectiveById() throws Exception { .andExpect(jsonPath(JSON_PATH_TITLE, Is.is(OBJECTIVE_TITLE_1))); } + @Test + void getObjectiveByIdWithAlignmentId() throws Exception { + BDDMockito.given(objectiveAuthorizationService.getEntityById(anyLong())).willReturn(objectiveAlignment); + + mvc.perform(get("/api/v2/objectives/9").contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$.id", Is.is(9))) + .andExpect(jsonPath(JSON_PATH_TITLE, Is.is("Objective Alignment"))) + .andExpect(jsonPath("$.alignedEntityId", Is.is("O42"))); + } + @Test void getObjectiveByIdFail() throws Exception { BDDMockito.given(objectiveAuthorizationService.getEntityById(anyLong())) @@ -116,6 +138,23 @@ void getObjectiveByIdFail() throws Exception { .andExpect(MockMvcResultMatchers.status().isNotFound()); } + @Test + void getAlignmentPossibilities() throws Exception { + BDDMockito.given(objectiveAuthorizationService.getAlignmentPossibilities(anyLong())) + .willReturn(List.of(alignmentPossibilities)); + + mvc.perform(get("/api/v2/objectives/alignmentPossibilities/5").contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(jsonPath("$[0].objectiveTitle", Is.is("This is my objective title"))) + .andExpect(jsonPath("$[0].objectiveId", Is.is(1))) + .andExpect(jsonPath("$[0].keyResultAlignmentsDtos[0].keyResultId", Is.is(3))) + .andExpect(jsonPath("$[0].keyResultAlignmentsDtos[1].keyResultId", Is.is(6))) + .andExpect(jsonPath("$[0].keyResultAlignmentsDtos[0].keyResultTitle", Is.is("KR Title 1"))) + .andExpect(jsonPath("$[0].keyResultAlignmentsDtos[1].keyResultTitle", Is.is("KR Title 2"))); + + verify(objectiveAuthorizationService, times(1)).getAlignmentPossibilities(5L); + } + @Test void shouldReturnObjectiveWhenCreatingNewObjective() throws Exception { ObjectiveDto testObjective = new ObjectiveDto(null, 1, "Program Faster", 1L, 1L, "GJ 22/23-Q2", From 0f182e2babc59889395a1b6a0d6edef71b7ffdbb Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 12 Feb 2024 13:44:38 +0100 Subject: [PATCH 005/119] Write ObjectiveAuthorizationService tests --- .../ObjectiveAuthorizationServiceTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java index 26f452d0a5..84e3c27c72 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java @@ -1,5 +1,7 @@ package ch.puzzle.okr.service.authorization; +import ch.puzzle.okr.dto.ObjectiveAlignmentsDto; +import ch.puzzle.okr.dto.keyresult.KeyResultAlignmentsDto; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.service.business.ObjectiveBusinessService; @@ -11,8 +13,11 @@ import org.springframework.http.HttpStatus; import org.springframework.web.server.ResponseStatusException; +import java.util.List; + import static ch.puzzle.okr.TestHelper.defaultAuthorizationUser; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.http.HttpStatus.UNAUTHORIZED; @@ -28,6 +33,10 @@ class ObjectiveAuthorizationServiceTest { private final AuthorizationUser authorizationUser = defaultAuthorizationUser(); private final Objective newObjective = Objective.Builder.builder().withId(5L).withTitle("Objective 1").build(); + private static final KeyResultAlignmentsDto keyResultAlignmentsDto1 = new KeyResultAlignmentsDto(3L, "KR Title 1"); + private static final KeyResultAlignmentsDto keyResultAlignmentsDto2 = new KeyResultAlignmentsDto(6L, "KR Title 2"); + private static final ObjectiveAlignmentsDto alignmentPossibilities = new ObjectiveAlignmentsDto(1L, + "This is my objective title", List.of(keyResultAlignmentsDto1, keyResultAlignmentsDto2)); @Test void createEntityShouldReturnObjectiveWhenAuthorized() { @@ -138,4 +147,17 @@ void deleteEntityByIdShouldThrowExceptionWhenNotAuthorized() { assertEquals(UNAUTHORIZED, exception.getStatusCode()); assertEquals(reason, exception.getReason()); } + + @Test + void getAlignmentPossibilitiesShouldReturnListWhenAuthorized() { + when(objectiveBusinessService.getAlignmentPossibilities(anyLong())).thenReturn(List.of(alignmentPossibilities)); + + List alignmentPossibilities = objectiveAuthorizationService.getAlignmentPossibilities(3L); + assertEquals("This is my objective title", alignmentPossibilities.get(0).objectiveTitle()); + assertEquals(1, alignmentPossibilities.get(0).objectiveId()); + assertEquals(3, alignmentPossibilities.get(0).keyResultAlignmentsDtos().get(0).keyResultId()); + assertEquals("KR Title 1", alignmentPossibilities.get(0).keyResultAlignmentsDtos().get(0).keyResultTitle()); + assertEquals(6, alignmentPossibilities.get(0).keyResultAlignmentsDtos().get(1).keyResultId()); + assertEquals("KR Title 2", alignmentPossibilities.get(0).keyResultAlignmentsDtos().get(1).keyResultTitle()); + } } From 521683f7d6f417578520ed25db0d1eef5a5be1cd Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 12 Feb 2024 15:41:25 +0100 Subject: [PATCH 006/119] Write AlignmentBusinessService tests --- .../business/AlignmentBusinessService.java | 4 +- .../business/ObjectiveBusinessService.java | 9 +- .../h2-db/data-test-h2/V100_0_0__TestData.sql | 2 +- .../AlignmentBusinessServiceTest.java | 226 ++++++++++++++++++ .../KeyResultBusinessServiceTest.java | 5 + .../ObjectiveBusinessServiceTest.java | 47 ++++ .../ObjectivePersistenceServiceIT.java | 8 + 7 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index 1726f7013b..1ee16419bd 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -76,7 +76,7 @@ public void updateEntity(Long objectiveId, Objective objective) { } } - private Alignment buildAlignmentModel(Objective alignedObjective) { + public Alignment buildAlignmentModel(Objective alignedObjective) { if (alignedObjective.getAlignedEntityId().startsWith("O")) { Objective targetObjective = objectivePersistenceService .findById(Long.valueOf(alignedObjective.getAlignedEntityId().replace("O", ""))); @@ -94,7 +94,7 @@ private Alignment buildAlignmentModel(Objective alignedObjective) { } } - private boolean isAlignmentTypeChange(Alignment alignment, Alignment savedAlignment) { + public boolean isAlignmentTypeChange(Alignment alignment, Alignment savedAlignment) { if (alignment instanceof ObjectiveAlignment && savedAlignment instanceof KeyResultAlignment) { return true; } else diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java index e2b4baa942..bbfb280e4e 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java @@ -76,7 +76,14 @@ public List getAlignmentPossibilities(Long quarterId) { public List getEntitiesByTeamId(Long id) { validator.validateOnGet(id); - return objectivePersistenceService.findObjectiveByTeamId(id); + + List objectiveList = objectivePersistenceService.findObjectiveByTeamId(id); + objectiveList.forEach(objective -> { + String alignmentId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(objective.getId()); + objective.setAlignedEntityId(alignmentId); + }); + + return objectiveList; } @Transactional diff --git a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql index 34aec66957..4dc4efad8a 100644 --- a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql +++ b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql @@ -115,7 +115,7 @@ values (1,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam insert into alignment (id, version, aligned_objective_id, alignment_type, target_key_result_id, target_objective_id) values (1,1, 4, 'objective', null, 3), - (2,1, 4, 'keyResult', 8, null); + (2,1, 9, 'keyResult', 8, null); insert into completed (id, version, objective_id, comment) values (1,1, 4, 'Das hat geklappt'), diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java new file mode 100644 index 0000000000..2c18571bbf --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -0,0 +1,226 @@ +package ch.puzzle.okr.service.business; + +import ch.puzzle.okr.TestHelper; +import ch.puzzle.okr.dto.ErrorDto; +import ch.puzzle.okr.exception.OkrResponseStatusException; +import ch.puzzle.okr.models.Objective; +import ch.puzzle.okr.models.alignment.Alignment; +import ch.puzzle.okr.models.alignment.KeyResultAlignment; +import ch.puzzle.okr.models.alignment.ObjectiveAlignment; +import ch.puzzle.okr.models.keyresult.KeyResult; +import ch.puzzle.okr.models.keyresult.KeyResultMetric; +import ch.puzzle.okr.service.persistence.AlignmentPersistenceService; +import ch.puzzle.okr.service.persistence.KeyResultPersistenceService; +import ch.puzzle.okr.service.persistence.ObjectivePersistenceService; +import ch.puzzle.okr.service.validation.AlignmentValidationService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static ch.puzzle.okr.models.State.DRAFT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +@ExtendWith(MockitoExtension.class) +class AlignmentBusinessServiceTest { + @Mock + ObjectivePersistenceService objectivePersistenceService; + @Mock + KeyResultPersistenceService keyResultPersistenceService; + @Mock + AlignmentValidationService alignmentValidationService; + @Mock + AlignmentPersistenceService alignmentPersistenceService; + @Mock + AlignmentValidationService validator; + @InjectMocks + private AlignmentBusinessService alignmentBusinessService; + + Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withState(DRAFT).build(); + Objective objective2 = Objective.Builder.builder().withId(8L).withTitle("Objective 2").withState(DRAFT).build(); + Objective objective3 = Objective.Builder.builder().withId(10L).withTitle("Objective 3").withState(DRAFT).build(); + Objective objectiveAlignedObjective = Objective.Builder.builder().withId(42L).withTitle("Objective 42") + .withState(DRAFT).withAlignedEntityId("O8").build(); + Objective keyResultAlignedObjective = Objective.Builder.builder().withId(45L).withTitle("Objective 45") + .withState(DRAFT).withAlignedEntityId("K5").build(); + Objective wrongAlignedObjective = Objective.Builder.builder().withId(48L).withTitle("Objective 48").withState(DRAFT) + .withAlignedEntityId("Hello").build(); + KeyResult metricKeyResult = KeyResultMetric.Builder.builder().withId(5L).withTitle("KR Title 1").build(); + ObjectiveAlignment objectiveALignment = ObjectiveAlignment.Builder.builder().withId(1L) + .withAlignedObjective(objective1).withTargetObjective(objective2).build(); + ObjectiveAlignment objectiveAlignment2 = ObjectiveAlignment.Builder.builder().withId(2L) + .withAlignedObjective(objective2).withTargetObjective(objective1).build(); + KeyResultAlignment keyResultAlignment = KeyResultAlignment.Builder.builder().withId(6L) + .withAlignedObjective(objective3).withTargetKeyResult(metricKeyResult).build(); + + @Test + void shouldGetTargetAlignmentIdObjective() { + when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(objectiveALignment); + String targetId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); + + assertEquals("O8", targetId); + verify(validator, times(1)).validateOnGet(5L); + } + + @Test + void shouldReturnNullWhenNoAlignmentFound() { + when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(null); + String targetId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); + + assertNull(targetId); + verify(validator, times(1)).validateOnGet(5L); + } + + @Test + void shouldGetTargetAlignmentIdKeyResult() { + when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(keyResultAlignment); + String targetId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); + + assertEquals("K5", targetId); + verify(validator, times(1)).validateOnGet(5L); + } + + @Test + void shouldCreateNewAlignment() { + when(objectivePersistenceService.findById(8L)).thenReturn(objective1); + + Alignment returnAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objectiveAlignedObjective) + .withTargetObjective(objective1).build(); + alignmentBusinessService.createEntity(objectiveAlignedObjective); + + verify(alignmentPersistenceService, times(1)).save(returnAlignment); + verify(validator, times(1)).validateOnCreate(returnAlignment); + } + + @Test + void shouldUpdateEntityNewAlignment() { + when(alignmentPersistenceService.findByAlignedObjectiveId(8L)).thenReturn(null); + when(objectivePersistenceService.findById(8L)).thenReturn(objective1); + + Alignment returnAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objectiveAlignedObjective) + .withTargetObjective(objective1).build(); + alignmentBusinessService.updateEntity(8L, objectiveAlignedObjective); + + verify(alignmentPersistenceService, times(1)).save(returnAlignment); + verify(validator, times(1)).validateOnCreate(returnAlignment); + } + + @Test + void shouldUpdateEntityDeleteAlignment() { + when(alignmentPersistenceService.findByAlignedObjectiveId(8L)).thenReturn(objectiveAlignment2); + + alignmentBusinessService.updateEntity(8L, objective3); + + verify(alignmentPersistenceService, times(1)).deleteById(2L); + verify(validator, times(1)).validateOnDelete(2L); + + } + + @Test + void shouldUpdateEntityChangeTargetId() { + when(alignmentPersistenceService.findByAlignedObjectiveId(8L)).thenReturn(objectiveAlignment2); + when(objectivePersistenceService.findById(8L)).thenReturn(objective1); + Alignment returnAlignment = ObjectiveAlignment.Builder.builder().withId(2L).withAlignedObjective(objectiveAlignedObjective) + .withTargetObjective(objective1).build(); + + alignmentBusinessService.updateEntity(8L, objectiveAlignedObjective); + + verify(alignmentPersistenceService, times(1)).save(returnAlignment); + verify(validator, times(1)).validateOnUpdate(returnAlignment.getId(), returnAlignment); + } + + @Test + void shouldUpdateEntityChangeObjectiveToKeyResult() { + when(alignmentPersistenceService.findByAlignedObjectiveId(8L)).thenReturn(objectiveAlignment2); + when(keyResultPersistenceService.findById(5L)).thenReturn(metricKeyResult); + Alignment returnAlignment = KeyResultAlignment.Builder.builder().withId(2L).withAlignedObjective(keyResultAlignedObjective) + .withTargetKeyResult(metricKeyResult).build(); + + alignmentBusinessService.updateEntity(8L, keyResultAlignedObjective); + + verify(alignmentPersistenceService, times(0)).save(returnAlignment); + verify(validator, times(1)).validateOnUpdate(returnAlignment.getId(), returnAlignment); + verify(alignmentPersistenceService, times(1)).recreateEntity(2L, returnAlignment); + } + + @Test + void shouldBuildAlignmentCorrectObjective() { + when(objectivePersistenceService.findById(8L)).thenReturn(objective1); + + Alignment returnAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objectiveAlignedObjective) + .withTargetObjective(objective1).build(); + Alignment alignment = alignmentBusinessService.buildAlignmentModel(objectiveAlignedObjective); + + assertEquals(returnAlignment, alignment); + assertTrue(alignment instanceof ObjectiveAlignment); + } + + @Test + void shouldBuildAlignmentCorrectKeyResult() { + when(keyResultPersistenceService.findById(5L)).thenReturn(metricKeyResult); + + Alignment returnAlignment = KeyResultAlignment.Builder.builder().withAlignedObjective(keyResultAlignedObjective) + .withTargetKeyResult(metricKeyResult).build(); + Alignment alignment = alignmentBusinessService.buildAlignmentModel(keyResultAlignedObjective); + + assertEquals(returnAlignment, alignment); + assertTrue(alignment instanceof KeyResultAlignment); + } + + @Test + void shouldThrowErrorWhenAlignedEntityIdIsIncorrect() { + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> alignmentBusinessService.buildAlignmentModel(wrongAlignedObjective)); + + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NOT_SET", List.of("alignedEntityId", "Hello"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void shouldReturnCorrectIsAlignmentTypeChange() { + assertTrue(alignmentBusinessService.isAlignmentTypeChange(keyResultAlignment, objectiveALignment)); + assertTrue(alignmentBusinessService.isAlignmentTypeChange(objectiveALignment, keyResultAlignment)); + assertFalse(alignmentBusinessService.isAlignmentTypeChange(objectiveALignment, objectiveALignment)); + assertFalse(alignmentBusinessService.isAlignmentTypeChange(keyResultAlignment, keyResultAlignment)); + } + + @Test + void shouldUpdateKeyResultIdOnChange() { + when(alignmentPersistenceService.findByKeyResultAlignmentId(1L)).thenReturn(List.of(keyResultAlignment)); + + alignmentBusinessService.updateKeyResultIdOnIdChange(1L, metricKeyResult); + keyResultAlignment.setAlignmentTarget(metricKeyResult); + verify(alignmentPersistenceService, times(1)).save(keyResultAlignment); + verify(validator, times(1)).validateOnUpdate(keyResultAlignment.getId(), keyResultAlignment); + } + + @Test + void shouldDeleteByObjectiveId() { + when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(objectiveALignment); + when(alignmentPersistenceService.findByObjectiveAlignmentId(5L)).thenReturn(List.of(objectiveAlignment2)); + + alignmentBusinessService.deleteAlignmentByObjectiveId(5L); + + verify(alignmentPersistenceService, times(1)).deleteById(objectiveALignment.getId()); + verify(alignmentPersistenceService, times(1)).deleteById(objectiveAlignment2.getId()); + } + + @Test + void shouldDeleteByKeyResultId() { + when(alignmentPersistenceService.findByKeyResultAlignmentId(5L)).thenReturn(List.of(keyResultAlignment)); + + alignmentBusinessService.deleteAlignmentByKeyResultId(5L); + + verify(alignmentPersistenceService, times(1)).deleteById(keyResultAlignment.getId()); + } +} diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/KeyResultBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/KeyResultBusinessServiceTest.java index e272bbbb8a..51ca684c6e 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/KeyResultBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/KeyResultBusinessServiceTest.java @@ -42,6 +42,8 @@ class KeyResultBusinessServiceTest { KeyResultValidationService validator; @Mock ActionBusinessService actionBusinessService; + @Mock + AlignmentBusinessService alignmentBusinessService; @InjectMocks private KeyResultBusinessService keyResultBusinessService; List keyResults; @@ -154,6 +156,7 @@ void shouldEditMetricKeyResultWhenATypeChange() { verify(checkInBusinessService, times(1)).getCheckInsByKeyResultId(1L); verify(actionBusinessService, times(1)).deleteEntitiesByKeyResultId(1L); verify(actionBusinessService, times(1)).createEntities(actions); + verify(alignmentBusinessService, times(1)).updateKeyResultIdOnIdChange(1L, newKeyresult); assertEquals(1L, newKeyresult.getId()); assertEquals("Keyresult Metric update", newKeyresult.getTitle()); } @@ -172,6 +175,7 @@ void shouldEditOrdinalKeyResultWhenATypeChange() { verify(checkInBusinessService, times(1)).getCheckInsByKeyResultId(1L); verify(actionBusinessService, times(1)).deleteEntitiesByKeyResultId(1L); verify(actionBusinessService, times(1)).createEntities(actions); + verify(alignmentBusinessService, times(1)).updateKeyResultIdOnIdChange(1L, newKeyresult); assertEquals(1L, newKeyresult.getId()); assertEquals("Keyresult Ordinal update", newKeyresult.getTitle()); } @@ -324,6 +328,7 @@ void shouldDeleteKeyResultAndAssociatedCheckInsAndActions() { verify(checkInBusinessService, times(1)).deleteEntityById(1L); verify(actionBusinessService, times(2)).deleteEntityById(3L); + verify(alignmentBusinessService, times(1)).deleteAlignmentByKeyResultId(1L); verify(keyResultPersistenceService, times(1)).deleteById(1L); } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java index f7360742c3..05da1abca3 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java @@ -1,5 +1,6 @@ package ch.puzzle.okr.service.business; +import ch.puzzle.okr.dto.ObjectiveAlignmentsDto; import ch.puzzle.okr.models.*; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.models.keyresult.KeyResult; @@ -40,6 +41,8 @@ class ObjectiveBusinessServiceTest { @Mock KeyResultBusinessService keyResultBusinessService; @Mock + AlignmentBusinessService alignmentBusinessService; + @Mock CompletedBusinessService completedBusinessService; @Mock ObjectiveValidationService validator = Mockito.mock(ObjectiveValidationService.class); @@ -52,9 +55,13 @@ class ObjectiveBusinessServiceTest { private final Objective fullObjective = Objective.Builder.builder().withTitle("FullObjective1").withCreatedBy(user) .withTeam(team1).withQuarter(quarter).withDescription("This is our description") .withModifiedOn(LocalDateTime.MAX).build(); + private final Objective fullObjective2 = Objective.Builder.builder().withTitle("FullObjective2").withCreatedBy(user) + .withTeam(team1).withQuarter(quarter).withDescription("This is our description") + .withModifiedOn(LocalDateTime.MAX).build(); private final KeyResult ordinalKeyResult = KeyResultOrdinal.Builder.builder().withCommitZone("Baum") .withStretchZone("Wald").withId(5L).withTitle("Keyresult Ordinal").withObjective(objective).build(); private final List keyResultList = List.of(ordinalKeyResult, ordinalKeyResult, ordinalKeyResult); + private final List objectiveList = List.of(objective, fullObjective, fullObjective2); @Test void getOneObjective() { @@ -62,6 +69,7 @@ void getOneObjective() { Objective realObjective = objectiveBusinessService.getEntityById(5L); + verify(alignmentBusinessService, times(1)).getTargetIdByAlignedObjectiveId(5L); assertEquals("Objective 1", realObjective.getTitle()); } @@ -71,6 +79,7 @@ void getEntitiesByTeamId() { List entities = objectiveBusinessService.getEntitiesByTeamId(5L); + verify(alignmentBusinessService, times(1)).getTargetIdByAlignedObjectiveId(5L); assertThat(entities).hasSameElementsAs(List.of(objective)); } @@ -96,6 +105,24 @@ void shouldSaveANewObjective() { objectiveBusinessService.createEntity(objective, authorizationUser); verify(objectivePersistenceService, times(1)).save(objective); + verify(alignmentBusinessService, times(0)).createEntity(any()); + assertEquals(DRAFT, objective.getState()); + assertEquals(user, objective.getCreatedBy()); + assertNull(objective.getCreatedOn()); + } + + @Test + void shouldSaveANewObjectiveWithAlignment() { + Objective objective = spy(Objective.Builder.builder().withTitle("Received Objective").withTeam(team1) + .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) + .withState(DRAFT).withAlignedEntityId("O42").build()); + + doNothing().when(objective).setCreatedOn(any()); + + Objective savedObjective = objectiveBusinessService.createEntity(objective, authorizationUser); + + verify(objectivePersistenceService, times(1)).save(objective); + verify(alignmentBusinessService, times(1)).createEntity(savedObjective); assertEquals(DRAFT, objective.getState()); assertEquals(user, objective.getCreatedBy()); assertNull(objective.getCreatedOn()); @@ -143,6 +170,7 @@ void updateEntityShouldHandleQuarterCorrectly(boolean hasKeyResultAnyCheckIns) { updatedEntity.getQuarter()); assertEquals(changedObjective.getDescription(), updatedEntity.getDescription()); assertEquals(changedObjective.getTitle(), updatedEntity.getTitle()); + verify(alignmentBusinessService, times(1)).updateEntity(updatedEntity.getId(), updatedEntity); } @Test @@ -153,6 +181,7 @@ void shouldDeleteObjectiveAndAssociatedKeyResults() { verify(keyResultBusinessService, times(3)).deleteEntityById(5L); verify(objectiveBusinessService, times(1)).deleteEntityById(1L); + verify(alignmentBusinessService, times(1)).deleteAlignmentByObjectiveId(1L); } @Test @@ -170,4 +199,22 @@ void shouldDuplicateObjective() { verify(keyResultBusinessService, times(4)).createEntity(any(), any()); verify(objectiveBusinessService, times(1)).createEntity(any(), any()); } + + @Test + void shouldReturnAlignmentPossibilities() { + when(objectivePersistenceService.findObjectiveByQuarterId(anyLong())).thenReturn(objectiveList); + when(keyResultBusinessService.getAllKeyResultsByObjective(anyLong())).thenReturn(keyResultList); + + List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); + + verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(5L); + assertEquals("O - Objective 1", alignmentsDtos.get(0).objectiveTitle()); + assertEquals(5, alignmentsDtos.get(0).objectiveId()); + assertEquals("K - Keyresult Ordinal", alignmentsDtos.get(0).keyResultAlignmentsDtos().get(0).keyResultTitle()); + assertEquals("O - FullObjective1", alignmentsDtos.get(1).objectiveTitle()); + assertEquals("O - FullObjective2", alignmentsDtos.get(2).objectiveTitle()); + assertEquals(List.of(), alignmentsDtos.get(2).keyResultAlignmentsDtos()); + + } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java index 8bb744a9aa..9b8f8425c9 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java @@ -253,4 +253,12 @@ void countByTeamAndQuarterShouldReturnCountValue() { assertEquals(2, count); } + + @Test + void findObjectiveByQuarterId() { + List objectiveList = objectivePersistenceService.findObjectiveByQuarterId(2L); + + assertEquals(7, objectiveList.size()); + assertEquals("Wir wollen die Kundenzufriedenheit steigern", objectiveList.get(0).getTitle()); + } } From fd8a393006f288e8a61d079faf4be023e98d5f70 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 12 Feb 2024 16:10:18 +0100 Subject: [PATCH 007/119] Write AlignmentPersistenceService tests --- .../AlignmentPersistenceService.java | 4 +- .../AlignmentBusinessServiceTest.java | 5 -- .../AlignmentPersistenceServiceIT.java | 80 +++++++++++++++++++ 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java index 8f3c8ffaec..8a25281006 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java @@ -24,13 +24,13 @@ public String getModelName() { } @Transactional - public void recreateEntity(Long id, Alignment alignment) { + public Alignment recreateEntity(Long id, Alignment alignment) { System.out.println(alignment.toString()); System.out.println("*".repeat(30)); // delete entity in order to prevent duplicates in case of changed keyResultType deleteById(id); System.out.printf("reached delete entity with %d", id); - save(alignment); + return save(alignment); } public Alignment findByAlignedObjectiveId(Long alignedObjectiveId) { diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java index 2c18571bbf..c0d2f96162 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -24,7 +24,6 @@ import static ch.puzzle.okr.models.State.DRAFT; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import static org.springframework.http.HttpStatus.BAD_REQUEST; @@ -96,7 +95,6 @@ void shouldCreateNewAlignment() { alignmentBusinessService.createEntity(objectiveAlignedObjective); verify(alignmentPersistenceService, times(1)).save(returnAlignment); - verify(validator, times(1)).validateOnCreate(returnAlignment); } @Test @@ -133,7 +131,6 @@ void shouldUpdateEntityChangeTargetId() { alignmentBusinessService.updateEntity(8L, objectiveAlignedObjective); verify(alignmentPersistenceService, times(1)).save(returnAlignment); - verify(validator, times(1)).validateOnUpdate(returnAlignment.getId(), returnAlignment); } @Test @@ -146,7 +143,6 @@ void shouldUpdateEntityChangeObjectiveToKeyResult() { alignmentBusinessService.updateEntity(8L, keyResultAlignedObjective); verify(alignmentPersistenceService, times(0)).save(returnAlignment); - verify(validator, times(1)).validateOnUpdate(returnAlignment.getId(), returnAlignment); verify(alignmentPersistenceService, times(1)).recreateEntity(2L, returnAlignment); } @@ -201,7 +197,6 @@ void shouldUpdateKeyResultIdOnChange() { alignmentBusinessService.updateKeyResultIdOnIdChange(1L, metricKeyResult); keyResultAlignment.setAlignmentTarget(metricKeyResult); verify(alignmentPersistenceService, times(1)).save(keyResultAlignment); - verify(validator, times(1)).validateOnUpdate(keyResultAlignment.getId(), keyResultAlignment); } @Test diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java index 6eff1fb0b3..ec47fb24c7 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java @@ -7,6 +7,7 @@ import ch.puzzle.okr.models.alignment.Alignment; import ch.puzzle.okr.models.alignment.KeyResultAlignment; import ch.puzzle.okr.models.alignment.ObjectiveAlignment; +import ch.puzzle.okr.models.keyresult.KeyResult; import ch.puzzle.okr.models.keyresult.KeyResultMetric; import ch.puzzle.okr.test.SpringIntegrationTest; import org.junit.jupiter.api.AfterEach; @@ -16,8 +17,10 @@ import java.util.List; +import static ch.puzzle.okr.models.State.DRAFT; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY; @SpringIntegrationTest @@ -130,6 +133,83 @@ void findByObjectiveAlignmentIdShouldReturnListOfAlignments() { assertAlignment(alignments.get(0)); } + @Test + void recreateEntityShouldUpdateAlignmentNoTypeChange() { + Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withState(DRAFT).build(); + Objective objective2 = Objective.Builder.builder().withId(8L).withTitle("Objective 2").withState(DRAFT).build(); + Objective objective3 = Objective.Builder.builder().withId(4L) + .withTitle("Build a company culture that kills the competition.").build(); + ObjectiveAlignment objectiveALignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective1) + .withTargetObjective(objective2).build(); + + createdAlignment = alignmentPersistenceService.save(objectiveALignment); + ObjectiveAlignment createObjectiveAlignment = (ObjectiveAlignment) createdAlignment; + createObjectiveAlignment.setAlignmentTarget(objective3); + Long alignmentId = createObjectiveAlignment.getId(); + + Alignment recreatedAlignment = alignmentPersistenceService.recreateEntity(createdAlignment.getId(), + createObjectiveAlignment); + + createObjectiveAlignment = (ObjectiveAlignment) recreatedAlignment; + + assertNotNull(recreatedAlignment.getId()); + assertEquals(4L, createObjectiveAlignment.getAlignmentTarget().getId()); + assertEquals("Build a company culture that kills the competition.", + createObjectiveAlignment.getAlignmentTarget().getTitle()); + + // Should delete the old Alignment + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> alignmentPersistenceService.findById(alignmentId)); + + List expectedErrors = List.of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of("Alignment", "200"))); + + assertEquals(NOT_FOUND, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + + // delete re-created Alignment in tearDown() + createdAlignment = recreatedAlignment; + } + + @Test + void recreateEntityShouldUpdateAlignmentWithTypeChange() { + Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withState(DRAFT).build(); + Objective objective2 = Objective.Builder.builder().withId(8L).withTitle("Objective 2").withState(DRAFT).build(); + KeyResult keyResult = KeyResultMetric.Builder.builder().withId(10L) + .withTitle("Im Durchschnitt soll die Lautstärke 60dB nicht überschreiten").build(); + ObjectiveAlignment objectiveALignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective1) + .withTargetObjective(objective2).build(); + + createdAlignment = alignmentPersistenceService.save(objectiveALignment); + + KeyResultAlignment keyResultAlignment = KeyResultAlignment.Builder.builder().withId(createdAlignment.getId()) + .withAlignedObjective(objective1).withTargetKeyResult(keyResult).build(); + + Alignment recreatedAlignment = alignmentPersistenceService.recreateEntity(keyResultAlignment.getId(), + keyResultAlignment); + + KeyResultAlignment returnedKeyResultAlignment = (KeyResultAlignment) recreatedAlignment; + + assertNotNull(recreatedAlignment.getId()); + assertEquals(createdAlignment.getAlignedObjective().getId(), recreatedAlignment.getAlignedObjective().getId()); + assertEquals("Im Durchschnitt soll die Lautstärke 60dB nicht überschreiten", + returnedKeyResultAlignment.getAlignmentTarget().getTitle()); + + Long alignmentId = createdAlignment.getId(); + // Should delete the old Alignment + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> alignmentPersistenceService.findById(alignmentId)); + + List expectedErrors = List.of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of("Alignment", 200L))); + + assertEquals(NOT_FOUND, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + + // delete re-created Alignment in tearDown() + createdAlignment = recreatedAlignment; + } + private void assertAlignment(Alignment alignment) { if (alignment instanceof ObjectiveAlignment objectiveAlignment) { assertAlignment(objectiveAlignment); From 9b6a4073dadb380b68e116ce22dfb16878f295c3 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 12 Feb 2024 16:30:11 +0100 Subject: [PATCH 008/119] Fix AlignmentPersistenceService tests --- .../persistence/AlignmentPersistenceServiceIT.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java index ec47fb24c7..6fb132da9a 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java @@ -161,14 +161,15 @@ void recreateEntityShouldUpdateAlignmentNoTypeChange() { OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> alignmentPersistenceService.findById(alignmentId)); - List expectedErrors = List.of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of("Alignment", "200"))); + List expectedErrors = List + .of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of("Alignment", alignmentId))); assertEquals(NOT_FOUND, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); // delete re-created Alignment in tearDown() - createdAlignment = recreatedAlignment; + createdAlignment = createObjectiveAlignment; } @Test @@ -200,14 +201,15 @@ void recreateEntityShouldUpdateAlignmentWithTypeChange() { OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> alignmentPersistenceService.findById(alignmentId)); - List expectedErrors = List.of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of("Alignment", 200L))); + List expectedErrors = List + .of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of("Alignment", alignmentId))); assertEquals(NOT_FOUND, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); // delete re-created Alignment in tearDown() - createdAlignment = recreatedAlignment; + createdAlignment = returnedKeyResultAlignment; } private void assertAlignment(Alignment alignment) { @@ -229,6 +231,6 @@ private void assertAlignment(ObjectiveAlignment objectiveAlignment) { private void assertAlignment(KeyResultAlignment keyResultAlignment) { assertEquals(2L, keyResultAlignment.getId()); assertEquals(8L, keyResultAlignment.getAlignmentTarget().getId()); - assertEquals(4L, keyResultAlignment.getAlignedObjective().getId()); + assertEquals(9L, keyResultAlignment.getAlignedObjective().getId()); } } From d81858e710545acd9e5918208d5b59d7f0e8e53a Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 12 Feb 2024 16:32:40 +0100 Subject: [PATCH 009/119] Fix frontend model error --- frontend/src/app/shared/testData.ts | 2 ++ frontend/src/app/shared/types/model/Objective.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index 930e424e55..f60920a318 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -364,6 +364,7 @@ export const objective: Objective = { quarterLabel: 'GJ 22/23-Q2', state: State.SUCCESSFUL, writeable: true, + alignedEntityId: null, }; export const objectiveWriteableFalse: Objective = { @@ -376,6 +377,7 @@ export const objectiveWriteableFalse: Objective = { quarterLabel: 'GJ 22/23-Q2', state: State.NOTSUCCESSFUL, writeable: false, + alignedEntityId: null, }; export const firstCheckIn: CheckInMin = { diff --git a/frontend/src/app/shared/types/model/Objective.ts b/frontend/src/app/shared/types/model/Objective.ts index eaef2c04bc..020d829755 100644 --- a/frontend/src/app/shared/types/model/Objective.ts +++ b/frontend/src/app/shared/types/model/Objective.ts @@ -14,5 +14,5 @@ export interface Objective { modifiedOn?: Date; createdBy?: User; writeable: boolean; - alignedEntityId: string; + alignedEntityId: string | null; } From 105a7d67bf92af7ae37d4456d2eda84e548e9773 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 15 Feb 2024 07:30:00 +0100 Subject: [PATCH 010/119] Write AlignmentValidationService tests --- .../AlignmentValidationService.java | 10 +- .../AlignmentValidationServiceTest.java | 296 ++++++++++++++++++ 2 files changed, 301 insertions(+), 5 deletions(-) create mode 100644 backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java diff --git a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java index 740d2a772b..2fda1896d6 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java @@ -43,24 +43,24 @@ public void validateOnUpdate(Long id, Alignment model) { validate(model); } - private static void throwExceptionWhenAlignedObjectIsNull(Alignment model) { + private void throwExceptionWhenAlignedObjectIsNull(Alignment model) { if (model.getAlignedObjective() == null) { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, - List.of("alignedObjectiveId", model.getAlignedObjective().getId())); + List.of("alignedObjectiveId")); } else if (model instanceof ObjectiveAlignment objectiveAlignment) { if (objectiveAlignment.getAlignmentTarget() == null) { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, - List.of("targetObjectiveId", objectiveAlignment.getAlignmentTarget().getId())); + List.of("targetObjectiveId", objectiveAlignment.getAlignedObjective().getId())); } } else if (model instanceof KeyResultAlignment keyResultAlignment) { if (keyResultAlignment.getAlignmentTarget() == null) { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, - List.of("targetKeyResultId", keyResultAlignment.getAlignmentTarget().getId())); + List.of("targetKeyResultId", keyResultAlignment.getAlignedObjective().getId())); } } } - private static void throwExceptionWhenAlignedIdIsSameAsTargetId(Alignment model) { + private void throwExceptionWhenAlignedIdIsSameAsTargetId(Alignment model) { if (model instanceof ObjectiveAlignment objectiveAlignment) { if (Objects.equals(objectiveAlignment.getAlignedObjective().getId(), objectiveAlignment.getAlignmentTarget().getId())) { diff --git a/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java new file mode 100644 index 0000000000..e9fd333dc3 --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java @@ -0,0 +1,296 @@ +package ch.puzzle.okr.service.validation; + +import ch.puzzle.okr.TestHelper; +import ch.puzzle.okr.dto.ErrorDto; +import ch.puzzle.okr.exception.OkrResponseStatusException; +import ch.puzzle.okr.models.Objective; +import ch.puzzle.okr.models.alignment.KeyResultAlignment; +import ch.puzzle.okr.models.alignment.ObjectiveAlignment; +import ch.puzzle.okr.models.keyresult.KeyResult; +import ch.puzzle.okr.models.keyresult.KeyResultMetric; +import ch.puzzle.okr.service.persistence.AlignmentPersistenceService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static ch.puzzle.okr.models.State.DRAFT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +@ExtendWith(MockitoExtension.class) +class AlignmentValidationServiceTest { + + @Mock + AlignmentPersistenceService alignmentPersistenceService; + @Spy + @InjectMocks + private AlignmentValidationService validator; + + Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withState(DRAFT).build(); + Objective objective2 = Objective.Builder.builder().withId(8L).withTitle("Objective 2").withState(DRAFT).build(); + Objective objective3 = Objective.Builder.builder().withId(10L).withTitle("Objective 3").withState(DRAFT).build(); + KeyResult metricKeyResult = KeyResultMetric.Builder.builder().withId(5L).withTitle("KR Title 1").build(); + ObjectiveAlignment objectiveALignment = ObjectiveAlignment.Builder.builder().withId(1L) + .withAlignedObjective(objective1).withTargetObjective(objective2).build(); + ObjectiveAlignment createAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective2) + .withTargetObjective(objective1).build(); + KeyResultAlignment keyResultAlignment = KeyResultAlignment.Builder.builder().withId(6L) + .withAlignedObjective(objective3).withTargetKeyResult(metricKeyResult).build(); + + @BeforeEach + void setUp() { + Mockito.lenient().when(alignmentPersistenceService.getModelName()).thenReturn("Alignment"); + } + + @Test + void validateOnGetShouldBeSuccessfulWhenValidActionId() { + validator.validateOnGet(1L); + + verify(validator, times(1)).validateOnGet(1L); + verify(validator, times(1)).throwExceptionWhenIdIsNull(1L); + } + + @Test + void validateOnGetShouldThrowExceptionIfIdIsNull() { + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnGet(null)); + + verify(validator, times(1)).throwExceptionWhenIdIsNull(null); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertEquals("ATTRIBUTE_NULL", exception.getReason()); + assertEquals(List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("ID", "Alignment"))), exception.getErrors()); + } + + @Test + void validateOnCreateShouldBeSuccessfulWhenAlignmentIsValid() { + when(alignmentPersistenceService.findByAlignedObjectiveId(anyLong())).thenReturn(null); + + validator.validateOnCreate(createAlignment); + + verify(validator, times(1)).throwExceptionWhenModelIsNull(createAlignment); + verify(validator, times(1)).throwExceptionWhenIdIsNotNull(null); + verify(alignmentPersistenceService, times(1)).findByAlignedObjectiveId(8L); + verify(validator, times(1)).validate(createAlignment); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenModelIsNull() { + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(null)); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertEquals("MODEL_NULL", exception.getReason()); + assertEquals(List.of(new ErrorDto("MODEL_NULL", List.of("Alignment"))), exception.getErrors()); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenIdIsNotNull() { + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(keyResultAlignment)); + + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NOT_NULL", List.of("ID", "Alignment"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenAlignedObjectiveIsNull() { + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withTargetObjective(objective2) + .build(); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(objectiveAlignment)); + + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("alignedObjectiveId"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenTargetObjectiveIsNull() { + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective2) + .build(); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(objectiveAlignment)); + + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetObjectiveId", "8"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenTargetKeyResultIsNull() { + KeyResultAlignment wrongKeyResultAlignment = KeyResultAlignment.Builder.builder() + .withAlignedObjective(objective2).build(); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(wrongKeyResultAlignment)); + + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetKeyResultId", "8"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenAlignedIdIsSameAsTargetId() { + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective2) + .withTargetObjective(objective2).build(); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(objectiveAlignment)); + + List expectedErrors = List.of(new ErrorDto("NOT_LINK_YOURSELF", List.of("targetObjectiveId", "8"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenAlignedObjectiveAlreadyExists() { + when(alignmentPersistenceService.findByAlignedObjectiveId(anyLong())).thenReturn(objectiveALignment); + + ObjectiveAlignment createAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective1).withTargetObjective(objective2).build(); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(createAlignment)); + + List expectedErrors = List.of(new ErrorDto("ALIGNMENT_ALREADY_EXISTS", List.of("alignedObjectiveId", "5"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnUpdateShouldBeSuccessfulWhenAlignmentIsValid() { + validator.validateOnUpdate(objectiveALignment.getId(), objectiveALignment); + + verify(validator, times(1)).throwExceptionWhenModelIsNull(objectiveALignment); + verify(validator, times(1)).throwExceptionWhenIdIsNull(objectiveALignment.getId()); + verify(validator, times(1)).validate(objectiveALignment); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenModelIsNull() { + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(1L, null)); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertEquals("MODEL_NULL", exception.getReason()); + assertEquals(List.of(new ErrorDto("MODEL_NULL", List.of("Alignment"))), exception.getErrors()); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenIdIsNull() { + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().build(); + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(null, objectiveAlignment)); + + verify(validator, times(1)).throwExceptionWhenModelIsNull(objectiveAlignment); + verify(validator, times(1)).throwExceptionWhenIdIsNull(null); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertEquals("ATTRIBUTE_NULL", exception.getReason()); + assertEquals(List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("ID", "Alignment"))), exception.getErrors()); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenAlignedObjectiveIsNull() { + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withId(3L) + .withTargetObjective(objective2).build(); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(3L, objectiveAlignment)); + + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("alignedObjectiveId"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenTargetObjectiveIsNull() { + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withId(3L) + .withAlignedObjective(objective2).build(); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(3L, objectiveAlignment)); + + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetObjectiveId", "8"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenTargetKeyResultIsNull() { + KeyResultAlignment wrongKeyResultAlignment = KeyResultAlignment.Builder.builder().withId(3L) + .withAlignedObjective(objective2).build(); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(3L, wrongKeyResultAlignment)); + + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetKeyResultId", "8"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenAlignedIdIsSameAsTargetId() { + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withId(3L) + .withAlignedObjective(objective2).withTargetObjective(objective2).build(); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(3L, objectiveAlignment)); + + List expectedErrors = List.of(new ErrorDto("NOT_LINK_YOURSELF", List.of("targetObjectiveId", "8"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnDeleteShouldBeSuccessfulWhenValidAlignmentId() { + validator.validateOnDelete(3L); + + verify(validator, times(1)).throwExceptionWhenIdIsNull(3L); + } + + @Test + void validateOnDeleteShouldThrowExceptionIfAlignmentIdIsNull() { + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnDelete(null)); + + verify(validator, times(1)).throwExceptionWhenIdIsNull(null); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertEquals("ATTRIBUTE_NULL", exception.getReason()); + assertEquals(List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("ID", "Alignment"))), exception.getErrors()); + } + +} From 5d719c9781c99ae9f7f1b1429e33f7f6a9e8d099 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 15 Feb 2024 07:50:54 +0100 Subject: [PATCH 011/119] Write test for objective form component --- .../objective-form.component.html | 1 - .../objective-form.component.spec.ts | 176 +++++++++++++++++- .../objective-form.component.ts | 15 +- frontend/src/app/shared/testData.ts | 13 ++ frontend/src/assets/i18n/de.json | 4 +- 5 files changed, 198 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index bf39188c51..6eb0ea6daa 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -87,7 +87,6 @@ -

{{ this.objectiveForm.value.alignment }}

{ let component: ObjectiveFormComponent; let fixture: ComponentFixture; @@ -105,6 +124,7 @@ describe('ObjectiveDialogComponent', () => { { provide: TeamService, useValue: teamService }, ], }); + jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of(alignmentPossibilities)); fixture = TestBed.createComponent(ObjectiveFormComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -170,6 +190,7 @@ describe('ObjectiveDialogComponent', () => { teamId: 2, title: title, writeable: true, + alignedEntityId: null, }, teamId: 1, }); @@ -182,7 +203,7 @@ describe('ObjectiveDialogComponent', () => { description: 'Test description', quarter: 0, team: 0, - relation: 0, + alignment: '', createKeyResults: false, }); @@ -198,6 +219,36 @@ describe('ObjectiveDialogComponent', () => { title: 'Test title', quarterId: 0, teamId: 0, + version: undefined, + alignedEntityId: '', + }); + }); + + it('should create objective with alignment', () => { + matDataMock.objective.objectiveId = undefined; + component.objectiveForm.setValue({ + title: 'Test title with alignment', + description: 'Test description', + quarter: 0, + team: 0, + alignment: 'K37', + createKeyResults: false, + }); + + objectiveService.createObjective.mockReturnValue(of({ ...objective, state: 'DRAFT' })); + component.onSubmit('DRAFT'); + + fixture.detectChanges(); + + expect(objectiveService.createObjective).toHaveBeenCalledWith({ + description: 'Test description', + id: undefined, + state: 'DRAFT', + title: 'Test title with alignment', + quarterId: 0, + teamId: 0, + version: undefined, + alignedEntityId: 'K37', }); }); @@ -208,7 +259,7 @@ describe('ObjectiveDialogComponent', () => { description: 'Test description', quarter: 1, team: 1, - relation: 0, + alignment: '', createKeyResults: false, }); @@ -224,6 +275,36 @@ describe('ObjectiveDialogComponent', () => { title: 'Test title', quarterId: 1, teamId: 1, + version: undefined, + alignedEntityId: '', + }); + }); + + it('should update objective with alignment', () => { + matDataMock.objective.objectiveId = 1; + component.objectiveForm.setValue({ + title: 'Test title with alignment', + description: 'Test description', + quarter: 1, + team: 1, + alignment: 'K37', + createKeyResults: false, + }); + + objectiveService.updateObjective.mockReturnValue(of({ ...objective, state: 'ONGOING' })); + component.onSubmit('DRAFT'); + + fixture.detectChanges(); + + expect(objectiveService.updateObjective).toHaveBeenCalledWith({ + description: 'Test description', + id: 1, + state: 'DRAFT', + title: 'Test title with alignment', + quarterId: 1, + teamId: 1, + version: undefined, + alignedEntityId: 'K37', }); }); @@ -254,6 +335,20 @@ describe('ObjectiveDialogComponent', () => { expect(rawFormValue.quarter).toBe(objective.quarterId); }); + it('should load default values into form onInit with defined objectiveId with an alignment', async () => { + matDataMock.objective.objectiveId = 1; + const routerHarness = await RouterTestingHarness.create(); + await routerHarness.navigateByUrl('/?quarter=2'); + objectiveService.getFullObjective.mockReturnValue(of(objectiveWithAlignment)); + component.ngOnInit(); + const rawFormValue = component.objectiveForm.getRawValue(); + expect(rawFormValue.title).toBe(objectiveWithAlignment.title); + expect(rawFormValue.description).toBe(objectiveWithAlignment.description); + expect(rawFormValue.team).toBe(objectiveWithAlignment.teamId); + expect(rawFormValue.quarter).toBe(objectiveWithAlignment.quarterId); + expect(rawFormValue.alignment).toBe(objectiveWithAlignment.alignedEntityId); + }); + it('should return correct value if allowed to save to backlog', async () => { component.quarters = quarterList; const isBacklogQuarterSpy = jest.spyOn(component, 'isBacklogQuarter'); @@ -333,6 +428,80 @@ describe('ObjectiveDialogComponent', () => { fixture.detectChanges(); expect(component.allowedOption(quarter)).toBeTruthy(); }); + + it('should load correct alignment possibilities', async () => { + let generatedPossibilities = [ + { + objectiveId: null, + objectiveTitle: 'Bitte wählen', + keyResultAlignmentsDtos: [], + }, + { + objectiveId: 1003, + objectiveTitle: 'O - Test Objective', + keyResultAlignmentsDtos: [], + }, + { + objectiveId: 1005, + objectiveTitle: 'O - Company will grow', + keyResultAlignmentsDtos: [ + { + keyResultId: 6, + keyResultTitle: 'K - New structure', + }, + ], + }, + ]; + + let componentValue = null; + component.alignmentPossibilities$.subscribe((value) => { + componentValue = value; + }); + expect(componentValue).toStrictEqual(generatedPossibilities); + }); + + it('should not load current Objective to alignment possibilities', async () => { + matDataMock.objective.objectiveId = 1; + component.objective = objectiveWithAlignment; + const routerHarness = await RouterTestingHarness.create(); + await routerHarness.navigateByUrl('/?quarter=2'); + objectiveService.getFullObjective.mockReturnValue(of(objectiveWithAlignment)); + component.ngOnInit(); + + let generatedPossibilities = [ + { + objectiveId: null, + objectiveTitle: 'Bitte wählen', + keyResultAlignmentsDtos: [], + }, + { + objectiveId: 1003, + objectiveTitle: 'O - Test Objective', + keyResultAlignmentsDtos: [], + }, + { + objectiveId: 1005, + objectiveTitle: 'O - Company will grow', + keyResultAlignmentsDtos: [ + { + keyResultId: 6, + keyResultTitle: 'K - New structure', + }, + ], + }, + ]; + + let componentValue = null; + component.alignmentPossibilities$.subscribe((value) => { + componentValue = value; + }); + expect(componentValue).toStrictEqual(generatedPossibilities); + }); + + it('should call ObjectiveService when updating Alignments', async () => { + component.updateAlignments(); + expect(objectiveService.getAlignmentPossibilities).toHaveBeenCalled(); + }); }); describe('Backlog quarter', () => { @@ -363,6 +532,7 @@ describe('ObjectiveDialogComponent', () => { { provide: ActivatedRoute, useValue: mockActivatedRoute }, ], }); + jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of(alignmentPossibilities)); fixture = TestBed.createComponent(ObjectiveFormComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index e75de9e003..641d423505 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -197,6 +197,7 @@ export class ObjectiveFormComponent implements OnInit { state: 'DRAFT' as State, teamId: 0, quarterId: 0, + alignedEntityId: null, } as Objective; } @@ -243,12 +244,14 @@ export class ObjectiveFormComponent implements OnInit { if (this.objective?.id) { value = value.filter((item) => !(item.objectiveId == this.objective!.id)); } - let noSelectOption: AlignmentPossibility = { - objectiveId: null, - objectiveTitle: 'Bitte wählen', - keyResultAlignmentsDtos: [], - }; - value.unshift(noSelectOption); + if (value[0].objectiveTitle != 'Bitte wählen') { + let noSelectOption: AlignmentPossibility = { + objectiveId: null, + objectiveTitle: 'Bitte wählen', + keyResultAlignmentsDtos: [], + }; + value.unshift(noSelectOption); + } this.alignmentPossibilities$ = of(value); }); } diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index f60920a318..4c0912a567 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -367,6 +367,19 @@ export const objective: Objective = { alignedEntityId: null, }; +export const objectiveWithAlignment: Objective = { + id: 5, + version: 1, + title: 'title', + description: 'description', + teamId: 2, + quarterId: 2, + quarterLabel: 'GJ 22/23-Q2', + state: State.SUCCESSFUL, + writeable: true, + alignedEntityId: 'O6', +}; + export const objectiveWriteableFalse: Objective = { id: 6, version: 1, diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index 669da7212f..66c0b430da 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -53,7 +53,9 @@ "NOT_AUTHORIZED_TO_WRITE": "Du bist nicht autorisiert, um dieses {0} zu bearbeiten.", "NOT_AUTHORIZED_TO_DELETE": "Du bist nicht autorisiert, um dieses {0} zu löschen.", "TOKEN_NULL": "Das erhaltene Token ist null.", - "ILLEGAL_CHANGE_OBJECTIVE_QUARTER": "Element kann nicht in ein anderes Quartal verlegt werden." + "ILLEGAL_CHANGE_OBJECTIVE_QUARTER": "Element kann nicht in ein anderes Quartal verlegt werden.", + "NOT_LINK_YOURSELF": "Das Objective kann nicht auf sich selbst zeigen.", + "ALIGNMENT_ALREADY_EXISTS": "Es existiert bereits ein Alignment ausgehend vom Objective mit der ID {1}." }, "SUCCESS": { "TEAM": { From ff20d0428e0ed23f5a80694af87f662b6aad2e0e Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 15 Feb 2024 07:52:06 +0100 Subject: [PATCH 012/119] Fix backend test --- .../okr/service/business/AlignmentBusinessServiceTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java index c0d2f96162..41ac9dd991 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -65,7 +65,6 @@ void shouldGetTargetAlignmentIdObjective() { String targetId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); assertEquals("O8", targetId); - verify(validator, times(1)).validateOnGet(5L); } @Test @@ -83,7 +82,6 @@ void shouldGetTargetAlignmentIdKeyResult() { String targetId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); assertEquals("K5", targetId); - verify(validator, times(1)).validateOnGet(5L); } @Test @@ -107,7 +105,6 @@ void shouldUpdateEntityNewAlignment() { alignmentBusinessService.updateEntity(8L, objectiveAlignedObjective); verify(alignmentPersistenceService, times(1)).save(returnAlignment); - verify(validator, times(1)).validateOnCreate(returnAlignment); } @Test From 48ce741254bc38a5a8985d6b1e3ea049d9459d2a Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 15 Feb 2024 08:53:51 +0100 Subject: [PATCH 013/119] Fix backend tests --- .../business/ObjectiveBusinessService.java | 8 +- .../ObjectiveBusinessServiceTest.java | 104 ++++++++++++++++-- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java index bbfb280e4e..7cdec3444c 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java @@ -101,8 +101,12 @@ public Objective updateEntity(Long id, Objective objective, AuthorizationUser au logger.debug("quarter has changed and is{}changeable, {}", not, objective); validator.validateOnUpdate(id, objective); savedObjective = objectivePersistenceService.save(objective); - savedObjective.setAlignedEntityId(objective.getAlignedEntityId()); - alignmentBusinessService.updateEntity(id, savedObjective); + String targetAlignmentId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(savedObjective.getId()); + if ((objective.getAlignedEntityId() != null) + || objective.getAlignedEntityId() == null && targetAlignmentId != null) { + savedObjective.setAlignedEntityId(objective.getAlignedEntityId()); + alignmentBusinessService.updateEntity(id, savedObjective); + } return savedObjective; } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java index 05da1abca3..be014484ba 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java @@ -51,7 +51,7 @@ class ObjectiveBusinessServiceTest { private final Quarter quarter = Quarter.Builder.builder().withId(1L).withLabel("GJ 22/23-Q2").build(); private final User user = User.Builder.builder().withId(1L).withFirstname("Bob").withLastname("Kaufmann") .withUsername("bkaufmann").withEmail("kaufmann@puzzle.ch").build(); - private final Objective objective = Objective.Builder.builder().withId(5L).withTitle("Objective 1").build(); + private final Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").build(); private final Objective fullObjective = Objective.Builder.builder().withTitle("FullObjective1").withCreatedBy(user) .withTeam(team1).withQuarter(quarter).withDescription("This is our description") .withModifiedOn(LocalDateTime.MAX).build(); @@ -59,13 +59,13 @@ class ObjectiveBusinessServiceTest { .withTeam(team1).withQuarter(quarter).withDescription("This is our description") .withModifiedOn(LocalDateTime.MAX).build(); private final KeyResult ordinalKeyResult = KeyResultOrdinal.Builder.builder().withCommitZone("Baum") - .withStretchZone("Wald").withId(5L).withTitle("Keyresult Ordinal").withObjective(objective).build(); + .withStretchZone("Wald").withId(5L).withTitle("Keyresult Ordinal").withObjective(objective1).build(); private final List keyResultList = List.of(ordinalKeyResult, ordinalKeyResult, ordinalKeyResult); - private final List objectiveList = List.of(objective, fullObjective, fullObjective2); + private final List objectiveList = List.of(objective1, fullObjective, fullObjective2); @Test void getOneObjective() { - when(objectivePersistenceService.findById(5L)).thenReturn(objective); + when(objectivePersistenceService.findById(5L)).thenReturn(objective1); Objective realObjective = objectiveBusinessService.getEntityById(5L); @@ -75,12 +75,12 @@ void getOneObjective() { @Test void getEntitiesByTeamId() { - when(objectivePersistenceService.findObjectiveByTeamId(anyLong())).thenReturn(List.of(objective)); + when(objectivePersistenceService.findObjectiveByTeamId(anyLong())).thenReturn(List.of(objective1)); List entities = objectiveBusinessService.getEntitiesByTeamId(5L); verify(alignmentBusinessService, times(1)).getTargetIdByAlignedObjectiveId(5L); - assertThat(entities).hasSameElementsAs(List.of(objective)); + assertThat(entities).hasSameElementsAs(List.of(objective1)); } @Test @@ -111,6 +111,94 @@ void shouldSaveANewObjective() { assertNull(objective.getCreatedOn()); } + @Test + void shouldUpdateAnObjective() { + Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) + .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) + .withState(DRAFT).build()); + + when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); + when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn(null); + when(objectivePersistenceService.save(any())).thenReturn(objective); + doNothing().when(objective).setCreatedOn(any()); + + Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, + authorizationUser); + + verify(objectivePersistenceService, times(1)).save(objective); + verify(alignmentBusinessService, times(0)).updateEntity(any(), any()); + assertEquals(objective.getTitle(), updatedObjective.getTitle()); + assertEquals(objective.getTeam(), updatedObjective.getTeam()); + assertNull(objective.getCreatedOn()); + } + + @Test + void shouldUpdateAnObjectiveWithAlignment() { + Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) + .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) + .withState(DRAFT).withAlignedEntityId("O53").build()); + + when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); + when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn("O41"); + when(objectivePersistenceService.save(any())).thenReturn(objective); + doNothing().when(objective).setCreatedOn(any()); + + Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, + authorizationUser); + + objective.setAlignedEntityId("O53"); + + verify(objectivePersistenceService, times(1)).save(objective); + verify(alignmentBusinessService, times(1)).updateEntity(objective.getId(), objective); + assertEquals(objective.getTitle(), updatedObjective.getTitle()); + assertEquals(objective.getTeam(), updatedObjective.getTeam()); + assertNull(objective.getCreatedOn()); + } + + @Test + void shouldUpdateAnObjectiveWithANewAlignment() { + Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) + .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) + .withState(DRAFT).withAlignedEntityId("O53").build()); + + when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); + when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn(null); + when(objectivePersistenceService.save(any())).thenReturn(objective); + doNothing().when(objective).setCreatedOn(any()); + + Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, + authorizationUser); + + objective.setAlignedEntityId("O53"); + + verify(objectivePersistenceService, times(1)).save(objective); + verify(alignmentBusinessService, times(1)).updateEntity(objective.getId(), objective); + assertEquals(objective.getTitle(), updatedObjective.getTitle()); + assertEquals(objective.getTeam(), updatedObjective.getTeam()); + assertNull(objective.getCreatedOn()); + } + + @Test + void shouldUpdateAnObjectiveWithAlignmentDelete() { + Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) + .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) + .withState(DRAFT).build()); + + when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); + when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn("O52"); + when(objectivePersistenceService.save(any())).thenReturn(objective); + doNothing().when(objective).setCreatedOn(any()); + + Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, + authorizationUser); + + verify(objectivePersistenceService, times(1)).save(objective); + verify(alignmentBusinessService, times(1)).updateEntity(objective.getId(), objective); + assertEquals(objective.getTitle(), updatedObjective.getTitle()); + assertEquals(objective.getTeam(), updatedObjective.getTeam()); + assertNull(objective.getCreatedOn()); + } + @Test void shouldSaveANewObjectiveWithAlignment() { Objective objective = spy(Objective.Builder.builder().withTitle("Received Objective").withTeam(team1) @@ -192,10 +280,10 @@ void shouldDuplicateObjective() { KeyResult keyResultMetric2 = KeyResultMetric.Builder.builder().withTitle("Metric2").withUnit(Unit.CHF).build(); List keyResults = List.of(keyResultOrdinal, keyResultOrdinal2, keyResultMetric, keyResultMetric2); - when(objectivePersistenceService.save(any())).thenReturn(objective); + when(objectivePersistenceService.save(any())).thenReturn(objective1); when(keyResultBusinessService.getAllKeyResultsByObjective(anyLong())).thenReturn(keyResults); - objectiveBusinessService.duplicateObjective(objective.getId(), objective, authorizationUser); + objectiveBusinessService.duplicateObjective(objective1.getId(), objective1, authorizationUser); verify(keyResultBusinessService, times(4)).createEntity(any(), any()); verify(objectiveBusinessService, times(1)).createEntity(any(), any()); } From fde9d6dd7299d57af315683bf205cf797a31ce20 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 15 Feb 2024 08:54:21 +0100 Subject: [PATCH 014/119] Write e2e tests --- .../cypress/e2e/objective-alignment.cy.ts | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 frontend/cypress/e2e/objective-alignment.cy.ts diff --git a/frontend/cypress/e2e/objective-alignment.cy.ts b/frontend/cypress/e2e/objective-alignment.cy.ts new file mode 100644 index 0000000000..c667251185 --- /dev/null +++ b/frontend/cypress/e2e/objective-alignment.cy.ts @@ -0,0 +1,128 @@ +import * as users from '../fixtures/users.json'; + +describe('OKR Objective Alignment e2e tests', () => { + beforeEach(() => { + cy.loginAsUser(users.gl); + cy.visit('/?quarter=2'); + }); + + it(`Create Objective with an Alignment`, () => { + cy.getByTestId('add-objective').first().click(); + + cy.getByTestId('title').first().clear().type('Objective with new alignment'); + cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); + cy.get('select#alignment').select('K - Steigern der URS um 25%'); + cy.getByTestId('safe').click(); + + cy.contains('Objective with new alignment'); + cy.getByTestId('objective') + .filter(':contains("Objective with new alignment")') + .last() + .getByTestId('three-dot-menu') + .click() + .wait(500) + .get('.objective-menu-option') + .contains('Objective bearbeiten') + .click(); + + cy.get('select#alignment option:selected').should('contain.text', 'K - Steigern der URS um 25%'); + }); + + it(`Update alignment of Objective`, () => { + cy.getByTestId('add-objective').first().click(); + + cy.getByTestId('title').first().clear().type('We change alignment of this Objective'); + cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); + cy.get('select#alignment').select('K - Steigern der URS um 25%'); + cy.getByTestId('safe').click(); + + cy.contains('We change alignment of this Objective'); + cy.getByTestId('objective') + .filter(':contains("We change alignment of this Objective")') + .last() + .getByTestId('three-dot-menu') + .click() + .wait(500) + .get('.objective-menu-option') + .contains('Objective bearbeiten') + .click(); + + cy.get('select#alignment').select('K - Antwortzeit für Supportanfragen um 33% verkürzen.'); + cy.getByTestId('safe').click(); + + cy.getByTestId('objective') + .filter(':contains("We change alignment of this Objective")') + .last() + .getByTestId('three-dot-menu') + .click() + .wait(500) + .get('.objective-menu-option') + .contains('Objective bearbeiten') + .click(); + + cy.get('select#alignment option:selected').should( + 'contain.text', + 'K - Antwortzeit für Supportanfragen um 33% verkürzen.', + ); + }); + + it(`Delete alignment of Objective`, () => { + cy.getByTestId('add-objective').first().click(); + + cy.getByTestId('title').first().clear().type('We delete the alignment'); + cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); + cy.get('select#alignment').select('K - Steigern der URS um 25%'); + cy.getByTestId('safe').click(); + + cy.contains('We delete the alignment'); + cy.getByTestId('objective') + .filter(':contains("We delete the alignment")') + .last() + .getByTestId('three-dot-menu') + .click() + .wait(500) + .get('.objective-menu-option') + .contains('Objective bearbeiten') + .click(); + + cy.get('select#alignment').select('Bitte wählen'); + cy.getByTestId('safe').click(); + + cy.getByTestId('objective') + .filter(':contains("We delete the alignment")') + .last() + .getByTestId('three-dot-menu') + .click() + .wait(500) + .get('.objective-menu-option') + .contains('Objective bearbeiten') + .click(); + + cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); + }); + + it.only(`Alignment Possibilites change when quarter change`, () => { + cy.visit('/?quarter=3'); + + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('We can link later on this'); + cy.getByTestId('safe').click(); + + cy.visit('/?quarter=2'); + + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('Quarter change objective'); + cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); + cy.get('select#alignment').select(1); + + cy.get('select#alignment option:selected').should( + 'contain.text', + 'O - Wir wollen die Kundenzufriedenheit steigern', + ); + + cy.get('select#quarter').select('GJ 22/23-Q3'); + cy.getByTestId('title').first().clear().type('A new title'); + cy.get('select#alignment').select(1); + cy.get('select#alignment option:selected').should('contain.text', 'O - We can link later on this'); + }); +}); From ade8cf96e1452bd6300d1360cca8edc4e69ddb6e Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 15 Feb 2024 10:23:26 +0100 Subject: [PATCH 015/119] Fix e2e tests --- .../business/AlignmentBusinessService.java | 7 ++++--- .../business/AlignmentBusinessServiceTest.java | 4 ---- .../business/ObjectiveBusinessServiceTest.java | 4 ++-- frontend/cypress/e2e/objective-alignment.cy.ts | 18 ++++++++++-------- frontend/cypress/e2e/tab.cy.ts | 2 ++ 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index 1ee16419bd..f7d8caee6a 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -112,9 +112,10 @@ public void updateKeyResultIdOnIdChange(Long oldId, KeyResult keyResult) { public void deleteAlignmentByObjectiveId(Long objectiveId) { Alignment alignment = alignmentPersistenceService.findByAlignedObjectiveId(objectiveId); - alignmentValidationService.validateOnDelete(alignment.getId()); - alignmentPersistenceService.deleteById(alignment.getId()); - + if (alignment != null) { + alignmentValidationService.validateOnDelete(alignment.getId()); + alignmentPersistenceService.deleteById(alignment.getId()); + } List alignmentList = alignmentPersistenceService.findByObjectiveAlignmentId(objectiveId); alignmentList.forEach(objectiveAlignment -> { alignmentValidationService.validateOnDelete(objectiveAlignment.getId()); diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java index 41ac9dd991..b5c39d55d8 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -34,8 +34,6 @@ class AlignmentBusinessServiceTest { @Mock KeyResultPersistenceService keyResultPersistenceService; @Mock - AlignmentValidationService alignmentValidationService; - @Mock AlignmentPersistenceService alignmentPersistenceService; @Mock AlignmentValidationService validator; @@ -114,8 +112,6 @@ void shouldUpdateEntityDeleteAlignment() { alignmentBusinessService.updateEntity(8L, objective3); verify(alignmentPersistenceService, times(1)).deleteById(2L); - verify(validator, times(1)).validateOnDelete(2L); - } @Test diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java index be014484ba..a21965c1eb 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java @@ -248,7 +248,7 @@ void updateEntityShouldHandleQuarterCorrectly(boolean hasKeyResultAnyCheckIns) { when(keyResultBusinessService.getAllKeyResultsByObjective(savedObjective.getId())).thenReturn(keyResultList); when(keyResultBusinessService.hasKeyResultAnyCheckIns(any())).thenReturn(hasKeyResultAnyCheckIns); when(objectivePersistenceService.save(changedObjective)).thenReturn(updatedObjective); - + when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn(null); boolean isImUsed = objectiveBusinessService.isImUsed(changedObjective); Objective updatedEntity = objectiveBusinessService.updateEntity(changedObjective.getId(), changedObjective, authorizationUser); @@ -258,7 +258,7 @@ void updateEntityShouldHandleQuarterCorrectly(boolean hasKeyResultAnyCheckIns) { updatedEntity.getQuarter()); assertEquals(changedObjective.getDescription(), updatedEntity.getDescription()); assertEquals(changedObjective.getTitle(), updatedEntity.getTitle()); - verify(alignmentBusinessService, times(1)).updateEntity(updatedEntity.getId(), updatedEntity); + verify(alignmentBusinessService, times(0)).updateEntity(any(), any()); } @Test diff --git a/frontend/cypress/e2e/objective-alignment.cy.ts b/frontend/cypress/e2e/objective-alignment.cy.ts index c667251185..7ccb089295 100644 --- a/frontend/cypress/e2e/objective-alignment.cy.ts +++ b/frontend/cypress/e2e/objective-alignment.cy.ts @@ -101,24 +101,26 @@ describe('OKR Objective Alignment e2e tests', () => { cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); }); - it.only(`Alignment Possibilites change when quarter change`, () => { + it(`Alignment Possibilites change when quarter change`, () => { cy.visit('/?quarter=3'); cy.getByTestId('add-objective').first().click(); cy.getByTestId('title').first().clear().type('We can link later on this'); cy.getByTestId('safe').click(); - cy.visit('/?quarter=2'); - cy.getByTestId('add-objective').first().click(); - cy.getByTestId('title').first().clear().type('Quarter change objective'); + cy.getByTestId('title').first().clear().type('There is my other alignment'); cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); + cy.get('select#alignment').select(1); + cy.get('select#alignment option:selected').should('contain.text', 'O - We can link later on this'); - cy.get('select#alignment option:selected').should( - 'contain.text', - 'O - Wir wollen die Kundenzufriedenheit steigern', - ); + cy.getByTestId('cancel').click(); + + cy.visit('/?quarter=4'); + + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('Quarter change objective'); cy.get('select#quarter').select('GJ 22/23-Q3'); cy.getByTestId('title').first().clear().type('A new title'); diff --git a/frontend/cypress/e2e/tab.cy.ts b/frontend/cypress/e2e/tab.cy.ts index f20f3f4889..bca8d6831c 100644 --- a/frontend/cypress/e2e/tab.cy.ts +++ b/frontend/cypress/e2e/tab.cy.ts @@ -213,6 +213,7 @@ describe('Tab workflow tests', () => { editInputFields('Edited by Cypress too'); cy.tabForward(); cy.tabForward(); + cy.tabForward(); cy.realPress('Enter'); cy.contains('Edited by Cypress'); }); @@ -231,6 +232,7 @@ describe('Tab workflow tests', () => { cy.focused().contains('GJ'); cy.realPress('ArrowDown'); cy.tabForward(); + cy.tabForward(); cy.focused().contains('Speichern'); cy.realPress('Enter'); cy.wait(500); From 8a8b455111db8902ac0d7158ec6b012eb9b0a183 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 15 Feb 2024 10:27:00 +0100 Subject: [PATCH 016/119] Adjust dropdown width --- .../dialog/objective-dialog/objective-form.component.html | 4 ++-- .../dialog/objective-dialog/objective-form.component.scss | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index 6eb0ea6daa..412aea1d69 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -53,7 +53,7 @@
Date: Wed, 28 Feb 2024 13:38:33 +0100 Subject: [PATCH 017/119] Add null check for possibility length --- .../shared/dialog/objective-dialog/objective-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 641d423505..175a1032d0 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -244,7 +244,7 @@ export class ObjectiveFormComponent implements OnInit { if (this.objective?.id) { value = value.filter((item) => !(item.objectiveId == this.objective!.id)); } - if (value[0].objectiveTitle != 'Bitte wählen') { + if (value.length == 0 || value[0].objectiveTitle != 'Bitte wählen') { let noSelectOption: AlignmentPossibility = { objectiveId: null, objectiveTitle: 'Bitte wählen', From 4b9666a75a07323dcdc44ebc0bbaf84307bedfa3 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 28 Feb 2024 13:38:54 +0100 Subject: [PATCH 018/119] Fix frontend test --- .../objective-dialog/objective-form.component.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index bbe3b024cf..794e267a6d 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -281,7 +281,9 @@ describe('ObjectiveDialogComponent', () => { }); it('should update objective with alignment', () => { + objectiveService.updateObjective.mockReset(); matDataMock.objective.objectiveId = 1; + component.state = 'DRAFT'; component.objectiveForm.setValue({ title: 'Test title with alignment', description: 'Test description', @@ -292,10 +294,10 @@ describe('ObjectiveDialogComponent', () => { }); objectiveService.updateObjective.mockReturnValue(of({ ...objective, state: 'ONGOING' })); - component.onSubmit('DRAFT'); - fixture.detectChanges(); + component.onSubmit('DRAFT'); + expect(objectiveService.updateObjective).toHaveBeenCalledWith({ description: 'Test description', id: 1, From 676cce1e39980cc6d13499880d36ede956216183 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 28 Feb 2024 13:52:21 +0100 Subject: [PATCH 019/119] Fix e2e test --- .../cypress/e2e/objective-alignment.cy.ts | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/frontend/cypress/e2e/objective-alignment.cy.ts b/frontend/cypress/e2e/objective-alignment.cy.ts index 7ccb089295..540aad6a85 100644 --- a/frontend/cypress/e2e/objective-alignment.cy.ts +++ b/frontend/cypress/e2e/objective-alignment.cy.ts @@ -11,7 +11,13 @@ describe('OKR Objective Alignment e2e tests', () => { cy.getByTestId('title').first().clear().type('Objective with new alignment'); cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); - cy.get('select#alignment').select('K - Steigern der URS um 25%'); + cy.get('select#alignment') + .contains('K - Steigern der URS um 25%') + .then(($option) => { + const optionValue = $option.attr('value'); + cy.get('select#alignment').select(optionValue!); + }); + cy.getByTestId('safe').click(); cy.contains('Objective with new alignment'); @@ -33,7 +39,12 @@ describe('OKR Objective Alignment e2e tests', () => { cy.getByTestId('title').first().clear().type('We change alignment of this Objective'); cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); - cy.get('select#alignment').select('K - Steigern der URS um 25%'); + cy.get('select#alignment') + .contains('K - Steigern der URS um 25%') + .then(($option) => { + const optionValue = $option.attr('value'); + cy.get('select#alignment').select(optionValue!); + }); cy.getByTestId('safe').click(); cy.contains('We change alignment of this Objective'); @@ -47,7 +58,12 @@ describe('OKR Objective Alignment e2e tests', () => { .contains('Objective bearbeiten') .click(); - cy.get('select#alignment').select('K - Antwortzeit für Supportanfragen um 33% verkürzen.'); + cy.get('select#alignment') + .contains('K - Antwortzeit für Supportanfragen um 33% verkürzen.') + .then(($option) => { + const optionValue = $option.attr('value'); + cy.get('select#alignment').select(optionValue!); + }); cy.getByTestId('safe').click(); cy.getByTestId('objective') @@ -71,7 +87,12 @@ describe('OKR Objective Alignment e2e tests', () => { cy.getByTestId('title').first().clear().type('We delete the alignment'); cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); - cy.get('select#alignment').select('K - Steigern der URS um 25%'); + cy.get('select#alignment') + .contains('K - Steigern der URS um 25%') + .then(($option) => { + const optionValue = $option.attr('value'); + cy.get('select#alignment').select(optionValue!); + }); cy.getByTestId('safe').click(); cy.contains('We delete the alignment'); @@ -113,18 +134,27 @@ describe('OKR Objective Alignment e2e tests', () => { cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); cy.get('select#alignment').select(1); - cy.get('select#alignment option:selected').should('contain.text', 'O - We can link later on this'); - cy.getByTestId('cancel').click(); + cy.get('select#alignment option:selected').then(($select) => { + const selectValue = $select.text(); + cy.getByTestId('quarterSelect').select('GJ 23/24-Q1'); + cy.getByTestId('title').first().clear().type('There is our other alignment'); - cy.visit('/?quarter=4'); + cy.get('select#alignment').select(1); - cy.getByTestId('add-objective').first().click(); - cy.getByTestId('title').first().clear().type('Quarter change objective'); + cy.get('select#alignment option:selected').should('not.contain.text', selectValue); + cy.getByTestId('cancel').click(); - cy.get('select#quarter').select('GJ 22/23-Q3'); - cy.getByTestId('title').first().clear().type('A new title'); - cy.get('select#alignment').select(1); - cy.get('select#alignment option:selected').should('contain.text', 'O - We can link later on this'); + cy.visit('/?quarter=4'); + + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('Quarter change objective'); + + cy.get('select#quarter').select('GJ 22/23-Q3'); + cy.getByTestId('title').first().clear().type('A new title'); + cy.get('select#alignment').select(1); + + cy.get('select#alignment option:selected').should('contain.text', selectValue); + }); }); }); From 86ae65583a1f26e9aa29c8e2c3d8e1dc067a7894 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 14 Mar 2024 13:52:14 +0100 Subject: [PATCH 020/119] Remove alignment when objective quarter change --- .../shared/dialog/objective-dialog/objective-form.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 175a1032d0..83c0b08cbc 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -258,6 +258,9 @@ export class ObjectiveFormComponent implements OnInit { updateAlignments() { this.generateAlignmentPossibilities(this.objectiveForm.value.quarter!); + this.objectiveForm.patchValue({ + alignment: 'Onull', + }); } protected readonly getQuarterLabel = getQuarterLabel; From 65c044b98341161beb0e31b9a846dc846c1dae5a Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 25 Apr 2024 07:43:57 +0200 Subject: [PATCH 021/119] Add missing quarter in testdata and fix bug when editing alignment multiple times --- .../service/business/AlignmentBusinessService.java | 14 +++++++------- .../db/h2-db/data-test-h2/V100_0_0__TestData.sql | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index f7d8caee6a..dfef1ee5a4 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -47,7 +47,7 @@ public String getTargetIdByAlignedObjectiveId(Long alignedObjectiveId) { } public void createEntity(Objective alignedObjective) { - Alignment alignment = buildAlignmentModel(alignedObjective); + Alignment alignment = buildAlignmentModel(alignedObjective, 0); alignmentValidationService.validateOnCreate(alignment); alignmentPersistenceService.save(alignment); } @@ -56,7 +56,7 @@ public void updateEntity(Long objectiveId, Objective objective) { Alignment savedAlignment = alignmentPersistenceService.findByAlignedObjectiveId(objectiveId); if (savedAlignment == null) { - Alignment alignment = buildAlignmentModel(objective); + Alignment alignment = buildAlignmentModel(objective, 0); alignmentValidationService.validateOnCreate(alignment); alignmentPersistenceService.save(alignment); } else { @@ -64,7 +64,8 @@ public void updateEntity(Long objectiveId, Objective objective) { alignmentValidationService.validateOnDelete(savedAlignment.getId()); alignmentPersistenceService.deleteById(savedAlignment.getId()); } else { - Alignment alignment = buildAlignmentModel(objective); + Alignment alignment = buildAlignmentModel(objective, savedAlignment.getVersion()); + alignment.setId(savedAlignment.getId()); alignmentValidationService.validateOnUpdate(savedAlignment.getId(), alignment); if (isAlignmentTypeChange(alignment, savedAlignment)) { @@ -76,18 +77,17 @@ public void updateEntity(Long objectiveId, Objective objective) { } } - public Alignment buildAlignmentModel(Objective alignedObjective) { + public Alignment buildAlignmentModel(Objective alignedObjective, int version) { if (alignedObjective.getAlignedEntityId().startsWith("O")) { Objective targetObjective = objectivePersistenceService .findById(Long.valueOf(alignedObjective.getAlignedEntityId().replace("O", ""))); return ObjectiveAlignment.Builder.builder().withAlignedObjective(alignedObjective) - .withTargetObjective(targetObjective).build(); + .withTargetObjective(targetObjective).withVersion(version).build(); } else if (alignedObjective.getAlignedEntityId().startsWith("K")) { KeyResult targetKeyResult = keyResultPersistenceService .findById(Long.valueOf(alignedObjective.getAlignedEntityId().replace("K", ""))); - return KeyResultAlignment.Builder.builder().withAlignedObjective(alignedObjective) - .withTargetKeyResult(targetKeyResult).build(); + .withTargetKeyResult(targetKeyResult).withVersion(version).build(); } else { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NOT_SET, List.of("alignedEntityId", alignedObjective.getAlignedEntityId())); diff --git a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql index 4dc4efad8a..c712d2532c 100644 --- a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql +++ b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql @@ -41,6 +41,7 @@ values (1, 'GJ 22/23-Q4', '2023-04-01', '2023-06-30'), (7, 'GJ 23/24-Q2', '2023-10-01', '2023-12-31'), (8, 'GJ 23/24-Q3', '2024-01-01', '2024-03-31'), (9, 'GJ 23/24-Q4', '2024-04-01', '2024-06-30'), + (10, 'GJ 24/25-Q1', '2024-07-01', '2024-09-30'), (199, 'Backlog', null, null); insert into team (id, version, name) From 22edb786e4c13ee7823cea9c4524ade3b21b9328 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 25 Apr 2024 14:24:41 +0200 Subject: [PATCH 022/119] Add Kein Alignment for removing existing alignment --- .../objective-form.component.html | 1 + .../objective-form.component.spec.ts | 89 ++++++++++++++++++- .../objective-form.component.ts | 35 ++++++-- 3 files changed, 112 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index 412aea1d69..480423313d 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -73,6 +73,7 @@ class="custom-select bg-white select-width" formControlName="alignment" id="alignment" + (change)="changeFirstAlignmentPossibility()" [attr.data-testId]="'alignmentSelect'" > diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index 794e267a6d..808e91fbc4 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -124,7 +124,7 @@ describe('ObjectiveDialogComponent', () => { { provide: TeamService, useValue: teamService }, ], }); - jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of(alignmentPossibilities)); + jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of([])); fixture = TestBed.createComponent(ObjectiveFormComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -432,6 +432,10 @@ describe('ObjectiveDialogComponent', () => { }); it('should load correct alignment possibilities', async () => { + jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of(alignmentPossibilities)); + fixture.detectChanges(); + component.ngOnInit(); + let generatedPossibilities = [ { objectiveId: null, @@ -465,15 +469,53 @@ describe('ObjectiveDialogComponent', () => { it('should not load current Objective to alignment possibilities', async () => { matDataMock.objective.objectiveId = 1; component.objective = objectiveWithAlignment; - const routerHarness = await RouterTestingHarness.create(); - await routerHarness.navigateByUrl('/?quarter=2'); objectiveService.getFullObjective.mockReturnValue(of(objectiveWithAlignment)); + jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of(alignmentPossibilities)); + fixture.detectChanges(); component.ngOnInit(); let generatedPossibilities = [ { objectiveId: null, - objectiveTitle: 'Bitte wählen', + objectiveTitle: 'Kein Alignment', + keyResultAlignmentsDtos: [], + }, + { + objectiveId: 1003, + objectiveTitle: 'O - Test Objective', + keyResultAlignmentsDtos: [], + }, + { + objectiveId: 1005, + objectiveTitle: 'O - Company will grow', + keyResultAlignmentsDtos: [ + { + keyResultId: 6, + keyResultTitle: 'K - New structure', + }, + ], + }, + ]; + + let componentValue = null; + component.alignmentPossibilities$.subscribe((value) => { + componentValue = value; + }); + expect(componentValue).toStrictEqual(generatedPossibilities); + }); + + it('should load Kein Alignment to alignment possibilities when objective have an alignment', async () => { + component.objective = objective; + component.data.objective.objectiveId = 5; + objectiveService.getFullObjective.mockReturnValue(of(objectiveWithAlignment)); + jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of(alignmentPossibilities)); + fixture.detectChanges(); + component.ngOnInit(); + + let generatedPossibilities = [ + { + objectiveId: null, + objectiveTitle: 'Kein Alignment', keyResultAlignmentsDtos: [], }, { @@ -500,6 +542,45 @@ describe('ObjectiveDialogComponent', () => { expect(componentValue).toStrictEqual(generatedPossibilities); }); + it('should load Kein Alignment to alignment possibilities when choosing one alignment', async () => { + jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of(alignmentPossibilities)); + objectiveService.getFullObjective.mockReturnValue(of(objective)); + component.objective = objective; + component.data.objective.objectiveId = 5; + fixture.detectChanges(); + component.ngOnInit(); + component.changeFirstAlignmentPossibility(); + + let currentPossibilities = [ + { + objectiveId: null, + objectiveTitle: 'Kein Alignment', + keyResultAlignmentsDtos: [], + }, + { + objectiveId: 1003, + objectiveTitle: 'O - Test Objective', + keyResultAlignmentsDtos: [], + }, + { + objectiveId: 1005, + objectiveTitle: 'O - Company will grow', + keyResultAlignmentsDtos: [ + { + keyResultId: 6, + keyResultTitle: 'K - New structure', + }, + ], + }, + ]; + + let componentValue = null; + component.alignmentPossibilities$.subscribe((value) => { + componentValue = value; + }); + expect(componentValue).toStrictEqual(currentPossibilities); + }); + it('should call ObjectiveService when updating Alignments', async () => { component.updateAlignments(); expect(objectiveService.getAlignmentPossibilities).toHaveBeenCalled(); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 83c0b08cbc..8a19fdf8ba 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -240,18 +240,25 @@ export class ObjectiveFormComponent implements OnInit { generateAlignmentPossibilities(quarterId: number) { this.alignmentPossibilities$ = this.objectiveService.getAlignmentPossibilities(quarterId); - this.alignmentPossibilities$.subscribe((value) => { + this.alignmentPossibilities$.subscribe((value: AlignmentPossibility[]) => { if (this.objective?.id) { - value = value.filter((item) => !(item.objectiveId == this.objective!.id)); + value = value.filter((item: AlignmentPossibility) => !(item.objectiveId == this.objective!.id)); } - if (value.length == 0 || value[0].objectiveTitle != 'Bitte wählen') { - let noSelectOption: AlignmentPossibility = { - objectiveId: null, - objectiveTitle: 'Bitte wählen', - keyResultAlignmentsDtos: [], - }; - value.unshift(noSelectOption); + let firstSelectOption = { + objectiveId: null, + objectiveTitle: 'Kein Alignment', + keyResultAlignmentsDtos: [], + }; + if (value.length != 0) { + if (this.objective?.alignedEntityId) { + if (value[0].objectiveTitle == 'Bitte wählen') { + value.splice(0, 1); + } + } else { + firstSelectOption.objectiveTitle = 'Bitte wählen'; + } } + value.unshift(firstSelectOption); this.alignmentPossibilities$ = of(value); }); } @@ -263,5 +270,15 @@ export class ObjectiveFormComponent implements OnInit { }); } + changeFirstAlignmentPossibility() { + this.alignmentPossibilities$.subscribe((value: AlignmentPossibility[]) => { + let element: AlignmentPossibility = value[0]; + element.objectiveTitle = 'Kein Alignment'; + value.splice(0, 1); + value.unshift(element); + this.alignmentPossibilities$ = of(value); + }); + } + protected readonly getQuarterLabel = getQuarterLabel; } From 62f80b1dd3d8ed491e460a69f164d5474a358e70 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 25 Apr 2024 15:02:05 +0200 Subject: [PATCH 023/119] Fix backend tests --- .../okr/service/business/AlignmentBusinessServiceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java index b5c39d55d8..66e7031054 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -145,7 +145,7 @@ void shouldBuildAlignmentCorrectObjective() { Alignment returnAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objectiveAlignedObjective) .withTargetObjective(objective1).build(); - Alignment alignment = alignmentBusinessService.buildAlignmentModel(objectiveAlignedObjective); + Alignment alignment = alignmentBusinessService.buildAlignmentModel(objectiveAlignedObjective, 0); assertEquals(returnAlignment, alignment); assertTrue(alignment instanceof ObjectiveAlignment); @@ -157,7 +157,7 @@ void shouldBuildAlignmentCorrectKeyResult() { Alignment returnAlignment = KeyResultAlignment.Builder.builder().withAlignedObjective(keyResultAlignedObjective) .withTargetKeyResult(metricKeyResult).build(); - Alignment alignment = alignmentBusinessService.buildAlignmentModel(keyResultAlignedObjective); + Alignment alignment = alignmentBusinessService.buildAlignmentModel(keyResultAlignedObjective, 0); assertEquals(returnAlignment, alignment); assertTrue(alignment instanceof KeyResultAlignment); @@ -166,7 +166,7 @@ void shouldBuildAlignmentCorrectKeyResult() { @Test void shouldThrowErrorWhenAlignedEntityIdIsIncorrect() { OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> alignmentBusinessService.buildAlignmentModel(wrongAlignedObjective)); + () -> alignmentBusinessService.buildAlignmentModel(wrongAlignedObjective, 0)); List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NOT_SET", List.of("alignedEntityId", "Hello"))); From 52d81103d7e73e9a60cc0ab6d7d5a251f6c8900f Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 25 Apr 2024 16:11:21 +0200 Subject: [PATCH 024/119] Fix e2e tests because of new quarter --- frontend/cypress/e2e/checkIn.cy.ts | 8 +- frontend/cypress/e2e/crud.cy.ts | 10 +-- frontend/cypress/e2e/duplicated-scoring.cy.ts | 4 +- .../cypress/e2e/objective-alignment.cy.ts | 8 +- .../objective-form.component.spec.ts | 81 +++++++++++++------ 5 files changed, 72 insertions(+), 39 deletions(-) diff --git a/frontend/cypress/e2e/checkIn.cy.ts b/frontend/cypress/e2e/checkIn.cy.ts index 937a6f51ca..e3df4724d0 100644 --- a/frontend/cypress/e2e/checkIn.cy.ts +++ b/frontend/cypress/e2e/checkIn.cy.ts @@ -259,8 +259,8 @@ describe('OKR Check-in e2e tests', () => { it(`Should display confirm dialog when creating checkin on draft objective`, () => { cy.getByTestId('add-objective').first().click(); - cy.fillOutObjective('draft objective title', 'safe-draft', '3'); - cy.visit('/?quarter=3'); + cy.fillOutObjective('draft objective title', 'safe-draft', '1'); + cy.visit('/?quarter=1'); cy.contains('draft objective title').first().parentsUntil('#objective-column').last().focus(); cy.tabForwardUntil('[data-testId="add-keyResult"]'); @@ -290,8 +290,8 @@ describe('OKR Check-in e2e tests', () => { it(`Should not display last value div if last checkin is not present`, () => { cy.getByTestId('add-objective').first().click(); - cy.fillOutObjective('new objective', 'safe', '3'); - cy.visit('/?quarter=3'); + cy.fillOutObjective('new objective', 'safe', '1'); + cy.visit('/?quarter=1'); cy.contains('new objective').first().parentsUntil('#objective-column').last().focus(); cy.tabForwardUntil('[data-testId="add-keyResult"]'); diff --git a/frontend/cypress/e2e/crud.cy.ts b/frontend/cypress/e2e/crud.cy.ts index bbe1dec6cc..d72430259f 100644 --- a/frontend/cypress/e2e/crud.cy.ts +++ b/frontend/cypress/e2e/crud.cy.ts @@ -12,8 +12,8 @@ describe('CRUD operations', () => { ].forEach(([objectiveTitle, buttonTestId, icon]) => { it(`Create objective, no keyresults`, () => { cy.getByTestId('add-objective').first().click(); - cy.fillOutObjective(objectiveTitle, buttonTestId, '3'); - cy.visit('/?quarter=3'); + cy.fillOutObjective(objectiveTitle, buttonTestId, '1'); + cy.visit('/?quarter=1'); const objective = cy.contains(objectiveTitle).first().parentsUntil('#objective-column').last(); objective.getByTestId('objective-state').should('have.attr', 'src', `assets/icons/${icon}`); }); @@ -25,7 +25,7 @@ describe('CRUD operations', () => { ].forEach(([objectiveTitle, buttonTestId, icon]) => { it(`Create objective, no keyresults`, () => { cy.getByTestId('add-objective').first().click(); - cy.fillOutObjective(objectiveTitle, buttonTestId, '3', '', true); + cy.fillOutObjective(objectiveTitle, buttonTestId, '1', '', true); cy.contains('Key Result erfassen'); }); }); @@ -42,8 +42,8 @@ describe('CRUD operations', () => { it(`Create objective, cancel`, () => { const objectiveTitle = 'this is a canceled objective'; cy.getByTestId('add-objective').first().click(); - cy.fillOutObjective(objectiveTitle, 'cancel', '3'); - cy.visit('/?quarter=3'); + cy.fillOutObjective(objectiveTitle, 'cancel', '1'); + cy.visit('/?quarter=1'); cy.contains(objectiveTitle).should('not.exist'); }); diff --git a/frontend/cypress/e2e/duplicated-scoring.cy.ts b/frontend/cypress/e2e/duplicated-scoring.cy.ts index d1152ced59..5cc5565794 100644 --- a/frontend/cypress/e2e/duplicated-scoring.cy.ts +++ b/frontend/cypress/e2e/duplicated-scoring.cy.ts @@ -22,8 +22,8 @@ describe('e2e test for scoring adjustment on objective duplicate', () => { cy.get('.objective').first().getByTestId('three-dot-menu').click(); cy.get('.mat-mdc-menu-content').contains('Objective duplizieren').click(); - cy.fillOutObjective('A duplicated Objective for this tool', 'safe', '3'); - cy.visit('/?quarter=3'); + cy.fillOutObjective('A duplicated Objective for this tool', 'safe', '1'); + cy.visit('/?quarter=1'); let scoringBlock1 = cy .getByTestId('objective') diff --git a/frontend/cypress/e2e/objective-alignment.cy.ts b/frontend/cypress/e2e/objective-alignment.cy.ts index 540aad6a85..1d6cb6f990 100644 --- a/frontend/cypress/e2e/objective-alignment.cy.ts +++ b/frontend/cypress/e2e/objective-alignment.cy.ts @@ -106,7 +106,7 @@ describe('OKR Objective Alignment e2e tests', () => { .contains('Objective bearbeiten') .click(); - cy.get('select#alignment').select('Bitte wählen'); + cy.get('select#alignment').select('Kein Alignment'); cy.getByTestId('safe').click(); cy.getByTestId('objective') @@ -123,7 +123,7 @@ describe('OKR Objective Alignment e2e tests', () => { }); it(`Alignment Possibilites change when quarter change`, () => { - cy.visit('/?quarter=3'); + cy.visit('/?quarter=1'); cy.getByTestId('add-objective').first().click(); cy.getByTestId('title').first().clear().type('We can link later on this'); @@ -145,12 +145,12 @@ describe('OKR Objective Alignment e2e tests', () => { cy.get('select#alignment option:selected').should('not.contain.text', selectValue); cy.getByTestId('cancel').click(); - cy.visit('/?quarter=4'); + cy.visit('/?quarter=2'); cy.getByTestId('add-objective').first().click(); cy.getByTestId('title').first().clear().type('Quarter change objective'); - cy.get('select#quarter').select('GJ 22/23-Q3'); + cy.get('select#quarter').select('GJ 22/23-Q4'); cy.getByTestId('title').first().clear().type('A new title'); cy.get('select#alignment').select(1); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index 808e91fbc4..059fb442d8 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -74,24 +74,6 @@ const mockActivatedRoute = { }, }; -const alignmentPossibilities = [ - { - objectiveId: 1003, - objectiveTitle: 'O - Test Objective', - keyResultAlignmentsDtos: [], - }, - { - objectiveId: 1005, - objectiveTitle: 'O - Company will grow', - keyResultAlignmentsDtos: [ - { - keyResultId: 6, - keyResultTitle: 'K - New structure', - }, - ], - }, -]; - describe('ObjectiveDialogComponent', () => { let component: ObjectiveFormComponent; let fixture: ComponentFixture; @@ -430,12 +412,66 @@ describe('ObjectiveDialogComponent', () => { fixture.detectChanges(); expect(component.allowedOption(quarter)).toBeTruthy(); }); + }); + + describe('AlignmentPossibilities', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + MatDialogModule, + MatIconModule, + MatFormFieldModule, + MatSelectModule, + ReactiveFormsModule, + MatInputModule, + NoopAnimationsModule, + MatCheckboxModule, + RouterTestingModule, + TranslateTestingModule.withTranslations({ + de: de, + }), + ], + declarations: [ObjectiveFormComponent, DialogHeaderComponent], + providers: [ + { provide: MatDialogRef, useValue: dialogMock }, + { provide: MAT_DIALOG_DATA, useValue: matDataMock }, + { provide: ObjectiveService, useValue: objectiveService }, + { provide: QuarterService, useValue: quarterService }, + { provide: TeamService, useValue: teamService }, + ], + }); + + let alignmentPossibilities = [ + { + objectiveId: 1003, + objectiveTitle: 'O - Test Objective', + keyResultAlignmentsDtos: [], + }, + { + objectiveId: 1005, + objectiveTitle: 'O - Company will grow', + keyResultAlignmentsDtos: [ + { + keyResultId: 6, + keyResultTitle: 'K - New structure', + }, + ], + }, + ]; - it('should load correct alignment possibilities', async () => { jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of(alignmentPossibilities)); + fixture = TestBed.createComponent(ObjectiveFormComponent); + component = fixture.componentInstance; fixture.detectChanges(); - component.ngOnInit(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + it('should load correct alignment possibilities', async () => { let generatedPossibilities = [ { objectiveId: null, @@ -470,7 +506,6 @@ describe('ObjectiveDialogComponent', () => { matDataMock.objective.objectiveId = 1; component.objective = objectiveWithAlignment; objectiveService.getFullObjective.mockReturnValue(of(objectiveWithAlignment)); - jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of(alignmentPossibilities)); fixture.detectChanges(); component.ngOnInit(); @@ -508,7 +543,6 @@ describe('ObjectiveDialogComponent', () => { component.objective = objective; component.data.objective.objectiveId = 5; objectiveService.getFullObjective.mockReturnValue(of(objectiveWithAlignment)); - jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of(alignmentPossibilities)); fixture.detectChanges(); component.ngOnInit(); @@ -543,7 +577,6 @@ describe('ObjectiveDialogComponent', () => { }); it('should load Kein Alignment to alignment possibilities when choosing one alignment', async () => { - jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of(alignmentPossibilities)); objectiveService.getFullObjective.mockReturnValue(of(objective)); component.objective = objective; component.data.objective.objectiveId = 5; @@ -615,7 +648,7 @@ describe('ObjectiveDialogComponent', () => { { provide: ActivatedRoute, useValue: mockActivatedRoute }, ], }); - jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of(alignmentPossibilities)); + jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of([])); fixture = TestBed.createComponent(ObjectiveFormComponent); component = fixture.componentInstance; fixture.detectChanges(); From fce32caaf895f2d88f282c7d6c55557774a0b118 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 29 Apr 2024 16:55:09 +0200 Subject: [PATCH 025/119] Implement typeahead --- .../okr/controller/ObjectiveController.java | 6 +- .../java/ch/puzzle/okr/dto/AlignmentDto.java | 6 + .../ch/puzzle/okr/dto/AlignmentObjectDto.java | 4 + .../ObjectiveAuthorizationService.java | 4 +- .../business/ObjectiveBusinessService.java | 51 +++++--- .../objective-form.component.html | 44 +++---- .../objective-form.component.scss | 15 +++ .../objective-form.component.ts | 118 +++++++++++++----- .../types/model/AlignmentPossibility.ts | 11 +- .../types/model/AlignmentPossibilityObject.ts | 5 + 10 files changed, 182 insertions(+), 82 deletions(-) create mode 100644 backend/src/main/java/ch/puzzle/okr/dto/AlignmentDto.java create mode 100644 backend/src/main/java/ch/puzzle/okr/dto/AlignmentObjectDto.java create mode 100644 frontend/src/app/shared/types/model/AlignmentPossibilityObject.ts diff --git a/backend/src/main/java/ch/puzzle/okr/controller/ObjectiveController.java b/backend/src/main/java/ch/puzzle/okr/controller/ObjectiveController.java index f608d87770..672afebf37 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/ObjectiveController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/ObjectiveController.java @@ -1,6 +1,6 @@ package ch.puzzle.okr.controller; -import ch.puzzle.okr.dto.ObjectiveAlignmentsDto; +import ch.puzzle.okr.dto.AlignmentDto; import ch.puzzle.okr.dto.ObjectiveDto; import ch.puzzle.okr.mapper.ObjectiveMapper; import ch.puzzle.okr.models.Objective; @@ -48,11 +48,11 @@ public ResponseEntity getObjective( @Operation(summary = "Get Alignment possibilities", description = "Get all possibilities to create an Alignment") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Returned all Alignment possibilities for an Objective", content = { - @Content(mediaType = "application/json", schema = @Schema(implementation = ObjectiveAlignmentsDto.class)) }), + @Content(mediaType = "application/json", schema = @Schema(implementation = AlignmentDto.class)) }), @ApiResponse(responseCode = "401", description = "Not authorized to get Alignment possibilities", content = @Content), @ApiResponse(responseCode = "404", description = "Did not find any possibilities to create an Alignment", content = @Content) }) @GetMapping("/alignmentPossibilities/{quarterId}") - public ResponseEntity> getAlignmentPossibilities( + public ResponseEntity> getAlignmentPossibilities( @Parameter(description = "The Quarter ID for getting Alignment possibilities.", required = true) @PathVariable Long quarterId) { return ResponseEntity.status(HttpStatus.OK) .body(objectiveAuthorizationService.getAlignmentPossibilities(quarterId)); diff --git a/backend/src/main/java/ch/puzzle/okr/dto/AlignmentDto.java b/backend/src/main/java/ch/puzzle/okr/dto/AlignmentDto.java new file mode 100644 index 0000000000..16389518ea --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/AlignmentDto.java @@ -0,0 +1,6 @@ +package ch.puzzle.okr.dto; + +import java.util.List; + +public record AlignmentDto(Long teamId, String teamName, List alignmentObjectDtos) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/dto/AlignmentObjectDto.java b/backend/src/main/java/ch/puzzle/okr/dto/AlignmentObjectDto.java new file mode 100644 index 0000000000..363278549b --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/AlignmentObjectDto.java @@ -0,0 +1,4 @@ +package ch.puzzle.okr.dto; + +public record AlignmentObjectDto(Long objectId, String objectTitle, String objectType) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationService.java b/backend/src/main/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationService.java index d5d7cdf105..cbeb5b484b 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationService.java @@ -1,6 +1,6 @@ package ch.puzzle.okr.service.authorization; -import ch.puzzle.okr.dto.ObjectiveAlignmentsDto; +import ch.puzzle.okr.dto.AlignmentDto; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.service.business.ObjectiveBusinessService; @@ -22,7 +22,7 @@ public Objective duplicateEntity(Long id, Objective objective) { return getBusinessService().duplicateObjective(id, objective, authorizationUser); } - public List getAlignmentPossibilities(Long quarterId) { + public List getAlignmentPossibilities(Long quarterId) { return getBusinessService().getAlignmentPossibilities(quarterId); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java index 7cdec3444c..03822c9c1d 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java @@ -1,8 +1,9 @@ package ch.puzzle.okr.service.business; -import ch.puzzle.okr.dto.ObjectiveAlignmentsDto; -import ch.puzzle.okr.dto.keyresult.KeyResultAlignmentsDto; +import ch.puzzle.okr.dto.AlignmentDto; +import ch.puzzle.okr.dto.AlignmentObjectDto; import ch.puzzle.okr.models.Objective; +import ch.puzzle.okr.models.Team; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.models.keyresult.KeyResult; import ch.puzzle.okr.models.keyresult.KeyResultMetric; @@ -16,9 +17,7 @@ import org.springframework.stereotype.Service; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import java.util.*; import static ch.puzzle.okr.Constants.KEY_RESULT_TYPE_METRIC; import static ch.puzzle.okr.Constants.KEY_RESULT_TYPE_ORDINAL; @@ -52,26 +51,40 @@ public Objective getEntityById(Long id) { return objective; } - public List getAlignmentPossibilities(Long quarterId) { + public List getAlignmentPossibilities(Long quarterId) { validator.validateOnGet(quarterId); List objectivesByQuarter = objectivePersistenceService.findObjectiveByQuarterId(quarterId); - List objectiveAlignmentsDtos = new ArrayList<>(); - - objectivesByQuarter.forEach(objective -> { - List keyResults = keyResultBusinessService.getAllKeyResultsByObjective(objective.getId()); - List keyResultAlignmentsDtos = new ArrayList<>(); - keyResults.forEach(keyResult -> { - KeyResultAlignmentsDto keyResultAlignmentsDto = new KeyResultAlignmentsDto(keyResult.getId(), - "K - " + keyResult.getTitle()); - keyResultAlignmentsDtos.add(keyResultAlignmentsDto); + List alignmentDtoList = new ArrayList<>(); + + List teamList = new ArrayList<>(); + objectivesByQuarter.forEach(objective -> teamList.add(objective.getTeam())); + Set set = new HashSet<>(teamList); + teamList.clear(); + teamList.addAll(set); + + teamList.forEach(team -> { + List filteredObjectiveList = objectivesByQuarter.stream() + .filter(objective -> objective.getTeam().equals(team)).toList(); + List alignmentObjectDtos = new ArrayList<>(); + + filteredObjectiveList.forEach(objective -> { + AlignmentObjectDto objectiveDto = new AlignmentObjectDto(objective.getId(), + "O - " + objective.getTitle(), "objective"); + alignmentObjectDtos.add(objectiveDto); + + List keyResults = keyResultBusinessService.getAllKeyResultsByObjective(objective.getId()); + keyResults.forEach(keyResult -> { + AlignmentObjectDto keyResultDto = new AlignmentObjectDto(keyResult.getId(), + "KR - " + keyResult.getTitle(), "keyResult"); + alignmentObjectDtos.add(keyResultDto); + }); }); - ObjectiveAlignmentsDto objectiveAlignmentsDto = new ObjectiveAlignmentsDto(objective.getId(), - "O - " + objective.getTitle(), keyResultAlignmentsDtos); - objectiveAlignmentsDtos.add(objectiveAlignmentsDto); + AlignmentDto alignmentDto = new AlignmentDto(team.getId(), team.getName(), alignmentObjectDtos); + alignmentDtoList.add(alignmentDto); }); - return objectiveAlignmentsDtos; + return alignmentDtoList; } public List getEntitiesByTeamId(Long id) { diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index 480423313d..e88d0eb969 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -1,7 +1,7 @@
@@ -10,11 +10,11 @@
@@ -68,25 +68,27 @@
- - - - - - - - - + class="alignment-input" + placeholder="{{ filteredOptions.length == 0 ? 'Kein Alignment vorhanden' : 'Bezug wählen' }}" + [matAutocomplete]="auto" + (input)="filter()" + (focus)="filter(); input.select()" + [value]="displayedValue" + /> + + @for (group of filteredOptions; track group) { + + @for (alignmentObject of group.alignmentObjectDtos; track alignmentObject) { + {{ alignmentObject.objectTitle }} + } + + } +
; + filteredOptions: AlignmentPossibility[] = []; + objectiveForm = new FormGroup({ title: new FormControl('', [Validators.required, Validators.minLength(2), Validators.maxLength(250)]), description: new FormControl('', [Validators.maxLength(4096)]), quarter: new FormControl(0, [Validators.required]), team: new FormControl({ value: 0, disabled: true }, [Validators.required]), - alignment: new FormControl(''), + alignment: new FormControl(null), createKeyResults: new FormControl(false), }); quarters$: Observable = of([]); @@ -37,7 +41,8 @@ export class ObjectiveFormComponent implements OnInit { objective: Objective | null = null; teams$: Observable = of([]); alignmentPossibilities$: Observable = of([]); - currentTeam: Subject = new Subject(); + currentTeam$: Subject = new Subject(); + currentTeam: Team | null = null; state: string | null = null; version!: number; protected readonly formInputCheck = formInputCheck; @@ -65,6 +70,20 @@ export class ObjectiveFormComponent implements OnInit { onSubmit(submitType: any): void { const value = this.objectiveForm.getRawValue(); const state = this.data.objective.objectiveId == null ? submitType : this.state; + + let alignmentEntity = value.alignment; + let alignment: string | null; + + if (alignmentEntity) { + if (alignmentEntity.objectType == 'objective') { + alignment = 'O' + alignmentEntity.objectId; + } else { + alignment = 'K' + alignmentEntity.objectId; + } + } else { + alignment = null; + } + let objectiveDTO: Objective = { id: this.data.objective.objectiveId, version: this.version, @@ -73,7 +92,7 @@ export class ObjectiveFormComponent implements OnInit { title: value.title, teamId: value.team, state: state, - alignedEntityId: value.alignment == 'Onull' ? null : value.alignment, + alignedEntityId: alignment, } as unknown as Objective; const submitFunction = this.getSubmitFunction(objectiveDTO.id, objectiveDTO); @@ -104,16 +123,17 @@ export class ObjectiveFormComponent implements OnInit { this.state = objective.state; this.version = objective.version; this.teams$.subscribe((value) => { - this.currentTeam.next(value.filter((team) => team.id == teamId)[0]); + let team: Team = value.filter((team: Team) => team.id == teamId)[0]; + this.currentTeam$.next(team); + this.currentTeam = team; }); - this.generateAlignmentPossibilities(quarterId); + this.generateAlignmentPossibilities(quarterId, objective, teamId!); this.objectiveForm.patchValue({ title: objective.title, description: objective.description, team: teamId, quarter: quarterId, - alignment: objective.alignedEntityId ? objective.alignedEntityId : 'Onull', }); }); } @@ -238,47 +258,83 @@ export class ObjectiveFormComponent implements OnInit { return GJ_REGEX_PATTERN.test(label); } - generateAlignmentPossibilities(quarterId: number) { + generateAlignmentPossibilities(quarterId: number, objective: Objective | null, teamId: number | null) { this.alignmentPossibilities$ = this.objectiveService.getAlignmentPossibilities(quarterId); this.alignmentPossibilities$.subscribe((value: AlignmentPossibility[]) => { - if (this.objective?.id) { - value = value.filter((item: AlignmentPossibility) => !(item.objectiveId == this.objective!.id)); + if (teamId) { + value = value.filter((item: AlignmentPossibility) => !(item.teamId == teamId)); } - let firstSelectOption = { - objectiveId: null, - objectiveTitle: 'Kein Alignment', - keyResultAlignmentsDtos: [], - }; - if (value.length != 0) { - if (this.objective?.alignedEntityId) { - if (value[0].objectiveTitle == 'Bitte wählen') { - value.splice(0, 1); - } - } else { - firstSelectOption.objectiveTitle = 'Bitte wählen'; + + if (objective) { + let alignment: string | null = objective.alignedEntityId; + if (alignment) { + let alignmentType: string = alignment.charAt(0); + let alignmentId: number = parseInt(alignment.substring(1)); + alignmentType = alignmentType == 'O' ? 'objective' : 'keyResult'; + let element: AlignmentPossibilityObject | null = this.findAlignmentObject(value, alignmentId, alignmentType); + this.objectiveForm.patchValue({ + alignment: element, + }); } } - value.unshift(firstSelectOption); + + this.filteredOptions = value.slice(); this.alignmentPossibilities$ = of(value); }); } + findAlignmentObject( + alignmentPossibilities: AlignmentPossibility[], + objectId: number, + objectType: string, + ): AlignmentPossibilityObject | null { + for (let possibility of alignmentPossibilities) { + let foundObject: AlignmentPossibilityObject | undefined = possibility.alignmentObjectDtos.find( + (alignmentObject: AlignmentPossibilityObject) => + alignmentObject.objectId === objectId && alignmentObject.objectType === objectType, + ); + if (foundObject) { + return foundObject; + } + } + return null; + } + updateAlignments() { - this.generateAlignmentPossibilities(this.objectiveForm.value.quarter!); + this.input.nativeElement.value = ''; + this.filteredOptions = []; this.objectiveForm.patchValue({ - alignment: 'Onull', + alignment: null, }); + this.generateAlignmentPossibilities(this.objectiveForm.value.quarter!, null, this.currentTeam!.id); } - changeFirstAlignmentPossibility() { + filter() { + let filterValue = this.input.nativeElement.value.toLowerCase(); this.alignmentPossibilities$.subscribe((value: AlignmentPossibility[]) => { - let element: AlignmentPossibility = value[0]; - element.objectiveTitle = 'Kein Alignment'; - value.splice(0, 1); - value.unshift(element); - this.alignmentPossibilities$ = of(value); + this.filteredOptions = value.filter((alignmentPossibility: AlignmentPossibility) => { + let teamMatch = alignmentPossibility.teamName.toLowerCase().includes(filterValue); + let objectMatch = alignmentPossibility.alignmentObjectDtos.some((obj) => + obj.objectTitle.toLowerCase().includes(filterValue), + ); + return teamMatch || objectMatch; + }); }); } + displayWith(value: any) { + if (value) { + return value.objectTitle; + } + } + + get displayedValue(): string { + if (this.input) { + return this.input.nativeElement.value; + } else { + return ''; + } + } + protected readonly getQuarterLabel = getQuarterLabel; } diff --git a/frontend/src/app/shared/types/model/AlignmentPossibility.ts b/frontend/src/app/shared/types/model/AlignmentPossibility.ts index e2b3e69673..7ca5e7f614 100644 --- a/frontend/src/app/shared/types/model/AlignmentPossibility.ts +++ b/frontend/src/app/shared/types/model/AlignmentPossibility.ts @@ -1,8 +1,7 @@ +import { AlignmentPossibilityObject } from './AlignmentPossibilityObject'; + export interface AlignmentPossibility { - objectiveId: number | null; - objectiveTitle: string; - keyResultAlignmentsDtos: { - keyResultId: number; - keyResultTitle: string; - }[]; + teamId: number; + teamName: string; + alignmentObjectDtos: AlignmentPossibilityObject[]; } diff --git a/frontend/src/app/shared/types/model/AlignmentPossibilityObject.ts b/frontend/src/app/shared/types/model/AlignmentPossibilityObject.ts new file mode 100644 index 0000000000..86c7491742 --- /dev/null +++ b/frontend/src/app/shared/types/model/AlignmentPossibilityObject.ts @@ -0,0 +1,5 @@ +export interface AlignmentPossibilityObject { + objectId: number; + objectTitle: string; + objectType: string; +} From 3d85282c5df732d1736173f11c50229d34d9e398 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 23 May 2024 15:28:31 +0200 Subject: [PATCH 026/119] Sort alignment possibilities alphabetically --- .../business/ObjectiveBusinessService.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java index 03822c9c1d..85944e1332 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java @@ -57,15 +57,15 @@ public List getAlignmentPossibilities(Long quarterId) { List objectivesByQuarter = objectivePersistenceService.findObjectiveByQuarterId(quarterId); List alignmentDtoList = new ArrayList<>(); - List teamList = new ArrayList<>(); - objectivesByQuarter.forEach(objective -> teamList.add(objective.getTeam())); - Set set = new HashSet<>(teamList); - teamList.clear(); - teamList.addAll(set); + Set teamSet = new HashSet<>(); + objectivesByQuarter.forEach(objective -> teamSet.add(objective.getTeam())); + List teamList = new ArrayList<>(teamSet.stream().sorted(Comparator.comparing(Team::getName)).toList()); teamList.forEach(team -> { List filteredObjectiveList = objectivesByQuarter.stream() - .filter(objective -> objective.getTeam().equals(team)).toList(); + .filter(objective -> objective.getTeam().equals(team)).toList().stream() + .sorted(Comparator.comparing(Objective::getTitle)).toList(); + List alignmentObjectDtos = new ArrayList<>(); filteredObjectiveList.forEach(objective -> { @@ -73,7 +73,9 @@ public List getAlignmentPossibilities(Long quarterId) { "O - " + objective.getTitle(), "objective"); alignmentObjectDtos.add(objectiveDto); - List keyResults = keyResultBusinessService.getAllKeyResultsByObjective(objective.getId()); + List keyResults = keyResultBusinessService.getAllKeyResultsByObjective(objective.getId()) + .stream().sorted(Comparator.comparing(KeyResult::getTitle)).toList(); + keyResults.forEach(keyResult -> { AlignmentObjectDto keyResultDto = new AlignmentObjectDto(keyResult.getId(), "KR - " + keyResult.getTitle(), "keyResult"); From d69208618dcf71897af80e8850345d2039cdc33e Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 24 May 2024 07:41:58 +0200 Subject: [PATCH 027/119] Use bold font for optgroup label --- .../dialog/objective-dialog/objective-form.component.html | 2 +- .../dialog/objective-dialog/objective-form.component.scss | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index e88d0eb969..612e3cdefd 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -82,7 +82,7 @@ /> @for (group of filteredOptions; track group) { - + @for (alignmentObject of group.alignmentObjectDtos; track alignmentObject) { {{ alignmentObject.objectTitle }} } diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss index 16ed70337c..f81849ff44 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss @@ -41,7 +41,6 @@ border: solid 1px #909090; padding: 0.44rem 0.625rem 0.375rem 0.625rem; cursor: pointer; - width: 92%; height: 1.18rem; box-sizing: content-box; overflow: hidden; From 6d4d977b3c1a617b39f781e1e5adf013dad84945 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 24 May 2024 11:35:40 +0200 Subject: [PATCH 028/119] Implement correct filter method for typeahead --- .../objective-form.component.ts | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index b95d3b1db6..1229c26d43 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -311,14 +311,38 @@ export class ObjectiveFormComponent implements OnInit { filter() { let filterValue = this.input.nativeElement.value.toLowerCase(); - this.alignmentPossibilities$.subscribe((value: AlignmentPossibility[]) => { - this.filteredOptions = value.filter((alignmentPossibility: AlignmentPossibility) => { - let teamMatch = alignmentPossibility.teamName.toLowerCase().includes(filterValue); - let objectMatch = alignmentPossibility.alignmentObjectDtos.some((obj) => - obj.objectTitle.toLowerCase().includes(filterValue), + this.alignmentPossibilities$.subscribe((alignmentPossibilities: AlignmentPossibility[]) => { + let filteredObjects: AlignmentPossibilityObject[] = alignmentPossibilities.flatMap( + (alignmentPossibility: AlignmentPossibility) => + alignmentPossibility.alignmentObjectDtos.filter((alignmentPossibilityObject: AlignmentPossibilityObject) => + alignmentPossibilityObject.objectTitle.toLowerCase().includes(filterValue), + ), + ); + + let matchingPossibilities: AlignmentPossibility[] = alignmentPossibilities.filter( + (possibility: AlignmentPossibility) => + filteredObjects.some((alignmentPossibilityObject: AlignmentPossibilityObject) => + possibility.alignmentObjectDtos.includes(alignmentPossibilityObject), + ), + ); + + matchingPossibilities = [...new Set(matchingPossibilities)]; + + let optionList = matchingPossibilities.map((possibility: AlignmentPossibility) => ({ + ...possibility, + alignmentObjectDtos: possibility.alignmentObjectDtos.filter( + (alignmentPossibilityObject: AlignmentPossibilityObject) => + filteredObjects.includes(alignmentPossibilityObject), + ), + })); + + if (optionList.length == 0) { + this.filteredOptions = alignmentPossibilities.filter((possibility: AlignmentPossibility) => + possibility.teamName.toLowerCase().includes(filterValue), ); - return teamMatch || objectMatch; - }); + } else { + this.filteredOptions = optionList; + } }); } From 6a6365ea1daa51faa80d168c2be30f7710943efc Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 24 May 2024 13:33:55 +0200 Subject: [PATCH 029/119] Fix bug with wrong quarter without queryParams --- .../dialog/objective-dialog/objective-form.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 1229c26d43..f288f70821 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -113,11 +113,11 @@ export class ObjectiveFormComponent implements OnInit { this.quarters = quarters; this.objective = objective; const teamId = isCreating ? objective.teamId : this.data.objective.teamId; - let quarterId = getValueFromQuery(this.route.snapshot.queryParams['quarter'], quarters[1].id)[0]; + let quarterId = getValueFromQuery(this.route.snapshot.queryParams['quarter'], quarters[2].id)[0]; let currentQuarter: Quarter | undefined = this.quarters.find((quarter) => quarter.id == quarterId); if (currentQuarter && !this.isBacklogQuarter(currentQuarter.label) && this.data.action == 'releaseBacklog') { - quarterId = quarters[1].id; + quarterId = quarters[2].id; } this.state = objective.state; From 306acd2c5e8f5ea48bfca59b85d5bcc6b2a39a71 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 24 May 2024 13:46:58 +0200 Subject: [PATCH 030/119] Add scrollLeft on input leave and adjust padding --- .../dialog/objective-dialog/objective-form.component.html | 1 + .../dialog/objective-dialog/objective-form.component.scss | 2 +- .../dialog/objective-dialog/objective-form.component.ts | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index 612e3cdefd..b9bdf5d247 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -78,6 +78,7 @@ [matAutocomplete]="auto" (input)="filter()" (focus)="filter(); input.select()" + (focusout)="scrollLeft()" [value]="displayedValue" /> diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss index f81849ff44..d5d63bee0a 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.scss @@ -39,7 +39,7 @@ .alignment-input { border: solid 1px #909090; - padding: 0.44rem 0.625rem 0.375rem 0.625rem; + padding: 0.44rem 0.625rem 0.375rem 0.625rem !important; cursor: pointer; height: 1.18rem; box-sizing: content-box; diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index f288f70821..bf218b4625 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -360,5 +360,9 @@ export class ObjectiveFormComponent implements OnInit { } } + scrollLeft() { + this.input.nativeElement.scrollLeft = 0; + } + protected readonly getQuarterLabel = getQuarterLabel; } From 6fbc03569407cbbd0fea27d087779b899bfeee6b Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 24 May 2024 18:10:25 +0200 Subject: [PATCH 031/119] Remove unused files --- .../java/ch/puzzle/okr/dto/ObjectiveAlignmentsDto.java | 9 --------- .../puzzle/okr/dto/keyresult/KeyResultAlignmentsDto.java | 4 ---- .../okr/service/business/AlignmentBusinessService.java | 6 ++---- .../ch/puzzle/okr/controller/ObjectiveControllerIT.java | 3 --- .../authorization/ObjectiveAuthorizationServiceTest.java | 2 -- .../service/business/ObjectiveBusinessServiceTest.java | 1 - 6 files changed, 2 insertions(+), 23 deletions(-) delete mode 100644 backend/src/main/java/ch/puzzle/okr/dto/ObjectiveAlignmentsDto.java delete mode 100644 backend/src/main/java/ch/puzzle/okr/dto/keyresult/KeyResultAlignmentsDto.java diff --git a/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveAlignmentsDto.java b/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveAlignmentsDto.java deleted file mode 100644 index 7c67d85a6a..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveAlignmentsDto.java +++ /dev/null @@ -1,9 +0,0 @@ -package ch.puzzle.okr.dto; - -import ch.puzzle.okr.dto.keyresult.KeyResultAlignmentsDto; - -import java.util.List; - -public record ObjectiveAlignmentsDto(Long objectiveId, String objectiveTitle, - List keyResultAlignmentsDtos) { -} diff --git a/backend/src/main/java/ch/puzzle/okr/dto/keyresult/KeyResultAlignmentsDto.java b/backend/src/main/java/ch/puzzle/okr/dto/keyresult/KeyResultAlignmentsDto.java deleted file mode 100644 index 6d3980c702..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/dto/keyresult/KeyResultAlignmentsDto.java +++ /dev/null @@ -1,4 +0,0 @@ -package ch.puzzle.okr.dto.keyresult; - -public record KeyResultAlignmentsDto(Long keyResultId, String keyResultTitle) { -} diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index dfef1ee5a4..ef9f954fbc 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -95,10 +95,8 @@ public Alignment buildAlignmentModel(Objective alignedObjective, int version) { } public boolean isAlignmentTypeChange(Alignment alignment, Alignment savedAlignment) { - if (alignment instanceof ObjectiveAlignment && savedAlignment instanceof KeyResultAlignment) { - return true; - } else - return alignment instanceof KeyResultAlignment && savedAlignment instanceof ObjectiveAlignment; + return (alignment instanceof ObjectiveAlignment && savedAlignment instanceof KeyResultAlignment) + || (alignment instanceof KeyResultAlignment && savedAlignment instanceof ObjectiveAlignment); } public void updateKeyResultIdOnIdChange(Long oldId, KeyResult keyResult) { diff --git a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java index 120a38fbdb..3ebfb0b5c2 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java @@ -1,8 +1,6 @@ package ch.puzzle.okr.controller; -import ch.puzzle.okr.dto.ObjectiveAlignmentsDto; import ch.puzzle.okr.dto.ObjectiveDto; -import ch.puzzle.okr.dto.keyresult.KeyResultAlignmentsDto; import ch.puzzle.okr.mapper.ObjectiveMapper; import ch.puzzle.okr.models.*; import ch.puzzle.okr.service.authorization.AuthorizationService; @@ -25,7 +23,6 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.web.server.ResponseStatusException; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java index 84e3c27c72..1f4def51ac 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java @@ -1,7 +1,5 @@ package ch.puzzle.okr.service.authorization; -import ch.puzzle.okr.dto.ObjectiveAlignmentsDto; -import ch.puzzle.okr.dto.keyresult.KeyResultAlignmentsDto; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.service.business.ObjectiveBusinessService; diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java index a21965c1eb..567a39ebcf 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java @@ -1,6 +1,5 @@ package ch.puzzle.okr.service.business; -import ch.puzzle.okr.dto.ObjectiveAlignmentsDto; import ch.puzzle.okr.models.*; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.models.keyresult.KeyResult; From 726d6126db9d4b603cb6011cee7c8997514348fc Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 27 May 2024 09:43:53 +0200 Subject: [PATCH 032/119] Write backend tests --- .../okr/controller/ObjectiveControllerIT.java | 26 ++-- .../ObjectiveAuthorizationServiceTest.java | 34 +++-- .../AlignmentBusinessServiceTest.java | 4 +- .../ObjectiveBusinessServiceTest.java | 133 +++++++++++++++--- .../ObjectivePersistenceServiceIT.java | 7 + 5 files changed, 164 insertions(+), 40 deletions(-) diff --git a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java index 3ebfb0b5c2..f13c1fe4e4 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java @@ -1,5 +1,7 @@ package ch.puzzle.okr.controller; +import ch.puzzle.okr.dto.AlignmentDto; +import ch.puzzle.okr.dto.AlignmentObjectDto; import ch.puzzle.okr.dto.ObjectiveDto; import ch.puzzle.okr.mapper.ObjectiveMapper; import ch.puzzle.okr.models.*; @@ -86,10 +88,11 @@ class ObjectiveControllerIT { DESCRIPTION, State.DRAFT, LocalDateTime.MIN, LocalDateTime.MIN, true, "O5"); private static final ObjectiveDto objectiveAlignmentDto = new ObjectiveDto(9L, 1, "Objective Alignment", 1L, 1L, "GJ 22/23-Q2", DESCRIPTION, State.DRAFT, LocalDateTime.MAX, LocalDateTime.MAX, true, "O42"); - private static final KeyResultAlignmentsDto keyResultAlignmentsDto1 = new KeyResultAlignmentsDto(3L, "KR Title 1"); - private static final KeyResultAlignmentsDto keyResultAlignmentsDto2 = new KeyResultAlignmentsDto(6L, "KR Title 2"); - private static final ObjectiveAlignmentsDto alignmentPossibilities = new ObjectiveAlignmentsDto(1L, - "This is my objective title", List.of(keyResultAlignmentsDto1, keyResultAlignmentsDto2)); + private static final AlignmentObjectDto alignmentObject1 = new AlignmentObjectDto(3L, "KR Title 1", "keyResult"); + private static final AlignmentObjectDto alignmentObject2 = new AlignmentObjectDto(1L, "Objective Title 1", + "objective"); + private static final AlignmentDto alignmentPossibilities = new AlignmentDto(1L, "Puzzle ITC", + List.of(alignmentObject1, alignmentObject2)); @Autowired private MockMvc mvc; @@ -141,13 +144,14 @@ void getAlignmentPossibilities() throws Exception { .willReturn(List.of(alignmentPossibilities)); mvc.perform(get("/api/v2/objectives/alignmentPossibilities/5").contentType(MediaType.APPLICATION_JSON)) - .andExpect(MockMvcResultMatchers.status().isOk()) - .andExpect(jsonPath("$[0].objectiveTitle", Is.is("This is my objective title"))) - .andExpect(jsonPath("$[0].objectiveId", Is.is(1))) - .andExpect(jsonPath("$[0].keyResultAlignmentsDtos[0].keyResultId", Is.is(3))) - .andExpect(jsonPath("$[0].keyResultAlignmentsDtos[1].keyResultId", Is.is(6))) - .andExpect(jsonPath("$[0].keyResultAlignmentsDtos[0].keyResultTitle", Is.is("KR Title 1"))) - .andExpect(jsonPath("$[0].keyResultAlignmentsDtos[1].keyResultTitle", Is.is("KR Title 2"))); + .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$[0].teamId", Is.is(1))) + .andExpect(jsonPath("$[0].teamName", Is.is("Puzzle ITC"))) + .andExpect(jsonPath("$[0].alignmentObjectDtos[0].objectId", Is.is(3))) + .andExpect(jsonPath("$[0].alignmentObjectDtos[0].objectTitle", Is.is("KR Title 1"))) + .andExpect(jsonPath("$[0].alignmentObjectDtos[0].objectType", Is.is("keyResult"))) + .andExpect(jsonPath("$[0].alignmentObjectDtos[1].objectId", Is.is(1))) + .andExpect(jsonPath("$[0].alignmentObjectDtos[1].objectTitle", Is.is("Objective Title 1"))) + .andExpect(jsonPath("$[0].alignmentObjectDtos[1].objectType", Is.is("objective"))); verify(objectiveAuthorizationService, times(1)).getAlignmentPossibilities(5L); } diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java index 1f4def51ac..fde8da6881 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java @@ -1,5 +1,7 @@ package ch.puzzle.okr.service.authorization; +import ch.puzzle.okr.dto.AlignmentDto; +import ch.puzzle.okr.dto.AlignmentObjectDto; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.service.business.ObjectiveBusinessService; @@ -31,10 +33,11 @@ class ObjectiveAuthorizationServiceTest { private final AuthorizationUser authorizationUser = defaultAuthorizationUser(); private final Objective newObjective = Objective.Builder.builder().withId(5L).withTitle("Objective 1").build(); - private static final KeyResultAlignmentsDto keyResultAlignmentsDto1 = new KeyResultAlignmentsDto(3L, "KR Title 1"); - private static final KeyResultAlignmentsDto keyResultAlignmentsDto2 = new KeyResultAlignmentsDto(6L, "KR Title 2"); - private static final ObjectiveAlignmentsDto alignmentPossibilities = new ObjectiveAlignmentsDto(1L, - "This is my objective title", List.of(keyResultAlignmentsDto1, keyResultAlignmentsDto2)); + private static final AlignmentObjectDto alignmentObject1 = new AlignmentObjectDto(3L, "KR Title 1", "keyResult"); + private static final AlignmentObjectDto alignmentObject2 = new AlignmentObjectDto(1L, "Objective Title 1", + "objective"); + private static final AlignmentDto alignmentPossibilities = new AlignmentDto(1L, "Puzzle ITC", + List.of(alignmentObject1, alignmentObject2)); @Test void createEntityShouldReturnObjectiveWhenAuthorized() { @@ -150,12 +153,21 @@ void deleteEntityByIdShouldThrowExceptionWhenNotAuthorized() { void getAlignmentPossibilitiesShouldReturnListWhenAuthorized() { when(objectiveBusinessService.getAlignmentPossibilities(anyLong())).thenReturn(List.of(alignmentPossibilities)); - List alignmentPossibilities = objectiveAuthorizationService.getAlignmentPossibilities(3L); - assertEquals("This is my objective title", alignmentPossibilities.get(0).objectiveTitle()); - assertEquals(1, alignmentPossibilities.get(0).objectiveId()); - assertEquals(3, alignmentPossibilities.get(0).keyResultAlignmentsDtos().get(0).keyResultId()); - assertEquals("KR Title 1", alignmentPossibilities.get(0).keyResultAlignmentsDtos().get(0).keyResultTitle()); - assertEquals(6, alignmentPossibilities.get(0).keyResultAlignmentsDtos().get(1).keyResultId()); - assertEquals("KR Title 2", alignmentPossibilities.get(0).keyResultAlignmentsDtos().get(1).keyResultTitle()); + List alignmentPossibilities = objectiveAuthorizationService.getAlignmentPossibilities(3L); + assertEquals("Puzzle ITC", alignmentPossibilities.get(0).teamName()); + assertEquals(1, alignmentPossibilities.get(0).teamId()); + assertEquals(3, alignmentPossibilities.get(0).alignmentObjectDtos().get(0).objectId()); + assertEquals("KR Title 1", alignmentPossibilities.get(0).alignmentObjectDtos().get(0).objectTitle()); + assertEquals("keyResult", alignmentPossibilities.get(0).alignmentObjectDtos().get(0).objectType()); + assertEquals(1, alignmentPossibilities.get(0).alignmentObjectDtos().get(1).objectId()); + assertEquals("objective", alignmentPossibilities.get(0).alignmentObjectDtos().get(1).objectType()); + } + + @Test + void getAlignmentPossibilitiesShouldReturnEmptyListWhenNoAlignments() { + when(objectiveBusinessService.getAlignmentPossibilities(anyLong())).thenReturn(List.of()); + + List alignmentPossibilities = objectiveAuthorizationService.getAlignmentPossibilities(3L); + assertEquals(0, alignmentPossibilities.size()); } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java index 66e7031054..a7cd90df48 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -148,7 +148,7 @@ void shouldBuildAlignmentCorrectObjective() { Alignment alignment = alignmentBusinessService.buildAlignmentModel(objectiveAlignedObjective, 0); assertEquals(returnAlignment, alignment); - assertTrue(alignment instanceof ObjectiveAlignment); + assertInstanceOf(ObjectiveAlignment.class, alignment); } @Test @@ -160,7 +160,7 @@ void shouldBuildAlignmentCorrectKeyResult() { Alignment alignment = alignmentBusinessService.buildAlignmentModel(keyResultAlignedObjective, 0); assertEquals(returnAlignment, alignment); - assertTrue(alignment instanceof KeyResultAlignment); + assertInstanceOf(KeyResultAlignment.class, alignment); } @Test diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java index 567a39ebcf..08d0ceb947 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java @@ -1,5 +1,7 @@ package ch.puzzle.okr.service.business; +import ch.puzzle.okr.dto.AlignmentDto; +import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.*; import ch.puzzle.okr.models.authorization.AuthorizationUser; import ch.puzzle.okr.models.keyresult.KeyResult; @@ -16,6 +18,7 @@ import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; import org.springframework.web.server.ResponseStatusException; import java.time.LocalDateTime; @@ -51,16 +54,25 @@ class ObjectiveBusinessServiceTest { private final User user = User.Builder.builder().withId(1L).withFirstname("Bob").withLastname("Kaufmann") .withUsername("bkaufmann").withEmail("kaufmann@puzzle.ch").build(); private final Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").build(); - private final Objective fullObjective = Objective.Builder.builder().withTitle("FullObjective1").withCreatedBy(user) - .withTeam(team1).withQuarter(quarter).withDescription("This is our description") + private final Objective fullObjectiveCreate = Objective.Builder.builder().withTitle("FullObjective1") + .withCreatedBy(user).withTeam(team1).withQuarter(quarter).withDescription("This is our description") .withModifiedOn(LocalDateTime.MAX).build(); - private final Objective fullObjective2 = Objective.Builder.builder().withTitle("FullObjective2").withCreatedBy(user) - .withTeam(team1).withQuarter(quarter).withDescription("This is our description") + private final Objective fullObjective1 = Objective.Builder.builder().withId(1L).withTitle("FullObjective1") + .withCreatedBy(user).withTeam(team1).withQuarter(quarter).withDescription("This is our description") .withModifiedOn(LocalDateTime.MAX).build(); + private final Objective fullObjective2 = Objective.Builder.builder().withId(2L).withTitle("FullObjective2") + .withCreatedBy(user).withTeam(team1).withQuarter(quarter).withDescription("This is our description") + .withModifiedOn(LocalDateTime.MAX).build(); + private final Team team2 = Team.Builder.builder().withId(3L).withName("Puzzle ITC").build(); + private final Objective fullObjective3 = Objective.Builder.builder().withId(3L).withTitle("FullObjective5") + .withCreatedBy(user).withTeam(team2).withQuarter(quarter).withDescription("This is our description") + .withModifiedOn(LocalDateTime.MAX).build(); + private final KeyResult ordinalKeyResult2 = KeyResultOrdinal.Builder.builder().withCommitZone("Baum") + .withStretchZone("Wald").withId(6L).withTitle("Keyresult Ordinal 6").withObjective(fullObjective3).build(); private final KeyResult ordinalKeyResult = KeyResultOrdinal.Builder.builder().withCommitZone("Baum") .withStretchZone("Wald").withId(5L).withTitle("Keyresult Ordinal").withObjective(objective1).build(); - private final List keyResultList = List.of(ordinalKeyResult, ordinalKeyResult, ordinalKeyResult); - private final List objectiveList = List.of(objective1, fullObjective, fullObjective2); + private final List keyResultList = List.of(ordinalKeyResult, ordinalKeyResult); + private final List objectiveList = List.of(fullObjective1, fullObjective2); @Test void getOneObjective() { @@ -219,7 +231,7 @@ void shouldSaveANewObjectiveWithAlignment() { void shouldNotThrowResponseStatusExceptionWhenPuttingNullId() { Objective objective1 = Objective.Builder.builder().withId(null).withTitle("Title") .withDescription("Description").withModifiedOn(LocalDateTime.now()).build(); - when(objectiveBusinessService.createEntity(objective1, authorizationUser)).thenReturn(fullObjective); + when(objectiveBusinessService.createEntity(objective1, authorizationUser)).thenReturn(fullObjectiveCreate); Objective savedObjective = objectiveBusinessService.createEntity(objective1, authorizationUser); assertNull(savedObjective.getId()); @@ -266,7 +278,7 @@ void shouldDeleteObjectiveAndAssociatedKeyResults() { objectiveBusinessService.deleteEntityById(1L); - verify(keyResultBusinessService, times(3)).deleteEntityById(5L); + verify(keyResultBusinessService, times(2)).deleteEntityById(5L); verify(objectiveBusinessService, times(1)).deleteEntityById(1L); verify(alignmentBusinessService, times(1)).deleteAlignmentByObjectiveId(1L); } @@ -292,16 +304,105 @@ void shouldReturnAlignmentPossibilities() { when(objectivePersistenceService.findObjectiveByQuarterId(anyLong())).thenReturn(objectiveList); when(keyResultBusinessService.getAllKeyResultsByObjective(anyLong())).thenReturn(keyResultList); - List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); + List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); + + verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(1L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(2L); + assertEquals("Team1", alignmentsDtos.get(0).teamName()); + assertEquals(1, alignmentsDtos.get(0).teamId()); + assertEquals(6, alignmentsDtos.get(0).alignmentObjectDtos().size()); + assertEquals(1, alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectId()); + assertEquals("O - FullObjective1", alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectTitle()); + assertEquals("objective", alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectType()); + assertEquals(5, alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectId()); + assertEquals("KR - Keyresult Ordinal", alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectTitle()); + assertEquals("keyResult", alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectType()); + assertEquals(5, alignmentsDtos.get(0).alignmentObjectDtos().get(2).objectId()); + assertEquals("KR - Keyresult Ordinal", alignmentsDtos.get(0).alignmentObjectDtos().get(2).objectTitle()); + assertEquals("keyResult", alignmentsDtos.get(0).alignmentObjectDtos().get(2).objectType()); + assertEquals(2, alignmentsDtos.get(0).alignmentObjectDtos().get(3).objectId()); + assertEquals("O - FullObjective2", alignmentsDtos.get(0).alignmentObjectDtos().get(3).objectTitle()); + assertEquals("objective", alignmentsDtos.get(0).alignmentObjectDtos().get(3).objectType()); + assertEquals(5, alignmentsDtos.get(0).alignmentObjectDtos().get(4).objectId()); + assertEquals("KR - Keyresult Ordinal", alignmentsDtos.get(0).alignmentObjectDtos().get(4).objectTitle()); + assertEquals("keyResult", alignmentsDtos.get(0).alignmentObjectDtos().get(4).objectType()); + assertEquals(5, alignmentsDtos.get(0).alignmentObjectDtos().get(5).objectId()); + assertEquals("KR - Keyresult Ordinal", alignmentsDtos.get(0).alignmentObjectDtos().get(5).objectTitle()); + assertEquals("keyResult", alignmentsDtos.get(0).alignmentObjectDtos().get(5).objectType()); + } + + @Test + void shouldReturnAlignmentPossibilitiesWithMultipleTeams() { + List objectiveList = List.of(fullObjective1, fullObjective2, fullObjective3); + List keyResultList = List.of(ordinalKeyResult, ordinalKeyResult2); + + when(objectivePersistenceService.findObjectiveByQuarterId(anyLong())).thenReturn(objectiveList); + when(keyResultBusinessService.getAllKeyResultsByObjective(anyLong())).thenReturn(keyResultList); + + List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); + + verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(1L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(2L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(3L); + assertEquals(2, alignmentsDtos.size()); + assertEquals("Puzzle ITC", alignmentsDtos.get(0).teamName()); + assertEquals(3, alignmentsDtos.get(0).teamId()); + assertEquals(3, alignmentsDtos.get(0).alignmentObjectDtos().size()); + assertEquals("Team1", alignmentsDtos.get(1).teamName()); + assertEquals(1, alignmentsDtos.get(1).teamId()); + assertEquals(6, alignmentsDtos.get(1).alignmentObjectDtos().size()); + assertEquals(3, alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectId()); + assertEquals("O - FullObjective5", alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectTitle()); + assertEquals("objective", alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectType()); + assertEquals(1, alignmentsDtos.get(1).alignmentObjectDtos().get(0).objectId()); + assertEquals("O - FullObjective1", alignmentsDtos.get(1).alignmentObjectDtos().get(0).objectTitle()); + assertEquals("objective", alignmentsDtos.get(1).alignmentObjectDtos().get(0).objectType()); + } + + @Test + void shouldReturnAlignmentPossibilitiesOnlyObjectives() { + when(objectivePersistenceService.findObjectiveByQuarterId(anyLong())).thenReturn(objectiveList); + when(keyResultBusinessService.getAllKeyResultsByObjective(anyLong())).thenReturn(List.of()); + + List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); - verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(5L); - assertEquals("O - Objective 1", alignmentsDtos.get(0).objectiveTitle()); - assertEquals(5, alignmentsDtos.get(0).objectiveId()); - assertEquals("K - Keyresult Ordinal", alignmentsDtos.get(0).keyResultAlignmentsDtos().get(0).keyResultTitle()); - assertEquals("O - FullObjective1", alignmentsDtos.get(1).objectiveTitle()); - assertEquals("O - FullObjective2", alignmentsDtos.get(2).objectiveTitle()); - assertEquals(List.of(), alignmentsDtos.get(2).keyResultAlignmentsDtos()); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(1L); + verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(2L); + assertEquals(2, alignmentsDtos.get(0).alignmentObjectDtos().size()); + assertEquals(1, alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectId()); + assertEquals("O - FullObjective1", alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectTitle()); + assertEquals("objective", alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectType()); + assertEquals(2, alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectId()); + assertEquals("O - FullObjective2", alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectTitle()); + assertEquals("objective", alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectType()); + } + + @Test + void shouldReturnEmptyAlignmentPossibilities() { + List objectiveList = List.of(); + + when(objectivePersistenceService.findObjectiveByQuarterId(anyLong())).thenReturn(objectiveList); + + List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); + + verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); + verify(keyResultBusinessService, times(0)).getAllKeyResultsByObjective(anyLong()); + assertEquals(0, alignmentsDtos.size()); + } + + @Test + void shouldThrowExceptionWhenQuarterIdIsNull() { + Mockito.doThrow(new OkrResponseStatusException(HttpStatus.BAD_REQUEST, "ATTRIBUTE_NULL")).when(validator) + .validateOnGet(null); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> { + objectiveBusinessService.getAlignmentPossibilities(null); + }); + assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); + assertEquals("ATTRIBUTE_NULL", exception.getReason()); } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java index 9b8f8425c9..bda4b4976a 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java @@ -261,4 +261,11 @@ void findObjectiveByQuarterId() { assertEquals(7, objectiveList.size()); assertEquals("Wir wollen die Kundenzufriedenheit steigern", objectiveList.get(0).getTitle()); } + + @Test + void findObjectiveByQuarterIdShouldReturnEmptyListWhenQuarterDoesNotExist() { + List objectives = objectivePersistenceService.findObjectiveByQuarterId(12345L); + + assertEquals(0, objectives.size()); + } } From b0220204a15703b55e5e7c30e419644c5c377e93 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 27 May 2024 10:49:09 +0200 Subject: [PATCH 033/119] Clean up frontend --- .../objective-form.component.html | 6 ++-- .../objective-form.component.ts | 30 ++++++++----------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index b9bdf5d247..597b0f6ed2 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -74,7 +74,9 @@ type="text" formControlName="alignment" class="alignment-input" - placeholder="{{ filteredOptions.length == 0 ? 'Kein Alignment vorhanden' : 'Bezug wählen' }}" + placeholder="{{ + (this.filteredOptions | async)?.length == 0 ? 'Kein Alignment vorhanden' : 'Bezug wählen' + }}" [matAutocomplete]="auto" (input)="filter()" (focus)="filter(); input.select()" @@ -82,7 +84,7 @@ [value]="displayedValue" /> - @for (group of filteredOptions; track group) { + @for (group of filteredOptions | async; track group) { @for (alignmentObject of group.alignmentObjectDtos; track alignmentObject) { {{ alignmentObject.objectTitle }} diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index bf218b4625..db78fea82e 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -26,7 +26,7 @@ import { AlignmentPossibilityObject } from '../../types/model/AlignmentPossibili }) export class ObjectiveFormComponent implements OnInit { @ViewChild('input') input!: ElementRef; - filteredOptions: AlignmentPossibility[] = []; + filteredOptions: Subject = new Subject(); objectiveForm = new FormGroup({ title: new FormControl('', [Validators.required, Validators.minLength(2), Validators.maxLength(250)]), @@ -71,18 +71,10 @@ export class ObjectiveFormComponent implements OnInit { const value = this.objectiveForm.getRawValue(); const state = this.data.objective.objectiveId == null ? submitType : this.state; - let alignmentEntity = value.alignment; - let alignment: string | null; - - if (alignmentEntity) { - if (alignmentEntity.objectType == 'objective') { - alignment = 'O' + alignmentEntity.objectId; - } else { - alignment = 'K' + alignmentEntity.objectId; - } - } else { - alignment = null; - } + let alignmentEntity: AlignmentPossibilityObject | null = value.alignment; + let alignment: string | null = alignmentEntity + ? (alignmentEntity.objectType == 'objective' ? 'O' : 'K') + alignmentEntity.objectId + : null; let objectiveDTO: Objective = { id: this.data.objective.objectiveId, @@ -278,7 +270,7 @@ export class ObjectiveFormComponent implements OnInit { } } - this.filteredOptions = value.slice(); + this.filteredOptions.next(value.slice()); this.alignmentPossibilities$ = of(value); }); } @@ -302,7 +294,7 @@ export class ObjectiveFormComponent implements OnInit { updateAlignments() { this.input.nativeElement.value = ''; - this.filteredOptions = []; + this.filteredOptions.next([]); this.objectiveForm.patchValue({ alignment: null, }); @@ -337,11 +329,13 @@ export class ObjectiveFormComponent implements OnInit { })); if (optionList.length == 0) { - this.filteredOptions = alignmentPossibilities.filter((possibility: AlignmentPossibility) => - possibility.teamName.toLowerCase().includes(filterValue), + this.filteredOptions.next( + alignmentPossibilities.filter((possibility: AlignmentPossibility) => + possibility.teamName.toLowerCase().includes(filterValue), + ), ); } else { - this.filteredOptions = optionList; + this.filteredOptions.next(optionList); } }); } From 7751a067cce10f3c85aaee5bcf0ed033f432b5c3 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 27 May 2024 13:23:40 +0200 Subject: [PATCH 034/119] Adjust frontend tests --- .../objective-form.component.html | 4 +- .../objective-form.component.spec.ts | 326 ++++++++++-------- .../objective-form.component.ts | 18 +- frontend/src/app/shared/testData.ts | 34 +- 4 files changed, 220 insertions(+), 162 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index 597b0f6ed2..e650325af8 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -75,7 +75,7 @@ formControlName="alignment" class="alignment-input" placeholder="{{ - (this.filteredOptions | async)?.length == 0 ? 'Kein Alignment vorhanden' : 'Bezug wählen' + (this.filteredOptions$ | async)?.length == 0 ? 'Kein Alignment vorhanden' : 'Bezug wählen' }}" [matAutocomplete]="auto" (input)="filter()" @@ -84,7 +84,7 @@ [value]="displayedValue" /> - @for (group of filteredOptions | async; track group) { + @for (group of filteredOptions$ | async; track group) { @for (alignmentObject of group.alignmentObjectDtos; track alignmentObject) { {{ alignmentObject.objectTitle }} diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index 059fb442d8..b4ae56f3b4 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -10,7 +10,18 @@ import { MatSelectModule } from '@angular/material/select'; import { ReactiveFormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ObjectiveService } from '../../services/objective.service'; -import { objective, objectiveWithAlignment, quarter, quarterList, teamMin1 } from '../../testData'; +import { + alignmentObject1, + alignmentObject2, + alignmentObject3, + alignmentPossibility1, + alignmentPossibility2, + objective, + objectiveWithAlignment, + quarter, + quarterList, + teamMin1, +} from '../../testData'; import { Observable, of } from 'rxjs'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { HarnessLoader } from '@angular/cdk/testing'; @@ -27,6 +38,9 @@ import { DialogHeaderComponent } from '../../custom/dialog-header/dialog-header. import { TranslateTestingModule } from 'ngx-translate-testing'; import * as de from '../../../../assets/i18n/de.json'; import { ActivatedRoute } from '@angular/router'; +import { MatAutocomplete } from '@angular/material/autocomplete'; +import { AlignmentPossibility } from '../../types/model/AlignmentPossibility'; +import { ElementRef } from '@angular/core'; let objectiveService = { getFullObjective: jest.fn(), @@ -90,6 +104,7 @@ describe('ObjectiveDialogComponent', () => { MatSelectModule, ReactiveFormsModule, MatInputModule, + MatAutocomplete, NoopAnimationsModule, MatCheckboxModule, RouterTestingModule, @@ -118,6 +133,8 @@ describe('ObjectiveDialogComponent', () => { }); it.each([['DRAFT'], ['ONGOING']])('onSubmit create', async (state: string) => { + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); + //Prepare data let title: string = 'title'; let description: string = 'description'; @@ -185,7 +202,7 @@ describe('ObjectiveDialogComponent', () => { description: 'Test description', quarter: 0, team: 0, - alignment: '', + alignment: null, createKeyResults: false, }); @@ -202,18 +219,18 @@ describe('ObjectiveDialogComponent', () => { quarterId: 0, teamId: 0, version: undefined, - alignedEntityId: '', + alignedEntityId: null, }); }); - it('should create objective with alignment', () => { + it('should create objective with alignment objective', () => { matDataMock.objective.objectiveId = undefined; component.objectiveForm.setValue({ title: 'Test title with alignment', description: 'Test description', quarter: 0, team: 0, - alignment: 'K37', + alignment: alignmentObject2, createKeyResults: false, }); @@ -230,7 +247,35 @@ describe('ObjectiveDialogComponent', () => { quarterId: 0, teamId: 0, version: undefined, - alignedEntityId: 'K37', + alignedEntityId: 'O2', + }); + }); + + it('should create objective with alignment keyResult', () => { + matDataMock.objective.objectiveId = undefined; + component.objectiveForm.setValue({ + title: 'Test title with alignment', + description: 'Test description', + quarter: 0, + team: 0, + alignment: alignmentObject3, + createKeyResults: false, + }); + + objectiveService.createObjective.mockReturnValue(of({ ...objective, state: 'DRAFT' })); + component.onSubmit('DRAFT'); + + fixture.detectChanges(); + + expect(objectiveService.createObjective).toHaveBeenCalledWith({ + description: 'Test description', + id: undefined, + state: 'DRAFT', + title: 'Test title with alignment', + quarterId: 0, + teamId: 0, + version: undefined, + alignedEntityId: 'K1', }); }); @@ -241,7 +286,7 @@ describe('ObjectiveDialogComponent', () => { description: 'Test description', quarter: 1, team: 1, - alignment: '', + alignment: null, createKeyResults: false, }); @@ -258,7 +303,7 @@ describe('ObjectiveDialogComponent', () => { quarterId: 1, teamId: 1, version: undefined, - alignedEntityId: '', + alignedEntityId: null, }); }); @@ -271,7 +316,7 @@ describe('ObjectiveDialogComponent', () => { description: 'Test description', quarter: 1, team: 1, - alignment: 'K37', + alignment: alignmentObject3, createKeyResults: false, }); @@ -288,7 +333,7 @@ describe('ObjectiveDialogComponent', () => { quarterId: 1, teamId: 1, version: undefined, - alignedEntityId: 'K37', + alignedEntityId: 'K1', }); }); @@ -323,6 +368,7 @@ describe('ObjectiveDialogComponent', () => { matDataMock.objective.objectiveId = 1; const routerHarness = await RouterTestingHarness.create(); await routerHarness.navigateByUrl('/?quarter=2'); + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); objectiveService.getFullObjective.mockReturnValue(of(objectiveWithAlignment)); component.ngOnInit(); const rawFormValue = component.objectiveForm.getRawValue(); @@ -330,7 +376,7 @@ describe('ObjectiveDialogComponent', () => { expect(rawFormValue.description).toBe(objectiveWithAlignment.description); expect(rawFormValue.team).toBe(objectiveWithAlignment.teamId); expect(rawFormValue.quarter).toBe(objectiveWithAlignment.quarterId); - expect(rawFormValue.alignment).toBe(objectiveWithAlignment.alignedEntityId); + expect(rawFormValue.alignment).toBe(alignmentObject2); }); it('should return correct value if allowed to save to backlog', async () => { @@ -425,6 +471,7 @@ describe('ObjectiveDialogComponent', () => { MatSelectModule, ReactiveFormsModule, MatInputModule, + MatAutocomplete, NoopAnimationsModule, MatCheckboxModule, RouterTestingModule, @@ -442,25 +489,6 @@ describe('ObjectiveDialogComponent', () => { ], }); - let alignmentPossibilities = [ - { - objectiveId: 1003, - objectiveTitle: 'O - Test Objective', - keyResultAlignmentsDtos: [], - }, - { - objectiveId: 1005, - objectiveTitle: 'O - Company will grow', - keyResultAlignmentsDtos: [ - { - keyResultId: 6, - keyResultTitle: 'K - New structure', - }, - ], - }, - ]; - - jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of(alignmentPossibilities)); fixture = TestBed.createComponent(ObjectiveFormComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -472,152 +500,151 @@ describe('ObjectiveDialogComponent', () => { }); it('should load correct alignment possibilities', async () => { - let generatedPossibilities = [ - { - objectiveId: null, - objectiveTitle: 'Bitte wählen', - keyResultAlignmentsDtos: [], - }, - { - objectiveId: 1003, - objectiveTitle: 'O - Test Objective', - keyResultAlignmentsDtos: [], - }, - { - objectiveId: 1005, - objectiveTitle: 'O - Company will grow', - keyResultAlignmentsDtos: [ - { - keyResultId: 6, - keyResultTitle: 'K - New structure', - }, - ], - }, - ]; + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); + component.generateAlignmentPossibilities(3, null, null); + let alignmentPossibilities = null; + component.alignmentPossibilities$.subscribe((value) => { + alignmentPossibilities = value; + }); - let componentValue = null; + expect(alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.filteredOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.objectiveForm.getRawValue().alignment).toEqual(null); + }); + + it('should load not include current team in alignment possibilities', async () => { + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); + component.generateAlignmentPossibilities(3, null, 1); + let alignmentPossibilities = null; component.alignmentPossibilities$.subscribe((value) => { - componentValue = value; + alignmentPossibilities = value; }); - expect(componentValue).toStrictEqual(generatedPossibilities); + + expect(alignmentPossibilities).toStrictEqual([alignmentPossibility2]); + expect(component.filteredOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.objectiveForm.getRawValue().alignment).toEqual(null); }); - it('should not load current Objective to alignment possibilities', async () => { - matDataMock.objective.objectiveId = 1; - component.objective = objectiveWithAlignment; - objectiveService.getFullObjective.mockReturnValue(of(objectiveWithAlignment)); - fixture.detectChanges(); - component.ngOnInit(); + it('should load existing objective alignment to objectiveForm', async () => { + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); + component.generateAlignmentPossibilities(3, objectiveWithAlignment, null); + let alignmentPossibilities = null; + component.alignmentPossibilities$.subscribe((value) => { + alignmentPossibilities = value; + }); - let generatedPossibilities = [ - { - objectiveId: null, - objectiveTitle: 'Kein Alignment', - keyResultAlignmentsDtos: [], - }, - { - objectiveId: 1003, - objectiveTitle: 'O - Test Objective', - keyResultAlignmentsDtos: [], - }, - { - objectiveId: 1005, - objectiveTitle: 'O - Company will grow', - keyResultAlignmentsDtos: [ - { - keyResultId: 6, - keyResultTitle: 'K - New structure', - }, - ], - }, - ]; + expect(alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.filteredOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.objectiveForm.getRawValue().alignment).toEqual(alignmentObject2); + }); - let componentValue = null; + it('should load existing keyResult alignment to objectiveForm', async () => { + objectiveWithAlignment.alignedEntityId = 'K1'; + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); + component.generateAlignmentPossibilities(3, objectiveWithAlignment, null); + let alignmentPossibilities = null; component.alignmentPossibilities$.subscribe((value) => { - componentValue = value; + alignmentPossibilities = value; }); - expect(componentValue).toStrictEqual(generatedPossibilities); + + expect(alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.filteredOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.objectiveForm.getRawValue().alignment).toEqual(alignmentObject3); }); - it('should load Kein Alignment to alignment possibilities when objective have an alignment', async () => { - component.objective = objective; - component.data.objective.objectiveId = 5; - objectiveService.getFullObjective.mockReturnValue(of(objectiveWithAlignment)); - fixture.detectChanges(); - component.ngOnInit(); + it('should filter correct alignment possibilities', async () => { + // Search for one title + component.input.nativeElement.value = 'palm'; + component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); + component.filter(); + let modifiedAlignmentPossibility: AlignmentPossibility = { + teamId: 1, + teamName: 'Puzzle ITC', + alignmentObjectDtos: [alignmentObject3], + }; + expect(component.filteredOptions$.getValue()).toEqual([modifiedAlignmentPossibility]); - let generatedPossibilities = [ - { - objectiveId: null, - objectiveTitle: 'Kein Alignment', - keyResultAlignmentsDtos: [], - }, + // Search for team name + component.input.nativeElement.value = 'Puzzle IT'; + component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); + component.filter(); + modifiedAlignmentPossibility = { + teamId: 1, + teamName: 'Puzzle ITC', + alignmentObjectDtos: [alignmentObject2, alignmentObject3], + }; + expect(component.filteredOptions$.getValue()).toEqual([modifiedAlignmentPossibility]); + + // Search for two objects + component.input.nativeElement.value = 'we'; + component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); + component.filter(); + let modifiedAlignmentPossibilities = [ { - objectiveId: 1003, - objectiveTitle: 'O - Test Objective', - keyResultAlignmentsDtos: [], + teamId: 1, + teamName: 'Puzzle ITC', + alignmentObjectDtos: [alignmentObject2, alignmentObject3], }, { - objectiveId: 1005, - objectiveTitle: 'O - Company will grow', - keyResultAlignmentsDtos: [ - { - keyResultId: 6, - keyResultTitle: 'K - New structure', - }, - ], + teamId: 2, + teamName: 'We are cube', + alignmentObjectDtos: [alignmentObject1], }, ]; + expect(component.filteredOptions$.getValue()).toEqual(modifiedAlignmentPossibilities); - let componentValue = null; - component.alignmentPossibilities$.subscribe((value) => { - componentValue = value; - }); - expect(componentValue).toStrictEqual(generatedPossibilities); + // No match + component.input.nativeElement.value = 'findus'; + component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); + component.filter(); + expect(component.filteredOptions$.getValue()).toEqual([]); }); - it('should load Kein Alignment to alignment possibilities when choosing one alignment', async () => { - objectiveService.getFullObjective.mockReturnValue(of(objective)); - component.objective = objective; - component.data.objective.objectiveId = 5; - fixture.detectChanges(); - component.ngOnInit(); - component.changeFirstAlignmentPossibility(); + it('should find correct alignment object', () => { + // objective + let alignmentObject = component.findAlignmentObject( + [alignmentPossibility1, alignmentPossibility2], + 1, + 'objective', + ); + expect(alignmentObject!.objectId).toEqual(1); + expect(alignmentObject!.objectTitle).toEqual('We want to increase the income'); + + // keyResult + alignmentObject = component.findAlignmentObject([alignmentPossibility1, alignmentPossibility2], 1, 'keyResult'); + expect(alignmentObject!.objectId).toEqual(1); + expect(alignmentObject!.objectTitle).toEqual('We buy 3 palms'); + + // no match + alignmentObject = component.findAlignmentObject([alignmentPossibility1, alignmentPossibility2], 133, 'keyResult'); + expect(alignmentObject).toEqual(null); + }); - let currentPossibilities = [ - { - objectiveId: null, - objectiveTitle: 'Kein Alignment', - keyResultAlignmentsDtos: [], - }, - { - objectiveId: 1003, - objectiveTitle: 'O - Test Objective', - keyResultAlignmentsDtos: [], - }, - { - objectiveId: 1005, - objectiveTitle: 'O - Company will grow', - keyResultAlignmentsDtos: [ - { - keyResultId: 6, - keyResultTitle: 'K - New structure', - }, - ], - }, - ]; + it('should display kein alignment vorhanden when no alignment possibility', () => { + component.filteredOptions$.next([alignmentPossibility1, alignmentPossibility2]); + fixture.detectChanges(); + expect(component.input.nativeElement.getAttribute('placeholder')).toEqual('Bezug wählen'); - let componentValue = null; - component.alignmentPossibilities$.subscribe((value) => { - componentValue = value; - }); - expect(componentValue).toStrictEqual(currentPossibilities); + component.filteredOptions$.next([]); + fixture.detectChanges(); + expect(component.input.nativeElement.getAttribute('placeholder')).toEqual('Kein Alignment vorhanden'); }); - it('should call ObjectiveService when updating Alignments', async () => { + it('should update alignments on quarter change', () => { + objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); component.updateAlignments(); + expect(component.input.nativeElement.value).toEqual(''); + expect(component.objectiveForm.getRawValue().alignment).toEqual(null); expect(objectiveService.getAlignmentPossibilities).toHaveBeenCalled(); }); + + it('should return correct displayedValue', () => { + component.input.nativeElement.value = 'O - Objective 1'; + expect(component.displayedValue).toEqual('O - Objective 1'); + + component.input = new ElementRef(document.createElement('input')); + expect(component.displayedValue).toEqual(''); + }); }); describe('Backlog quarter', () => { @@ -631,6 +658,7 @@ describe('ObjectiveDialogComponent', () => { MatSelectModule, ReactiveFormsModule, MatInputModule, + MatAutocomplete, NoopAnimationsModule, MatCheckboxModule, RouterTestingModule, diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index db78fea82e..01523bf190 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -4,7 +4,7 @@ import { Quarter } from '../../types/model/Quarter'; import { TeamService } from '../../services/team.service'; import { Team } from '../../types/model/Team'; import { QuarterService } from '../../services/quarter.service'; -import { forkJoin, Observable, of, Subject } from 'rxjs'; +import { BehaviorSubject, forkJoin, Observable, of } from 'rxjs'; import { ObjectiveService } from '../../services/objective.service'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { State } from '../../types/enums/State'; @@ -26,7 +26,7 @@ import { AlignmentPossibilityObject } from '../../types/model/AlignmentPossibili }) export class ObjectiveFormComponent implements OnInit { @ViewChild('input') input!: ElementRef; - filteredOptions: Subject = new Subject(); + filteredOptions$: BehaviorSubject = new BehaviorSubject([]); objectiveForm = new FormGroup({ title: new FormControl('', [Validators.required, Validators.minLength(2), Validators.maxLength(250)]), @@ -41,8 +41,7 @@ export class ObjectiveFormComponent implements OnInit { objective: Objective | null = null; teams$: Observable = of([]); alignmentPossibilities$: Observable = of([]); - currentTeam$: Subject = new Subject(); - currentTeam: Team | null = null; + currentTeam$: BehaviorSubject = new BehaviorSubject(null); state: string | null = null; version!: number; protected readonly formInputCheck = formInputCheck; @@ -117,7 +116,6 @@ export class ObjectiveFormComponent implements OnInit { this.teams$.subscribe((value) => { let team: Team = value.filter((team: Team) => team.id == teamId)[0]; this.currentTeam$.next(team); - this.currentTeam = team; }); this.generateAlignmentPossibilities(quarterId, objective, teamId!); @@ -270,7 +268,7 @@ export class ObjectiveFormComponent implements OnInit { } } - this.filteredOptions.next(value.slice()); + this.filteredOptions$.next(value.slice()); this.alignmentPossibilities$ = of(value); }); } @@ -294,11 +292,11 @@ export class ObjectiveFormComponent implements OnInit { updateAlignments() { this.input.nativeElement.value = ''; - this.filteredOptions.next([]); + this.filteredOptions$.next([]); this.objectiveForm.patchValue({ alignment: null, }); - this.generateAlignmentPossibilities(this.objectiveForm.value.quarter!, null, this.currentTeam!.id); + this.generateAlignmentPossibilities(this.objectiveForm.value.quarter!, null, this.currentTeam$.getValue()!.id); } filter() { @@ -329,13 +327,13 @@ export class ObjectiveFormComponent implements OnInit { })); if (optionList.length == 0) { - this.filteredOptions.next( + this.filteredOptions$.next( alignmentPossibilities.filter((possibility: AlignmentPossibility) => possibility.teamName.toLowerCase().includes(filterValue), ), ); } else { - this.filteredOptions.next(optionList); + this.filteredOptions$.next(optionList); } }); } diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index 4c0912a567..913f76f962 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -19,6 +19,8 @@ import { Action } from './types/model/Action'; import { OrganisationState } from './types/enums/OrganisationState'; import { Organisation } from './types/model/Organisation'; import { Dashboard } from './types/model/Dashboard'; +import { AlignmentPossibilityObject } from './types/model/AlignmentPossibilityObject'; +import { AlignmentPossibility } from './types/model/AlignmentPossibility'; export const organisationActive = { id: 1, @@ -377,7 +379,7 @@ export const objectiveWithAlignment: Objective = { quarterLabel: 'GJ 22/23-Q2', state: State.SUCCESSFUL, writeable: true, - alignedEntityId: 'O6', + alignedEntityId: 'O2', }; export const objectiveWriteableFalse: Objective = { @@ -630,3 +632,33 @@ export const keyResultActions: KeyResultMetric = { actionList: [action1, action2], writeable: true, }; + +export const alignmentObject1: AlignmentPossibilityObject = { + objectId: 1, + objectTitle: 'We want to increase the income', + objectType: 'objective', +}; + +export const alignmentObject2: AlignmentPossibilityObject = { + objectId: 2, + objectTitle: 'Our office has more plants for we', + objectType: 'objective', +}; + +export const alignmentObject3: AlignmentPossibilityObject = { + objectId: 1, + objectTitle: 'We buy 3 palms', + objectType: 'keyResult', +}; + +export const alignmentPossibility1: AlignmentPossibility = { + teamId: 1, + teamName: 'Puzzle ITC', + alignmentObjectDtos: [alignmentObject2, alignmentObject3], +}; + +export const alignmentPossibility2: AlignmentPossibility = { + teamId: 2, + teamName: 'We are cube', + alignmentObjectDtos: [alignmentObject1], +}; From 1bb84dffbc73d4b59115f52bbcd86416234475f4 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 27 May 2024 16:57:54 +0200 Subject: [PATCH 035/119] Adjust e2e tests --- .../cypress/e2e/objective-alignment.cy.ts | 145 +++++++++++------- .../objective-form.component.html | 1 + 2 files changed, 94 insertions(+), 52 deletions(-) diff --git a/frontend/cypress/e2e/objective-alignment.cy.ts b/frontend/cypress/e2e/objective-alignment.cy.ts index 1d6cb6f990..22770ba7ff 100644 --- a/frontend/cypress/e2e/objective-alignment.cy.ts +++ b/frontend/cypress/e2e/objective-alignment.cy.ts @@ -10,13 +10,10 @@ describe('OKR Objective Alignment e2e tests', () => { cy.getByTestId('add-objective').first().click(); cy.getByTestId('title').first().clear().type('Objective with new alignment'); - cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); - cy.get('select#alignment') - .contains('K - Steigern der URS um 25%') - .then(($option) => { - const optionValue = $option.attr('value'); - cy.get('select#alignment').select(optionValue!); - }); + cy.getByTestId('alignmentInput').first().should('have.attr', 'placeholder', 'Bezug wählen'); + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); cy.getByTestId('safe').click(); @@ -31,20 +28,18 @@ describe('OKR Objective Alignment e2e tests', () => { .contains('Objective bearbeiten') .click(); - cy.get('select#alignment option:selected').should('contain.text', 'K - Steigern der URS um 25%'); + cy.getByTestId('alignmentInput') + .first() + .should('have.value', 'O - Als BBT wollen wir den Arbeitsalltag der Members von Puzzle ITC erleichtern.'); }); it(`Update alignment of Objective`, () => { cy.getByTestId('add-objective').first().click(); cy.getByTestId('title').first().clear().type('We change alignment of this Objective'); - cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); - cy.get('select#alignment') - .contains('K - Steigern der URS um 25%') - .then(($option) => { - const optionValue = $option.attr('value'); - cy.get('select#alignment').select(optionValue!); - }); + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); cy.getByTestId('safe').click(); cy.contains('We change alignment of this Objective'); @@ -58,12 +53,11 @@ describe('OKR Objective Alignment e2e tests', () => { .contains('Objective bearbeiten') .click(); - cy.get('select#alignment') - .contains('K - Antwortzeit für Supportanfragen um 33% verkürzen.') - .then(($option) => { - const optionValue = $option.attr('value'); - cy.get('select#alignment').select(optionValue!); - }); + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('Delete'); + cy.realPress('ArrowDown'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); cy.getByTestId('safe').click(); cy.getByTestId('objective') @@ -76,23 +70,18 @@ describe('OKR Objective Alignment e2e tests', () => { .contains('Objective bearbeiten') .click(); - cy.get('select#alignment option:selected').should( - 'contain.text', - 'K - Antwortzeit für Supportanfragen um 33% verkürzen.', - ); + cy.getByTestId('alignmentInput') + .first() + .should('have.value', 'KR - Das BBT hilft den Membern 20% mehr beim Töggelen'); }); it(`Delete alignment of Objective`, () => { cy.getByTestId('add-objective').first().click(); cy.getByTestId('title').first().clear().type('We delete the alignment'); - cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); - cy.get('select#alignment') - .contains('K - Steigern der URS um 25%') - .then(($option) => { - const optionValue = $option.attr('value'); - cy.get('select#alignment').select(optionValue!); - }); + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); cy.getByTestId('safe').click(); cy.contains('We delete the alignment'); @@ -106,7 +95,9 @@ describe('OKR Objective Alignment e2e tests', () => { .contains('Objective bearbeiten') .click(); - cy.get('select#alignment').select('Kein Alignment'); + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('Delete'); + cy.tabForward(); cy.getByTestId('safe').click(); cy.getByTestId('objective') @@ -119,42 +110,92 @@ describe('OKR Objective Alignment e2e tests', () => { .contains('Objective bearbeiten') .click(); - cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); + cy.getByTestId('alignmentInput').first().should('have.attr', 'placeholder', 'Bezug wählen'); }); - it(`Alignment Possibilites change when quarter change`, () => { + it(`Alignment Possibilities change when quarter change`, () => { cy.visit('/?quarter=1'); + cy.get('mat-chip:visible:contains("Alle")').click(); + cy.get('mat-chip:visible:contains("Alle")').click(); + cy.get('mat-chip:visible:contains("/BBT")').click(); + cy.getByTestId('add-objective').first().click(); cy.getByTestId('title').first().clear().type('We can link later on this'); cy.getByTestId('safe').click(); + cy.get('mat-chip:visible:contains("Alle")').click(); + cy.getByTestId('add-objective').first().click(); cy.getByTestId('title').first().clear().type('There is my other alignment'); - cy.get('select#alignment option:selected').should('contain.text', 'Bitte wählen'); + cy.getByTestId('alignmentInput').first().should('have.attr', 'placeholder', 'Bezug wählen'); + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); - cy.get('select#alignment').select(1); + cy.getByTestId('alignmentInput') + .first() + .invoke('val') + .then((val) => { + const selectValue = val; + cy.getByTestId('quarterSelect').select('GJ 23/24-Q1'); + cy.getByTestId('title').first().clear().type('There is our other alignment'); - cy.get('select#alignment option:selected').then(($select) => { - const selectValue = $select.text(); - cy.getByTestId('quarterSelect').select('GJ 23/24-Q1'); - cy.getByTestId('title').first().clear().type('There is our other alignment'); + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); - cy.get('select#alignment').select(1); + cy.getByTestId('alignmentInput').first().should('not.have.value', selectValue); - cy.get('select#alignment option:selected').should('not.contain.text', selectValue); - cy.getByTestId('cancel').click(); + cy.getByTestId('cancel').click(); - cy.visit('/?quarter=2'); + cy.visit('/?quarter=2'); + + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('Quarter change objective'); + + cy.get('select#quarter').select('GJ 22/23-Q4'); + cy.getByTestId('title').first().clear().type('A new title'); + cy.tabForwardUntil('[data-testId="alignmentInput"]'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); + + cy.getByTestId('alignmentInput').first().should('have.value', selectValue); + }); + }); + + it(`Correct placeholder`, () => { + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('This is an objective which we dont save'); + cy.getByTestId('alignmentInput').first().should('have.attr', 'placeholder', 'Bezug wählen'); + + cy.getByTestId('quarterSelect').select('GJ 23/24-Q3'); + cy.getByTestId('title').first().clear().type('We changed the quarter'); + + cy.getByTestId('alignmentInput').first().should('have.attr', 'placeholder', 'Kein Alignment vorhanden'); + }); + + it(`Correct filtering`, () => { + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('Die urs steigt'); + cy.getByTestId('safe').click(); + + cy.scrollTo('top'); + cy.get('mat-chip:visible:contains("Puzzle ITC")').click(); + cy.get('mat-chip:visible:contains("/BBT")').click(); + cy.getByTestId('add-objective').first().click(); + cy.getByTestId('title').first().clear().type('Ein alignment objective'); - cy.getByTestId('add-objective').first().click(); - cy.getByTestId('title').first().clear().type('Quarter change objective'); + cy.getByTestId('alignmentInput').clear().type('urs'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); - cy.get('select#quarter').select('GJ 22/23-Q4'); - cy.getByTestId('title').first().clear().type('A new title'); - cy.get('select#alignment').select(1); + cy.getByTestId('alignmentInput').first().should('have.value', 'O - Die urs steigt'); - cy.get('select#alignment option:selected').should('contain.text', selectValue); - }); + cy.getByTestId('alignmentInput').clear().type('urs'); + cy.realPress('ArrowDown'); + cy.realPress('ArrowDown'); + cy.realPress('Enter'); + cy.getByTestId('alignmentInput').first().should('have.value', 'KR - Steigern der URS um 25%'); }); }); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index e650325af8..a9a41bbc52 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -78,6 +78,7 @@ (this.filteredOptions$ | async)?.length == 0 ? 'Kein Alignment vorhanden' : 'Bezug wählen' }}" [matAutocomplete]="auto" + [attr.data-testId]="'alignmentInput'" (input)="filter()" (focus)="filter(); input.select()" (focusout)="scrollLeft()" From 63eab27e5f942875d38ed2d96d8bd1701a4c528f Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 29 May 2024 15:41:50 +0200 Subject: [PATCH 036/119] Fix frontend tests --- .../objective-form.component.spec.ts | 30 +++++++++---------- .../objective-form.component.ts | 15 ++++------ frontend/src/app/shared/testData.ts | 2 +- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index b4ae56f3b4..79bd67f658 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -53,9 +53,9 @@ let objectiveService = { const quarterService = { getAllQuarters(): Observable { return of([ + { id: 199, startDate: null, endDate: null, label: 'Backlog' }, { id: 1, startDate: quarter.startDate, endDate: quarter.endDate, label: quarter.label }, { id: 2, startDate: quarter.startDate, endDate: quarter.endDate, label: quarter.label }, - { id: 199, startDate: null, endDate: null, label: 'Backlog' }, ]); }, }; @@ -145,7 +145,7 @@ describe('ObjectiveDialogComponent', () => { team = teams[0].id; }); quarterService.getAllQuarters().subscribe((quarters) => { - quarter = quarters[1].id; + quarter = quarters[2].id; }); // Get input elements and set values @@ -381,8 +381,8 @@ describe('ObjectiveDialogComponent', () => { it('should return correct value if allowed to save to backlog', async () => { component.quarters = quarterList; - const isBacklogQuarterSpy = jest.spyOn(component, 'isBacklogQuarter'); - isBacklogQuarterSpy.mockReturnValue(false); + const isBacklogQuarterSpy = jest.spyOn(component, 'isNotBacklogQuarter'); + isBacklogQuarterSpy.mockReturnValue(true); component.data.action = 'duplicate'; fixture.detectChanges(); @@ -396,6 +396,7 @@ describe('ObjectiveDialogComponent', () => { expect(component.allowedToSaveBacklog()).toBeTruthy(); component.state = 'ONGOING'; + isBacklogQuarterSpy.mockReturnValue(false); fixture.detectChanges(); expect(component.allowedToSaveBacklog()).toBeFalsy(); @@ -512,7 +513,7 @@ describe('ObjectiveDialogComponent', () => { expect(component.objectiveForm.getRawValue().alignment).toEqual(null); }); - it('should load not include current team in alignment possibilities', async () => { + it('should not include current team in alignment possibilities', async () => { objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); component.generateAlignmentPossibilities(3, null, 1); let alignmentPossibilities = null; @@ -521,7 +522,7 @@ describe('ObjectiveDialogComponent', () => { }); expect(alignmentPossibilities).toStrictEqual([alignmentPossibility2]); - expect(component.filteredOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.filteredOptions$.getValue()).toEqual([alignmentPossibility2]); expect(component.objectiveForm.getRawValue().alignment).toEqual(null); }); @@ -679,6 +680,13 @@ describe('ObjectiveDialogComponent', () => { jest.spyOn(objectiveService, 'getAlignmentPossibilities').mockReturnValue(of([])); fixture = TestBed.createComponent(ObjectiveFormComponent); component = fixture.componentInstance; + component.data = { + objective: { + objectiveId: 1, + teamId: 1, + }, + action: 'releaseBacklog', + }; fixture.detectChanges(); loader = TestbedHarnessEnvironment.loader(fixture); }); @@ -688,15 +696,7 @@ describe('ObjectiveDialogComponent', () => { }); it('should set correct default value if objective is released in backlog', async () => { - component.data = { - objective: { - objectiveId: 1, - teamId: 1, - }, - action: 'releaseBacklog', - }; - - const isBacklogQuarterSpy = jest.spyOn(component, 'isBacklogQuarter'); + const isBacklogQuarterSpy = jest.spyOn(component, 'isNotBacklogQuarter'); isBacklogQuarterSpy.mockReturnValue(false); const routerHarness = await RouterTestingHarness.create(); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 01523bf190..ddcfbbdb6d 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -38,7 +38,6 @@ export class ObjectiveFormComponent implements OnInit { }); quarters$: Observable = of([]); quarters: Quarter[] = []; - objective: Objective | null = null; teams$: Observable = of([]); alignmentPossibilities$: Observable = of([]); currentTeam$: BehaviorSubject = new BehaviorSubject(null); @@ -102,20 +101,18 @@ export class ObjectiveFormComponent implements OnInit { forkJoin([objective$, this.quarters$]).subscribe(([objective, quarters]) => { this.quarters = quarters; - this.objective = objective; const teamId = isCreating ? objective.teamId : this.data.objective.teamId; let quarterId = getValueFromQuery(this.route.snapshot.queryParams['quarter'], quarters[2].id)[0]; let currentQuarter: Quarter | undefined = this.quarters.find((quarter) => quarter.id == quarterId); - if (currentQuarter && !this.isBacklogQuarter(currentQuarter.label) && this.data.action == 'releaseBacklog') { + if (currentQuarter && !this.isNotBacklogQuarter(currentQuarter.label) && this.data.action == 'releaseBacklog') { quarterId = quarters[2].id; } this.state = objective.state; this.version = objective.version; this.teams$.subscribe((value) => { - let team: Team = value.filter((team: Team) => team.id == teamId)[0]; - this.currentTeam$.next(team); + this.currentTeam$.next(value.filter((team: Team) => team.id == teamId)[0]); }); this.generateAlignmentPossibilities(quarterId, objective, teamId!); @@ -216,12 +213,12 @@ export class ObjectiveFormComponent implements OnInit { (quarter) => quarter.id == this.objectiveForm.value.quarter, ); if (currentQuarter) { - let isBacklogCurrent: boolean = !this.isBacklogQuarter(currentQuarter.label); + let isBacklogCurrent: boolean = this.isNotBacklogQuarter(currentQuarter.label); if (this.data.action == 'duplicate') return true; if (this.data.objective.objectiveId) { - return isBacklogCurrent ? this.state == 'DRAFT' : true; + return !isBacklogCurrent ? this.state == 'DRAFT' : true; } else { - return !isBacklogCurrent; + return isBacklogCurrent; } } else { return true; @@ -244,7 +241,7 @@ export class ObjectiveFormComponent implements OnInit { } } - isBacklogQuarter(label: string) { + isNotBacklogQuarter(label: string) { return GJ_REGEX_PATTERN.test(label); } diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index 913f76f962..86093db50f 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -137,7 +137,7 @@ export const quarterBacklog: Quarter = { endDate: null, } as Quarter; -export const quarterList: Quarter[] = [quarter1, quarter2, quarterBacklog]; +export const quarterList: Quarter[] = [quarterBacklog, quarter1, quarter2]; export const checkInMetric: CheckInMin = { id: 815, From 33e7b7649dbd813df2e428263738f90acd836443 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 30 May 2024 11:43:45 +0200 Subject: [PATCH 037/119] Adjust filter function --- .../objective-form.component.spec.ts | 13 ++++++++++--- .../objective-dialog/objective-form.component.ts | 15 ++++++--------- frontend/src/app/shared/testData.ts | 4 ++-- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index 79bd67f658..899d254285 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -526,6 +526,13 @@ describe('ObjectiveDialogComponent', () => { expect(component.objectiveForm.getRawValue().alignment).toEqual(null); }); + it('should return team and objective with same text in alignment possibilities', async () => { + component.input.nativeElement.value = 'puzzle'; + component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); + component.filter(); + expect(component.filteredOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + }); + it('should load existing objective alignment to objectiveForm', async () => { objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); component.generateAlignmentPossibilities(3, objectiveWithAlignment, null); @@ -577,14 +584,14 @@ describe('ObjectiveDialogComponent', () => { expect(component.filteredOptions$.getValue()).toEqual([modifiedAlignmentPossibility]); // Search for two objects - component.input.nativeElement.value = 'we'; + component.input.nativeElement.value = 'buy'; component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); component.filter(); let modifiedAlignmentPossibilities = [ { teamId: 1, teamName: 'Puzzle ITC', - alignmentObjectDtos: [alignmentObject2, alignmentObject3], + alignmentObjectDtos: [alignmentObject3], }, { teamId: 2, @@ -609,7 +616,7 @@ describe('ObjectiveDialogComponent', () => { 'objective', ); expect(alignmentObject!.objectId).toEqual(1); - expect(alignmentObject!.objectTitle).toEqual('We want to increase the income'); + expect(alignmentObject!.objectTitle).toEqual('We want to increase the income puzzle buy'); // keyResult alignmentObject = component.findAlignmentObject([alignmentPossibility1, alignmentPossibility2], 1, 'keyResult'); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index ddcfbbdb6d..a8e4b8e09b 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -299,6 +299,10 @@ export class ObjectiveFormComponent implements OnInit { filter() { let filterValue = this.input.nativeElement.value.toLowerCase(); this.alignmentPossibilities$.subscribe((alignmentPossibilities: AlignmentPossibility[]) => { + let matchingTeams = alignmentPossibilities.filter((possibility: AlignmentPossibility) => + possibility.teamName.toLowerCase().includes(filterValue), + ); + let filteredObjects: AlignmentPossibilityObject[] = alignmentPossibilities.flatMap( (alignmentPossibility: AlignmentPossibility) => alignmentPossibility.alignmentObjectDtos.filter((alignmentPossibilityObject: AlignmentPossibilityObject) => @@ -323,15 +327,8 @@ export class ObjectiveFormComponent implements OnInit { ), })); - if (optionList.length == 0) { - this.filteredOptions$.next( - alignmentPossibilities.filter((possibility: AlignmentPossibility) => - possibility.teamName.toLowerCase().includes(filterValue), - ), - ); - } else { - this.filteredOptions$.next(optionList); - } + let finalArray = matchingTeams.concat(optionList); + this.filteredOptions$.next([...new Set(finalArray)]); }); } diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index 86093db50f..17790a00c0 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -635,13 +635,13 @@ export const keyResultActions: KeyResultMetric = { export const alignmentObject1: AlignmentPossibilityObject = { objectId: 1, - objectTitle: 'We want to increase the income', + objectTitle: 'We want to increase the income puzzle buy', objectType: 'objective', }; export const alignmentObject2: AlignmentPossibilityObject = { objectId: 2, - objectTitle: 'Our office has more plants for we', + objectTitle: 'Our office has more plants for', objectType: 'objective', }; From 1114772e57662b85896b4b15be6b1cc10811fa9d Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 3 Jun 2024 10:33:49 +0200 Subject: [PATCH 038/119] Fix bug with duplicated alignment possibilities --- .../objective-dialog/objective-form.component.spec.ts | 8 ++++---- .../dialog/objective-dialog/objective-form.component.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index 899d254285..87677017e6 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -38,7 +38,7 @@ import { DialogHeaderComponent } from '../../custom/dialog-header/dialog-header. import { TranslateTestingModule } from 'ngx-translate-testing'; import * as de from '../../../../assets/i18n/de.json'; import { ActivatedRoute } from '@angular/router'; -import { MatAutocomplete } from '@angular/material/autocomplete'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { AlignmentPossibility } from '../../types/model/AlignmentPossibility'; import { ElementRef } from '@angular/core'; @@ -104,7 +104,7 @@ describe('ObjectiveDialogComponent', () => { MatSelectModule, ReactiveFormsModule, MatInputModule, - MatAutocomplete, + MatAutocompleteModule, NoopAnimationsModule, MatCheckboxModule, RouterTestingModule, @@ -472,7 +472,7 @@ describe('ObjectiveDialogComponent', () => { MatSelectModule, ReactiveFormsModule, MatInputModule, - MatAutocomplete, + MatAutocompleteModule, NoopAnimationsModule, MatCheckboxModule, RouterTestingModule, @@ -666,7 +666,7 @@ describe('ObjectiveDialogComponent', () => { MatSelectModule, ReactiveFormsModule, MatInputModule, - MatAutocomplete, + MatAutocompleteModule, NoopAnimationsModule, MatCheckboxModule, RouterTestingModule, diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index a8e4b8e09b..7725c7ca2f 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -297,9 +297,9 @@ export class ObjectiveFormComponent implements OnInit { } filter() { - let filterValue = this.input.nativeElement.value.toLowerCase(); + let filterValue: string = this.input.nativeElement.value.toLowerCase(); this.alignmentPossibilities$.subscribe((alignmentPossibilities: AlignmentPossibility[]) => { - let matchingTeams = alignmentPossibilities.filter((possibility: AlignmentPossibility) => + let matchingTeams: AlignmentPossibility[] = alignmentPossibilities.filter((possibility: AlignmentPossibility) => possibility.teamName.toLowerCase().includes(filterValue), ); @@ -327,7 +327,7 @@ export class ObjectiveFormComponent implements OnInit { ), })); - let finalArray = matchingTeams.concat(optionList); + let finalArray: AlignmentPossibility[] = filterValue == '' ? matchingTeams : matchingTeams.concat(optionList); this.filteredOptions$.next([...new Set(finalArray)]); }); } From 268738e988bd99a2fe956a757a05cea3b416f859 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 3 Jun 2024 10:58:37 +0200 Subject: [PATCH 039/119] Simplify backend function --- .../okr/service/business/ObjectiveBusinessService.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java index 85944e1332..2fc9c40a54 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java @@ -57,9 +57,8 @@ public List getAlignmentPossibilities(Long quarterId) { List objectivesByQuarter = objectivePersistenceService.findObjectiveByQuarterId(quarterId); List alignmentDtoList = new ArrayList<>(); - Set teamSet = new HashSet<>(); - objectivesByQuarter.forEach(objective -> teamSet.add(objective.getTeam())); - List teamList = new ArrayList<>(teamSet.stream().sorted(Comparator.comparing(Team::getName)).toList()); + List teamList = objectivesByQuarter.stream().map(Objective::getTeam).distinct() + .sorted(Comparator.comparing(Team::getName)).toList(); teamList.forEach(team -> { List filteredObjectiveList = objectivesByQuarter.stream() From 87596bc9ea84fb1d83f5a9ddbf12746bcf2a441f Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 3 Jun 2024 13:50:33 +0200 Subject: [PATCH 040/119] Add check for alignment in same team --- .../src/main/java/ch/puzzle/okr/ErrorKey.java | 2 +- .../AlignmentValidationService.java | 26 +++++- .../AlignmentValidationServiceTest.java | 87 ++++++++++++++++++- frontend/src/assets/i18n/de.json | 1 + 4 files changed, 112 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/ErrorKey.java b/backend/src/main/java/ch/puzzle/okr/ErrorKey.java index b9fdf4c89a..47a11e39af 100644 --- a/backend/src/main/java/ch/puzzle/okr/ErrorKey.java +++ b/backend/src/main/java/ch/puzzle/okr/ErrorKey.java @@ -4,5 +4,5 @@ public enum ErrorKey { ATTRIBUTE_NULL, ATTRIBUTE_CHANGED, ATTRIBUTE_SET_FORBIDDEN, ATTRIBUTE_NOT_SET, ATTRIBUTE_CANNOT_CHANGE, ATTRIBUTE_MUST_BE_DRAFT, KEY_RESULT_CONVERSION, ALREADY_EXISTS_SAME_NAME, CONVERT_TOKEN, DATA_HAS_BEEN_UPDATED, MODEL_NULL, MODEL_WITH_ID_NOT_FOUND, NOT_AUTHORIZED_TO_READ, NOT_AUTHORIZED_TO_WRITE, NOT_AUTHORIZED_TO_DELETE, - TOKEN_NULL, NOT_LINK_YOURSELF, ALIGNMENT_ALREADY_EXISTS + TOKEN_NULL, NOT_LINK_YOURSELF, NOT_LINK_IN_SAME_TEAM, ALIGNMENT_ALREADY_EXISTS } diff --git a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java index 2fda1896d6..efba046531 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java @@ -2,11 +2,13 @@ import ch.puzzle.okr.ErrorKey; import ch.puzzle.okr.exception.OkrResponseStatusException; +import ch.puzzle.okr.models.Team; import ch.puzzle.okr.models.alignment.Alignment; import ch.puzzle.okr.models.alignment.KeyResultAlignment; import ch.puzzle.okr.models.alignment.ObjectiveAlignment; import ch.puzzle.okr.repository.AlignmentRepository; import ch.puzzle.okr.service.persistence.AlignmentPersistenceService; +import ch.puzzle.okr.service.persistence.TeamPersistenceService; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @@ -18,10 +20,13 @@ public class AlignmentValidationService extends ValidationBase { private final AlignmentPersistenceService alignmentPersistenceService; + private final TeamPersistenceService teamPersistenceService; - public AlignmentValidationService(AlignmentPersistenceService alignmentPersistenceService) { + public AlignmentValidationService(AlignmentPersistenceService alignmentPersistenceService, + TeamPersistenceService teamPersistenceService) { super(alignmentPersistenceService); this.alignmentPersistenceService = alignmentPersistenceService; + this.teamPersistenceService = teamPersistenceService; } @Override @@ -30,6 +35,7 @@ public void validateOnCreate(Alignment model) { throwExceptionWhenIdIsNotNull(model.getId()); throwExceptionWhenAlignedObjectIsNull(model); throwExceptionWhenAlignedIdIsSameAsTargetId(model); + throwExceptionWhenAlignmentIsInSameTeam(model); throwExceptionWhenAlignedObjectiveAlreadyExists(model); validate(model); } @@ -40,6 +46,7 @@ public void validateOnUpdate(Long id, Alignment model) { throwExceptionWhenIdIsNull(model.getId()); throwExceptionWhenAlignedObjectIsNull(model); throwExceptionWhenAlignedIdIsSameAsTargetId(model); + throwExceptionWhenAlignmentIsInSameTeam(model); validate(model); } @@ -60,6 +67,23 @@ private void throwExceptionWhenAlignedObjectIsNull(Alignment model) { } } + private void throwExceptionWhenAlignmentIsInSameTeam(Alignment model) { + Team alignedTeam = teamPersistenceService.findById(model.getAlignedObjective().getTeam().getId()); + Team targetTeam = null; + + if (model instanceof ObjectiveAlignment objectiveAlignment) { + targetTeam = teamPersistenceService.findById(objectiveAlignment.getAlignmentTarget().getTeam().getId()); + } else if (model instanceof KeyResultAlignment keyResultAlignment) { + targetTeam = teamPersistenceService + .findById(keyResultAlignment.getAlignmentTarget().getObjective().getTeam().getId()); + } + + if (alignedTeam.equals(targetTeam)) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.NOT_LINK_IN_SAME_TEAM, + List.of("teamId", targetTeam.getId())); + } + } + private void throwExceptionWhenAlignedIdIsSameAsTargetId(Alignment model) { if (model instanceof ObjectiveAlignment objectiveAlignment) { if (Objects.equals(objectiveAlignment.getAlignedObjective().getId(), diff --git a/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java index e9fd333dc3..e70413fd23 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java @@ -4,11 +4,13 @@ import ch.puzzle.okr.dto.ErrorDto; import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.Objective; +import ch.puzzle.okr.models.Team; import ch.puzzle.okr.models.alignment.KeyResultAlignment; import ch.puzzle.okr.models.alignment.ObjectiveAlignment; import ch.puzzle.okr.models.keyresult.KeyResult; import ch.puzzle.okr.models.keyresult.KeyResultMetric; import ch.puzzle.okr.service.persistence.AlignmentPersistenceService; +import ch.puzzle.okr.service.persistence.TeamPersistenceService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -31,12 +33,18 @@ class AlignmentValidationServiceTest { @Mock AlignmentPersistenceService alignmentPersistenceService; + @Mock + TeamPersistenceService teamPersistenceService; @Spy @InjectMocks private AlignmentValidationService validator; - Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withState(DRAFT).build(); - Objective objective2 = Objective.Builder.builder().withId(8L).withTitle("Objective 2").withState(DRAFT).build(); + Team team1 = Team.Builder.builder().withId(1L).withName("Puzzle ITC").build(); + Team team2 = Team.Builder.builder().withId(2L).withName("BBT").build(); + Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withTeam(team1) + .withState(DRAFT).build(); + Objective objective2 = Objective.Builder.builder().withId(8L).withTitle("Objective 2").withTeam(team2) + .withState(DRAFT).build(); Objective objective3 = Objective.Builder.builder().withId(10L).withTitle("Objective 3").withState(DRAFT).build(); KeyResult metricKeyResult = KeyResultMetric.Builder.builder().withId(5L).withTitle("KR Title 1").build(); ObjectiveAlignment objectiveALignment = ObjectiveAlignment.Builder.builder().withId(1L) @@ -74,6 +82,8 @@ void validateOnGetShouldThrowExceptionIfIdIsNull() { @Test void validateOnCreateShouldBeSuccessfulWhenAlignmentIsValid() { when(alignmentPersistenceService.findByAlignedObjectiveId(anyLong())).thenReturn(null); + when(teamPersistenceService.findById(1L)).thenReturn(team1); + when(teamPersistenceService.findById(2L)).thenReturn(team2); validator.validateOnCreate(createAlignment); @@ -165,9 +175,45 @@ void validateOnCreateShouldThrowExceptionWhenAlignedIdIsSameAsTargetId() { assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); } + @Test + void validateOnCreateShouldThrowExceptionWhenAlignmentIsInSameTeamObjective() { + when(teamPersistenceService.findById(2L)).thenReturn(team2); + Objective objective = objective1; + objective.setTeam(team2); + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective) + .withTargetObjective(objective2).build(); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(objectiveAlignment)); + + List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "2"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnCreateShouldThrowExceptionWhenAlignmentIsInSameTeamKeyResult() { + when(teamPersistenceService.findById(1L)).thenReturn(team1); + KeyResult keyResult = KeyResultMetric.Builder.builder().withId(3L).withTitle("KeyResult 1").withObjective(objective1).build(); + KeyResultAlignment keyResultAlignment1 = KeyResultAlignment.Builder.builder().withAlignedObjective(objective1).withTargetKeyResult(keyResult).build(); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(keyResultAlignment1)); + + List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "1"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + @Test void validateOnCreateShouldThrowExceptionWhenAlignedObjectiveAlreadyExists() { when(alignmentPersistenceService.findByAlignedObjectiveId(anyLong())).thenReturn(objectiveALignment); + when(teamPersistenceService.findById(1L)).thenReturn(team1); + when(teamPersistenceService.findById(2L)).thenReturn(team2); ObjectiveAlignment createAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective1).withTargetObjective(objective2).build(); @@ -183,6 +229,9 @@ void validateOnCreateShouldThrowExceptionWhenAlignedObjectiveAlreadyExists() { @Test void validateOnUpdateShouldBeSuccessfulWhenAlignmentIsValid() { + when(teamPersistenceService.findById(1L)).thenReturn(team1); + when(teamPersistenceService.findById(2L)).thenReturn(team2); + validator.validateOnUpdate(objectiveALignment.getId(), objectiveALignment); verify(validator, times(1)).throwExceptionWhenModelIsNull(objectiveALignment); @@ -274,6 +323,40 @@ void validateOnUpdateShouldThrowExceptionWhenAlignedIdIsSameAsTargetId() { assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); } + @Test + void validateOnUpdateShouldThrowExceptionWhenAlignmentIsInSameTeamObjective() { + when(teamPersistenceService.findById(2L)).thenReturn(team2); + Objective objective = objective1; + objective.setTeam(team2); + ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withId(3L) + .withAlignedObjective(objective).withTargetObjective(objective2).build(); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(2L, objectiveAlignment)); + + List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "2"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void validateOnUpdateShouldThrowExceptionWhenAlignmentIsInSameTeamKeyResult() { + when(teamPersistenceService.findById(1L)).thenReturn(team1); + KeyResult keyResult = KeyResultMetric.Builder.builder().withId(3L).withTitle("KeyResult 1").withObjective(objective1).build(); + KeyResultAlignment keyResultAlignment1 = KeyResultAlignment.Builder.builder().withId(2L).withAlignedObjective(objective1).withTargetKeyResult(keyResult).build(); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnUpdate(2L, keyResultAlignment1)); + + List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "1"))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + @Test void validateOnDeleteShouldBeSuccessfulWhenValidAlignmentId() { validator.validateOnDelete(3L); diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index 66c0b430da..a7353961ff 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -55,6 +55,7 @@ "TOKEN_NULL": "Das erhaltene Token ist null.", "ILLEGAL_CHANGE_OBJECTIVE_QUARTER": "Element kann nicht in ein anderes Quartal verlegt werden.", "NOT_LINK_YOURSELF": "Das Objective kann nicht auf sich selbst zeigen.", + "NOT_LINK_IN_SAME_TEAM": "Das Objective kann nicht auf ein Objekt im selben Team zeigen.", "ALIGNMENT_ALREADY_EXISTS": "Es existiert bereits ein Alignment ausgehend vom Objective mit der ID {1}." }, "SUCCESS": { From ad341b015bbe21dacdecfbe75f33d59020a81095 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 6 Jun 2024 08:42:27 +0200 Subject: [PATCH 041/119] Fix sonar code smells --- .../AlignmentValidationService.java | 22 +++++++++---------- .../AlignmentPersistenceServiceIT.java | 9 ++++---- .../objective-form.component.ts | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java index efba046531..8a656cd679 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java @@ -59,11 +59,11 @@ private void throwExceptionWhenAlignedObjectIsNull(Alignment model) { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, List.of("targetObjectiveId", objectiveAlignment.getAlignedObjective().getId())); } - } else if (model instanceof KeyResultAlignment keyResultAlignment) { - if (keyResultAlignment.getAlignmentTarget() == null) { - throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, - List.of("targetKeyResultId", keyResultAlignment.getAlignedObjective().getId())); - } + } else if (model instanceof KeyResultAlignment keyResultAlignment + && (keyResultAlignment.getAlignmentTarget() == null)) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, + List.of("targetKeyResultId", keyResultAlignment.getAlignedObjective().getId())); + } } @@ -85,12 +85,12 @@ private void throwExceptionWhenAlignmentIsInSameTeam(Alignment model) { } private void throwExceptionWhenAlignedIdIsSameAsTargetId(Alignment model) { - if (model instanceof ObjectiveAlignment objectiveAlignment) { - if (Objects.equals(objectiveAlignment.getAlignedObjective().getId(), - objectiveAlignment.getAlignmentTarget().getId())) { - throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.NOT_LINK_YOURSELF, - List.of("targetObjectiveId", objectiveAlignment.getAlignmentTarget().getId())); - } + if (model instanceof ObjectiveAlignment objectiveAlignment + && (Objects.equals(objectiveAlignment.getAlignedObjective().getId(), + objectiveAlignment.getAlignmentTarget().getId()))) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.NOT_LINK_YOURSELF, + List.of("targetObjectiveId", objectiveAlignment.getAlignmentTarget().getId())); + } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java index 6fb132da9a..2116488c78 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java @@ -28,6 +28,7 @@ class AlignmentPersistenceServiceIT { @Autowired private AlignmentPersistenceService alignmentPersistenceService; private Alignment createdAlignment; + private final String ALIGNMENT = "Alignment"; private static ObjectiveAlignment createObjectiveAlignment(Long id) { return ObjectiveAlignment.Builder.builder().withId(id) @@ -102,7 +103,7 @@ void updateAlignmentShouldThrowExceptionWhenAlreadyUpdated() { OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> alignmentPersistenceService.save(updateAlignment)); - List expectedErrors = List.of(new ErrorDto("DATA_HAS_BEEN_UPDATED", List.of("Alignment"))); + List expectedErrors = List.of(new ErrorDto("DATA_HAS_BEEN_UPDATED", List.of(ALIGNMENT))); assertEquals(UNPROCESSABLE_ENTITY, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); @@ -114,7 +115,7 @@ void findByAlignedObjectiveIdShouldReturnAlignmentModel() { Alignment alignment = alignmentPersistenceService.findByAlignedObjectiveId(4L); assertNotNull(alignment); - assertEquals(alignment.getAlignedObjective().getId(), 4); + assertEquals(4, alignment.getAlignedObjective().getId()); } @Test @@ -162,7 +163,7 @@ void recreateEntityShouldUpdateAlignmentNoTypeChange() { () -> alignmentPersistenceService.findById(alignmentId)); List expectedErrors = List - .of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of("Alignment", alignmentId))); + .of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of(ALIGNMENT, alignmentId))); assertEquals(NOT_FOUND, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); @@ -202,7 +203,7 @@ void recreateEntityShouldUpdateAlignmentWithTypeChange() { () -> alignmentPersistenceService.findById(alignmentId)); List expectedErrors = List - .of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of("Alignment", alignmentId))); + .of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of(ALIGNMENT, alignmentId))); assertEquals(NOT_FOUND, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 7725c7ca2f..767716d530 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -249,7 +249,7 @@ export class ObjectiveFormComponent implements OnInit { this.alignmentPossibilities$ = this.objectiveService.getAlignmentPossibilities(quarterId); this.alignmentPossibilities$.subscribe((value: AlignmentPossibility[]) => { if (teamId) { - value = value.filter((item: AlignmentPossibility) => !(item.teamId == teamId)); + value = value.filter((item: AlignmentPossibility) => item.teamId != teamId); } if (objective) { From 2ab2cdc97d5ebb6354d6138a2cb105d0b9fca4b1 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 6 Jun 2024 09:48:03 +0200 Subject: [PATCH 042/119] Clean up code --- .../ch/puzzle/okr/mapper/ObjectiveMapper.java | 2 + .../business/AlignmentBusinessService.java | 92 ++++++----- .../business/ObjectiveBusinessService.java | 46 +++--- .../AlignmentPersistenceService.java | 9 +- .../KeyResultPersistenceService.java | 9 +- .../AlignmentValidationService.java | 23 +-- .../okr/controller/ObjectiveControllerIT.java | 21 +-- .../controller/OrganisationControllerIT.java | 3 +- .../mapper/AlignmentSelectionMapperTest.java | 15 +- .../puzzle/okr/mapper/OverviewMapperTest.java | 24 +-- .../ObjectiveAuthorizationServiceTest.java | 57 ++++++- .../AlignmentBusinessServiceTest.java | 60 ++++++- ...AlignmentSelectionBusinessServiceTest.java | 3 +- .../ObjectiveBusinessServiceTest.java | 138 ++++++++++------- .../AlignmentPersistenceServiceIT.java | 71 +++++---- .../AlignmentValidationServiceTest.java | 146 ++++++++++++------ .../objective-form.component.html | 8 +- .../objective-form.component.spec.ts | 56 ++++--- .../objective-form.component.ts | 51 +++--- 19 files changed, 533 insertions(+), 301 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java index c72994f56c..74e4208311 100644 --- a/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java +++ b/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java @@ -19,6 +19,8 @@ public ObjectiveMapper(TeamBusinessService teamBusinessService, QuarterBusinessS this.quarterBusinessService = quarterBusinessService; } + // TODO: Adjust Unit Tests of ObjectiveMapper after merge of multitenancy-main + public ObjectiveDto toDto(Objective objective) { return new ObjectiveDto(objective.getId(), objective.getVersion(), objective.getTitle(), objective.getTeam().getId(), objective.getQuarter().getId(), objective.getQuarter().getLabel(), diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index ef9f954fbc..cf554b9fa7 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -56,36 +56,49 @@ public void updateEntity(Long objectiveId, Objective objective) { Alignment savedAlignment = alignmentPersistenceService.findByAlignedObjectiveId(objectiveId); if (savedAlignment == null) { - Alignment alignment = buildAlignmentModel(objective, 0); - alignmentValidationService.validateOnCreate(alignment); - alignmentPersistenceService.save(alignment); + createEntity(objective); + } else { + handleExistingAlignment(objective, savedAlignment); + } + } + + private void handleExistingAlignment(Objective objective, Alignment savedAlignment) { + if (objective.getAlignedEntityId() == null) { + validateAndDeleteAlignmentById(savedAlignment.getId()); } else { - if (objective.getAlignedEntityId() == null) { - alignmentValidationService.validateOnDelete(savedAlignment.getId()); - alignmentPersistenceService.deleteById(savedAlignment.getId()); - } else { - Alignment alignment = buildAlignmentModel(objective, savedAlignment.getVersion()); - - alignment.setId(savedAlignment.getId()); - alignmentValidationService.validateOnUpdate(savedAlignment.getId(), alignment); - if (isAlignmentTypeChange(alignment, savedAlignment)) { - alignmentPersistenceService.recreateEntity(savedAlignment.getId(), alignment); - } else { - alignmentPersistenceService.save(alignment); - } - } + validateAndUpdateAlignment(objective, savedAlignment); + } + } + + private void validateAndUpdateAlignment(Objective objective, Alignment savedAlignment) { + Alignment alignment = buildAlignmentModel(objective, savedAlignment.getVersion()); + + alignment.setId(savedAlignment.getId()); + alignmentValidationService.validateOnUpdate(savedAlignment.getId(), alignment); + updateAlignment(savedAlignment, alignment); + } + + private void updateAlignment(Alignment savedAlignment, Alignment alignment) { + if (isAlignmentTypeChange(alignment, savedAlignment)) { + alignmentPersistenceService.recreateEntity(savedAlignment.getId(), alignment); + } else { + alignmentPersistenceService.save(alignment); } } public Alignment buildAlignmentModel(Objective alignedObjective, int version) { if (alignedObjective.getAlignedEntityId().startsWith("O")) { - Objective targetObjective = objectivePersistenceService - .findById(Long.valueOf(alignedObjective.getAlignedEntityId().replace("O", ""))); - return ObjectiveAlignment.Builder.builder().withAlignedObjective(alignedObjective) - .withTargetObjective(targetObjective).withVersion(version).build(); + Long entityId = Long.valueOf(alignedObjective.getAlignedEntityId().replace("O", "")); + + Objective targetObjective = objectivePersistenceService.findById(entityId); + return ObjectiveAlignment.Builder.builder() // + .withAlignedObjective(alignedObjective) // + .withTargetObjective(targetObjective) // + .withVersion(version).build(); } else if (alignedObjective.getAlignedEntityId().startsWith("K")) { - KeyResult targetKeyResult = keyResultPersistenceService - .findById(Long.valueOf(alignedObjective.getAlignedEntityId().replace("K", ""))); + Long entityId = Long.valueOf(alignedObjective.getAlignedEntityId().replace("K", "")); + + KeyResult targetKeyResult = keyResultPersistenceService.findById(entityId); return KeyResultAlignment.Builder.builder().withAlignedObjective(alignedObjective) .withTargetKeyResult(targetKeyResult).withVersion(version).build(); } else { @@ -99,9 +112,10 @@ public boolean isAlignmentTypeChange(Alignment alignment, Alignment savedAlignme || (alignment instanceof KeyResultAlignment && savedAlignment instanceof ObjectiveAlignment); } - public void updateKeyResultIdOnIdChange(Long oldId, KeyResult keyResult) { - List alignments = alignmentPersistenceService.findByKeyResultAlignmentId(oldId); - alignments.forEach(alignment -> { + public void updateKeyResultIdOnIdChange(Long oldKeyResultId, KeyResult keyResult) { + List keyResultAlignmentList = alignmentPersistenceService + .findByKeyResultAlignmentId(oldKeyResultId); + keyResultAlignmentList.forEach(alignment -> { alignment.setAlignmentTarget(keyResult); alignmentValidationService.validateOnUpdate(alignment.getId(), alignment); alignmentPersistenceService.save(alignment); @@ -111,21 +125,23 @@ public void updateKeyResultIdOnIdChange(Long oldId, KeyResult keyResult) { public void deleteAlignmentByObjectiveId(Long objectiveId) { Alignment alignment = alignmentPersistenceService.findByAlignedObjectiveId(objectiveId); if (alignment != null) { - alignmentValidationService.validateOnDelete(alignment.getId()); - alignmentPersistenceService.deleteById(alignment.getId()); + validateAndDeleteAlignmentById(alignment.getId()); } - List alignmentList = alignmentPersistenceService.findByObjectiveAlignmentId(objectiveId); - alignmentList.forEach(objectiveAlignment -> { - alignmentValidationService.validateOnDelete(objectiveAlignment.getId()); - alignmentPersistenceService.deleteById(objectiveAlignment.getId()); - }); + List objectiveAlignmentList = alignmentPersistenceService + .findByObjectiveAlignmentId(objectiveId); + objectiveAlignmentList + .forEach(objectiveAlignment -> validateAndDeleteAlignmentById(objectiveAlignment.getId())); } public void deleteAlignmentByKeyResultId(Long keyResultId) { - List alignmentList = alignmentPersistenceService.findByKeyResultAlignmentId(keyResultId); - alignmentList.forEach(keyResultAlignment -> { - alignmentValidationService.validateOnDelete(keyResultAlignment.getId()); - alignmentPersistenceService.deleteById(keyResultAlignment.getId()); - }); + List keyResultAlignmentList = alignmentPersistenceService + .findByKeyResultAlignmentId(keyResultId); + keyResultAlignmentList + .forEach(keyResultAlignment -> validateAndDeleteAlignmentById(keyResultAlignment.getId())); + } + + private void validateAndDeleteAlignmentById(Long alignmentId) { + alignmentValidationService.validateOnDelete(alignmentId); + alignmentPersistenceService.deleteById(alignmentId); } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java index 2fc9c40a54..4244d5eb99 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java @@ -57,37 +57,45 @@ public List getAlignmentPossibilities(Long quarterId) { List objectivesByQuarter = objectivePersistenceService.findObjectiveByQuarterId(quarterId); List alignmentDtoList = new ArrayList<>(); - List teamList = objectivesByQuarter.stream().map(Objective::getTeam).distinct() - .sorted(Comparator.comparing(Team::getName)).toList(); + List teamList = objectivesByQuarter.stream() // + .map(Objective::getTeam) // + .distinct() // + .sorted(Comparator.comparing(Team::getName)) // + .toList(); teamList.forEach(team -> { List filteredObjectiveList = objectivesByQuarter.stream() - .filter(objective -> objective.getTeam().equals(team)).toList().stream() + .filter(objective -> objective.getTeam().equals(team)) .sorted(Comparator.comparing(Objective::getTitle)).toList(); - List alignmentObjectDtos = new ArrayList<>(); + List alignmentObjectDtoList = generateAlignmentObjects(filteredObjectiveList); - filteredObjectiveList.forEach(objective -> { - AlignmentObjectDto objectiveDto = new AlignmentObjectDto(objective.getId(), - "O - " + objective.getTitle(), "objective"); - alignmentObjectDtos.add(objectiveDto); - - List keyResults = keyResultBusinessService.getAllKeyResultsByObjective(objective.getId()) - .stream().sorted(Comparator.comparing(KeyResult::getTitle)).toList(); - - keyResults.forEach(keyResult -> { - AlignmentObjectDto keyResultDto = new AlignmentObjectDto(keyResult.getId(), - "KR - " + keyResult.getTitle(), "keyResult"); - alignmentObjectDtos.add(keyResultDto); - }); - }); - AlignmentDto alignmentDto = new AlignmentDto(team.getId(), team.getName(), alignmentObjectDtos); + AlignmentDto alignmentDto = new AlignmentDto(team.getId(), team.getName(), alignmentObjectDtoList); alignmentDtoList.add(alignmentDto); }); return alignmentDtoList; } + private List generateAlignmentObjects(List filteredObjectiveList) { + List alignmentObjectDtoList = new ArrayList<>(); + filteredObjectiveList.forEach(objective -> { + AlignmentObjectDto objectiveDto = new AlignmentObjectDto(objective.getId(), "O - " + objective.getTitle(), + "objective"); + alignmentObjectDtoList.add(objectiveDto); + + List keyResultList = keyResultBusinessService.getAllKeyResultsByObjective(objective.getId()) + .stream().sorted(Comparator.comparing(KeyResult::getTitle)).toList(); + + keyResultList.forEach(keyResult -> { + AlignmentObjectDto keyResultDto = new AlignmentObjectDto(keyResult.getId(), + "KR - " + keyResult.getTitle(), "keyResult"); + alignmentObjectDtoList.add(keyResultDto); + }); + }); + return alignmentObjectDtoList; + } + public List getEntitiesByTeamId(Long id) { validator.validateOnGet(id); diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java index 8a25281006..dc69d6b1fc 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceService.java @@ -5,6 +5,8 @@ import ch.puzzle.okr.models.alignment.ObjectiveAlignment; import ch.puzzle.okr.repository.AlignmentRepository; import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.List; @@ -13,6 +15,7 @@ @Service public class AlignmentPersistenceService extends PersistenceBase { + private static final Logger logger = LoggerFactory.getLogger(AlignmentPersistenceService.class); protected AlignmentPersistenceService(AlignmentRepository repository) { super(repository); @@ -25,11 +28,11 @@ public String getModelName() { @Transactional public Alignment recreateEntity(Long id, Alignment alignment) { - System.out.println(alignment.toString()); - System.out.println("*".repeat(30)); + logger.debug("Delete and create new Alignment in order to prevent duplicates in case of changed Type"); + logger.debug("{}", alignment); // delete entity in order to prevent duplicates in case of changed keyResultType deleteById(id); - System.out.printf("reached delete entity with %d", id); + logger.debug("Reached delete entity with id {}", id); return save(alignment); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/KeyResultPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/KeyResultPersistenceService.java index 14e223bff5..d8a4da87a4 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/KeyResultPersistenceService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/KeyResultPersistenceService.java @@ -3,6 +3,8 @@ import ch.puzzle.okr.models.keyresult.KeyResult; import ch.puzzle.okr.repository.KeyResultRepository; import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.List; @@ -11,6 +13,7 @@ @Service public class KeyResultPersistenceService extends PersistenceBase { + private static final Logger logger = LoggerFactory.getLogger(KeyResultPersistenceService.class); protected KeyResultPersistenceService(KeyResultRepository repository) { super(repository); @@ -27,11 +30,11 @@ public List getKeyResultsByObjective(Long objectiveId) { @Transactional public KeyResult recreateEntity(Long id, KeyResult keyResult) { - System.out.println(keyResult.toString()); - System.out.println("*".repeat(30)); + logger.debug("Delete KeyResult in order to prevent duplicates in case of changed keyResultType"); + logger.debug("{}", keyResult); // delete entity in order to prevent duplicates in case of changed keyResultType deleteById(id); - System.out.printf("reached delete entity with %d", id); + logger.debug("Reached delete entity with id {}", id); return save(keyResult); } diff --git a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java index 8a656cd679..40bbba8ee5 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java @@ -33,10 +33,10 @@ public AlignmentValidationService(AlignmentPersistenceService alignmentPersisten public void validateOnCreate(Alignment model) { throwExceptionWhenModelIsNull(model); throwExceptionWhenIdIsNotNull(model.getId()); - throwExceptionWhenAlignedObjectIsNull(model); + throwExceptionWhenAlignmentObjectIsNull(model); throwExceptionWhenAlignedIdIsSameAsTargetId(model); throwExceptionWhenAlignmentIsInSameTeam(model); - throwExceptionWhenAlignedObjectiveAlreadyExists(model); + throwExceptionWhenAlignmentWithAlignedObjectiveAlreadyExists(model); validate(model); } @@ -44,13 +44,13 @@ public void validateOnCreate(Alignment model) { public void validateOnUpdate(Long id, Alignment model) { throwExceptionWhenModelIsNull(model); throwExceptionWhenIdIsNull(model.getId()); - throwExceptionWhenAlignedObjectIsNull(model); + throwExceptionWhenAlignmentObjectIsNull(model); throwExceptionWhenAlignedIdIsSameAsTargetId(model); throwExceptionWhenAlignmentIsInSameTeam(model); validate(model); } - private void throwExceptionWhenAlignedObjectIsNull(Alignment model) { + private void throwExceptionWhenAlignmentObjectIsNull(Alignment model) { if (model.getAlignedObjective() == null) { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, List.of("alignedObjectiveId")); @@ -68,19 +68,20 @@ private void throwExceptionWhenAlignedObjectIsNull(Alignment model) { } private void throwExceptionWhenAlignmentIsInSameTeam(Alignment model) { - Team alignedTeam = teamPersistenceService.findById(model.getAlignedObjective().getTeam().getId()); - Team targetTeam = null; + Team alignedObjectiveTeam = teamPersistenceService.findById(model.getAlignedObjective().getTeam().getId()); + Team targetObjectTeam = null; if (model instanceof ObjectiveAlignment objectiveAlignment) { - targetTeam = teamPersistenceService.findById(objectiveAlignment.getAlignmentTarget().getTeam().getId()); + targetObjectTeam = teamPersistenceService + .findById(objectiveAlignment.getAlignmentTarget().getTeam().getId()); } else if (model instanceof KeyResultAlignment keyResultAlignment) { - targetTeam = teamPersistenceService + targetObjectTeam = teamPersistenceService .findById(keyResultAlignment.getAlignmentTarget().getObjective().getTeam().getId()); } - if (alignedTeam.equals(targetTeam)) { + if (alignedObjectiveTeam.equals(targetObjectTeam)) { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.NOT_LINK_IN_SAME_TEAM, - List.of("teamId", targetTeam.getId())); + List.of("teamId", targetObjectTeam.getId())); } } @@ -94,7 +95,7 @@ private void throwExceptionWhenAlignedIdIsSameAsTargetId(Alignment model) { } } - private void throwExceptionWhenAlignedObjectiveAlreadyExists(Alignment model) { + private void throwExceptionWhenAlignmentWithAlignedObjectiveAlreadyExists(Alignment model) { if (this.alignmentPersistenceService.findByAlignedObjectiveId(model.getAlignedObjective().getId()) != null) { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ALIGNMENT_ALREADY_EXISTS, List.of("alignedObjectiveId", model.getAlignedObjective().getId())); diff --git a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java index f13c1fe4e4..2143d06632 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java @@ -33,6 +33,7 @@ import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static ch.puzzle.okr.TestConstants.*; @WithMockUser(value = "spring") @ExtendWith(MockitoExtension.class) @@ -44,8 +45,8 @@ class ObjectiveControllerIT { private static final String EVERYTHING_FINE_DESCRIPTION = "Everything Fine"; private static final String TITLE = "Hunting"; private static final String URL_BASE_OBJECTIVE = "/api/v2/objectives"; - private static final String URL_OBJECTIVE_5 = "/api/v2/objectives/5"; - private static final String URL_OBJECTIVE_10 = "/api/v2/objectives/10"; + private static final String URL_OBJECTIVE_5 = URL_BASE_OBJECTIVE + "/5"; + private static final String URL_OBJECTIVE_10 = URL_BASE_OBJECTIVE + "/10"; private static final String JSON = """ { "title": "FullObjective", "ownerId": 1, "ownerFirstname": "Bob", "ownerLastname": "Kaufmann", @@ -66,7 +67,7 @@ class ObjectiveControllerIT { "description": "This is our description", "progress": 33.3 } """; - private static final String RESPONSE_NEW_OBJECTIVE = """ + private static final String JSON_RESPONSE_NEW_OBJECTIVE = """ {"id":null,"version":1,"title":"Program Faster","teamId":1,"quarterId":1,"quarterLabel":"GJ 22/23-Q2","description":"Just be faster","state":"DRAFT","createdOn":null,"modifiedOn":null,"writeable":true,"alignedEntityId":null}"""; private static final String JSON_PATH_TITLE = "$.title"; private static final Objective objective1 = Objective.Builder.builder().withId(5L).withTitle(OBJECTIVE_TITLE_1) @@ -91,7 +92,7 @@ class ObjectiveControllerIT { private static final AlignmentObjectDto alignmentObject1 = new AlignmentObjectDto(3L, "KR Title 1", "keyResult"); private static final AlignmentObjectDto alignmentObject2 = new AlignmentObjectDto(1L, "Objective Title 1", "objective"); - private static final AlignmentDto alignmentPossibilities = new AlignmentDto(1L, "Puzzle ITC", + private static final AlignmentDto alignmentPossibilities = new AlignmentDto(1L, TEAM_PUZZLE, List.of(alignmentObject1, alignmentObject2)); @Autowired @@ -123,7 +124,7 @@ void getObjectiveById() throws Exception { void getObjectiveByIdWithAlignmentId() throws Exception { BDDMockito.given(objectiveAuthorizationService.getEntityById(anyLong())).willReturn(objectiveAlignment); - mvc.perform(get("/api/v2/objectives/9").contentType(MediaType.APPLICATION_JSON)) + mvc.perform(get(URL_BASE_OBJECTIVE + "/9").contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$.id", Is.is(9))) .andExpect(jsonPath(JSON_PATH_TITLE, Is.is("Objective Alignment"))) .andExpect(jsonPath("$.alignedEntityId", Is.is("O42"))); @@ -143,9 +144,9 @@ void getAlignmentPossibilities() throws Exception { BDDMockito.given(objectiveAuthorizationService.getAlignmentPossibilities(anyLong())) .willReturn(List.of(alignmentPossibilities)); - mvc.perform(get("/api/v2/objectives/alignmentPossibilities/5").contentType(MediaType.APPLICATION_JSON)) + mvc.perform(get(URL_BASE_OBJECTIVE + "/alignmentPossibilities/5").contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$[0].teamId", Is.is(1))) - .andExpect(jsonPath("$[0].teamName", Is.is("Puzzle ITC"))) + .andExpect(jsonPath("$[0].teamName", Is.is(TEAM_PUZZLE))) .andExpect(jsonPath("$[0].alignmentObjectDtos[0].objectId", Is.is(3))) .andExpect(jsonPath("$[0].alignmentObjectDtos[0].objectTitle", Is.is("KR Title 1"))) .andExpect(jsonPath("$[0].alignmentObjectDtos[0].objectType", Is.is("keyResult"))) @@ -167,7 +168,7 @@ void shouldReturnObjectiveWhenCreatingNewObjective() throws Exception { mvc.perform(post(URL_BASE_OBJECTIVE).contentType(MediaType.APPLICATION_JSON) .with(SecurityMockMvcRequestPostProcessors.csrf()).content(CREATE_NEW_OBJECTIVE)) .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()) - .andExpect(MockMvcResultMatchers.content().string(RESPONSE_NEW_OBJECTIVE)); + .andExpect(MockMvcResultMatchers.content().string(JSON_RESPONSE_NEW_OBJECTIVE)); verify(objectiveAuthorizationService, times(1)).createEntity(any()); } @@ -247,7 +248,7 @@ void throwExceptionWhenObjectiveWithIdCantBeFoundWhileDeleting() throws Exceptio doThrow(new ResponseStatusException(HttpStatus.NOT_FOUND, "Objective not found")) .when(objectiveAuthorizationService).deleteEntityById(anyLong()); - mvc.perform(delete("/api/v2/objectives/1000").with(SecurityMockMvcRequestPostProcessors.csrf())) + mvc.perform(delete(URL_BASE_OBJECTIVE + "/1000").with(SecurityMockMvcRequestPostProcessors.csrf())) .andExpect(MockMvcResultMatchers.status().isNotFound()); } @@ -257,7 +258,7 @@ void shouldReturnIsCreatedWhenObjectiveWasDuplicated() throws Exception { BDDMockito.given(objectiveAuthorizationService.getAuthorizationService()).willReturn(authorizationService); BDDMockito.given(objectiveMapper.toDto(objective1)).willReturn(objective1Dto); - mvc.perform(post("/api/v2/objectives/{id}", objective1.getId()).contentType(MediaType.APPLICATION_JSON) + mvc.perform(post(URL_BASE_OBJECTIVE + "/{id}", objective1.getId()).contentType(MediaType.APPLICATION_JSON) .content(JSON).with(SecurityMockMvcRequestPostProcessors.csrf())) .andExpect(MockMvcResultMatchers.status().isCreated()) .andExpect(jsonPath("$.id", Is.is(objective1Dto.id().intValue()))) diff --git a/backend/src/test/java/ch/puzzle/okr/controller/OrganisationControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/OrganisationControllerIT.java index 055dcec6dc..d6b663fe39 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/OrganisationControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/OrganisationControllerIT.java @@ -24,6 +24,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static ch.puzzle.okr.TestConstants.*; @WithMockUser(value = "spring") @ExtendWith(MockitoExtension.class) @@ -31,7 +32,7 @@ class OrganisationControllerIT { /* Team test objects */ - private static final Team PUZZLE = Team.Builder.builder().withId(1L).withName("PUZZLE ITC").build(); + private static final Team PUZZLE = Team.Builder.builder().withId(1L).withName(TEAM_PUZZLE).build(); private static final Team BBT = Team.Builder.builder().withId(1L).withName("/BBT").build(); /* Organisation test objects */ diff --git a/backend/src/test/java/ch/puzzle/okr/mapper/AlignmentSelectionMapperTest.java b/backend/src/test/java/ch/puzzle/okr/mapper/AlignmentSelectionMapperTest.java index 26b799ff15..8a7f6fb366 100644 --- a/backend/src/test/java/ch/puzzle/okr/mapper/AlignmentSelectionMapperTest.java +++ b/backend/src/test/java/ch/puzzle/okr/mapper/AlignmentSelectionMapperTest.java @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static ch.puzzle.okr.TestConstants.*; class AlignmentSelectionMapperTest { private final AlignmentSelectionMapper alignmentSelectionMapper = new AlignmentSelectionMapper(); @@ -25,7 +26,7 @@ void toDtoShouldReturnEmptyListWhenNoObjectiveFound() { void toDtoShouldReturnOneElementWhenObjectiveFound() { List alignmentSelections = List.of(AlignmentSelection.Builder.builder() .withAlignmentSelectionId(AlignmentSelectionId.Builder.builder().withObjectiveId(1L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").build()); + .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").build()); List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); assertEquals(1, alignmentObjectiveDtos.size()); @@ -37,7 +38,7 @@ void toDtoShouldReturnOneElementWhenObjectiveWithKeyResultFound() { List alignmentSelections = List.of(AlignmentSelection.Builder.builder() .withAlignmentSelectionId( AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(3L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1") + .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1") .withKeyResultTitle("Key Result 3").build()); List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); @@ -51,12 +52,12 @@ void toDtoShouldReturnOneElementWhenObjectiveWithTwoKeyResultsFound() { AlignmentSelection.Builder.builder() .withAlignmentSelectionId( AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(3L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1") + .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1") .withKeyResultTitle("Key Result 3").build(), AlignmentSelection.Builder.builder() .withAlignmentSelectionId( AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(5L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1") + .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1") .withKeyResultTitle("Key Result 5").build()); List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); @@ -70,17 +71,17 @@ void toDtoShouldReturnOneElementWhenTwoObjectivesWithKeyResultsFound() { AlignmentSelection.Builder.builder() .withAlignmentSelectionId( AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(3L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1") + .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1") .withKeyResultTitle("Key Result 3").build(), AlignmentSelection.Builder.builder() .withAlignmentSelectionId( AlignmentSelectionId.Builder.builder().withObjectiveId(5L).withKeyResultId(6L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 5") + .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 5") .withKeyResultTitle("Key Result 6").build(), AlignmentSelection.Builder.builder() .withAlignmentSelectionId( AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(9L).build()) - .withTeamId(2L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1") + .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1") .withKeyResultTitle("Key Result 9").build()); List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); diff --git a/backend/src/test/java/ch/puzzle/okr/mapper/OverviewMapperTest.java b/backend/src/test/java/ch/puzzle/okr/mapper/OverviewMapperTest.java index 5e4c51ad9b..e17fe68a1d 100644 --- a/backend/src/test/java/ch/puzzle/okr/mapper/OverviewMapperTest.java +++ b/backend/src/test/java/ch/puzzle/okr/mapper/OverviewMapperTest.java @@ -23,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static ch.puzzle.okr.TestConstants.*; @ExtendWith(MockitoExtension.class) class OverviewMapperTest { @@ -42,9 +43,8 @@ void toDtoShouldReturnEmptyListWhenNoTeamFound() { @Test void toDtoShouldReturnEmptyListWhenTeamFound() { - List overviews = List - .of(Overview.Builder.builder().withOverviewId(OverviewId.Builder.builder().withTeamId(2L).build()) - .withTeamName("Puzzle ITC").build()); + List overviews = List.of(Overview.Builder.builder() + .withOverviewId(OverviewId.Builder.builder().withTeamId(2L).build()).withTeamName(TEAM_PUZZLE).build()); List overviewDtos = overviewMapper.toDto(overviews); assertEquals(1, overviewDtos.size()); @@ -55,7 +55,7 @@ void toDtoShouldReturnEmptyListWhenTeamFound() { void toDtoShouldReturnOneElementWhenObjectiveFound() { List overviews = List.of(Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").build()); + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").build()); List overviewDtos = overviewMapper.toDto(overviews); assertEquals(1, overviewDtos.size()); @@ -68,7 +68,7 @@ void toDtoShouldReturnOneElementWhenObjectiveWithKeyResultFound() { List overviews = List.of(Overview.Builder.builder() .withOverviewId( OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L).withKeyResultId(3L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") .withKeyResultType(KEY_RESULT_TYPE_METRIC).build()); List overviewDtos = overviewMapper.toDto(overviews); @@ -82,7 +82,7 @@ void toDtoShouldReturnOneElementWhenObjectiveWithKeyResultAndCheckInsFound() { List overviews = List.of(Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L).withKeyResultId(3L) .withCheckInId(4L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") .withKeyResultType(KEY_RESULT_TYPE_METRIC).withCheckInValue(27.5).withConfidence(5).build()); List overviewDtos = overviewMapper.toDto(overviews); @@ -97,12 +97,12 @@ void toDtoShouldReturnOneElementWhenObjectiveWithTwoKeyResultAndCheckInFound() { Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L) .withKeyResultId(3L).withCheckInId(4L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") .withKeyResultType(KEY_RESULT_TYPE_ORDINAL).withCheckInZone("COMMIT").build(), Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L) .withKeyResultId(5L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 5") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 5") .withKeyResultType(KEY_RESULT_TYPE_METRIC).build()); List overviewDtos = overviewMapper.toDto(overviews); @@ -117,13 +117,13 @@ void toDtoShouldReturnOneElementWhenTwoObjectivesWithKeyResultAndCheckInFound() Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L) .withKeyResultId(3L).withCheckInId(4L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") .withKeyResultType(KEY_RESULT_TYPE_METRIC).withBaseline(20.0).withStretchGoal(37.0) .withUnit("TCHF").withCheckInValue(27.5).build(), Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(5L).withTeamId(2L) .withKeyResultId(6L).withCheckInId(7L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 5").withKeyResultTitle("Key Result 6") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 5").withKeyResultTitle("Key Result 6") .withKeyResultType(KEY_RESULT_TYPE_ORDINAL).withCommitZone("commit").withTargetZone("target") .withStretchZone("stretch").withCheckInZone("checkIn").build()); List overviewDtos = overviewMapper.toDto(overviews); @@ -155,7 +155,7 @@ void toDtoShouldReturnOneElementWhenTwoTeamsWithObjectivesAndKeyResultsFound() { Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L) .withKeyResultId(3L).withCheckInId(4L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") .withKeyResultType(KEY_RESULT_TYPE_ORDINAL).withCheckInZone("TARGET").build(), Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(5L).withTeamId(4L) @@ -181,7 +181,7 @@ void toDtoShouldThrowExceptionWhenKeyResultTypeNotSupported() { List overviews = List.of(Overview.Builder.builder() .withOverviewId(OverviewId.Builder.builder().withObjectiveId(1L).withTeamId(2L).withKeyResultId(3L) .withCheckInId(4L).build()) - .withTeamName("Puzzle ITC").withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") + .withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").withKeyResultTitle("Key Result 1") .withKeyResultType("unknown").withCheckInZone("TARGET").build()); OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java index fde8da6881..2f3afa034a 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.http.HttpStatus.UNAUTHORIZED; +import static ch.puzzle.okr.TestConstants.*; @ExtendWith(MockitoExtension.class) class ObjectiveAuthorizationServiceTest { @@ -30,92 +31,122 @@ class ObjectiveAuthorizationServiceTest { ObjectiveBusinessService objectiveBusinessService; @Mock AuthorizationService authorizationService; - private final AuthorizationUser authorizationUser = defaultAuthorizationUser(); + private static final String JUNIT_TEST_REASON = "junit test reason"; + + private final AuthorizationUser authorizationUser = defaultAuthorizationUser(); private final Objective newObjective = Objective.Builder.builder().withId(5L).withTitle("Objective 1").build(); private static final AlignmentObjectDto alignmentObject1 = new AlignmentObjectDto(3L, "KR Title 1", "keyResult"); private static final AlignmentObjectDto alignmentObject2 = new AlignmentObjectDto(1L, "Objective Title 1", "objective"); - private static final AlignmentDto alignmentPossibilities = new AlignmentDto(1L, "Puzzle ITC", + private static final AlignmentDto alignmentPossibilities = new AlignmentDto(1L, TEAM_PUZZLE, List.of(alignmentObject1, alignmentObject2)); @Test void createEntityShouldReturnObjectiveWhenAuthorized() { + // arrange when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); when(objectiveBusinessService.createEntity(newObjective, authorizationUser)).thenReturn(newObjective); + // act Objective objective = objectiveAuthorizationService.createEntity(newObjective); + + // assert assertEquals(newObjective, objective); } @Test void createEntityShouldThrowExceptionWhenNotAuthorized() { - String reason = "junit test reason"; + // arrange + String reason = JUNIT_TEST_REASON; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); doThrow(new ResponseStatusException(HttpStatus.UNAUTHORIZED, reason)).when(authorizationService) .hasRoleCreateOrUpdate(newObjective, authorizationUser); + // act ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> objectiveAuthorizationService.createEntity(newObjective)); + + // assert assertEquals(UNAUTHORIZED, exception.getStatusCode()); assertEquals(reason, exception.getReason()); } @Test void getEntityByIdShouldReturnObjectiveWhenAuthorized() { + // arrange Long id = 13L; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); when(objectiveBusinessService.getEntityById(id)).thenReturn(newObjective); + // act Objective objective = objectiveAuthorizationService.getEntityById(id); + + // assert assertEquals(newObjective, objective); } @Test void getEntityByIdShouldReturnObjectiveWritableWhenAuthorized() { + // arrange Long id = 13L; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); when(authorizationService.isWriteable(newObjective, authorizationUser)).thenReturn(true); when(objectiveBusinessService.getEntityById(id)).thenReturn(newObjective); + // act Objective objective = objectiveAuthorizationService.getEntityById(id); + + // assert assertTrue(objective.isWriteable()); } @Test void getEntityByIdShouldThrowExceptionWhenNotAuthorized() { + // arrange Long id = 13L; - String reason = "junit test reason"; + String reason = JUNIT_TEST_REASON; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); doThrow(new ResponseStatusException(HttpStatus.UNAUTHORIZED, reason)).when(authorizationService) .hasRoleReadByObjectiveId(id, authorizationUser); + // act ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> objectiveAuthorizationService.getEntityById(id)); + + // assert assertEquals(UNAUTHORIZED, exception.getStatusCode()); assertEquals(reason, exception.getReason()); } @Test void updateEntityShouldReturnUpdatedObjectiveWhenAuthorized() { + // arrange Long id = 13L; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); when(objectiveBusinessService.updateEntity(id, newObjective, authorizationUser)).thenReturn(newObjective); + // act Objective Objective = objectiveAuthorizationService.updateEntity(id, newObjective); + + // assert assertEquals(newObjective, Objective); } @Test void updateEntityShouldThrowExceptionWhenNotAuthorized() { + // arrange Long id = 13L; - String reason = "junit test reason"; + String reason = JUNIT_TEST_REASON; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); doThrow(new ResponseStatusException(HttpStatus.UNAUTHORIZED, reason)).when(authorizationService) .hasRoleCreateOrUpdate(newObjective, authorizationUser); + // act ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> objectiveAuthorizationService.updateEntity(id, newObjective)); + + // assert assertEquals(UNAUTHORIZED, exception.getStatusCode()); assertEquals(reason, exception.getReason()); } @@ -137,24 +168,32 @@ void deleteEntityByIdShouldPassThroughWhenAuthorized() { @Test void deleteEntityByIdShouldThrowExceptionWhenNotAuthorized() { + // arrange Long id = 13L; - String reason = "junit test reason"; + String reason = JUNIT_TEST_REASON; when(authorizationService.getAuthorizationUser()).thenReturn(authorizationUser); doThrow(new ResponseStatusException(HttpStatus.UNAUTHORIZED, reason)).when(authorizationService) .hasRoleDeleteByObjectiveId(id, authorizationUser); + // act ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> objectiveAuthorizationService.deleteEntityById(id)); + + // assert assertEquals(UNAUTHORIZED, exception.getStatusCode()); assertEquals(reason, exception.getReason()); } @Test void getAlignmentPossibilitiesShouldReturnListWhenAuthorized() { + // arrange when(objectiveBusinessService.getAlignmentPossibilities(anyLong())).thenReturn(List.of(alignmentPossibilities)); + // act List alignmentPossibilities = objectiveAuthorizationService.getAlignmentPossibilities(3L); - assertEquals("Puzzle ITC", alignmentPossibilities.get(0).teamName()); + + // assert + assertEquals(TEAM_PUZZLE, alignmentPossibilities.get(0).teamName()); assertEquals(1, alignmentPossibilities.get(0).teamId()); assertEquals(3, alignmentPossibilities.get(0).alignmentObjectDtos().get(0).objectId()); assertEquals("KR Title 1", alignmentPossibilities.get(0).alignmentObjectDtos().get(0).objectTitle()); @@ -165,9 +204,13 @@ void getAlignmentPossibilitiesShouldReturnListWhenAuthorized() { @Test void getAlignmentPossibilitiesShouldReturnEmptyListWhenNoAlignments() { + // arrange when(objectiveBusinessService.getAlignmentPossibilities(anyLong())).thenReturn(List.of()); + // act List alignmentPossibilities = objectiveAuthorizationService.getAlignmentPossibilities(3L); + + // assert assertEquals(0, alignmentPossibilities.size()); } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java index a7cd90df48..ae3f688bb0 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -59,117 +59,153 @@ class AlignmentBusinessServiceTest { @Test void shouldGetTargetAlignmentIdObjective() { + // arrange when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(objectiveALignment); + + // act String targetId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); + // assert assertEquals("O8", targetId); } @Test void shouldReturnNullWhenNoAlignmentFound() { + // arrange when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(null); + + // act String targetId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); - assertNull(targetId); + // assert verify(validator, times(1)).validateOnGet(5L); + assertNull(targetId); } @Test void shouldGetTargetAlignmentIdKeyResult() { + // arrange when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(keyResultAlignment); + + // act String targetId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); + // assert assertEquals("K5", targetId); } @Test void shouldCreateNewAlignment() { + // arrange when(objectivePersistenceService.findById(8L)).thenReturn(objective1); - Alignment returnAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objectiveAlignedObjective) .withTargetObjective(objective1).build(); + + // act alignmentBusinessService.createEntity(objectiveAlignedObjective); + // assert verify(alignmentPersistenceService, times(1)).save(returnAlignment); } @Test void shouldUpdateEntityNewAlignment() { + // arrange when(alignmentPersistenceService.findByAlignedObjectiveId(8L)).thenReturn(null); when(objectivePersistenceService.findById(8L)).thenReturn(objective1); - Alignment returnAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objectiveAlignedObjective) .withTargetObjective(objective1).build(); + + // act alignmentBusinessService.updateEntity(8L, objectiveAlignedObjective); + // assert verify(alignmentPersistenceService, times(1)).save(returnAlignment); } @Test void shouldUpdateEntityDeleteAlignment() { + // arrange when(alignmentPersistenceService.findByAlignedObjectiveId(8L)).thenReturn(objectiveAlignment2); + // act alignmentBusinessService.updateEntity(8L, objective3); + // assert verify(alignmentPersistenceService, times(1)).deleteById(2L); } @Test void shouldUpdateEntityChangeTargetId() { + // arrange when(alignmentPersistenceService.findByAlignedObjectiveId(8L)).thenReturn(objectiveAlignment2); when(objectivePersistenceService.findById(8L)).thenReturn(objective1); Alignment returnAlignment = ObjectiveAlignment.Builder.builder().withId(2L).withAlignedObjective(objectiveAlignedObjective) .withTargetObjective(objective1).build(); + // act alignmentBusinessService.updateEntity(8L, objectiveAlignedObjective); + // assert verify(alignmentPersistenceService, times(1)).save(returnAlignment); } @Test void shouldUpdateEntityChangeObjectiveToKeyResult() { + // arrange when(alignmentPersistenceService.findByAlignedObjectiveId(8L)).thenReturn(objectiveAlignment2); when(keyResultPersistenceService.findById(5L)).thenReturn(metricKeyResult); Alignment returnAlignment = KeyResultAlignment.Builder.builder().withId(2L).withAlignedObjective(keyResultAlignedObjective) .withTargetKeyResult(metricKeyResult).build(); + // act alignmentBusinessService.updateEntity(8L, keyResultAlignedObjective); + // assert verify(alignmentPersistenceService, times(0)).save(returnAlignment); verify(alignmentPersistenceService, times(1)).recreateEntity(2L, returnAlignment); } @Test void shouldBuildAlignmentCorrectObjective() { + // arrange when(objectivePersistenceService.findById(8L)).thenReturn(objective1); - Alignment returnAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objectiveAlignedObjective) .withTargetObjective(objective1).build(); + + // act Alignment alignment = alignmentBusinessService.buildAlignmentModel(objectiveAlignedObjective, 0); + // assert assertEquals(returnAlignment, alignment); assertInstanceOf(ObjectiveAlignment.class, alignment); } @Test void shouldBuildAlignmentCorrectKeyResult() { + // arrange when(keyResultPersistenceService.findById(5L)).thenReturn(metricKeyResult); - Alignment returnAlignment = KeyResultAlignment.Builder.builder().withAlignedObjective(keyResultAlignedObjective) .withTargetKeyResult(metricKeyResult).build(); + + // act Alignment alignment = alignmentBusinessService.buildAlignmentModel(keyResultAlignedObjective, 0); + // assert assertEquals(returnAlignment, alignment); assertInstanceOf(KeyResultAlignment.class, alignment); } @Test void shouldThrowErrorWhenAlignedEntityIdIsIncorrect() { + // arrange + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NOT_SET", List.of("alignedEntityId", "Hello"))); + + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> alignmentBusinessService.buildAlignmentModel(wrongAlignedObjective, 0)); - List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NOT_SET", List.of("alignedEntityId", "Hello"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -185,30 +221,40 @@ void shouldReturnCorrectIsAlignmentTypeChange() { @Test void shouldUpdateKeyResultIdOnChange() { + // arrange when(alignmentPersistenceService.findByKeyResultAlignmentId(1L)).thenReturn(List.of(keyResultAlignment)); + // act alignmentBusinessService.updateKeyResultIdOnIdChange(1L, metricKeyResult); keyResultAlignment.setAlignmentTarget(metricKeyResult); + + // assert verify(alignmentPersistenceService, times(1)).save(keyResultAlignment); } @Test void shouldDeleteByObjectiveId() { + // arrange when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(objectiveALignment); when(alignmentPersistenceService.findByObjectiveAlignmentId(5L)).thenReturn(List.of(objectiveAlignment2)); + // act alignmentBusinessService.deleteAlignmentByObjectiveId(5L); + // assert verify(alignmentPersistenceService, times(1)).deleteById(objectiveALignment.getId()); verify(alignmentPersistenceService, times(1)).deleteById(objectiveAlignment2.getId()); } @Test void shouldDeleteByKeyResultId() { + // arrange when(alignmentPersistenceService.findByKeyResultAlignmentId(5L)).thenReturn(List.of(keyResultAlignment)); + // act alignmentBusinessService.deleteAlignmentByKeyResultId(5L); + // assert verify(alignmentPersistenceService, times(1)).deleteById(keyResultAlignment.getId()); } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessServiceTest.java index 961b1e12cb..f0071bfe61 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessServiceTest.java @@ -13,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; +import static ch.puzzle.okr.TestConstants.*; @ExtendWith(MockitoExtension.class) class AlignmentSelectionBusinessServiceTest { @@ -24,7 +25,7 @@ class AlignmentSelectionBusinessServiceTest { private static AlignmentSelection createAlignmentSelection() { return AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(9L, 15L)) - .withTeamId(5L).withTeamName("Puzzle ITC").withObjectiveTitle("Objective 9").withQuarterId(2L) + .withTeamId(5L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 9").withQuarterId(2L) .withQuarterLabel("GJ 23/24-Q1").withKeyResultTitle("Key Result 15").build(); } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java index 08d0ceb947..21675c450d 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java @@ -1,6 +1,7 @@ package ch.puzzle.okr.service.business; import ch.puzzle.okr.dto.AlignmentDto; +import ch.puzzle.okr.dto.AlignmentObjectDto; import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.*; import ch.puzzle.okr.models.authorization.AuthorizationUser; @@ -31,6 +32,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import static org.springframework.http.HttpStatus.NOT_FOUND; +import static ch.puzzle.okr.TestConstants.*; @ExtendWith(MockitoExtension.class) class ObjectiveBusinessServiceTest { @@ -49,7 +51,12 @@ class ObjectiveBusinessServiceTest { @Mock ObjectiveValidationService validator = Mockito.mock(ObjectiveValidationService.class); - private final Team team1 = Team.Builder.builder().withId(1L).withName("Team1").build(); + private static final String TEAM_1 = "Team1"; + private static final String OBJECTIVE = "objective"; + private static final String FULL_OBJECTIVE_1 = "O - FullObjective1"; + private static final String FULL_OBJECTIVE_2 = "O - FullObjective2"; + + private final Team team1 = Team.Builder.builder().withId(1L).withName(TEAM_1).build(); private final Quarter quarter = Quarter.Builder.builder().withId(1L).withLabel("GJ 22/23-Q2").build(); private final User user = User.Builder.builder().withId(1L).withFirstname("Bob").withLastname("Kaufmann") .withUsername("bkaufmann").withEmail("kaufmann@puzzle.ch").build(); @@ -63,7 +70,7 @@ class ObjectiveBusinessServiceTest { private final Objective fullObjective2 = Objective.Builder.builder().withId(2L).withTitle("FullObjective2") .withCreatedBy(user).withTeam(team1).withQuarter(quarter).withDescription("This is our description") .withModifiedOn(LocalDateTime.MAX).build(); - private final Team team2 = Team.Builder.builder().withId(3L).withName("Puzzle ITC").build(); + private final Team team2 = Team.Builder.builder().withId(3L).withName(TEAM_PUZZLE).build(); private final Objective fullObjective3 = Objective.Builder.builder().withId(3L).withTitle("FullObjective5") .withCreatedBy(user).withTeam(team2).withQuarter(quarter).withDescription("This is our description") .withModifiedOn(LocalDateTime.MAX).build(); @@ -73,48 +80,72 @@ class ObjectiveBusinessServiceTest { .withStretchZone("Wald").withId(5L).withTitle("Keyresult Ordinal").withObjective(objective1).build(); private final List keyResultList = List.of(ordinalKeyResult, ordinalKeyResult); private final List objectiveList = List.of(fullObjective1, fullObjective2); + private final AlignmentObjectDto alignmentObjectDto1 = new AlignmentObjectDto(1L, FULL_OBJECTIVE_1, OBJECTIVE); + private final AlignmentObjectDto alignmentObjectDto2 = new AlignmentObjectDto(5L, "KR - Keyresult Ordinal", + "keyResult"); + private final AlignmentObjectDto alignmentObjectDto3 = new AlignmentObjectDto(5L, "KR - Keyresult Ordinal", + "keyResult"); + private final AlignmentObjectDto alignmentObjectDto4 = new AlignmentObjectDto(2L, FULL_OBJECTIVE_2, OBJECTIVE); + private final AlignmentObjectDto alignmentObjectDto5 = new AlignmentObjectDto(5L, "KR - Keyresult Ordinal", + "keyResult"); + private final AlignmentObjectDto alignmentObjectDto6 = new AlignmentObjectDto(5L, "KR - Keyresult Ordinal", + "keyResult"); + private final AlignmentDto alignmentDto = new AlignmentDto(1L, TEAM_1, List.of(alignmentObjectDto1, + alignmentObjectDto2, alignmentObjectDto3, alignmentObjectDto4, alignmentObjectDto5, alignmentObjectDto6)); @Test void getOneObjective() { + // arrange when(objectivePersistenceService.findById(5L)).thenReturn(objective1); + // act Objective realObjective = objectiveBusinessService.getEntityById(5L); + // assert verify(alignmentBusinessService, times(1)).getTargetIdByAlignedObjectiveId(5L); assertEquals("Objective 1", realObjective.getTitle()); } @Test void getEntitiesByTeamId() { + // arrange when(objectivePersistenceService.findObjectiveByTeamId(anyLong())).thenReturn(List.of(objective1)); + // act List entities = objectiveBusinessService.getEntitiesByTeamId(5L); + // assert verify(alignmentBusinessService, times(1)).getTargetIdByAlignedObjectiveId(5L); assertThat(entities).hasSameElementsAs(List.of(objective1)); } @Test void shouldNotFindTheObjective() { + // arrange when(objectivePersistenceService.findById(6L)) .thenThrow(new ResponseStatusException(NOT_FOUND, "Objective with id 6 not found")); + // act ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> objectiveBusinessService.getEntityById(6L)); + + // assert assertEquals(NOT_FOUND, exception.getStatusCode()); assertEquals("Objective with id 6 not found", exception.getReason()); } @Test void shouldSaveANewObjective() { + // arrange Objective objective = spy(Objective.Builder.builder().withTitle("Received Objective").withTeam(team1) .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) .withState(DRAFT).build()); - doNothing().when(objective).setCreatedOn(any()); + // act objectiveBusinessService.createEntity(objective, authorizationUser); + // assert verify(objectivePersistenceService, times(1)).save(objective); verify(alignmentBusinessService, times(0)).createEntity(any()); assertEquals(DRAFT, objective.getState()); @@ -124,18 +155,20 @@ void shouldSaveANewObjective() { @Test void shouldUpdateAnObjective() { + // arrange Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) .withState(DRAFT).build()); - when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn(null); when(objectivePersistenceService.save(any())).thenReturn(objective); doNothing().when(objective).setCreatedOn(any()); + // act Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, authorizationUser); + // assert verify(objectivePersistenceService, times(1)).save(objective); verify(alignmentBusinessService, times(0)).updateEntity(any(), any()); assertEquals(objective.getTitle(), updatedObjective.getTitle()); @@ -145,20 +178,21 @@ void shouldUpdateAnObjective() { @Test void shouldUpdateAnObjectiveWithAlignment() { + // arrange Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) .withState(DRAFT).withAlignedEntityId("O53").build()); - when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn("O41"); when(objectivePersistenceService.save(any())).thenReturn(objective); doNothing().when(objective).setCreatedOn(any()); + // act Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, authorizationUser); - objective.setAlignedEntityId("O53"); + // assert verify(objectivePersistenceService, times(1)).save(objective); verify(alignmentBusinessService, times(1)).updateEntity(objective.getId(), objective); assertEquals(objective.getTitle(), updatedObjective.getTitle()); @@ -168,20 +202,21 @@ void shouldUpdateAnObjectiveWithAlignment() { @Test void shouldUpdateAnObjectiveWithANewAlignment() { + // arrange Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) .withState(DRAFT).withAlignedEntityId("O53").build()); - when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn(null); when(objectivePersistenceService.save(any())).thenReturn(objective); doNothing().when(objective).setCreatedOn(any()); + // act Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, authorizationUser); - objective.setAlignedEntityId("O53"); + // assert verify(objectivePersistenceService, times(1)).save(objective); verify(alignmentBusinessService, times(1)).updateEntity(objective.getId(), objective); assertEquals(objective.getTitle(), updatedObjective.getTitle()); @@ -191,18 +226,20 @@ void shouldUpdateAnObjectiveWithANewAlignment() { @Test void shouldUpdateAnObjectiveWithAlignmentDelete() { + // arrange Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) .withState(DRAFT).build()); - when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn("O52"); when(objectivePersistenceService.save(any())).thenReturn(objective); doNothing().when(objective).setCreatedOn(any()); + // act Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, authorizationUser); + // assert verify(objectivePersistenceService, times(1)).save(objective); verify(alignmentBusinessService, times(1)).updateEntity(objective.getId(), objective); assertEquals(objective.getTitle(), updatedObjective.getTitle()); @@ -212,14 +249,16 @@ void shouldUpdateAnObjectiveWithAlignmentDelete() { @Test void shouldSaveANewObjectiveWithAlignment() { + // arrange Objective objective = spy(Objective.Builder.builder().withTitle("Received Objective").withTeam(team1) .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) .withState(DRAFT).withAlignedEntityId("O42").build()); - doNothing().when(objective).setCreatedOn(any()); + // act Objective savedObjective = objectiveBusinessService.createEntity(objective, authorizationUser); + // assert verify(objectivePersistenceService, times(1)).save(objective); verify(alignmentBusinessService, times(1)).createEntity(savedObjective); assertEquals(DRAFT, objective.getState()); @@ -229,11 +268,15 @@ void shouldSaveANewObjectiveWithAlignment() { @Test void shouldNotThrowResponseStatusExceptionWhenPuttingNullId() { + // arrange Objective objective1 = Objective.Builder.builder().withId(null).withTitle("Title") .withDescription("Description").withModifiedOn(LocalDateTime.now()).build(); when(objectiveBusinessService.createEntity(objective1, authorizationUser)).thenReturn(fullObjectiveCreate); + // act Objective savedObjective = objectiveBusinessService.createEntity(objective1, authorizationUser); + + // assert assertNull(savedObjective.getId()); assertEquals("FullObjective1", savedObjective.getTitle()); assertEquals("Bob", savedObjective.getCreatedBy().getFirstname()); @@ -274,10 +317,13 @@ void updateEntityShouldHandleQuarterCorrectly(boolean hasKeyResultAnyCheckIns) { @Test void shouldDeleteObjectiveAndAssociatedKeyResults() { + // arrange when(keyResultBusinessService.getAllKeyResultsByObjective(1L)).thenReturn(keyResultList); + // act objectiveBusinessService.deleteEntityById(1L); + // assert verify(keyResultBusinessService, times(2)).deleteEntityById(5L); verify(objectiveBusinessService, times(1)).deleteEntityById(1L); verify(alignmentBusinessService, times(1)).deleteAlignmentByObjectiveId(1L); @@ -285,109 +331,94 @@ void shouldDeleteObjectiveAndAssociatedKeyResults() { @Test void shouldDuplicateObjective() { + // arrange KeyResult keyResultOrdinal = KeyResultOrdinal.Builder.builder().withTitle("Ordinal").build(); KeyResult keyResultOrdinal2 = KeyResultOrdinal.Builder.builder().withTitle("Ordinal2").build(); KeyResult keyResultMetric = KeyResultMetric.Builder.builder().withTitle("Metric").withUnit(Unit.FTE).build(); KeyResult keyResultMetric2 = KeyResultMetric.Builder.builder().withTitle("Metric2").withUnit(Unit.CHF).build(); List keyResults = List.of(keyResultOrdinal, keyResultOrdinal2, keyResultMetric, keyResultMetric2); - when(objectivePersistenceService.save(any())).thenReturn(objective1); when(keyResultBusinessService.getAllKeyResultsByObjective(anyLong())).thenReturn(keyResults); + // act objectiveBusinessService.duplicateObjective(objective1.getId(), objective1, authorizationUser); + + // assert verify(keyResultBusinessService, times(4)).createEntity(any(), any()); verify(objectiveBusinessService, times(1)).createEntity(any(), any()); } @Test void shouldReturnAlignmentPossibilities() { + // arrange when(objectivePersistenceService.findObjectiveByQuarterId(anyLong())).thenReturn(objectiveList); when(keyResultBusinessService.getAllKeyResultsByObjective(anyLong())).thenReturn(keyResultList); + // act List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); + // assert verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(1L); verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(2L); - assertEquals("Team1", alignmentsDtos.get(0).teamName()); - assertEquals(1, alignmentsDtos.get(0).teamId()); - assertEquals(6, alignmentsDtos.get(0).alignmentObjectDtos().size()); - assertEquals(1, alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectId()); - assertEquals("O - FullObjective1", alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectTitle()); - assertEquals("objective", alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectType()); - assertEquals(5, alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectId()); - assertEquals("KR - Keyresult Ordinal", alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectTitle()); - assertEquals("keyResult", alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectType()); - assertEquals(5, alignmentsDtos.get(0).alignmentObjectDtos().get(2).objectId()); - assertEquals("KR - Keyresult Ordinal", alignmentsDtos.get(0).alignmentObjectDtos().get(2).objectTitle()); - assertEquals("keyResult", alignmentsDtos.get(0).alignmentObjectDtos().get(2).objectType()); - assertEquals(2, alignmentsDtos.get(0).alignmentObjectDtos().get(3).objectId()); - assertEquals("O - FullObjective2", alignmentsDtos.get(0).alignmentObjectDtos().get(3).objectTitle()); - assertEquals("objective", alignmentsDtos.get(0).alignmentObjectDtos().get(3).objectType()); - assertEquals(5, alignmentsDtos.get(0).alignmentObjectDtos().get(4).objectId()); - assertEquals("KR - Keyresult Ordinal", alignmentsDtos.get(0).alignmentObjectDtos().get(4).objectTitle()); - assertEquals("keyResult", alignmentsDtos.get(0).alignmentObjectDtos().get(4).objectType()); - assertEquals(5, alignmentsDtos.get(0).alignmentObjectDtos().get(5).objectId()); - assertEquals("KR - Keyresult Ordinal", alignmentsDtos.get(0).alignmentObjectDtos().get(5).objectTitle()); - assertEquals("keyResult", alignmentsDtos.get(0).alignmentObjectDtos().get(5).objectType()); + assertEquals(alignmentsDtos, List.of(alignmentDto)); } @Test void shouldReturnAlignmentPossibilitiesWithMultipleTeams() { + // arrange List objectiveList = List.of(fullObjective1, fullObjective2, fullObjective3); - List keyResultList = List.of(ordinalKeyResult, ordinalKeyResult2); - when(objectivePersistenceService.findObjectiveByQuarterId(anyLong())).thenReturn(objectiveList); when(keyResultBusinessService.getAllKeyResultsByObjective(anyLong())).thenReturn(keyResultList); + AlignmentObjectDto alignmentObjectDto1 = new AlignmentObjectDto(3L, "O - FullObjective5", OBJECTIVE); + AlignmentObjectDto alignmentObjectDto2 = new AlignmentObjectDto(5L, "KR - Keyresult Ordinal", "keyResult"); + AlignmentObjectDto alignmentObjectDto3 = new AlignmentObjectDto(5L, "KR - Keyresult Ordinal", "keyResult"); + AlignmentDto alignmentDto = new AlignmentDto(3L, TEAM_PUZZLE, + List.of(alignmentObjectDto1, alignmentObjectDto2, alignmentObjectDto3)); + // act List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); + // assert verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(1L); verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(2L); verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(3L); - assertEquals(2, alignmentsDtos.size()); - assertEquals("Puzzle ITC", alignmentsDtos.get(0).teamName()); - assertEquals(3, alignmentsDtos.get(0).teamId()); - assertEquals(3, alignmentsDtos.get(0).alignmentObjectDtos().size()); - assertEquals("Team1", alignmentsDtos.get(1).teamName()); - assertEquals(1, alignmentsDtos.get(1).teamId()); - assertEquals(6, alignmentsDtos.get(1).alignmentObjectDtos().size()); - assertEquals(3, alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectId()); - assertEquals("O - FullObjective5", alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectTitle()); - assertEquals("objective", alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectType()); - assertEquals(1, alignmentsDtos.get(1).alignmentObjectDtos().get(0).objectId()); - assertEquals("O - FullObjective1", alignmentsDtos.get(1).alignmentObjectDtos().get(0).objectTitle()); - assertEquals("objective", alignmentsDtos.get(1).alignmentObjectDtos().get(0).objectType()); + assertEquals(alignmentsDtos, List.of(alignmentDto, this.alignmentDto)); } @Test void shouldReturnAlignmentPossibilitiesOnlyObjectives() { + // arrange when(objectivePersistenceService.findObjectiveByQuarterId(anyLong())).thenReturn(objectiveList); when(keyResultBusinessService.getAllKeyResultsByObjective(anyLong())).thenReturn(List.of()); + // act List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); + // assert verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(1L); verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(2L); assertEquals(2, alignmentsDtos.get(0).alignmentObjectDtos().size()); assertEquals(1, alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectId()); - assertEquals("O - FullObjective1", alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectTitle()); - assertEquals("objective", alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectType()); + assertEquals(FULL_OBJECTIVE_1, alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectTitle()); + assertEquals(OBJECTIVE, alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectType()); assertEquals(2, alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectId()); - assertEquals("O - FullObjective2", alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectTitle()); - assertEquals("objective", alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectType()); + assertEquals(FULL_OBJECTIVE_2, alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectTitle()); + assertEquals(OBJECTIVE, alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectType()); } @Test void shouldReturnEmptyAlignmentPossibilities() { + // arrange List objectiveList = List.of(); - when(objectivePersistenceService.findObjectiveByQuarterId(anyLong())).thenReturn(objectiveList); + // act List alignmentsDtos = objectiveBusinessService.getAlignmentPossibilities(5L); + // assert verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); verify(keyResultBusinessService, times(0)).getAllKeyResultsByObjective(anyLong()); assertEquals(0, alignmentsDtos.size()); @@ -395,13 +426,16 @@ void shouldReturnEmptyAlignmentPossibilities() { @Test void shouldThrowExceptionWhenQuarterIdIsNull() { + // arrange Mockito.doThrow(new OkrResponseStatusException(HttpStatus.BAD_REQUEST, "ATTRIBUTE_NULL")).when(validator) .validateOnGet(null); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> { objectiveBusinessService.getAlignmentPossibilities(null); }); + // assert assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); assertEquals("ATTRIBUTE_NULL", exception.getReason()); } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java index 2116488c78..428c415aa1 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java @@ -62,10 +62,13 @@ void tearDown() { @Test void saveAlignmentShouldSaveNewObjectiveAlignment() { + // arrange Alignment alignment = createObjectiveAlignment(null); + // act createdAlignment = alignmentPersistenceService.save(alignment); + // assert assertNotNull(createdAlignment.getId()); assertEquals(5L, createdAlignment.getAlignedObjective().getId()); assertEquals(4L, ((ObjectiveAlignment) createdAlignment).getAlignmentTarget().getId()); @@ -73,10 +76,13 @@ void saveAlignmentShouldSaveNewObjectiveAlignment() { @Test void saveAlignmentShouldSaveNewKeyResultAlignment() { + // arrange Alignment alignment = createKeyResultAlignment(null); + // act createdAlignment = alignmentPersistenceService.save(alignment); + // assert assertNotNull(createdAlignment.getId()); assertEquals(5L, createdAlignment.getAlignedObjective().getId()); assertEquals(8L, ((KeyResultAlignment) createdAlignment).getAlignmentTarget().getId()); @@ -84,27 +90,32 @@ void saveAlignmentShouldSaveNewKeyResultAlignment() { @Test void updateAlignmentShouldSaveKeyResultAlignment() { + // arrange createdAlignment = alignmentPersistenceService.save(createKeyResultAlignment(null)); Alignment updateAlignment = createKeyResultAlignment(createdAlignment.getId(), createdAlignment.getVersion()); updateAlignment.setAlignedObjective(Objective.Builder.builder().withId(8L).build()); + // act Alignment updatedAlignment = alignmentPersistenceService.save(updateAlignment); + // assert assertEquals(createdAlignment.getId(), updatedAlignment.getId()); assertEquals(createdAlignment.getVersion() + 1, updatedAlignment.getVersion()); } @Test void updateAlignmentShouldThrowExceptionWhenAlreadyUpdated() { + // arrange createdAlignment = alignmentPersistenceService.save(createKeyResultAlignment(null)); Alignment updateAlignment = createKeyResultAlignment(createdAlignment.getId(), 0); updateAlignment.setAlignedObjective(Objective.Builder.builder().withId(8L).build()); + List expectedErrors = List.of(new ErrorDto("DATA_HAS_BEEN_UPDATED", List.of(ALIGNMENT))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> alignmentPersistenceService.save(updateAlignment)); - List expectedErrors = List.of(new ErrorDto("DATA_HAS_BEEN_UPDATED", List.of(ALIGNMENT))); - + // assert assertEquals(UNPROCESSABLE_ENTITY, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -112,62 +123,59 @@ void updateAlignmentShouldThrowExceptionWhenAlreadyUpdated() { @Test void findByAlignedObjectiveIdShouldReturnAlignmentModel() { + // act Alignment alignment = alignmentPersistenceService.findByAlignedObjectiveId(4L); + // assert assertNotNull(alignment); assertEquals(4, alignment.getAlignedObjective().getId()); } @Test void findByKeyResultAlignmentIdShouldReturnListOfAlignments() { + // act List alignments = alignmentPersistenceService.findByKeyResultAlignmentId(8L); + // assert assertEquals(1, alignments.size()); assertAlignment(alignments.get(0)); } @Test void findByObjectiveAlignmentIdShouldReturnListOfAlignments() { + // act List alignments = alignmentPersistenceService.findByObjectiveAlignmentId(3L); + // assert assertEquals(1, alignments.size()); assertAlignment(alignments.get(0)); } @Test void recreateEntityShouldUpdateAlignmentNoTypeChange() { + // arrange Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withState(DRAFT).build(); Objective objective2 = Objective.Builder.builder().withId(8L).withTitle("Objective 2").withState(DRAFT).build(); Objective objective3 = Objective.Builder.builder().withId(4L) .withTitle("Build a company culture that kills the competition.").build(); ObjectiveAlignment objectiveALignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective1) .withTargetObjective(objective2).build(); - createdAlignment = alignmentPersistenceService.save(objectiveALignment); ObjectiveAlignment createObjectiveAlignment = (ObjectiveAlignment) createdAlignment; createObjectiveAlignment.setAlignmentTarget(objective3); Long alignmentId = createObjectiveAlignment.getId(); + // act Alignment recreatedAlignment = alignmentPersistenceService.recreateEntity(createdAlignment.getId(), createObjectiveAlignment); - createObjectiveAlignment = (ObjectiveAlignment) recreatedAlignment; + // assert assertNotNull(recreatedAlignment.getId()); assertEquals(4L, createObjectiveAlignment.getAlignmentTarget().getId()); assertEquals("Build a company culture that kills the competition.", createObjectiveAlignment.getAlignmentTarget().getTitle()); - - // Should delete the old Alignment - OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> alignmentPersistenceService.findById(alignmentId)); - - List expectedErrors = List - .of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of(ALIGNMENT, alignmentId))); - - assertEquals(NOT_FOUND, exception.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + shouldDeleteOldAlignment(alignmentId); // delete re-created Alignment in tearDown() createdAlignment = createObjectiveAlignment; @@ -175,39 +183,29 @@ void recreateEntityShouldUpdateAlignmentNoTypeChange() { @Test void recreateEntityShouldUpdateAlignmentWithTypeChange() { + // arrange Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withState(DRAFT).build(); Objective objective2 = Objective.Builder.builder().withId(8L).withTitle("Objective 2").withState(DRAFT).build(); KeyResult keyResult = KeyResultMetric.Builder.builder().withId(10L) .withTitle("Im Durchschnitt soll die Lautstärke 60dB nicht überschreiten").build(); ObjectiveAlignment objectiveALignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective1) .withTargetObjective(objective2).build(); - createdAlignment = alignmentPersistenceService.save(objectiveALignment); - KeyResultAlignment keyResultAlignment = KeyResultAlignment.Builder.builder().withId(createdAlignment.getId()) .withAlignedObjective(objective1).withTargetKeyResult(keyResult).build(); + Long alignmentId = createdAlignment.getId(); + // act Alignment recreatedAlignment = alignmentPersistenceService.recreateEntity(keyResultAlignment.getId(), keyResultAlignment); - KeyResultAlignment returnedKeyResultAlignment = (KeyResultAlignment) recreatedAlignment; + // assert assertNotNull(recreatedAlignment.getId()); assertEquals(createdAlignment.getAlignedObjective().getId(), recreatedAlignment.getAlignedObjective().getId()); assertEquals("Im Durchschnitt soll die Lautstärke 60dB nicht überschreiten", returnedKeyResultAlignment.getAlignmentTarget().getTitle()); - - Long alignmentId = createdAlignment.getId(); - // Should delete the old Alignment - OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> alignmentPersistenceService.findById(alignmentId)); - - List expectedErrors = List - .of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of(ALIGNMENT, alignmentId))); - - assertEquals(NOT_FOUND, exception.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + shouldDeleteOldAlignment(alignmentId); // delete re-created Alignment in tearDown() createdAlignment = returnedKeyResultAlignment; @@ -223,6 +221,19 @@ private void assertAlignment(Alignment alignment) { } } + private void shouldDeleteOldAlignment(Long alignmentId) { + // Should delete the old Alignment + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> alignmentPersistenceService.findById(alignmentId)); + + List expectedErrors = List + .of(ErrorDto.of("MODEL_WITH_ID_NOT_FOUND", List.of(ALIGNMENT, alignmentId))); + + assertEquals(NOT_FOUND, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + private void assertAlignment(ObjectiveAlignment objectiveAlignment) { assertEquals(1L, objectiveAlignment.getId()); assertEquals(3L, objectiveAlignment.getAlignmentTarget().getId()); diff --git a/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java index e70413fd23..8d370e4bc0 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java @@ -27,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static ch.puzzle.okr.TestConstants.*; @ExtendWith(MockitoExtension.class) class AlignmentValidationServiceTest { @@ -39,7 +40,7 @@ class AlignmentValidationServiceTest { @InjectMocks private AlignmentValidationService validator; - Team team1 = Team.Builder.builder().withId(1L).withName("Puzzle ITC").build(); + Team team1 = Team.Builder.builder().withId(1L).withName(TEAM_PUZZLE).build(); Team team2 = Team.Builder.builder().withId(2L).withName("BBT").build(); Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withTeam(team1) .withState(DRAFT).build(); @@ -61,43 +62,51 @@ void setUp() { @Test void validateOnGetShouldBeSuccessfulWhenValidActionId() { + // act validator.validateOnGet(1L); + // assert verify(validator, times(1)).validateOnGet(1L); verify(validator, times(1)).throwExceptionWhenIdIsNull(1L); } @Test void validateOnGetShouldThrowExceptionIfIdIsNull() { + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnGet(null)); + // assert verify(validator, times(1)).throwExceptionWhenIdIsNull(null); - assertEquals(BAD_REQUEST, exception.getStatusCode()); assertEquals("ATTRIBUTE_NULL", exception.getReason()); assertEquals(List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("ID", "Alignment"))), exception.getErrors()); } @Test - void validateOnCreateShouldBeSuccessfulWhenAlignmentIsValid() { - when(alignmentPersistenceService.findByAlignedObjectiveId(anyLong())).thenReturn(null); + void validateOnCreateShouldBeSuccessfulWhenAlignmentIsValid() { + // arrange + when(alignmentPersistenceService.findByAlignedObjectiveId(anyLong())).thenReturn(null); when(teamPersistenceService.findById(1L)).thenReturn(team1); when(teamPersistenceService.findById(2L)).thenReturn(team2); - validator.validateOnCreate(createAlignment); + // act + validator.validateOnCreate(createAlignment); - verify(validator, times(1)).throwExceptionWhenModelIsNull(createAlignment); - verify(validator, times(1)).throwExceptionWhenIdIsNotNull(null); - verify(alignmentPersistenceService, times(1)).findByAlignedObjectiveId(8L); - verify(validator, times(1)).validate(createAlignment); - } + // assert + verify(validator, times(1)).throwExceptionWhenModelIsNull(createAlignment); + verify(validator, times(1)).throwExceptionWhenIdIsNotNull(null); + verify(alignmentPersistenceService, times(1)).findByAlignedObjectiveId(8L); + verify(validator, times(1)).validate(createAlignment); + } @Test void validateOnCreateShouldThrowExceptionWhenModelIsNull() { + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnCreate(null)); + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertEquals("MODEL_NULL", exception.getReason()); assertEquals(List.of(new ErrorDto("MODEL_NULL", List.of("Alignment"))), exception.getErrors()); @@ -105,11 +114,14 @@ void validateOnCreateShouldThrowExceptionWhenModelIsNull() { @Test void validateOnCreateShouldThrowExceptionWhenIdIsNotNull() { + // arrange + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NOT_NULL", List.of("ID", "Alignment"))); + + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnCreate(keyResultAlignment)); - List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NOT_NULL", List.of("ID", "Alignment"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -117,14 +129,16 @@ void validateOnCreateShouldThrowExceptionWhenIdIsNotNull() { @Test void validateOnCreateShouldThrowExceptionWhenAlignedObjectiveIsNull() { + // arrange ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withTargetObjective(objective2) .build(); + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("alignedObjectiveId"))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnCreate(objectiveAlignment)); - List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("alignedObjectiveId"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -132,14 +146,16 @@ void validateOnCreateShouldThrowExceptionWhenAlignedObjectiveIsNull() { @Test void validateOnCreateShouldThrowExceptionWhenTargetObjectiveIsNull() { + // arrange ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective2) .build(); + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetObjectiveId", "8"))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnCreate(objectiveAlignment)); - List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetObjectiveId", "8"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -147,14 +163,16 @@ void validateOnCreateShouldThrowExceptionWhenTargetObjectiveIsNull() { @Test void validateOnCreateShouldThrowExceptionWhenTargetKeyResultIsNull() { + // arrange KeyResultAlignment wrongKeyResultAlignment = KeyResultAlignment.Builder.builder() .withAlignedObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetKeyResultId", "8"))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnCreate(wrongKeyResultAlignment)); - List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetKeyResultId", "8"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -162,14 +180,16 @@ void validateOnCreateShouldThrowExceptionWhenTargetKeyResultIsNull() { @Test void validateOnCreateShouldThrowExceptionWhenAlignedIdIsSameAsTargetId() { + // arrange ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective2) .withTargetObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("NOT_LINK_YOURSELF", List.of("targetObjectiveId", "8"))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnCreate(objectiveAlignment)); - List expectedErrors = List.of(new ErrorDto("NOT_LINK_YOURSELF", List.of("targetObjectiveId", "8"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -177,17 +197,19 @@ void validateOnCreateShouldThrowExceptionWhenAlignedIdIsSameAsTargetId() { @Test void validateOnCreateShouldThrowExceptionWhenAlignmentIsInSameTeamObjective() { + // arrange when(teamPersistenceService.findById(2L)).thenReturn(team2); Objective objective = objective1; objective.setTeam(team2); ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective) .withTargetObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "2"))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnCreate(objectiveAlignment)); - List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "2"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -195,45 +217,51 @@ void validateOnCreateShouldThrowExceptionWhenAlignmentIsInSameTeamObjective() { @Test void validateOnCreateShouldThrowExceptionWhenAlignmentIsInSameTeamKeyResult() { + // arrange when(teamPersistenceService.findById(1L)).thenReturn(team1); KeyResult keyResult = KeyResultMetric.Builder.builder().withId(3L).withTitle("KeyResult 1").withObjective(objective1).build(); KeyResultAlignment keyResultAlignment1 = KeyResultAlignment.Builder.builder().withAlignedObjective(objective1).withTargetKeyResult(keyResult).build(); + List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "1"))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnCreate(keyResultAlignment1)); - List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "1"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); } @Test - void validateOnCreateShouldThrowExceptionWhenAlignedObjectiveAlreadyExists() { + void validateOnCreateShouldThrowExceptionWhenAlignedObjectiveAlreadyExists() { + // arrange when(alignmentPersistenceService.findByAlignedObjectiveId(anyLong())).thenReturn(objectiveALignment); when(teamPersistenceService.findById(1L)).thenReturn(team1); when(teamPersistenceService.findById(2L)).thenReturn(team2); + ObjectiveAlignment createAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective1).withTargetObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("ALIGNMENT_ALREADY_EXISTS", List.of("alignedObjectiveId", "5"))); - ObjectiveAlignment createAlignment = ObjectiveAlignment.Builder.builder().withAlignedObjective(objective1).withTargetObjective(objective2).build(); - - OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> validator.validateOnCreate(createAlignment)); - - List expectedErrors = List.of(new ErrorDto("ALIGNMENT_ALREADY_EXISTS", List.of("alignedObjectiveId", "5"))); + // act + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnCreate(createAlignment)); - assertEquals(BAD_REQUEST, exception.getStatusCode()); - assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); - assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); - } + // assert + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } @Test void validateOnUpdateShouldBeSuccessfulWhenAlignmentIsValid() { + // arrange when(teamPersistenceService.findById(1L)).thenReturn(team1); when(teamPersistenceService.findById(2L)).thenReturn(team2); + // act validator.validateOnUpdate(objectiveALignment.getId(), objectiveALignment); + // assert verify(validator, times(1)).throwExceptionWhenModelIsNull(objectiveALignment); verify(validator, times(1)).throwExceptionWhenIdIsNull(objectiveALignment.getId()); verify(validator, times(1)).validate(objectiveALignment); @@ -241,9 +269,11 @@ void validateOnUpdateShouldBeSuccessfulWhenAlignmentIsValid() { @Test void validateOnUpdateShouldThrowExceptionWhenModelIsNull() { + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnUpdate(1L, null)); + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertEquals("MODEL_NULL", exception.getReason()); assertEquals(List.of(new ErrorDto("MODEL_NULL", List.of("Alignment"))), exception.getErrors()); @@ -251,13 +281,16 @@ void validateOnUpdateShouldThrowExceptionWhenModelIsNull() { @Test void validateOnUpdateShouldThrowExceptionWhenIdIsNull() { + // arrange ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().build(); + + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnUpdate(null, objectiveAlignment)); + // assert verify(validator, times(1)).throwExceptionWhenModelIsNull(objectiveAlignment); verify(validator, times(1)).throwExceptionWhenIdIsNull(null); - assertEquals(BAD_REQUEST, exception.getStatusCode()); assertEquals("ATTRIBUTE_NULL", exception.getReason()); assertEquals(List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("ID", "Alignment"))), exception.getErrors()); @@ -265,14 +298,16 @@ void validateOnUpdateShouldThrowExceptionWhenIdIsNull() { @Test void validateOnUpdateShouldThrowExceptionWhenAlignedObjectiveIsNull() { + // arrange ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withId(3L) .withTargetObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("alignedObjectiveId"))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnUpdate(3L, objectiveAlignment)); - List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("alignedObjectiveId"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -280,14 +315,16 @@ void validateOnUpdateShouldThrowExceptionWhenAlignedObjectiveIsNull() { @Test void validateOnUpdateShouldThrowExceptionWhenTargetObjectiveIsNull() { + // arrange ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withId(3L) .withAlignedObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetObjectiveId", "8"))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnUpdate(3L, objectiveAlignment)); - List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetObjectiveId", "8"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -295,14 +332,16 @@ void validateOnUpdateShouldThrowExceptionWhenTargetObjectiveIsNull() { @Test void validateOnUpdateShouldThrowExceptionWhenTargetKeyResultIsNull() { + // arrange KeyResultAlignment wrongKeyResultAlignment = KeyResultAlignment.Builder.builder().withId(3L) .withAlignedObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetKeyResultId", "8"))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnUpdate(3L, wrongKeyResultAlignment)); - List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("targetKeyResultId", "8"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -310,14 +349,16 @@ void validateOnUpdateShouldThrowExceptionWhenTargetKeyResultIsNull() { @Test void validateOnUpdateShouldThrowExceptionWhenAlignedIdIsSameAsTargetId() { + // arrange ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withId(3L) .withAlignedObjective(objective2).withTargetObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("NOT_LINK_YOURSELF", List.of("targetObjectiveId", "8"))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnUpdate(3L, objectiveAlignment)); - List expectedErrors = List.of(new ErrorDto("NOT_LINK_YOURSELF", List.of("targetObjectiveId", "8"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -325,17 +366,19 @@ void validateOnUpdateShouldThrowExceptionWhenAlignedIdIsSameAsTargetId() { @Test void validateOnUpdateShouldThrowExceptionWhenAlignmentIsInSameTeamObjective() { + // arrange when(teamPersistenceService.findById(2L)).thenReturn(team2); Objective objective = objective1; objective.setTeam(team2); ObjectiveAlignment objectiveAlignment = ObjectiveAlignment.Builder.builder().withId(3L) .withAlignedObjective(objective).withTargetObjective(objective2).build(); + List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "2"))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnUpdate(2L, objectiveAlignment)); - List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "2"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -343,15 +386,17 @@ void validateOnUpdateShouldThrowExceptionWhenAlignmentIsInSameTeamObjective() { @Test void validateOnUpdateShouldThrowExceptionWhenAlignmentIsInSameTeamKeyResult() { + // arrange when(teamPersistenceService.findById(1L)).thenReturn(team1); KeyResult keyResult = KeyResultMetric.Builder.builder().withId(3L).withTitle("KeyResult 1").withObjective(objective1).build(); KeyResultAlignment keyResultAlignment1 = KeyResultAlignment.Builder.builder().withId(2L).withAlignedObjective(objective1).withTargetKeyResult(keyResult).build(); + List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "1"))); + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnUpdate(2L, keyResultAlignment1)); - List expectedErrors = List.of(new ErrorDto("NOT_LINK_IN_SAME_TEAM", List.of("teamId", "1"))); - + // assert assertEquals(BAD_REQUEST, exception.getStatusCode()); assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); @@ -359,18 +404,21 @@ void validateOnUpdateShouldThrowExceptionWhenAlignmentIsInSameTeamKeyResult() { @Test void validateOnDeleteShouldBeSuccessfulWhenValidAlignmentId() { + // act validator.validateOnDelete(3L); + // assert verify(validator, times(1)).throwExceptionWhenIdIsNull(3L); } @Test void validateOnDeleteShouldThrowExceptionIfAlignmentIdIsNull() { + // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, () -> validator.validateOnDelete(null)); + // assert verify(validator, times(1)).throwExceptionWhenIdIsNull(null); - assertEquals(BAD_REQUEST, exception.getStatusCode()); assertEquals("ATTRIBUTE_NULL", exception.getReason()); assertEquals(List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("ID", "Alignment"))), exception.getErrors()); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index a9a41bbc52..e88271b068 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -70,22 +70,22 @@
- @for (group of filteredOptions$ | async; track group) { + @for (group of filteredAlignmentOptions$ | async; track group) { @for (alignmentObject of group.alignmentObjectDtos; track alignmentObject) { {{ alignmentObject.objectTitle }} diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index 87677017e6..4b8e150f8b 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -509,7 +509,7 @@ describe('ObjectiveDialogComponent', () => { }); expect(alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); - expect(component.filteredOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); expect(component.objectiveForm.getRawValue().alignment).toEqual(null); }); @@ -522,15 +522,15 @@ describe('ObjectiveDialogComponent', () => { }); expect(alignmentPossibilities).toStrictEqual([alignmentPossibility2]); - expect(component.filteredOptions$.getValue()).toEqual([alignmentPossibility2]); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility2]); expect(component.objectiveForm.getRawValue().alignment).toEqual(null); }); it('should return team and objective with same text in alignment possibilities', async () => { - component.input.nativeElement.value = 'puzzle'; + component.alignmentInput.nativeElement.value = 'puzzle'; component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); component.filter(); - expect(component.filteredOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); }); it('should load existing objective alignment to objectiveForm', async () => { @@ -542,7 +542,7 @@ describe('ObjectiveDialogComponent', () => { }); expect(alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); - expect(component.filteredOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); expect(component.objectiveForm.getRawValue().alignment).toEqual(alignmentObject2); }); @@ -556,13 +556,13 @@ describe('ObjectiveDialogComponent', () => { }); expect(alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); - expect(component.filteredOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); expect(component.objectiveForm.getRawValue().alignment).toEqual(alignmentObject3); }); it('should filter correct alignment possibilities', async () => { // Search for one title - component.input.nativeElement.value = 'palm'; + component.alignmentInput.nativeElement.value = 'palm'; component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); component.filter(); let modifiedAlignmentPossibility: AlignmentPossibility = { @@ -570,10 +570,10 @@ describe('ObjectiveDialogComponent', () => { teamName: 'Puzzle ITC', alignmentObjectDtos: [alignmentObject3], }; - expect(component.filteredOptions$.getValue()).toEqual([modifiedAlignmentPossibility]); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([modifiedAlignmentPossibility]); // Search for team name - component.input.nativeElement.value = 'Puzzle IT'; + component.alignmentInput.nativeElement.value = 'Puzzle IT'; component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); component.filter(); modifiedAlignmentPossibility = { @@ -581,10 +581,10 @@ describe('ObjectiveDialogComponent', () => { teamName: 'Puzzle ITC', alignmentObjectDtos: [alignmentObject2, alignmentObject3], }; - expect(component.filteredOptions$.getValue()).toEqual([modifiedAlignmentPossibility]); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([modifiedAlignmentPossibility]); // Search for two objects - component.input.nativeElement.value = 'buy'; + component.alignmentInput.nativeElement.value = 'buy'; component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); component.filter(); let modifiedAlignmentPossibilities = [ @@ -599,18 +599,18 @@ describe('ObjectiveDialogComponent', () => { alignmentObjectDtos: [alignmentObject1], }, ]; - expect(component.filteredOptions$.getValue()).toEqual(modifiedAlignmentPossibilities); + expect(component.filteredAlignmentOptions$.getValue()).toEqual(modifiedAlignmentPossibilities); // No match - component.input.nativeElement.value = 'findus'; + component.alignmentInput.nativeElement.value = 'findus'; component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); component.filter(); - expect(component.filteredOptions$.getValue()).toEqual([]); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([]); }); it('should find correct alignment object', () => { // objective - let alignmentObject = component.findAlignmentObject( + let alignmentObject = component.findAlignmentPossibilityObject( [alignmentPossibility1, alignmentPossibility2], 1, 'objective', @@ -619,38 +619,46 @@ describe('ObjectiveDialogComponent', () => { expect(alignmentObject!.objectTitle).toEqual('We want to increase the income puzzle buy'); // keyResult - alignmentObject = component.findAlignmentObject([alignmentPossibility1, alignmentPossibility2], 1, 'keyResult'); + alignmentObject = component.findAlignmentPossibilityObject( + [alignmentPossibility1, alignmentPossibility2], + 1, + 'keyResult', + ); expect(alignmentObject!.objectId).toEqual(1); expect(alignmentObject!.objectTitle).toEqual('We buy 3 palms'); // no match - alignmentObject = component.findAlignmentObject([alignmentPossibility1, alignmentPossibility2], 133, 'keyResult'); + alignmentObject = component.findAlignmentPossibilityObject( + [alignmentPossibility1, alignmentPossibility2], + 133, + 'keyResult', + ); expect(alignmentObject).toEqual(null); }); it('should display kein alignment vorhanden when no alignment possibility', () => { - component.filteredOptions$.next([alignmentPossibility1, alignmentPossibility2]); + component.filteredAlignmentOptions$.next([alignmentPossibility1, alignmentPossibility2]); fixture.detectChanges(); - expect(component.input.nativeElement.getAttribute('placeholder')).toEqual('Bezug wählen'); + expect(component.alignmentInput.nativeElement.getAttribute('placeholder')).toEqual('Bezug wählen'); - component.filteredOptions$.next([]); + component.filteredAlignmentOptions$.next([]); fixture.detectChanges(); - expect(component.input.nativeElement.getAttribute('placeholder')).toEqual('Kein Alignment vorhanden'); + expect(component.alignmentInput.nativeElement.getAttribute('placeholder')).toEqual('Kein Alignment vorhanden'); }); it('should update alignments on quarter change', () => { objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); component.updateAlignments(); - expect(component.input.nativeElement.value).toEqual(''); + expect(component.alignmentInput.nativeElement.value).toEqual(''); expect(component.objectiveForm.getRawValue().alignment).toEqual(null); expect(objectiveService.getAlignmentPossibilities).toHaveBeenCalled(); }); it('should return correct displayedValue', () => { - component.input.nativeElement.value = 'O - Objective 1'; + component.alignmentInput.nativeElement.value = 'O - Objective 1'; expect(component.displayedValue).toEqual('O - Objective 1'); - component.input = new ElementRef(document.createElement('input')); + component.alignmentInput = new ElementRef(document.createElement('input')); expect(component.displayedValue).toEqual(''); }); }); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 767716d530..7a54524ce3 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -25,8 +25,7 @@ import { AlignmentPossibilityObject } from '../../types/model/AlignmentPossibili changeDetection: ChangeDetectionStrategy.OnPush, }) export class ObjectiveFormComponent implements OnInit { - @ViewChild('input') input!: ElementRef; - filteredOptions$: BehaviorSubject = new BehaviorSubject([]); + @ViewChild('alignmentInput') alignmentInput!: ElementRef; objectiveForm = new FormGroup({ title: new FormControl('', [Validators.required, Validators.minLength(2), Validators.maxLength(250)]), @@ -40,6 +39,7 @@ export class ObjectiveFormComponent implements OnInit { quarters: Quarter[] = []; teams$: Observable = of([]); alignmentPossibilities$: Observable = of([]); + filteredAlignmentOptions$: BehaviorSubject = new BehaviorSubject([]); currentTeam$: BehaviorSubject = new BehaviorSubject(null); state: string | null = null; version!: number; @@ -69,9 +69,9 @@ export class ObjectiveFormComponent implements OnInit { const value = this.objectiveForm.getRawValue(); const state = this.data.objective.objectiveId == null ? submitType : this.state; - let alignmentEntity: AlignmentPossibilityObject | null = value.alignment; - let alignment: string | null = alignmentEntity - ? (alignmentEntity.objectType == 'objective' ? 'O' : 'K') + alignmentEntity.objectId + let alignment: AlignmentPossibilityObject | null = value.alignment; + let alignmentEntity: string | null = alignment + ? (alignment.objectType == 'objective' ? 'O' : 'K') + alignment.objectId : null; let objectiveDTO: Objective = { @@ -82,7 +82,7 @@ export class ObjectiveFormComponent implements OnInit { title: value.title, teamId: value.team, state: state, - alignedEntityId: alignment, + alignedEntityId: alignmentEntity, } as unknown as Objective; const submitFunction = this.getSubmitFunction(objectiveDTO.id, objectiveDTO); @@ -253,24 +253,28 @@ export class ObjectiveFormComponent implements OnInit { } if (objective) { - let alignment: string | null = objective.alignedEntityId; - if (alignment) { - let alignmentType: string = alignment.charAt(0); - let alignmentId: number = parseInt(alignment.substring(1)); + let alignmentEntity: string | null = objective.alignedEntityId; + if (alignmentEntity) { + let alignmentType: string = alignmentEntity.charAt(0); + let alignmentId: number = parseInt(alignmentEntity.substring(1)); alignmentType = alignmentType == 'O' ? 'objective' : 'keyResult'; - let element: AlignmentPossibilityObject | null = this.findAlignmentObject(value, alignmentId, alignmentType); + let alignmentPossibilityObject: AlignmentPossibilityObject | null = this.findAlignmentPossibilityObject( + value, + alignmentId, + alignmentType, + ); this.objectiveForm.patchValue({ - alignment: element, + alignment: alignmentPossibilityObject, }); } } - this.filteredOptions$.next(value.slice()); + this.filteredAlignmentOptions$.next(value.slice()); this.alignmentPossibilities$ = of(value); }); } - findAlignmentObject( + findAlignmentPossibilityObject( alignmentPossibilities: AlignmentPossibility[], objectId: number, objectType: string, @@ -288,8 +292,8 @@ export class ObjectiveFormComponent implements OnInit { } updateAlignments() { - this.input.nativeElement.value = ''; - this.filteredOptions$.next([]); + this.alignmentInput.nativeElement.value = ''; + this.filteredAlignmentOptions$.next([]); this.objectiveForm.patchValue({ alignment: null, }); @@ -297,7 +301,7 @@ export class ObjectiveFormComponent implements OnInit { } filter() { - let filterValue: string = this.input.nativeElement.value.toLowerCase(); + let filterValue: string = this.alignmentInput.nativeElement.value.toLowerCase(); this.alignmentPossibilities$.subscribe((alignmentPossibilities: AlignmentPossibility[]) => { let matchingTeams: AlignmentPossibility[] = alignmentPossibilities.filter((possibility: AlignmentPossibility) => possibility.teamName.toLowerCase().includes(filterValue), @@ -319,7 +323,7 @@ export class ObjectiveFormComponent implements OnInit { matchingPossibilities = [...new Set(matchingPossibilities)]; - let optionList = matchingPossibilities.map((possibility: AlignmentPossibility) => ({ + let alignmentOptionList = matchingPossibilities.map((possibility: AlignmentPossibility) => ({ ...possibility, alignmentObjectDtos: possibility.alignmentObjectDtos.filter( (alignmentPossibilityObject: AlignmentPossibilityObject) => @@ -327,8 +331,9 @@ export class ObjectiveFormComponent implements OnInit { ), })); - let finalArray: AlignmentPossibility[] = filterValue == '' ? matchingTeams : matchingTeams.concat(optionList); - this.filteredOptions$.next([...new Set(finalArray)]); + let concatAlignmentOptionList: AlignmentPossibility[] = + filterValue == '' ? matchingTeams : matchingTeams.concat(alignmentOptionList); + this.filteredAlignmentOptions$.next([...new Set(concatAlignmentOptionList)]); }); } @@ -339,15 +344,15 @@ export class ObjectiveFormComponent implements OnInit { } get displayedValue(): string { - if (this.input) { - return this.input.nativeElement.value; + if (this.alignmentInput) { + return this.alignmentInput.nativeElement.value; } else { return ''; } } scrollLeft() { - this.input.nativeElement.scrollLeft = 0; + this.alignmentInput.nativeElement.scrollLeft = 0; } protected readonly getQuarterLabel = getQuarterLabel; From c563856e904b6159a0fc93e1ba3352ee5a11b267 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 7 Jun 2024 12:58:54 +0200 Subject: [PATCH 043/119] Remove AlignmentSelection --- .../okr/controller/AlignmentController.java | 45 ------ .../okr/mapper/AlignmentSelectionMapper.java | 58 -------- .../models/alignment/AlignmentSelection.java | 140 ------------------ .../alignment/AlignmentSelectionId.java | 83 ----------- .../AlignmentSelectionRepository.java | 15 -- .../AlignmentSelectionBusinessService.java | 23 --- .../AlignmentSelectionPersistenceService.java | 21 --- .../V1_0_0__current-db-schema-for-testing.sql | 15 -- .../V2_1_3__removeAlignmentSelection.sql | 1 + .../okr/controller/AlignmentControllerIT.java | 97 ------------ .../mapper/AlignmentSelectionMapperTest.java | 92 ------------ ...AlignmentSelectionBusinessServiceTest.java | 43 ------ ...lignmentSelectionPersistenceServiceIT.java | 48 ------ .../repositoriesAndPersistenceServices.csv | 1 - 14 files changed, 1 insertion(+), 681 deletions(-) delete mode 100644 backend/src/main/java/ch/puzzle/okr/controller/AlignmentController.java delete mode 100644 backend/src/main/java/ch/puzzle/okr/mapper/AlignmentSelectionMapper.java delete mode 100644 backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelection.java delete mode 100644 backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelectionId.java delete mode 100644 backend/src/main/java/ch/puzzle/okr/repository/AlignmentSelectionRepository.java delete mode 100644 backend/src/main/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessService.java delete mode 100644 backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceService.java create mode 100644 backend/src/main/resources/db/migration/V2_1_3__removeAlignmentSelection.sql delete mode 100644 backend/src/test/java/ch/puzzle/okr/controller/AlignmentControllerIT.java delete mode 100644 backend/src/test/java/ch/puzzle/okr/mapper/AlignmentSelectionMapperTest.java delete mode 100644 backend/src/test/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessServiceTest.java delete mode 100644 backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceServiceIT.java diff --git a/backend/src/main/java/ch/puzzle/okr/controller/AlignmentController.java b/backend/src/main/java/ch/puzzle/okr/controller/AlignmentController.java deleted file mode 100644 index 85b6a77ec1..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/controller/AlignmentController.java +++ /dev/null @@ -1,45 +0,0 @@ -package ch.puzzle.okr.controller; - -import ch.puzzle.okr.dto.alignment.AlignmentObjectiveDto; -import ch.puzzle.okr.mapper.AlignmentSelectionMapper; -import ch.puzzle.okr.service.business.AlignmentSelectionBusinessService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -@RestController -@RequestMapping("api/v2/alignments") -public class AlignmentController { - private final AlignmentSelectionMapper alignmentSelectionMapper; - private final AlignmentSelectionBusinessService alignmentSelectionBusinessService; - - public AlignmentController(AlignmentSelectionMapper alignmentSelectionMapper, - AlignmentSelectionBusinessService alignmentSelectionBusinessService) { - this.alignmentSelectionMapper = alignmentSelectionMapper; - this.alignmentSelectionBusinessService = alignmentSelectionBusinessService; - } - - @Operation(summary = "Get all objectives and their key results to select the alignment", description = "Get a list of objectives with their key results to select the alignment") - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Returned a list of objectives with their key results to select the alignment", content = { - @Content(mediaType = "application/json", schema = @Schema(implementation = AlignmentObjectiveDto.class)) }), - @ApiResponse(responseCode = "400", description = "Can't return list of objectives with their key results to select the alignment", content = @Content) }) - @GetMapping("/selections") - public ResponseEntity> getAlignmentSelections( - @RequestParam(required = false, defaultValue = "", name = "quarter") Long quarterFilter, - @RequestParam(required = false, defaultValue = "", name = "team") Long teamFilter) { - return ResponseEntity.status(HttpStatus.OK) - .body(alignmentSelectionMapper.toDto(alignmentSelectionBusinessService - .getAlignmentSelectionByQuarterIdAndTeamIdNot(quarterFilter, teamFilter))); - } -} diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/AlignmentSelectionMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/AlignmentSelectionMapper.java deleted file mode 100644 index 547181b95e..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/mapper/AlignmentSelectionMapper.java +++ /dev/null @@ -1,58 +0,0 @@ -package ch.puzzle.okr.mapper; - -import ch.puzzle.okr.dto.alignment.AlignmentKeyResultDto; -import ch.puzzle.okr.dto.alignment.AlignmentObjectiveDto; -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -@Component -public class AlignmentSelectionMapper { - - public List toDto(List alignments) { - List alignmentDtos = new ArrayList<>(); - alignments.forEach(alignment -> processObjectives(alignmentDtos, alignment)); - return alignmentDtos; - } - - private Optional getMatchingObjectiveDto(Long objectiveId, - List objectives) { - return objectives.stream().filter(objectiveDto -> Objects.equals(objectiveId, objectiveDto.id())).findFirst(); - } - - private void processObjectives(List objectiveDtos, AlignmentSelection alignment) { - Optional objectiveDto = getMatchingObjectiveDto( - alignment.getAlignmentSelectionId().getObjectiveId(), objectiveDtos); - if (objectiveDto.isPresent()) { - processKeyResults(objectiveDto.get(), alignment); - } else { - AlignmentObjectiveDto alignmentObjectiveDto = createObjectiveDto(alignment); - objectiveDtos.add(alignmentObjectiveDto); - processKeyResults(alignmentObjectiveDto, alignment); - } - } - - private void processKeyResults(AlignmentObjectiveDto objectiveDto, AlignmentSelection alignment) { - if (isValidId(alignment.getAlignmentSelectionId().getKeyResultId())) { - objectiveDto.keyResults().add(createKeyResultDto(alignment)); - } - } - - private AlignmentObjectiveDto createObjectiveDto(AlignmentSelection alignment) { - return new AlignmentObjectiveDto(alignment.getAlignmentSelectionId().getObjectiveId(), - alignment.getObjectiveTitle(), new ArrayList<>()); - } - - private AlignmentKeyResultDto createKeyResultDto(AlignmentSelection alignment) { - return new AlignmentKeyResultDto(alignment.getAlignmentSelectionId().getKeyResultId(), - alignment.getKeyResultTitle()); - } - - private boolean isValidId(Long id) { - return id != null && id > -1; - } -} diff --git a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelection.java b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelection.java deleted file mode 100644 index 0246817184..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelection.java +++ /dev/null @@ -1,140 +0,0 @@ -package ch.puzzle.okr.models.alignment; - -import jakarta.persistence.EmbeddedId; -import jakarta.persistence.Entity; -import org.hibernate.annotations.Immutable; - -import java.util.Objects; - -@Entity -@Immutable -public class AlignmentSelection { - - @EmbeddedId - private AlignmentSelectionId alignmentSelectionId; - - private Long teamId; - private String teamName; - private String objectiveTitle; - private Long quarterId; - private String quarterLabel; - private String keyResultTitle; - - public AlignmentSelection() { - } - - private AlignmentSelection(Builder builder) { - alignmentSelectionId = builder.alignmentSelectionId; - teamId = builder.teamId; - teamName = builder.teamName; - objectiveTitle = builder.objectiveTitle; - quarterId = builder.quarterId; - quarterLabel = builder.quarterLabel; - keyResultTitle = builder.keyResultTitle; - } - - public AlignmentSelectionId getAlignmentSelectionId() { - return alignmentSelectionId; - } - - public Long getTeamId() { - return teamId; - } - - public String getObjectiveTitle() { - return objectiveTitle; - } - - public Long getQuarterId() { - return quarterId; - } - - public String getKeyResultTitle() { - return keyResultTitle; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - AlignmentSelection alignmentSelection = (AlignmentSelection) o; - return Objects.equals(alignmentSelectionId, alignmentSelection.alignmentSelectionId) - && Objects.equals(teamId, alignmentSelection.teamId) - && Objects.equals(objectiveTitle, alignmentSelection.objectiveTitle) - && Objects.equals(quarterId, alignmentSelection.quarterId) - && Objects.equals(keyResultTitle, alignmentSelection.keyResultTitle); - } - - @Override - public int hashCode() { - return Objects.hash(alignmentSelectionId, teamId, objectiveTitle, quarterId, keyResultTitle); - } - - @Override - public String toString() { - return "AlignmentSelection{" + "alignmentSelectionId=" + alignmentSelectionId + ", teamId='" + teamId - + ", teamName='" + teamName + '\'' + ", objectiveTitle='" + objectiveTitle + '\'' + ", quarterId=" - + quarterId + ", quarterLabel='" + quarterLabel + '\'' + ", keyResultTitle='" + keyResultTitle + '\'' - + '}'; - } - - public static final class Builder { - private AlignmentSelectionId alignmentSelectionId; - - private Long teamId; - private String teamName; - private String objectiveTitle; - private Long quarterId; - private String quarterLabel; - private String keyResultTitle; - - public Builder() { - // This builder can be empty, so that it can get called - } - - public static Builder builder() { - return new Builder(); - } - - public Builder withAlignmentSelectionId(AlignmentSelectionId alignmentSelectionId) { - this.alignmentSelectionId = alignmentSelectionId; - return this; - } - - public Builder withTeamId(Long teamId) { - this.teamId = teamId; - return this; - } - - public Builder withTeamName(String teamName) { - this.teamName = teamName; - return this; - } - - public Builder withObjectiveTitle(String objectiveTitle) { - this.objectiveTitle = objectiveTitle; - return this; - } - - public Builder withQuarterId(Long quarterId) { - this.quarterId = quarterId; - return this; - } - - public Builder withQuarterLabel(String quarterLabel) { - this.quarterLabel = quarterLabel; - return this; - } - - public Builder withKeyResultTitle(String keyResultTitle) { - this.keyResultTitle = keyResultTitle; - return this; - } - - public AlignmentSelection build() { - return new AlignmentSelection(this); - } - } -} diff --git a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelectionId.java b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelectionId.java deleted file mode 100644 index 75c52cf2b8..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentSelectionId.java +++ /dev/null @@ -1,83 +0,0 @@ -package ch.puzzle.okr.models.alignment; - -import jakarta.persistence.Embeddable; - -import java.io.Serializable; -import java.util.Objects; - -@Embeddable -public class AlignmentSelectionId implements Serializable { - - private Long objectiveId; - private Long keyResultId; - - public AlignmentSelectionId() { - } - - private AlignmentSelectionId(Long objectiveId, Long keyResultId) { - this.objectiveId = objectiveId; - this.keyResultId = keyResultId; - } - - private AlignmentSelectionId(Builder builder) { - this(builder.objectiveId, builder.keyResultId); - } - - public static AlignmentSelectionId of(Long objectiveId, Long keyResultId) { - return new AlignmentSelectionId(objectiveId, keyResultId); - } - - public Long getObjectiveId() { - return objectiveId; - } - - public Long getKeyResultId() { - return keyResultId; - } - - @Override - public String toString() { - return "AlignmentSelectionId{" + "objectiveId=" + objectiveId + ", keyResultId=" + keyResultId + '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - AlignmentSelectionId that = (AlignmentSelectionId) o; - return Objects.equals(objectiveId, that.objectiveId) && Objects.equals(keyResultId, that.keyResultId); - } - - @Override - public int hashCode() { - return Objects.hash(objectiveId, keyResultId); - } - - public static final class Builder { - private Long objectiveId; - private Long keyResultId; - - private Builder() { - } - - public static Builder builder() { - return new Builder(); - } - - public Builder withObjectiveId(Long objectiveId) { - this.objectiveId = objectiveId; - return this; - } - - public Builder withKeyResultId(Long keyResultId) { - this.keyResultId = keyResultId; - return this; - } - - public AlignmentSelectionId build() { - return new AlignmentSelectionId(this); - } - } -} diff --git a/backend/src/main/java/ch/puzzle/okr/repository/AlignmentSelectionRepository.java b/backend/src/main/java/ch/puzzle/okr/repository/AlignmentSelectionRepository.java deleted file mode 100644 index 50896b44f3..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/repository/AlignmentSelectionRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package ch.puzzle.okr.repository; - -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.models.alignment.AlignmentSelectionId; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.List; - -public interface AlignmentSelectionRepository extends ReadOnlyRepository { - - @Query(value = "from AlignmentSelection where quarterId = :quarter_id and teamId != :ignoredTeamId") - List getAlignmentSelectionByQuarterIdAndTeamIdNot(@Param("quarter_id") Long quarterId, - @Param("ignoredTeamId") Long ignoredTeamId); -} diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessService.java deleted file mode 100644 index 8e6feefed1..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessService.java +++ /dev/null @@ -1,23 +0,0 @@ -package ch.puzzle.okr.service.business; - -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.service.persistence.AlignmentSelectionPersistenceService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class AlignmentSelectionBusinessService { - - private final AlignmentSelectionPersistenceService alignmentSelectionPersistenceService; - - public AlignmentSelectionBusinessService( - AlignmentSelectionPersistenceService alignmentSelectionPersistenceService) { - this.alignmentSelectionPersistenceService = alignmentSelectionPersistenceService; - } - - public List getAlignmentSelectionByQuarterIdAndTeamIdNot(Long quarterId, Long ignoredTeamId) { - return alignmentSelectionPersistenceService.getAlignmentSelectionByQuarterIdAndTeamIdNot(quarterId, - ignoredTeamId); - } -} diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceService.java deleted file mode 100644 index 3a439073b8..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceService.java +++ /dev/null @@ -1,21 +0,0 @@ -package ch.puzzle.okr.service.persistence; - -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.repository.AlignmentSelectionRepository; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class AlignmentSelectionPersistenceService { - - private final AlignmentSelectionRepository alignmentSelectionRepository; - - public AlignmentSelectionPersistenceService(AlignmentSelectionRepository alignmentSelectionRepository) { - this.alignmentSelectionRepository = alignmentSelectionRepository; - } - - public List getAlignmentSelectionByQuarterIdAndTeamIdNot(Long quarterId, Long ignoredTeamId) { - return alignmentSelectionRepository.getAlignmentSelectionByQuarterIdAndTeamIdNot(quarterId, ignoredTeamId); - } -} diff --git a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql index 51fbbfc187..19f8f23ee7 100644 --- a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql +++ b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql @@ -208,21 +208,6 @@ create table if not exists alignment foreign key (target_objective_id) references objective ); -DROP VIEW IF EXISTS ALIGNMENT_SELECTION; -CREATE VIEW ALIGNMENT_SELECTION AS -SELECT O.ID AS "OBJECTIVE_ID", - O.TITLE AS "OBJECTIVE_TITLE", - T.ID AS "TEAM_ID", - T.NAME AS "TEAM_NAME", - Q.ID AS "QUARTER_ID", - Q.LABEL AS "QUARTER_LABEL", - COALESCE(KR.ID, -1) AS "KEY_RESULT_ID", - KR.TITLE AS "KEY_RESULT_TITLE" -FROM OBJECTIVE O - LEFT JOIN TEAM T ON O.TEAM_ID = T.ID - LEFT JOIN QUARTER Q ON O.QUARTER_ID = Q.ID - LEFT JOIN KEY_RESULT KR ON O.ID = KR.OBJECTIVE_ID; - create table if not exists organisation ( id bigint not null, diff --git a/backend/src/main/resources/db/migration/V2_1_3__removeAlignmentSelection.sql b/backend/src/main/resources/db/migration/V2_1_3__removeAlignmentSelection.sql new file mode 100644 index 0000000000..751c03e89b --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_1_3__removeAlignmentSelection.sql @@ -0,0 +1 @@ +drop view if exists alignment_selection; \ No newline at end of file diff --git a/backend/src/test/java/ch/puzzle/okr/controller/AlignmentControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/AlignmentControllerIT.java deleted file mode 100644 index c0372930db..0000000000 --- a/backend/src/test/java/ch/puzzle/okr/controller/AlignmentControllerIT.java +++ /dev/null @@ -1,97 +0,0 @@ -package ch.puzzle.okr.controller; - -import ch.puzzle.okr.mapper.AlignmentSelectionMapper; -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.models.alignment.AlignmentSelectionId; -import ch.puzzle.okr.service.business.AlignmentSelectionBusinessService; -import org.hamcrest.Matchers; -import org.hamcrest.core.Is; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.BDDMockito; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; -import org.springframework.http.MediaType; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.result.MockMvcResultMatchers; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import static org.mockito.ArgumentMatchers.any; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; - -@WithMockUser(value = "spring") -@ExtendWith(MockitoExtension.class) -@WebMvcTest(AlignmentController.class) -class AlignmentControllerIT { - @Autowired - private MockMvc mvc; - @MockBean - private AlignmentSelectionBusinessService alignmentSelectionBusinessService; - @SpyBean - private AlignmentSelectionMapper alignmentSelectionMapper; - - static String alignmentObjectiveName = "Objective 5"; - static List alignmentSelectionPuzzle = List.of( - AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(1L, 20L)) - .withObjectiveTitle("Objective 1").withKeyResultTitle("KeyResult 20").build(), - AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(1L, 40L)) - .withObjectiveTitle("Objective 1").withKeyResultTitle("KeyResult 40").build()); - static List alignmentSelectionOKR = List.of( - AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(5L, 21L)) - .withObjectiveTitle(alignmentObjectiveName).withKeyResultTitle("KeyResult 21").build(), - AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(5L, 41L)) - .withObjectiveTitle(alignmentObjectiveName).withKeyResultTitle("KeyResult 41").build(), - AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(5L, 61L)) - .withObjectiveTitle(alignmentObjectiveName).withKeyResultTitle("KeyResult 61").build(), - AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(5L, 81L)) - .withObjectiveTitle(alignmentObjectiveName).withKeyResultTitle("KeyResult 81").build()); - static AlignmentSelection alignmentSelectionEmptyKeyResults = AlignmentSelection.Builder.builder() - .withAlignmentSelectionId(AlignmentSelectionId.of(8L, null)).withObjectiveTitle("Objective 8").build(); - - @Test - void shouldGetAllObjectivesWithKeyResults() throws Exception { - List alignmentSelections = new ArrayList<>(); - alignmentSelections.addAll(alignmentSelectionPuzzle); - alignmentSelections.addAll(alignmentSelectionOKR); - alignmentSelections.add(alignmentSelectionEmptyKeyResults); - BDDMockito.given(alignmentSelectionBusinessService.getAlignmentSelectionByQuarterIdAndTeamIdNot(2L, 4L)) - .willReturn(alignmentSelections); - - mvc.perform(get("/api/v2/alignments/selections?quarter=2&team=4").contentType(MediaType.APPLICATION_JSON)) - .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$", Matchers.hasSize(3))) - .andExpect(jsonPath("$[0].id", Is.is(1))).andExpect(jsonPath("$[0].keyResults[0].id", Is.is(20))) - .andExpect(jsonPath("$[0].keyResults[1].id", Is.is(40))).andExpect(jsonPath("$[1].id", Is.is(5))) - .andExpect(jsonPath("$[1].keyResults[0].id", Is.is(21))) - .andExpect(jsonPath("$[1].keyResults[1].id", Is.is(41))) - .andExpect(jsonPath("$[1].keyResults[2].id", Is.is(61))) - .andExpect(jsonPath("$[1].keyResults[3].id", Is.is(81))).andExpect(jsonPath("$[2].id", Is.is(8))) - .andExpect(jsonPath("$[2].keyResults.size()", Is.is(0))); - } - - @Test - void shouldGetAllObjectivesWithKeyResultsIfAllObjectivesFiltered() throws Exception { - BDDMockito.given(alignmentSelectionBusinessService.getAlignmentSelectionByQuarterIdAndTeamIdNot(any(), any())) - .willReturn(Collections.emptyList()); - - mvc.perform(get("/api/v2/alignments/selections").contentType(MediaType.APPLICATION_JSON)) - .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$", Matchers.hasSize(0))); - } - - @Test - void shouldReturnObjectiveWithEmptyKeyResultListWhenNoKeyResultsInFilteredQuarter() throws Exception { - BDDMockito.given(alignmentSelectionBusinessService.getAlignmentSelectionByQuarterIdAndTeamIdNot(2L, 4L)) - .willReturn(List.of(alignmentSelectionEmptyKeyResults)); - - mvc.perform(get("/api/v2/alignments/selections?quarter=2&team=4").contentType(MediaType.APPLICATION_JSON)) - .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$", Matchers.hasSize(1))) - .andExpect(jsonPath("$[0].id", Is.is(8))).andExpect(jsonPath("$[0].keyResults.size()", Is.is(0))); - } -} diff --git a/backend/src/test/java/ch/puzzle/okr/mapper/AlignmentSelectionMapperTest.java b/backend/src/test/java/ch/puzzle/okr/mapper/AlignmentSelectionMapperTest.java deleted file mode 100644 index 8a7f6fb366..0000000000 --- a/backend/src/test/java/ch/puzzle/okr/mapper/AlignmentSelectionMapperTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package ch.puzzle.okr.mapper; - -import ch.puzzle.okr.dto.alignment.AlignmentObjectiveDto; -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.models.alignment.AlignmentSelectionId; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static ch.puzzle.okr.TestConstants.*; - -class AlignmentSelectionMapperTest { - private final AlignmentSelectionMapper alignmentSelectionMapper = new AlignmentSelectionMapper(); - - @Test - void toDtoShouldReturnEmptyListWhenNoObjectiveFound() { - List alignmentSelections = List.of(); - List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); - - assertTrue(alignmentObjectiveDtos.isEmpty()); - } - - @Test - void toDtoShouldReturnOneElementWhenObjectiveFound() { - List alignmentSelections = List.of(AlignmentSelection.Builder.builder() - .withAlignmentSelectionId(AlignmentSelectionId.Builder.builder().withObjectiveId(1L).build()) - .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1").build()); - List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); - - assertEquals(1, alignmentObjectiveDtos.size()); - assertEquals(0, alignmentObjectiveDtos.get(0).keyResults().size()); - } - - @Test - void toDtoShouldReturnOneElementWhenObjectiveWithKeyResultFound() { - List alignmentSelections = List.of(AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(3L).build()) - .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1") - .withKeyResultTitle("Key Result 3").build()); - List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); - - assertEquals(1, alignmentObjectiveDtos.size()); - assertEquals(1, alignmentObjectiveDtos.get(0).keyResults().size()); - } - - @Test - void toDtoShouldReturnOneElementWhenObjectiveWithTwoKeyResultsFound() { - List alignmentSelections = List.of( - AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(3L).build()) - .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1") - .withKeyResultTitle("Key Result 3").build(), - AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(5L).build()) - .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1") - .withKeyResultTitle("Key Result 5").build()); - List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); - - assertEquals(1, alignmentObjectiveDtos.size()); - assertEquals(2, alignmentObjectiveDtos.get(0).keyResults().size()); - } - - @Test - void toDtoShouldReturnOneElementWhenTwoObjectivesWithKeyResultsFound() { - List alignmentSelections = List.of( - AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(3L).build()) - .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1") - .withKeyResultTitle("Key Result 3").build(), - AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(5L).withKeyResultId(6L).build()) - .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 5") - .withKeyResultTitle("Key Result 6").build(), - AlignmentSelection.Builder.builder() - .withAlignmentSelectionId( - AlignmentSelectionId.Builder.builder().withObjectiveId(1L).withKeyResultId(9L).build()) - .withTeamId(2L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 1") - .withKeyResultTitle("Key Result 9").build()); - List alignmentObjectiveDtos = alignmentSelectionMapper.toDto(alignmentSelections); - - assertEquals(2, alignmentObjectiveDtos.size()); - assertEquals(2, alignmentObjectiveDtos.get(0).keyResults().size()); - assertEquals(1, alignmentObjectiveDtos.get(1).keyResults().size()); - } -} diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessServiceTest.java deleted file mode 100644 index f0071bfe61..0000000000 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentSelectionBusinessServiceTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package ch.puzzle.okr.service.business; - -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.models.alignment.AlignmentSelectionId; -import ch.puzzle.okr.service.persistence.AlignmentSelectionPersistenceService; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -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.*; -import static ch.puzzle.okr.TestConstants.*; - -@ExtendWith(MockitoExtension.class) -class AlignmentSelectionBusinessServiceTest { - - @InjectMocks - AlignmentSelectionBusinessService alignmentSelectionBusinessService; - @Mock - AlignmentSelectionPersistenceService alignmentSelectionPersistenceService; - - private static AlignmentSelection createAlignmentSelection() { - return AlignmentSelection.Builder.builder().withAlignmentSelectionId(AlignmentSelectionId.of(9L, 15L)) - .withTeamId(5L).withTeamName(TEAM_PUZZLE).withObjectiveTitle("Objective 9").withQuarterId(2L) - .withQuarterLabel("GJ 23/24-Q1").withKeyResultTitle("Key Result 15").build(); - } - - @Test - void getAlignmentSelectionByQuarterIdAndTeamIdNotShouldReturnListOfAlignmentSelections() { - when(alignmentSelectionPersistenceService.getAlignmentSelectionByQuarterIdAndTeamIdNot(2L, 4L)) - .thenReturn(List.of(createAlignmentSelection())); - - List alignmentSelections = alignmentSelectionBusinessService - .getAlignmentSelectionByQuarterIdAndTeamIdNot(2L, 4L); - - assertEquals(1, alignmentSelections.size()); - verify(alignmentSelectionPersistenceService, times(1)).getAlignmentSelectionByQuarterIdAndTeamIdNot(2L, 4L); - } -} diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceServiceIT.java deleted file mode 100644 index d01a662f88..0000000000 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentSelectionPersistenceServiceIT.java +++ /dev/null @@ -1,48 +0,0 @@ -package ch.puzzle.okr.service.persistence; - -import ch.puzzle.okr.models.alignment.AlignmentSelection; -import ch.puzzle.okr.models.alignment.AlignmentSelectionId; -import ch.puzzle.okr.test.SpringIntegrationTest; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.List; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringIntegrationTest -class AlignmentSelectionPersistenceServiceIT { - @Autowired - private AlignmentSelectionPersistenceService alignmentSelectionPersistenceService; - - @Test - void getAlignmentSelectionByQuarterIdAndTeamIdNotShouldReturnAlignmentSelections() { - List alignmentSelections = alignmentSelectionPersistenceService - .getAlignmentSelectionByQuarterIdAndTeamIdNot(2L, 4L); - - assertEquals(12, alignmentSelections.size()); - alignmentSelections.forEach(alignmentSelection -> assertTrue( - matchAlignmentSelectionId(alignmentSelection.getAlignmentSelectionId()))); - } - - private boolean matchAlignmentSelectionId(AlignmentSelectionId alignmentSelectionId) { - return getExpectedAlignmentSelectionIds().anyMatch(id -> id.equals(alignmentSelectionId)); - } - - private static Stream getExpectedAlignmentSelectionIds() { - return Stream.of(AlignmentSelectionId.of(9L, 15L), // - AlignmentSelectionId.of(9L, 16L), // - AlignmentSelectionId.of(9L, 17L), // - AlignmentSelectionId.of(4L, 6L), // - AlignmentSelectionId.of(4L, 7L), // - AlignmentSelectionId.of(4L, 8L), // - AlignmentSelectionId.of(3L, 3L), // - AlignmentSelectionId.of(3L, 4L), // - AlignmentSelectionId.of(3L, 5L), // - AlignmentSelectionId.of(8L, 18L), // - AlignmentSelectionId.of(8L, 19L), // - AlignmentSelectionId.of(10L, -1L)); - } -} diff --git a/backend/src/test/resources/repositoriesAndPersistenceServices.csv b/backend/src/test/resources/repositoriesAndPersistenceServices.csv index 36f53411b3..671175a7f6 100644 --- a/backend/src/test/resources/repositoriesAndPersistenceServices.csv +++ b/backend/src/test/resources/repositoriesAndPersistenceServices.csv @@ -1,7 +1,6 @@ repository,persistenceService,validationService ActionRepository,ActionPersistenceService,ActionValidationService AlignmentRepository,AlignmentPersistenceService,"" -AlignmentSelectionRepository,AlignmentSelectionPersistenceService,"" CheckInRepository,CheckInPersistenceService,CheckInValidationService CompletedRepository,CompletedPersistenceService,CompletedValidationService KeyResultRepository,KeyResultPersistenceService,KeyResultValidationService From 50cb7be95eaa435e56c340594b87216074c28fb2 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 12 Jun 2024 15:31:19 +0200 Subject: [PATCH 044/119] Refactor code --- .../objective-form.component.spec.ts | 38 +++------- .../objective-form.component.ts | 69 +++++++++---------- 2 files changed, 44 insertions(+), 63 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index 4b8e150f8b..2c0ec8c045 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -503,12 +503,8 @@ describe('ObjectiveDialogComponent', () => { it('should load correct alignment possibilities', async () => { objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); component.generateAlignmentPossibilities(3, null, null); - let alignmentPossibilities = null; - component.alignmentPossibilities$.subscribe((value) => { - alignmentPossibilities = value; - }); - expect(alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); expect(component.objectiveForm.getRawValue().alignment).toEqual(null); }); @@ -516,19 +512,15 @@ describe('ObjectiveDialogComponent', () => { it('should not include current team in alignment possibilities', async () => { objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); component.generateAlignmentPossibilities(3, null, 1); - let alignmentPossibilities = null; - component.alignmentPossibilities$.subscribe((value) => { - alignmentPossibilities = value; - }); - expect(alignmentPossibilities).toStrictEqual([alignmentPossibility2]); + expect(component.alignmentPossibilities).toStrictEqual([alignmentPossibility2]); expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility2]); expect(component.objectiveForm.getRawValue().alignment).toEqual(null); }); it('should return team and objective with same text in alignment possibilities', async () => { component.alignmentInput.nativeElement.value = 'puzzle'; - component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); + component.alignmentPossibilities = [alignmentPossibility1, alignmentPossibility2]; component.filter(); expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); }); @@ -536,12 +528,8 @@ describe('ObjectiveDialogComponent', () => { it('should load existing objective alignment to objectiveForm', async () => { objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); component.generateAlignmentPossibilities(3, objectiveWithAlignment, null); - let alignmentPossibilities = null; - component.alignmentPossibilities$.subscribe((value) => { - alignmentPossibilities = value; - }); - expect(alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); expect(component.objectiveForm.getRawValue().alignment).toEqual(alignmentObject2); }); @@ -550,12 +538,8 @@ describe('ObjectiveDialogComponent', () => { objectiveWithAlignment.alignedEntityId = 'K1'; objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); component.generateAlignmentPossibilities(3, objectiveWithAlignment, null); - let alignmentPossibilities = null; - component.alignmentPossibilities$.subscribe((value) => { - alignmentPossibilities = value; - }); - expect(alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); + expect(component.alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); expect(component.objectiveForm.getRawValue().alignment).toEqual(alignmentObject3); }); @@ -563,7 +547,7 @@ describe('ObjectiveDialogComponent', () => { it('should filter correct alignment possibilities', async () => { // Search for one title component.alignmentInput.nativeElement.value = 'palm'; - component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); + component.alignmentPossibilities = [alignmentPossibility1, alignmentPossibility2]; component.filter(); let modifiedAlignmentPossibility: AlignmentPossibility = { teamId: 1, @@ -574,7 +558,7 @@ describe('ObjectiveDialogComponent', () => { // Search for team name component.alignmentInput.nativeElement.value = 'Puzzle IT'; - component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); + component.alignmentPossibilities = [alignmentPossibility1, alignmentPossibility2]; component.filter(); modifiedAlignmentPossibility = { teamId: 1, @@ -585,7 +569,7 @@ describe('ObjectiveDialogComponent', () => { // Search for two objects component.alignmentInput.nativeElement.value = 'buy'; - component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); + component.alignmentPossibilities = [alignmentPossibility1, alignmentPossibility2]; component.filter(); let modifiedAlignmentPossibilities = [ { @@ -603,7 +587,7 @@ describe('ObjectiveDialogComponent', () => { // No match component.alignmentInput.nativeElement.value = 'findus'; - component.alignmentPossibilities$ = of([alignmentPossibility1, alignmentPossibility2]); + component.alignmentPossibilities = [alignmentPossibility1, alignmentPossibility2]; component.filter(); expect(component.filteredAlignmentOptions$.getValue()).toEqual([]); }); @@ -656,10 +640,10 @@ describe('ObjectiveDialogComponent', () => { it('should return correct displayedValue', () => { component.alignmentInput.nativeElement.value = 'O - Objective 1'; - expect(component.displayedValue).toEqual('O - Objective 1'); + expect(component.displayedValue()).toEqual('O - Objective 1'); component.alignmentInput = new ElementRef(document.createElement('input')); - expect(component.displayedValue).toEqual(''); + expect(component.displayedValue()).toEqual(''); }); }); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 7a54524ce3..51ba59a887 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -38,7 +38,7 @@ export class ObjectiveFormComponent implements OnInit { quarters$: Observable = of([]); quarters: Quarter[] = []; teams$: Observable = of([]); - alignmentPossibilities$: Observable = of([]); + alignmentPossibilities: AlignmentPossibility[] = []; filteredAlignmentOptions$: BehaviorSubject = new BehaviorSubject([]); currentTeam$: BehaviorSubject = new BehaviorSubject(null); state: string | null = null; @@ -246,10 +246,9 @@ export class ObjectiveFormComponent implements OnInit { } generateAlignmentPossibilities(quarterId: number, objective: Objective | null, teamId: number | null) { - this.alignmentPossibilities$ = this.objectiveService.getAlignmentPossibilities(quarterId); - this.alignmentPossibilities$.subscribe((value: AlignmentPossibility[]) => { + this.objectiveService.getAlignmentPossibilities(quarterId).subscribe((alignmentPossibilities: AlignmentPossibility[]) => { if (teamId) { - value = value.filter((item: AlignmentPossibility) => item.teamId != teamId); + alignmentPossibilities = alignmentPossibilities.filter((item: AlignmentPossibility) => item.teamId != teamId); } if (objective) { @@ -259,7 +258,7 @@ export class ObjectiveFormComponent implements OnInit { let alignmentId: number = parseInt(alignmentEntity.substring(1)); alignmentType = alignmentType == 'O' ? 'objective' : 'keyResult'; let alignmentPossibilityObject: AlignmentPossibilityObject | null = this.findAlignmentPossibilityObject( - value, + alignmentPossibilities, alignmentId, alignmentType, ); @@ -269,8 +268,8 @@ export class ObjectiveFormComponent implements OnInit { } } - this.filteredAlignmentOptions$.next(value.slice()); - this.alignmentPossibilities$ = of(value); + this.filteredAlignmentOptions$.next(alignmentPossibilities.slice()); + this.alignmentPossibilities = alignmentPossibilities; }); } @@ -302,39 +301,37 @@ export class ObjectiveFormComponent implements OnInit { filter() { let filterValue: string = this.alignmentInput.nativeElement.value.toLowerCase(); - this.alignmentPossibilities$.subscribe((alignmentPossibilities: AlignmentPossibility[]) => { - let matchingTeams: AlignmentPossibility[] = alignmentPossibilities.filter((possibility: AlignmentPossibility) => - possibility.teamName.toLowerCase().includes(filterValue), - ); + let matchingTeams: AlignmentPossibility[] = this.alignmentPossibilities.filter( + (possibility: AlignmentPossibility) => possibility.teamName.toLowerCase().includes(filterValue), + ); - let filteredObjects: AlignmentPossibilityObject[] = alignmentPossibilities.flatMap( - (alignmentPossibility: AlignmentPossibility) => - alignmentPossibility.alignmentObjectDtos.filter((alignmentPossibilityObject: AlignmentPossibilityObject) => - alignmentPossibilityObject.objectTitle.toLowerCase().includes(filterValue), - ), - ); + let filteredObjects: AlignmentPossibilityObject[] = this.alignmentPossibilities.flatMap( + (alignmentPossibility: AlignmentPossibility) => + alignmentPossibility.alignmentObjectDtos.filter((alignmentPossibilityObject: AlignmentPossibilityObject) => + alignmentPossibilityObject.objectTitle.toLowerCase().includes(filterValue), + ), + ); - let matchingPossibilities: AlignmentPossibility[] = alignmentPossibilities.filter( - (possibility: AlignmentPossibility) => - filteredObjects.some((alignmentPossibilityObject: AlignmentPossibilityObject) => - possibility.alignmentObjectDtos.includes(alignmentPossibilityObject), - ), - ); + let matchingPossibilities: AlignmentPossibility[] = this.alignmentPossibilities.filter( + (possibility: AlignmentPossibility) => + filteredObjects.some((alignmentPossibilityObject: AlignmentPossibilityObject) => + possibility.alignmentObjectDtos.includes(alignmentPossibilityObject), + ), + ); - matchingPossibilities = [...new Set(matchingPossibilities)]; + matchingPossibilities = [...new Set(matchingPossibilities)]; - let alignmentOptionList = matchingPossibilities.map((possibility: AlignmentPossibility) => ({ - ...possibility, - alignmentObjectDtos: possibility.alignmentObjectDtos.filter( - (alignmentPossibilityObject: AlignmentPossibilityObject) => - filteredObjects.includes(alignmentPossibilityObject), - ), - })); + let alignmentOptionList = matchingPossibilities.map((possibility: AlignmentPossibility) => ({ + ...possibility, + alignmentObjectDtos: possibility.alignmentObjectDtos.filter( + (alignmentPossibilityObject: AlignmentPossibilityObject) => + filteredObjects.includes(alignmentPossibilityObject), + ), + })); - let concatAlignmentOptionList: AlignmentPossibility[] = - filterValue == '' ? matchingTeams : matchingTeams.concat(alignmentOptionList); - this.filteredAlignmentOptions$.next([...new Set(concatAlignmentOptionList)]); - }); + let concatAlignmentOptionList: AlignmentPossibility[] = + filterValue == '' ? matchingTeams : matchingTeams.concat(alignmentOptionList); + this.filteredAlignmentOptions$.next([...new Set(concatAlignmentOptionList)]); } displayWith(value: any) { @@ -343,7 +340,7 @@ export class ObjectiveFormComponent implements OnInit { } } - get displayedValue(): string { + displayedValue(): string { if (this.alignmentInput) { return this.alignmentInput.nativeElement.value; } else { From ccc6c12f3be5f7003e1e46b792879c432b97e324 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 12 Jun 2024 13:32:10 +0000 Subject: [PATCH 045/119] [FM] Automated formating frontend --- .../objective-form.component.ts | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 51ba59a887..80689c1c28 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -246,31 +246,33 @@ export class ObjectiveFormComponent implements OnInit { } generateAlignmentPossibilities(quarterId: number, objective: Objective | null, teamId: number | null) { - this.objectiveService.getAlignmentPossibilities(quarterId).subscribe((alignmentPossibilities: AlignmentPossibility[]) => { - if (teamId) { - alignmentPossibilities = alignmentPossibilities.filter((item: AlignmentPossibility) => item.teamId != teamId); - } + this.objectiveService + .getAlignmentPossibilities(quarterId) + .subscribe((alignmentPossibilities: AlignmentPossibility[]) => { + if (teamId) { + alignmentPossibilities = alignmentPossibilities.filter((item: AlignmentPossibility) => item.teamId != teamId); + } - if (objective) { - let alignmentEntity: string | null = objective.alignedEntityId; - if (alignmentEntity) { - let alignmentType: string = alignmentEntity.charAt(0); - let alignmentId: number = parseInt(alignmentEntity.substring(1)); - alignmentType = alignmentType == 'O' ? 'objective' : 'keyResult'; - let alignmentPossibilityObject: AlignmentPossibilityObject | null = this.findAlignmentPossibilityObject( - alignmentPossibilities, - alignmentId, - alignmentType, - ); - this.objectiveForm.patchValue({ - alignment: alignmentPossibilityObject, - }); + if (objective) { + let alignmentEntity: string | null = objective.alignedEntityId; + if (alignmentEntity) { + let alignmentType: string = alignmentEntity.charAt(0); + let alignmentId: number = parseInt(alignmentEntity.substring(1)); + alignmentType = alignmentType == 'O' ? 'objective' : 'keyResult'; + let alignmentPossibilityObject: AlignmentPossibilityObject | null = this.findAlignmentPossibilityObject( + alignmentPossibilities, + alignmentId, + alignmentType, + ); + this.objectiveForm.patchValue({ + alignment: alignmentPossibilityObject, + }); + } } - } - this.filteredAlignmentOptions$.next(alignmentPossibilities.slice()); - this.alignmentPossibilities = alignmentPossibilities; - }); + this.filteredAlignmentOptions$.next(alignmentPossibilities.slice()); + this.alignmentPossibilities = alignmentPossibilities; + }); } findAlignmentPossibilityObject( From 2198b5df8e355fd6404e4162c556439460a5ef74 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 13 Jun 2024 07:22:35 +0200 Subject: [PATCH 046/119] Remove bug with duplicated alignment possibilities --- .../objective-form.component.spec.ts | 9 ++- .../objective-form.component.ts | 60 ++++++++++++++----- frontend/src/app/shared/testData.ts | 2 +- 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index 2c0ec8c045..8587e1bf35 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -592,6 +592,13 @@ describe('ObjectiveDialogComponent', () => { expect(component.filteredAlignmentOptions$.getValue()).toEqual([]); }); + it('should not include alignment object when already containing in team', async () => { + component.alignmentInput.nativeElement.value = 'puzzle'; + component.alignmentPossibilities = [alignmentPossibility1]; + component.filter(); + expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1]); + }); + it('should find correct alignment object', () => { // objective let alignmentObject = component.findAlignmentPossibilityObject( @@ -609,7 +616,7 @@ describe('ObjectiveDialogComponent', () => { 'keyResult', ); expect(alignmentObject!.objectId).toEqual(1); - expect(alignmentObject!.objectTitle).toEqual('We buy 3 palms'); + expect(alignmentObject!.objectTitle).toEqual('We buy 3 palms puzzle'); // no match alignmentObject = component.findAlignmentPossibilityObject( diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 80689c1c28..3998b7a75b 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -307,33 +307,63 @@ export class ObjectiveFormComponent implements OnInit { (possibility: AlignmentPossibility) => possibility.teamName.toLowerCase().includes(filterValue), ); - let filteredObjects: AlignmentPossibilityObject[] = this.alignmentPossibilities.flatMap( - (alignmentPossibility: AlignmentPossibility) => - alignmentPossibility.alignmentObjectDtos.filter((alignmentPossibilityObject: AlignmentPossibilityObject) => - alignmentPossibilityObject.objectTitle.toLowerCase().includes(filterValue), - ), + let filteredObjects: AlignmentPossibilityObject[] = + this.getMatchingAlignmentPossibilityObjectsByInputFilter(filterValue); + let matchingPossibilities: AlignmentPossibility[] = + this.getAlignmentPossibilityFromAlignmentObject(filteredObjects); + matchingPossibilities = [...new Set(matchingPossibilities)]; + + let alignmentOptionList: AlignmentPossibility[] = this.removeNotMatchingObjectsFromAlignmentObject( + matchingPossibilities, + filteredObjects, + ); + alignmentOptionList = this.removeAlignmentObjectWhenAlreadyContainingInMatchingTeam( + alignmentOptionList, + matchingTeams, ); - let matchingPossibilities: AlignmentPossibility[] = this.alignmentPossibilities.filter( - (possibility: AlignmentPossibility) => - filteredObjects.some((alignmentPossibilityObject: AlignmentPossibilityObject) => - possibility.alignmentObjectDtos.includes(alignmentPossibilityObject), - ), + let concatAlignmentOptionList: AlignmentPossibility[] = + filterValue == '' ? matchingTeams : matchingTeams.concat(alignmentOptionList); + this.filteredAlignmentOptions$.next([...new Set(concatAlignmentOptionList)]); + } + + getMatchingAlignmentPossibilityObjectsByInputFilter(filterValue: string): AlignmentPossibilityObject[] { + return this.alignmentPossibilities.flatMap((alignmentPossibility: AlignmentPossibility) => + alignmentPossibility.alignmentObjectDtos.filter((alignmentPossibilityObject: AlignmentPossibilityObject) => + alignmentPossibilityObject.objectTitle.toLowerCase().includes(filterValue), + ), ); + } - matchingPossibilities = [...new Set(matchingPossibilities)]; + getAlignmentPossibilityFromAlignmentObject(filteredObjects: AlignmentPossibilityObject[]): AlignmentPossibility[] { + return this.alignmentPossibilities.filter((possibility: AlignmentPossibility) => + filteredObjects.some((alignmentPossibilityObject: AlignmentPossibilityObject) => + possibility.alignmentObjectDtos.includes(alignmentPossibilityObject), + ), + ); + } - let alignmentOptionList = matchingPossibilities.map((possibility: AlignmentPossibility) => ({ + removeNotMatchingObjectsFromAlignmentObject( + matchingPossibilities: AlignmentPossibility[], + filteredObjects: AlignmentPossibilityObject[], + ): AlignmentPossibility[] { + return matchingPossibilities.map((possibility: AlignmentPossibility) => ({ ...possibility, alignmentObjectDtos: possibility.alignmentObjectDtos.filter( (alignmentPossibilityObject: AlignmentPossibilityObject) => filteredObjects.includes(alignmentPossibilityObject), ), })); + } - let concatAlignmentOptionList: AlignmentPossibility[] = - filterValue == '' ? matchingTeams : matchingTeams.concat(alignmentOptionList); - this.filteredAlignmentOptions$.next([...new Set(concatAlignmentOptionList)]); + removeAlignmentObjectWhenAlreadyContainingInMatchingTeam( + alignmentOptionList: AlignmentPossibility[], + matchingTeams: AlignmentPossibility[], + ): AlignmentPossibility[] { + return alignmentOptionList.filter( + (alignmentOption) => + !matchingTeams.some((alignmentPossibility) => alignmentPossibility.teamId === alignmentOption.teamId), + ); } displayWith(value: any) { diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index 17790a00c0..615d82add9 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -647,7 +647,7 @@ export const alignmentObject2: AlignmentPossibilityObject = { export const alignmentObject3: AlignmentPossibilityObject = { objectId: 1, - objectTitle: 'We buy 3 palms', + objectTitle: 'We buy 3 palms puzzle', objectType: 'keyResult', }; From 8f95dd1eecb535e329cd37d87fc0ed6c6e2720e2 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 13 Jun 2024 08:20:14 +0200 Subject: [PATCH 047/119] Use object for alignedEntity --- .../java/ch/puzzle/okr/dto/ObjectiveDto.java | 3 ++- .../okr/dto/alignment/AlignedEntityDto.java | 4 +++ .../ch/puzzle/okr/mapper/ObjectiveMapper.java | 4 +-- .../java/ch/puzzle/okr/models/Objective.java | 21 ++++++++------- .../business/AlignmentBusinessService.java | 19 +++++++------- .../business/ObjectiveBusinessService.java | 20 +++++++------- .../okr/controller/ObjectiveControllerIT.java | 19 +++++++++----- .../AlignmentBusinessServiceTest.java | 26 +++++++++++-------- .../ObjectiveBusinessServiceTest.java | 18 ++++++++----- .../objective-form.component.spec.ts | 14 +++++----- .../objective-form.component.ts | 22 ++++++++-------- frontend/src/app/shared/testData.ts | 6 ++--- .../src/app/shared/types/model/Objective.ts | 5 +++- 13 files changed, 103 insertions(+), 78 deletions(-) create mode 100644 backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignedEntityDto.java diff --git a/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveDto.java b/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveDto.java index 559ab6336e..e4754b7bef 100644 --- a/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveDto.java +++ b/backend/src/main/java/ch/puzzle/okr/dto/ObjectiveDto.java @@ -1,10 +1,11 @@ package ch.puzzle.okr.dto; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import ch.puzzle.okr.models.State; import java.time.LocalDateTime; public record ObjectiveDto(Long id, int version, String title, Long teamId, Long quarterId, String quarterLabel, String description, State state, LocalDateTime createdOn, LocalDateTime modifiedOn, boolean writeable, - String alignedEntityId) { + AlignedEntityDto alignedEntity) { } diff --git a/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignedEntityDto.java b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignedEntityDto.java new file mode 100644 index 0000000000..9a4b94b86d --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignedEntityDto.java @@ -0,0 +1,4 @@ +package ch.puzzle.okr.dto.alignment; + +public record AlignedEntityDto(Long id, String type) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java b/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java index 74e4208311..cd340040f9 100644 --- a/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java +++ b/backend/src/main/java/ch/puzzle/okr/mapper/ObjectiveMapper.java @@ -25,7 +25,7 @@ public ObjectiveDto toDto(Objective objective) { return new ObjectiveDto(objective.getId(), objective.getVersion(), objective.getTitle(), objective.getTeam().getId(), objective.getQuarter().getId(), objective.getQuarter().getLabel(), objective.getDescription(), objective.getState(), objective.getCreatedOn(), objective.getModifiedOn(), - objective.isWriteable(), objective.getAlignedEntityId()); + objective.isWriteable(), objective.getAlignedEntity()); } public Objective toObjective(ObjectiveDto objectiveDto) { @@ -34,6 +34,6 @@ public Objective toObjective(ObjectiveDto objectiveDto) { .withDescription(objectiveDto.description()).withModifiedOn(LocalDateTime.now()) .withState(objectiveDto.state()).withCreatedOn(objectiveDto.createdOn()) .withQuarter(quarterBusinessService.getQuarterById(objectiveDto.quarterId())) - .withAlignedEntityId(objectiveDto.alignedEntityId()).build(); + .withAlignedEntity(objectiveDto.alignedEntity()).build(); } } diff --git a/backend/src/main/java/ch/puzzle/okr/models/Objective.java b/backend/src/main/java/ch/puzzle/okr/models/Objective.java index 1a174a73e4..caff1e0395 100644 --- a/backend/src/main/java/ch/puzzle/okr/models/Objective.java +++ b/backend/src/main/java/ch/puzzle/okr/models/Objective.java @@ -1,5 +1,6 @@ package ch.puzzle.okr.models; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import jakarta.persistence.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -52,7 +53,7 @@ public class Objective implements WriteableInterface { private User modifiedBy; private transient boolean writeable; - private transient String alignedEntityId; + private transient AlignedEntityDto alignedEntity; public Objective() { } @@ -69,7 +70,7 @@ private Objective(Builder builder) { setState(builder.state); setCreatedOn(builder.createdOn); setModifiedBy(builder.modifiedBy); - setAlignedEntityId(builder.alignedEntityId); + setAlignedEntity(builder.alignedEntity); } public Long getId() { @@ -162,12 +163,12 @@ public void setWriteable(boolean writeable) { this.writeable = writeable; } - public String getAlignedEntityId() { - return alignedEntityId; + public AlignedEntityDto getAlignedEntity() { + return alignedEntity; } - public void setAlignedEntityId(String alignedEntityId) { - this.alignedEntityId = alignedEntityId; + public void setAlignedEntity(AlignedEntityDto alignedEntity) { + this.alignedEntity = alignedEntity; } @Override @@ -175,7 +176,7 @@ public String toString() { return "Objective{" + "id=" + id + ", version=" + version + ", title='" + title + '\'' + ", createdBy=" + createdBy + ", team=" + team + ", quarter=" + quarter + ", description='" + description + '\'' + ", modifiedOn=" + modifiedOn + ", state=" + state + ", createdOn=" + createdOn + ", modifiedBy=" - + modifiedBy + ", writeable=" + writeable + ", alignedEntityId=" + alignedEntityId + '\'' + '}'; + + modifiedBy + ", writeable=" + writeable + ", alignedEntity=" + alignedEntity + '\'' + '}'; } @Override @@ -211,7 +212,7 @@ public static final class Builder { private State state; private LocalDateTime createdOn; private User modifiedBy; - private String alignedEntityId; + private AlignedEntityDto alignedEntity; private Builder() { } @@ -275,8 +276,8 @@ public Builder withModifiedBy(User modifiedBy) { return this; } - public Builder withAlignedEntityId(String alignedEntityId) { - this.alignedEntityId = alignedEntityId; + public Builder withAlignedEntity(AlignedEntityDto alignedEntity) { + this.alignedEntity = alignedEntity; return this; } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index cf554b9fa7..460b6513f2 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -1,6 +1,7 @@ package ch.puzzle.okr.service.business; import ch.puzzle.okr.ErrorKey; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.alignment.Alignment; @@ -34,13 +35,13 @@ public AlignmentBusinessService(AlignmentPersistenceService alignmentPersistence this.keyResultPersistenceService = keyResultPersistenceService; } - public String getTargetIdByAlignedObjectiveId(Long alignedObjectiveId) { + public AlignedEntityDto getTargetIdByAlignedObjectiveId(Long alignedObjectiveId) { alignmentValidationService.validateOnGet(alignedObjectiveId); Alignment alignment = alignmentPersistenceService.findByAlignedObjectiveId(alignedObjectiveId); if (alignment instanceof KeyResultAlignment keyResultAlignment) { - return "K" + keyResultAlignment.getAlignmentTarget().getId(); + return new AlignedEntityDto(keyResultAlignment.getAlignmentTarget().getId(), "keyResult"); } else if (alignment instanceof ObjectiveAlignment objectiveAlignment) { - return "O" + objectiveAlignment.getAlignmentTarget().getId(); + return new AlignedEntityDto(objectiveAlignment.getAlignmentTarget().getId(), "objective"); } else { return null; } @@ -63,7 +64,7 @@ public void updateEntity(Long objectiveId, Objective objective) { } private void handleExistingAlignment(Objective objective, Alignment savedAlignment) { - if (objective.getAlignedEntityId() == null) { + if (objective.getAlignedEntity() == null) { validateAndDeleteAlignmentById(savedAlignment.getId()); } else { validateAndUpdateAlignment(objective, savedAlignment); @@ -87,23 +88,23 @@ private void updateAlignment(Alignment savedAlignment, Alignment alignment) { } public Alignment buildAlignmentModel(Objective alignedObjective, int version) { - if (alignedObjective.getAlignedEntityId().startsWith("O")) { - Long entityId = Long.valueOf(alignedObjective.getAlignedEntityId().replace("O", "")); + if (alignedObjective.getAlignedEntity().type().equals("objective")) { + Long entityId = alignedObjective.getAlignedEntity().id(); Objective targetObjective = objectivePersistenceService.findById(entityId); return ObjectiveAlignment.Builder.builder() // .withAlignedObjective(alignedObjective) // .withTargetObjective(targetObjective) // .withVersion(version).build(); - } else if (alignedObjective.getAlignedEntityId().startsWith("K")) { - Long entityId = Long.valueOf(alignedObjective.getAlignedEntityId().replace("K", "")); + } else if (alignedObjective.getAlignedEntity().type().equals("keyResult")) { + Long entityId = alignedObjective.getAlignedEntity().id(); KeyResult targetKeyResult = keyResultPersistenceService.findById(entityId); return KeyResultAlignment.Builder.builder().withAlignedObjective(alignedObjective) .withTargetKeyResult(targetKeyResult).withVersion(version).build(); } else { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NOT_SET, - List.of("alignedEntityId", alignedObjective.getAlignedEntityId())); + List.of("alignedEntity", alignedObjective.getAlignedEntity())); } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java index 4244d5eb99..7157f6ef85 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java @@ -2,6 +2,7 @@ import ch.puzzle.okr.dto.AlignmentDto; import ch.puzzle.okr.dto.AlignmentObjectDto; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.Team; import ch.puzzle.okr.models.authorization.AuthorizationUser; @@ -46,8 +47,8 @@ public Objective getEntityById(Long id) { validator.validateOnGet(id); Objective objective = objectivePersistenceService.findById(id); - String alignmentId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(objective.getId()); - objective.setAlignedEntityId(alignmentId); + AlignedEntityDto alignedEntity = alignmentBusinessService.getTargetIdByAlignedObjectiveId(objective.getId()); + objective.setAlignedEntity(alignedEntity); return objective; } @@ -101,8 +102,9 @@ public List getEntitiesByTeamId(Long id) { List objectiveList = objectivePersistenceService.findObjectiveByTeamId(id); objectiveList.forEach(objective -> { - String alignmentId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(objective.getId()); - objective.setAlignedEntityId(alignmentId); + AlignedEntityDto alignedEntity = alignmentBusinessService + .getTargetIdByAlignedObjectiveId(objective.getId()); + objective.setAlignedEntity(alignedEntity); }); return objectiveList; @@ -123,10 +125,10 @@ public Objective updateEntity(Long id, Objective objective, AuthorizationUser au logger.debug("quarter has changed and is{}changeable, {}", not, objective); validator.validateOnUpdate(id, objective); savedObjective = objectivePersistenceService.save(objective); - String targetAlignmentId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(savedObjective.getId()); - if ((objective.getAlignedEntityId() != null) - || objective.getAlignedEntityId() == null && targetAlignmentId != null) { - savedObjective.setAlignedEntityId(objective.getAlignedEntityId()); + AlignedEntityDto alignedEntity = alignmentBusinessService + .getTargetIdByAlignedObjectiveId(savedObjective.getId()); + if ((objective.getAlignedEntity() != null) || objective.getAlignedEntity() == null && alignedEntity != null) { + savedObjective.setAlignedEntity(objective.getAlignedEntity()); alignmentBusinessService.updateEntity(id, savedObjective); } return savedObjective; @@ -156,7 +158,7 @@ public Objective createEntity(Objective objective, AuthorizationUser authorizati objective.setCreatedOn(LocalDateTime.now()); validator.validateOnCreate(objective); Objective savedObjective = objectivePersistenceService.save(objective); - if (objective.getAlignedEntityId() != null) { + if (objective.getAlignedEntity() != null) { alignmentBusinessService.createEntity(savedObjective); } return savedObjective; diff --git a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java index 2143d06632..0e763ff779 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java @@ -3,6 +3,7 @@ import ch.puzzle.okr.dto.AlignmentDto; import ch.puzzle.okr.dto.AlignmentObjectDto; import ch.puzzle.okr.dto.ObjectiveDto; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import ch.puzzle.okr.mapper.ObjectiveMapper; import ch.puzzle.okr.models.*; import ch.puzzle.okr.service.authorization.AuthorizationService; @@ -41,6 +42,7 @@ class ObjectiveControllerIT { private static final String OBJECTIVE_TITLE_1 = "Objective 1"; private static final String OBJECTIVE_TITLE_2 = "Objective 2"; + private static final String OBJECTIVE = "objective"; private static final String DESCRIPTION = "This is our description"; private static final String EVERYTHING_FINE_DESCRIPTION = "Everything Fine"; private static final String TITLE = "Hunting"; @@ -68,12 +70,13 @@ class ObjectiveControllerIT { } """; private static final String JSON_RESPONSE_NEW_OBJECTIVE = """ - {"id":null,"version":1,"title":"Program Faster","teamId":1,"quarterId":1,"quarterLabel":"GJ 22/23-Q2","description":"Just be faster","state":"DRAFT","createdOn":null,"modifiedOn":null,"writeable":true,"alignedEntityId":null}"""; + {"id":null,"version":1,"title":"Program Faster","teamId":1,"quarterId":1,"quarterLabel":"GJ 22/23-Q2","description":"Just be faster","state":"DRAFT","createdOn":null,"modifiedOn":null,"writeable":true,"alignedEntity":null}"""; private static final String JSON_PATH_TITLE = "$.title"; + private static final AlignedEntityDto alignedEntityDtoObjective = new AlignedEntityDto(42L, OBJECTIVE); private static final Objective objective1 = Objective.Builder.builder().withId(5L).withTitle(OBJECTIVE_TITLE_1) .build(); private static final Objective objectiveAlignment = Objective.Builder.builder().withId(9L) - .withTitle("Objective Alignment").withAlignedEntityId("O42").build(); + .withTitle("Objective Alignment").withAlignedEntity(alignedEntityDtoObjective).build(); private static final Objective objective2 = Objective.Builder.builder().withId(7L).withTitle(OBJECTIVE_TITLE_2) .build(); private static final User user = User.Builder.builder().withId(1L).withFirstname("Bob").withLastname("Kaufmann") @@ -86,12 +89,13 @@ class ObjectiveControllerIT { private static final ObjectiveDto objective1Dto = new ObjectiveDto(5L, 1, OBJECTIVE_TITLE_1, 1L, 1L, "GJ 22/23-Q2", DESCRIPTION, State.DRAFT, LocalDateTime.MAX, LocalDateTime.MAX, true, null); private static final ObjectiveDto objective2Dto = new ObjectiveDto(7L, 1, OBJECTIVE_TITLE_2, 1L, 1L, "GJ 22/23-Q2", - DESCRIPTION, State.DRAFT, LocalDateTime.MIN, LocalDateTime.MIN, true, "O5"); + DESCRIPTION, State.DRAFT, LocalDateTime.MIN, LocalDateTime.MIN, true, new AlignedEntityDto(5L, OBJECTIVE)); private static final ObjectiveDto objectiveAlignmentDto = new ObjectiveDto(9L, 1, "Objective Alignment", 1L, 1L, - "GJ 22/23-Q2", DESCRIPTION, State.DRAFT, LocalDateTime.MAX, LocalDateTime.MAX, true, "O42"); + "GJ 22/23-Q2", DESCRIPTION, State.DRAFT, LocalDateTime.MAX, LocalDateTime.MAX, true, + alignedEntityDtoObjective); private static final AlignmentObjectDto alignmentObject1 = new AlignmentObjectDto(3L, "KR Title 1", "keyResult"); private static final AlignmentObjectDto alignmentObject2 = new AlignmentObjectDto(1L, "Objective Title 1", - "objective"); + OBJECTIVE); private static final AlignmentDto alignmentPossibilities = new AlignmentDto(1L, TEAM_PUZZLE, List.of(alignmentObject1, alignmentObject2)); @@ -127,7 +131,8 @@ void getObjectiveByIdWithAlignmentId() throws Exception { mvc.perform(get(URL_BASE_OBJECTIVE + "/9").contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$.id", Is.is(9))) .andExpect(jsonPath(JSON_PATH_TITLE, Is.is("Objective Alignment"))) - .andExpect(jsonPath("$.alignedEntityId", Is.is("O42"))); + .andExpect(jsonPath("$.alignedEntity.id", Is.is(42))) + .andExpect(jsonPath("$.alignedEntity.type", Is.is(OBJECTIVE))); } @Test @@ -152,7 +157,7 @@ void getAlignmentPossibilities() throws Exception { .andExpect(jsonPath("$[0].alignmentObjectDtos[0].objectType", Is.is("keyResult"))) .andExpect(jsonPath("$[0].alignmentObjectDtos[1].objectId", Is.is(1))) .andExpect(jsonPath("$[0].alignmentObjectDtos[1].objectTitle", Is.is("Objective Title 1"))) - .andExpect(jsonPath("$[0].alignmentObjectDtos[1].objectType", Is.is("objective"))); + .andExpect(jsonPath("$[0].alignmentObjectDtos[1].objectType", Is.is(OBJECTIVE))); verify(objectiveAuthorizationService, times(1)).getAlignmentPossibilities(5L); } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java index ae3f688bb0..6737dd466b 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -2,6 +2,7 @@ import ch.puzzle.okr.TestHelper; import ch.puzzle.okr.dto.ErrorDto; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.alignment.Alignment; @@ -43,12 +44,14 @@ class AlignmentBusinessServiceTest { Objective objective1 = Objective.Builder.builder().withId(5L).withTitle("Objective 1").withState(DRAFT).build(); Objective objective2 = Objective.Builder.builder().withId(8L).withTitle("Objective 2").withState(DRAFT).build(); Objective objective3 = Objective.Builder.builder().withId(10L).withTitle("Objective 3").withState(DRAFT).build(); + AlignedEntityDto alignedEntityDtoObjective = new AlignedEntityDto(8L, "objective"); + AlignedEntityDto alignedEntityDtoKeyResult = new AlignedEntityDto(5L, "keyResult"); Objective objectiveAlignedObjective = Objective.Builder.builder().withId(42L).withTitle("Objective 42") - .withState(DRAFT).withAlignedEntityId("O8").build(); + .withState(DRAFT).withAlignedEntity(alignedEntityDtoObjective).build(); Objective keyResultAlignedObjective = Objective.Builder.builder().withId(45L).withTitle("Objective 45") - .withState(DRAFT).withAlignedEntityId("K5").build(); + .withState(DRAFT).withAlignedEntity(alignedEntityDtoKeyResult).build(); Objective wrongAlignedObjective = Objective.Builder.builder().withId(48L).withTitle("Objective 48").withState(DRAFT) - .withAlignedEntityId("Hello").build(); + .withAlignedEntity(new AlignedEntityDto(0L, "Hello")).build(); KeyResult metricKeyResult = KeyResultMetric.Builder.builder().withId(5L).withTitle("KR Title 1").build(); ObjectiveAlignment objectiveALignment = ObjectiveAlignment.Builder.builder().withId(1L) .withAlignedObjective(objective1).withTargetObjective(objective2).build(); @@ -63,10 +66,10 @@ void shouldGetTargetAlignmentIdObjective() { when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(objectiveALignment); // act - String targetId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); + AlignedEntityDto alignedEntity = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); // assert - assertEquals("O8", targetId); + assertEquals(alignedEntityDtoObjective, alignedEntity); } @Test @@ -75,11 +78,11 @@ void shouldReturnNullWhenNoAlignmentFound() { when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(null); // act - String targetId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); + AlignedEntityDto alignedEntity = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); // assert verify(validator, times(1)).validateOnGet(5L); - assertNull(targetId); + assertNull(alignedEntity); } @Test @@ -88,10 +91,10 @@ void shouldGetTargetAlignmentIdKeyResult() { when(alignmentPersistenceService.findByAlignedObjectiveId(5L)).thenReturn(keyResultAlignment); // act - String targetId = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); + AlignedEntityDto alignedEntity = alignmentBusinessService.getTargetIdByAlignedObjectiveId(5L); // assert - assertEquals("K5", targetId); + assertEquals(alignedEntityDtoKeyResult, alignedEntity); } @Test @@ -197,9 +200,10 @@ void shouldBuildAlignmentCorrectKeyResult() { } @Test - void shouldThrowErrorWhenAlignedEntityIdIsIncorrect() { + void shouldThrowErrorWhenAlignedEntityIsIncorrect() { // arrange - List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NOT_SET", List.of("alignedEntityId", "Hello"))); + List expectedErrors = List.of(new ErrorDto("ATTRIBUTE_NOT_SET", + List.of("alignedEntity", new AlignedEntityDto(0L, "Hello").toString()))); // act OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java index 21675c450d..cb352ce339 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java @@ -2,6 +2,7 @@ import ch.puzzle.okr.dto.AlignmentDto; import ch.puzzle.okr.dto.AlignmentObjectDto; +import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.*; import ch.puzzle.okr.models.authorization.AuthorizationUser; @@ -92,6 +93,7 @@ class ObjectiveBusinessServiceTest { "keyResult"); private final AlignmentDto alignmentDto = new AlignmentDto(1L, TEAM_1, List.of(alignmentObjectDto1, alignmentObjectDto2, alignmentObjectDto3, alignmentObjectDto4, alignmentObjectDto5, alignmentObjectDto6)); + AlignedEntityDto alignedEntityDtoObjective = new AlignedEntityDto(53L, OBJECTIVE); @Test void getOneObjective() { @@ -181,16 +183,17 @@ void shouldUpdateAnObjectiveWithAlignment() { // arrange Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) - .withState(DRAFT).withAlignedEntityId("O53").build()); + .withState(DRAFT).withAlignedEntity(alignedEntityDtoObjective).build()); when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); - when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn("O41"); + when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())) + .thenReturn(new AlignedEntityDto(41L, OBJECTIVE)); when(objectivePersistenceService.save(any())).thenReturn(objective); doNothing().when(objective).setCreatedOn(any()); // act Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, authorizationUser); - objective.setAlignedEntityId("O53"); + objective.setAlignedEntity(alignedEntityDtoObjective); // assert verify(objectivePersistenceService, times(1)).save(objective); @@ -205,7 +208,7 @@ void shouldUpdateAnObjectiveWithANewAlignment() { // arrange Objective objective = spy(Objective.Builder.builder().withId(3L).withTitle("Received Objective").withTeam(team1) .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) - .withState(DRAFT).withAlignedEntityId("O53").build()); + .withState(DRAFT).withAlignedEntity(alignedEntityDtoObjective).build()); when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn(null); when(objectivePersistenceService.save(any())).thenReturn(objective); @@ -214,7 +217,7 @@ void shouldUpdateAnObjectiveWithANewAlignment() { // act Objective updatedObjective = objectiveBusinessService.updateEntity(objective.getId(), objective, authorizationUser); - objective.setAlignedEntityId("O53"); + objective.setAlignedEntity(alignedEntityDtoObjective); // assert verify(objectivePersistenceService, times(1)).save(objective); @@ -231,7 +234,8 @@ void shouldUpdateAnObjectiveWithAlignmentDelete() { .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) .withState(DRAFT).build()); when(objectivePersistenceService.findById(anyLong())).thenReturn(objective); - when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())).thenReturn("O52"); + when(alignmentBusinessService.getTargetIdByAlignedObjectiveId(any())) + .thenReturn(new AlignedEntityDto(52L, "objective")); when(objectivePersistenceService.save(any())).thenReturn(objective); doNothing().when(objective).setCreatedOn(any()); @@ -252,7 +256,7 @@ void shouldSaveANewObjectiveWithAlignment() { // arrange Objective objective = spy(Objective.Builder.builder().withTitle("Received Objective").withTeam(team1) .withQuarter(quarter).withDescription("The description").withModifiedOn(null).withModifiedBy(null) - .withState(DRAFT).withAlignedEntityId("O42").build()); + .withState(DRAFT).withAlignedEntity(new AlignedEntityDto(42L, OBJECTIVE)).build()); doNothing().when(objective).setCreatedOn(any()); // act diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index 8587e1bf35..ff916641b3 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -189,7 +189,7 @@ describe('ObjectiveDialogComponent', () => { teamId: 2, title: title, writeable: true, - alignedEntityId: null, + alignedEntity: null, }, teamId: 1, }); @@ -219,7 +219,7 @@ describe('ObjectiveDialogComponent', () => { quarterId: 0, teamId: 0, version: undefined, - alignedEntityId: null, + alignedEntity: null, }); }); @@ -247,7 +247,7 @@ describe('ObjectiveDialogComponent', () => { quarterId: 0, teamId: 0, version: undefined, - alignedEntityId: 'O2', + alignedEntity: { id: 2, type: 'objective' }, }); }); @@ -275,7 +275,7 @@ describe('ObjectiveDialogComponent', () => { quarterId: 0, teamId: 0, version: undefined, - alignedEntityId: 'K1', + alignedEntity: { id: 1, type: 'keyResult' }, }); }); @@ -303,7 +303,7 @@ describe('ObjectiveDialogComponent', () => { quarterId: 1, teamId: 1, version: undefined, - alignedEntityId: null, + alignedEntity: null, }); }); @@ -333,7 +333,7 @@ describe('ObjectiveDialogComponent', () => { quarterId: 1, teamId: 1, version: undefined, - alignedEntityId: 'K1', + alignedEntity: { id: 1, type: 'keyResult' }, }); }); @@ -535,7 +535,7 @@ describe('ObjectiveDialogComponent', () => { }); it('should load existing keyResult alignment to objectiveForm', async () => { - objectiveWithAlignment.alignedEntityId = 'K1'; + objectiveWithAlignment.alignedEntity = { id: 1, type: 'keyResult' }; objectiveService.getAlignmentPossibilities.mockReturnValue(of([alignmentPossibility1, alignmentPossibility2])); component.generateAlignmentPossibilities(3, objectiveWithAlignment, null); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 3998b7a75b..6d7ed9ee03 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -70,8 +70,11 @@ export class ObjectiveFormComponent implements OnInit { const state = this.data.objective.objectiveId == null ? submitType : this.state; let alignment: AlignmentPossibilityObject | null = value.alignment; - let alignmentEntity: string | null = alignment - ? (alignment.objectType == 'objective' ? 'O' : 'K') + alignment.objectId + let alignedEntity: { id: number; type: string } | null = alignment + ? { + id: alignment.objectId, + type: alignment.objectType, + } : null; let objectiveDTO: Objective = { @@ -82,7 +85,7 @@ export class ObjectiveFormComponent implements OnInit { title: value.title, teamId: value.team, state: state, - alignedEntityId: alignmentEntity, + alignedEntity: alignedEntity, } as unknown as Objective; const submitFunction = this.getSubmitFunction(objectiveDTO.id, objectiveDTO); @@ -204,7 +207,7 @@ export class ObjectiveFormComponent implements OnInit { state: 'DRAFT' as State, teamId: 0, quarterId: 0, - alignedEntityId: null, + alignedEntity: null, } as Objective; } @@ -254,15 +257,12 @@ export class ObjectiveFormComponent implements OnInit { } if (objective) { - let alignmentEntity: string | null = objective.alignedEntityId; - if (alignmentEntity) { - let alignmentType: string = alignmentEntity.charAt(0); - let alignmentId: number = parseInt(alignmentEntity.substring(1)); - alignmentType = alignmentType == 'O' ? 'objective' : 'keyResult'; + let alignedEntity: { id: number; type: string } | null = objective.alignedEntity; + if (alignedEntity) { let alignmentPossibilityObject: AlignmentPossibilityObject | null = this.findAlignmentPossibilityObject( alignmentPossibilities, - alignmentId, - alignmentType, + alignedEntity.id, + alignedEntity.type, ); this.objectiveForm.patchValue({ alignment: alignmentPossibilityObject, diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index 615d82add9..ef2872c754 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -366,7 +366,7 @@ export const objective: Objective = { quarterLabel: 'GJ 22/23-Q2', state: State.SUCCESSFUL, writeable: true, - alignedEntityId: null, + alignedEntity: null, }; export const objectiveWithAlignment: Objective = { @@ -379,7 +379,7 @@ export const objectiveWithAlignment: Objective = { quarterLabel: 'GJ 22/23-Q2', state: State.SUCCESSFUL, writeable: true, - alignedEntityId: 'O2', + alignedEntity: { id: 2, type: 'objective' }, }; export const objectiveWriteableFalse: Objective = { @@ -392,7 +392,7 @@ export const objectiveWriteableFalse: Objective = { quarterLabel: 'GJ 22/23-Q2', state: State.NOTSUCCESSFUL, writeable: false, - alignedEntityId: null, + alignedEntity: null, }; export const firstCheckIn: CheckInMin = { diff --git a/frontend/src/app/shared/types/model/Objective.ts b/frontend/src/app/shared/types/model/Objective.ts index 020d829755..c83e313057 100644 --- a/frontend/src/app/shared/types/model/Objective.ts +++ b/frontend/src/app/shared/types/model/Objective.ts @@ -14,5 +14,8 @@ export interface Objective { modifiedOn?: Date; createdBy?: User; writeable: boolean; - alignedEntityId: string | null; + alignedEntity: { + id: number; + type: string; + } | null; } From 798b9ffa34c60b405f6e0bd85bc56e60d172c8bb Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 13 Jun 2024 08:27:07 +0200 Subject: [PATCH 048/119] Rename alignmentObjects --- .../main/java/ch/puzzle/okr/dto/AlignmentDto.java | 2 +- .../okr/controller/ObjectiveControllerIT.java | 12 ++++++------ .../ObjectiveAuthorizationServiceTest.java | 10 +++++----- .../business/ObjectiveBusinessServiceTest.java | 14 +++++++------- .../objective-dialog/objective-form.component.html | 2 +- .../objective-form.component.spec.ts | 8 ++++---- .../objective-dialog/objective-form.component.ts | 11 +++++------ frontend/src/app/shared/testData.ts | 4 ++-- .../app/shared/types/model/AlignmentPossibility.ts | 2 +- 9 files changed, 32 insertions(+), 33 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/dto/AlignmentDto.java b/backend/src/main/java/ch/puzzle/okr/dto/AlignmentDto.java index 16389518ea..3ac7114429 100644 --- a/backend/src/main/java/ch/puzzle/okr/dto/AlignmentDto.java +++ b/backend/src/main/java/ch/puzzle/okr/dto/AlignmentDto.java @@ -2,5 +2,5 @@ import java.util.List; -public record AlignmentDto(Long teamId, String teamName, List alignmentObjectDtos) { +public record AlignmentDto(Long teamId, String teamName, List alignmentObjects) { } diff --git a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java index 0e763ff779..f7c733f9a0 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/ObjectiveControllerIT.java @@ -152,12 +152,12 @@ void getAlignmentPossibilities() throws Exception { mvc.perform(get(URL_BASE_OBJECTIVE + "/alignmentPossibilities/5").contentType(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk()).andExpect(jsonPath("$[0].teamId", Is.is(1))) .andExpect(jsonPath("$[0].teamName", Is.is(TEAM_PUZZLE))) - .andExpect(jsonPath("$[0].alignmentObjectDtos[0].objectId", Is.is(3))) - .andExpect(jsonPath("$[0].alignmentObjectDtos[0].objectTitle", Is.is("KR Title 1"))) - .andExpect(jsonPath("$[0].alignmentObjectDtos[0].objectType", Is.is("keyResult"))) - .andExpect(jsonPath("$[0].alignmentObjectDtos[1].objectId", Is.is(1))) - .andExpect(jsonPath("$[0].alignmentObjectDtos[1].objectTitle", Is.is("Objective Title 1"))) - .andExpect(jsonPath("$[0].alignmentObjectDtos[1].objectType", Is.is(OBJECTIVE))); + .andExpect(jsonPath("$[0].alignmentObjects[0].objectId", Is.is(3))) + .andExpect(jsonPath("$[0].alignmentObjects[0].objectTitle", Is.is("KR Title 1"))) + .andExpect(jsonPath("$[0].alignmentObjects[0].objectType", Is.is("keyResult"))) + .andExpect(jsonPath("$[0].alignmentObjects[1].objectId", Is.is(1))) + .andExpect(jsonPath("$[0].alignmentObjects[1].objectTitle", Is.is("Objective Title 1"))) + .andExpect(jsonPath("$[0].alignmentObjects[1].objectType", Is.is(OBJECTIVE))); verify(objectiveAuthorizationService, times(1)).getAlignmentPossibilities(5L); } diff --git a/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java index 2f3afa034a..6aa9a1ad42 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationServiceTest.java @@ -195,11 +195,11 @@ void getAlignmentPossibilitiesShouldReturnListWhenAuthorized() { // assert assertEquals(TEAM_PUZZLE, alignmentPossibilities.get(0).teamName()); assertEquals(1, alignmentPossibilities.get(0).teamId()); - assertEquals(3, alignmentPossibilities.get(0).alignmentObjectDtos().get(0).objectId()); - assertEquals("KR Title 1", alignmentPossibilities.get(0).alignmentObjectDtos().get(0).objectTitle()); - assertEquals("keyResult", alignmentPossibilities.get(0).alignmentObjectDtos().get(0).objectType()); - assertEquals(1, alignmentPossibilities.get(0).alignmentObjectDtos().get(1).objectId()); - assertEquals("objective", alignmentPossibilities.get(0).alignmentObjectDtos().get(1).objectType()); + assertEquals(3, alignmentPossibilities.get(0).alignmentObjects().get(0).objectId()); + assertEquals("KR Title 1", alignmentPossibilities.get(0).alignmentObjects().get(0).objectTitle()); + assertEquals("keyResult", alignmentPossibilities.get(0).alignmentObjects().get(0).objectType()); + assertEquals(1, alignmentPossibilities.get(0).alignmentObjects().get(1).objectId()); + assertEquals("objective", alignmentPossibilities.get(0).alignmentObjects().get(1).objectType()); } @Test diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java index cb352ce339..36e1304af7 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/ObjectiveBusinessServiceTest.java @@ -404,13 +404,13 @@ void shouldReturnAlignmentPossibilitiesOnlyObjectives() { verify(objectivePersistenceService, times(1)).findObjectiveByQuarterId(5L); verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(1L); verify(keyResultBusinessService, times(1)).getAllKeyResultsByObjective(2L); - assertEquals(2, alignmentsDtos.get(0).alignmentObjectDtos().size()); - assertEquals(1, alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectId()); - assertEquals(FULL_OBJECTIVE_1, alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectTitle()); - assertEquals(OBJECTIVE, alignmentsDtos.get(0).alignmentObjectDtos().get(0).objectType()); - assertEquals(2, alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectId()); - assertEquals(FULL_OBJECTIVE_2, alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectTitle()); - assertEquals(OBJECTIVE, alignmentsDtos.get(0).alignmentObjectDtos().get(1).objectType()); + assertEquals(2, alignmentsDtos.get(0).alignmentObjects().size()); + assertEquals(1, alignmentsDtos.get(0).alignmentObjects().get(0).objectId()); + assertEquals(FULL_OBJECTIVE_1, alignmentsDtos.get(0).alignmentObjects().get(0).objectTitle()); + assertEquals(OBJECTIVE, alignmentsDtos.get(0).alignmentObjects().get(0).objectType()); + assertEquals(2, alignmentsDtos.get(0).alignmentObjects().get(1).objectId()); + assertEquals(FULL_OBJECTIVE_2, alignmentsDtos.get(0).alignmentObjects().get(1).objectTitle()); + assertEquals(OBJECTIVE, alignmentsDtos.get(0).alignmentObjects().get(1).objectType()); } @Test diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html index e88271b068..3dccca69da 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html @@ -87,7 +87,7 @@ @for (group of filteredAlignmentOptions$ | async; track group) { - @for (alignmentObject of group.alignmentObjectDtos; track alignmentObject) { + @for (alignmentObject of group.alignmentObjects; track alignmentObject) { {{ alignmentObject.objectTitle }} } diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index ff916641b3..c117fcb782 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -552,7 +552,7 @@ describe('ObjectiveDialogComponent', () => { let modifiedAlignmentPossibility: AlignmentPossibility = { teamId: 1, teamName: 'Puzzle ITC', - alignmentObjectDtos: [alignmentObject3], + alignmentObjects: [alignmentObject3], }; expect(component.filteredAlignmentOptions$.getValue()).toEqual([modifiedAlignmentPossibility]); @@ -563,7 +563,7 @@ describe('ObjectiveDialogComponent', () => { modifiedAlignmentPossibility = { teamId: 1, teamName: 'Puzzle ITC', - alignmentObjectDtos: [alignmentObject2, alignmentObject3], + alignmentObjects: [alignmentObject2, alignmentObject3], }; expect(component.filteredAlignmentOptions$.getValue()).toEqual([modifiedAlignmentPossibility]); @@ -575,12 +575,12 @@ describe('ObjectiveDialogComponent', () => { { teamId: 1, teamName: 'Puzzle ITC', - alignmentObjectDtos: [alignmentObject3], + alignmentObjects: [alignmentObject3], }, { teamId: 2, teamName: 'We are cube', - alignmentObjectDtos: [alignmentObject1], + alignmentObjects: [alignmentObject1], }, ]; expect(component.filteredAlignmentOptions$.getValue()).toEqual(modifiedAlignmentPossibilities); diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts index 6d7ed9ee03..c881ef4372 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts @@ -281,7 +281,7 @@ export class ObjectiveFormComponent implements OnInit { objectType: string, ): AlignmentPossibilityObject | null { for (let possibility of alignmentPossibilities) { - let foundObject: AlignmentPossibilityObject | undefined = possibility.alignmentObjectDtos.find( + let foundObject: AlignmentPossibilityObject | undefined = possibility.alignmentObjects.find( (alignmentObject: AlignmentPossibilityObject) => alignmentObject.objectId === objectId && alignmentObject.objectType === objectType, ); @@ -329,7 +329,7 @@ export class ObjectiveFormComponent implements OnInit { getMatchingAlignmentPossibilityObjectsByInputFilter(filterValue: string): AlignmentPossibilityObject[] { return this.alignmentPossibilities.flatMap((alignmentPossibility: AlignmentPossibility) => - alignmentPossibility.alignmentObjectDtos.filter((alignmentPossibilityObject: AlignmentPossibilityObject) => + alignmentPossibility.alignmentObjects.filter((alignmentPossibilityObject: AlignmentPossibilityObject) => alignmentPossibilityObject.objectTitle.toLowerCase().includes(filterValue), ), ); @@ -338,7 +338,7 @@ export class ObjectiveFormComponent implements OnInit { getAlignmentPossibilityFromAlignmentObject(filteredObjects: AlignmentPossibilityObject[]): AlignmentPossibility[] { return this.alignmentPossibilities.filter((possibility: AlignmentPossibility) => filteredObjects.some((alignmentPossibilityObject: AlignmentPossibilityObject) => - possibility.alignmentObjectDtos.includes(alignmentPossibilityObject), + possibility.alignmentObjects.includes(alignmentPossibilityObject), ), ); } @@ -349,9 +349,8 @@ export class ObjectiveFormComponent implements OnInit { ): AlignmentPossibility[] { return matchingPossibilities.map((possibility: AlignmentPossibility) => ({ ...possibility, - alignmentObjectDtos: possibility.alignmentObjectDtos.filter( - (alignmentPossibilityObject: AlignmentPossibilityObject) => - filteredObjects.includes(alignmentPossibilityObject), + alignmentObjects: possibility.alignmentObjects.filter((alignmentPossibilityObject: AlignmentPossibilityObject) => + filteredObjects.includes(alignmentPossibilityObject), ), })); } diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index ef2872c754..65dac779cb 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -654,11 +654,11 @@ export const alignmentObject3: AlignmentPossibilityObject = { export const alignmentPossibility1: AlignmentPossibility = { teamId: 1, teamName: 'Puzzle ITC', - alignmentObjectDtos: [alignmentObject2, alignmentObject3], + alignmentObjects: [alignmentObject2, alignmentObject3], }; export const alignmentPossibility2: AlignmentPossibility = { teamId: 2, teamName: 'We are cube', - alignmentObjectDtos: [alignmentObject1], + alignmentObjects: [alignmentObject1], }; diff --git a/frontend/src/app/shared/types/model/AlignmentPossibility.ts b/frontend/src/app/shared/types/model/AlignmentPossibility.ts index 7ca5e7f614..e275888502 100644 --- a/frontend/src/app/shared/types/model/AlignmentPossibility.ts +++ b/frontend/src/app/shared/types/model/AlignmentPossibility.ts @@ -3,5 +3,5 @@ import { AlignmentPossibilityObject } from './AlignmentPossibilityObject'; export interface AlignmentPossibility { teamId: number; teamName: string; - alignmentObjectDtos: AlignmentPossibilityObject[]; + alignmentObjects: AlignmentPossibilityObject[]; } From 8bedc87298c5323a0bdb77d187e23cca3dea2ad6 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 4 Apr 2024 12:45:20 +0200 Subject: [PATCH 049/119] Add new migration with AlignmentView --- .../migration/V2_1_3__createAlignmentView.sql | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql diff --git a/backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql b/backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql new file mode 100644 index 0000000000..06f28a94a8 --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql @@ -0,0 +1,56 @@ +DROP VIEW IF EXISTS alignment_view; +CREATE VIEW alignment_view AS +SELECT + concat(oa.id, coalesce(a.target_objective_id, a.target_key_result_id)) as unique_id, + oa.id as id, + oa.title as title, + ott.id as team_id, + ott.name as team_name, + oa.quarter_id as quarter_id, + oa.state as state, + 'objective' as object_type, + 'source' as connection_item, + coalesce(a.target_objective_id, a.target_key_result_id) as ref_id, + a.alignment_type as ref_type +FROM alignment a + LEFT JOIN objective oa ON oa.id = a.aligned_objective_id + LEFT JOIN team ott ON ott.id = oa.team_id + +UNION + +SELECT + concat(ot.id, a.aligned_objective_id) as unique_id, + ot.id as id, + ot.title as title, + ott.id as team_id, + ott.name as team_name, + ot.quarter_id as quarter_id, + ot.state as state, + 'objective' as object_type, + 'target' as connection_item, + a.aligned_objective_id as ref_id, + 'objective' as ref_type +FROM alignment a + LEFT JOIN objective ot ON ot.id = a.target_objective_id + LEFT JOIN team ott ON ott.id = ot.team_id +where alignment_type = 'objective' + +UNION + +SELECT + concat(krt.id, a.aligned_objective_id) as unique_id, + krt.id as id, + krt.title as title, + ott.id as team_id, + ott.name as team_name, + o.quarter_id as quarter_id, + null as state, + 'keyResult' as object_type, + 'target' as connection_item, + a.aligned_objective_id as ref_id, + 'objective' as ref_type +FROM alignment a + LEFT JOIN key_result krt ON krt.id = a.target_key_result_id + LEFT JOIN objective o ON o.id = krt.objective_id + LEFT JOIN team ott ON ott.id = o.team_id +where alignment_type = 'keyResult'; \ No newline at end of file From 5d6282d395e20a58741660a8c5fef43b3b95f5a4 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 4 Apr 2024 12:45:33 +0200 Subject: [PATCH 050/119] Add new model AlignmentView --- .../okr/models/alignment/AlignmentView.java | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java diff --git a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java new file mode 100644 index 0000000000..520ba22b2b --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java @@ -0,0 +1,212 @@ +package ch.puzzle.okr.models.alignment; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.Immutable; + +@Entity +@Immutable +public class AlignmentView { + + @Id + private Long uniqueId; + private Long id; + private String title; + private Long teamId; + private String teamName; + private Long quarterId; + private String state; + private String objectType; + private String connectionItem; + private Long refId; + private String refType; + + public AlignmentView() { + } + + private AlignmentView(Builder builder) { + setUniqueId(builder.uniqueId); + setId(builder.id); + setTitle(builder.title); + setTeamId(builder.teamId); + setTeamName(builder.teamName); + setQuarterId(builder.quarterId); + setState(builder.state); + setObjectType(builder.objectType); + setConnectionItem(builder.connectionItem); + setRefId(builder.refId); + setRefType(builder.refType); + } + + public Long getUniqueId() { + return uniqueId; + } + + public void setUniqueId(Long uniqueId) { + this.uniqueId = uniqueId; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Long getTeamId() { + return teamId; + } + + public void setTeamId(Long teamId) { + this.teamId = teamId; + } + + public String getTeamName() { + return teamName; + } + + public void setTeamName(String teamName) { + this.teamName = teamName; + } + + public Long getQuarterId() { + return quarterId; + } + + public void setQuarterId(Long quarterId) { + this.quarterId = quarterId; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getObjectType() { + return objectType; + } + + public void setObjectType(String objectType) { + this.objectType = objectType; + } + + public String getConnectionItem() { + return connectionItem; + } + + public void setConnectionItem(String connectionItem) { + this.connectionItem = connectionItem; + } + + public Long getRefId() { + return refId; + } + + public void setRefId(Long refId) { + this.refId = refId; + } + + public String getRefType() { + return refType; + } + + public void setRefType(String refType) { + this.refType = refType; + } + + @Override + public String toString() { + return "AlignmentView{" + "uniqueId=" + uniqueId + ", id=" + id + ", title='" + title + '\'' + ", teamId=" + + teamId + ", teamName='" + teamName + '\'' + ", quarterId=" + quarterId + ", state='" + state + '\'' + + ", objectType='" + objectType + '\'' + ", connectionItem='" + connectionItem + '\'' + ", refId=" + + refId + ", refType='" + refType + '\'' + '}'; + } + + public static final class Builder { + private Long uniqueId; + private Long id; + private String title; + private Long teamId; + private String teamName; + private Long quarterId; + private String state; + private String objectType; + private String connectionItem; + private Long refId; + private String refType; + + public Builder() { + } + + public Builder withUniqueId(Long val) { + uniqueId = val; + return this; + } + + public Builder withId(Long val) { + id = val; + return this; + } + + public Builder withTitle(String val) { + title = val; + return this; + } + + public Builder withTeamId(Long val) { + teamId = val; + return this; + } + + public Builder withTeamName(String val) { + teamName = val; + return this; + } + + public Builder withQuarterId(Long val) { + quarterId = val; + return this; + } + + public Builder withState(String val) { + state = val; + return this; + } + + public Builder withObjectType(String val) { + objectType = val; + return this; + } + + public Builder withConnectionItem(String val) { + connectionItem = val; + return this; + } + + public Builder withRefId(Long val) { + refId = val; + return this; + } + + public Builder withRefType(String val) { + refType = val; + return this; + } + + public AlignmentView build() { + return new AlignmentView(this); + } + } +} From 6347fa9debf8d45eb78935da8d109ee9654b1b68 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 4 Apr 2024 12:46:34 +0200 Subject: [PATCH 051/119] Add Repository and PersistenceService for AlignmentView --- .../main/java/ch/puzzle/okr/Constants.java | 1 + .../repository/AlignmentViewRepository.java | 14 ++++++++++ .../AlignmentViewPersistenceService.java | 26 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 backend/src/main/java/ch/puzzle/okr/repository/AlignmentViewRepository.java create mode 100644 backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceService.java diff --git a/backend/src/main/java/ch/puzzle/okr/Constants.java b/backend/src/main/java/ch/puzzle/okr/Constants.java index 38220a0e91..5dccc96d07 100644 --- a/backend/src/main/java/ch/puzzle/okr/Constants.java +++ b/backend/src/main/java/ch/puzzle/okr/Constants.java @@ -12,6 +12,7 @@ private Constants() { public static final String CHECK_IN = "Check-in"; public static final String ACTION = "Action"; public static final String ALIGNMENT = "Alignment"; + public static final String ALIGNMENT_VIEW = "AlignmentView"; public static final String COMPLETED = "Completed"; public static final String ORGANISATION = "Organisation"; public static final String QUARTER = "Quarter"; diff --git a/backend/src/main/java/ch/puzzle/okr/repository/AlignmentViewRepository.java b/backend/src/main/java/ch/puzzle/okr/repository/AlignmentViewRepository.java new file mode 100644 index 0000000000..b1bd63c4f3 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/repository/AlignmentViewRepository.java @@ -0,0 +1,14 @@ +package ch.puzzle.okr.repository; + +import ch.puzzle.okr.models.alignment.AlignmentView; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface AlignmentViewRepository extends CrudRepository { + + @Query(value = "SELECT * FROM alignment_view where quarter_id = :quarterId ", nativeQuery = true) + List getAlignmentViewByQuarterId(@Param("quarterId") Long quarterId); +} diff --git a/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceService.java b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceService.java new file mode 100644 index 0000000000..74bb0d9038 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceService.java @@ -0,0 +1,26 @@ +package ch.puzzle.okr.service.persistence; + +import ch.puzzle.okr.models.alignment.AlignmentView; +import ch.puzzle.okr.repository.AlignmentViewRepository; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static ch.puzzle.okr.Constants.ALIGNMENT_VIEW; + +@Service +public class AlignmentViewPersistenceService extends PersistenceBase { + + protected AlignmentViewPersistenceService(AlignmentViewRepository repository) { + super(repository); + } + + @Override + public String getModelName() { + return ALIGNMENT_VIEW; + } + + public List getAlignmentViewListByQuarterId(Long quarterId) { + return getRepository().getAlignmentViewByQuarterId(quarterId); + } +} From 44eaaa541ca2d407a822600eb85b8111af05dc51 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 4 Apr 2024 12:49:57 +0200 Subject: [PATCH 052/119] Add validateOnAlignmentGet Method in AlignmentValidationService --- .../service/validation/AlignmentValidationService.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java index 40bbba8ee5..c37461d3b6 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java @@ -101,4 +101,12 @@ private void throwExceptionWhenAlignmentWithAlignedObjectiveAlreadyExists(Alignm List.of("alignedObjectiveId", model.getAlignedObjective().getId())); } } + + public void validateOnAlignmentGet(Long quarterId, List teamFilter) { + if (quarterId == null) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NOT_SET, "quarterId"); + } else if (teamFilter == null || teamFilter.isEmpty()) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NOT_SET, "teamFilter"); + } + } } From 681b433a54b59b727a77a4751c5e07df781bf2a6 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 4 Apr 2024 12:52:37 +0200 Subject: [PATCH 053/119] Use validation in AlignmentBusinessService --- .../business/AlignmentBusinessService.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index 460b6513f2..0cecd4e6d0 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -5,10 +5,12 @@ import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.alignment.Alignment; +import ch.puzzle.okr.models.alignment.AlignmentView; import ch.puzzle.okr.models.alignment.KeyResultAlignment; import ch.puzzle.okr.models.alignment.ObjectiveAlignment; import ch.puzzle.okr.models.keyresult.KeyResult; import ch.puzzle.okr.service.persistence.AlignmentPersistenceService; +import ch.puzzle.okr.service.persistence.AlignmentViewPersistenceService; import ch.puzzle.okr.service.persistence.KeyResultPersistenceService; import ch.puzzle.okr.service.persistence.ObjectivePersistenceService; import ch.puzzle.okr.service.validation.AlignmentValidationService; @@ -24,15 +26,18 @@ public class AlignmentBusinessService { private final AlignmentValidationService alignmentValidationService; private final ObjectivePersistenceService objectivePersistenceService; private final KeyResultPersistenceService keyResultPersistenceService; + private final AlignmentViewPersistenceService alignmentViewPersistenceService; public AlignmentBusinessService(AlignmentPersistenceService alignmentPersistenceService, AlignmentValidationService alignmentValidationService, ObjectivePersistenceService objectivePersistenceService, - KeyResultPersistenceService keyResultPersistenceService) { + KeyResultPersistenceService keyResultPersistenceService, + AlignmentViewPersistenceService alignmentViewPersistenceService) { this.alignmentPersistenceService = alignmentPersistenceService; this.alignmentValidationService = alignmentValidationService; this.objectivePersistenceService = objectivePersistenceService; this.keyResultPersistenceService = keyResultPersistenceService; + this.alignmentViewPersistenceService = alignmentViewPersistenceService; } public AlignedEntityDto getTargetIdByAlignedObjectiveId(Long alignedObjectiveId) { @@ -145,4 +150,15 @@ private void validateAndDeleteAlignmentById(Long alignmentId) { alignmentValidationService.validateOnDelete(alignmentId); alignmentPersistenceService.deleteById(alignmentId); } + + public List getAlignmentsByFilters(Long quarterFilter, List teamFilter, + String objectiveFilter) { + alignmentValidationService.validateOnAlignmentGet(quarterFilter, teamFilter); + + List alignmentViewList = alignmentViewPersistenceService + .getAlignmentViewListByQuarterId(quarterFilter); + + return alignmentViewList; + + } } From 89625d32cfc2ec80ade1b7d1d50cb1a5a64ee841 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 4 Apr 2024 13:47:08 +0200 Subject: [PATCH 054/119] Filter AlignmentViews which are not matching filter --- .../business/AlignmentBusinessService.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index 0cecd4e6d0..2ea6083c45 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -14,10 +14,14 @@ import ch.puzzle.okr.service.persistence.KeyResultPersistenceService; import ch.puzzle.okr.service.persistence.ObjectivePersistenceService; import ch.puzzle.okr.service.validation.AlignmentValidationService; +import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.Optional; @Service public class AlignmentBusinessService { @@ -40,6 +44,10 @@ public AlignmentBusinessService(AlignmentPersistenceService alignmentPersistence this.alignmentViewPersistenceService = alignmentViewPersistenceService; } + protected record DividedAlignmentViewLists(List correctAlignments, + List wrongAlignments) { + } + public AlignedEntityDto getTargetIdByAlignedObjectiveId(Long alignedObjectiveId) { alignmentValidationService.validateOnGet(alignedObjectiveId); Alignment alignment = alignmentPersistenceService.findByAlignedObjectiveId(alignedObjectiveId); @@ -157,8 +165,28 @@ public List getAlignmentsByFilters(Long quarterFilter, List List alignmentViewList = alignmentViewPersistenceService .getAlignmentViewListByQuarterId(quarterFilter); + DividedAlignmentViewLists dividedAlignmentViewLists = filterAlignmentViews(alignmentViewList, teamFilter, + objectiveFilter); return alignmentViewList; } + + protected DividedAlignmentViewLists filterAlignmentViews(List alignmentViewList, + List teamFilter, String objectiveFilter) { + List filteredList = alignmentViewList.stream() + .filter(alignmentView -> teamFilter.contains(alignmentView.getTeamId())).toList(); + + boolean isObjectiveFilterDefined = StringUtils.isNotBlank(objectiveFilter); + if (isObjectiveFilterDefined) { + filteredList = filteredList.stream() + .filter(alignmentView -> Objects.equals(alignmentView.getObjectType(), "objective") + && alignmentView.getTitle().toLowerCase().contains(objectiveFilter.toLowerCase())) + .toList(); + } + List correctAlignments = filteredList; + List wrongAlignments = alignmentViewList.stream() + .filter(alignmentView -> !correctAlignments.contains(alignmentView)).toList(); + return new DividedAlignmentViewLists(correctAlignments, wrongAlignments); + } } From 7525a2eb0766f78b557a584afb56fdc53b989915 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 4 Apr 2024 14:25:12 +0200 Subject: [PATCH 055/119] Add alignment data for local development --- .../V2_0_99__newQuarterData.sql | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql b/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql index bbaff68591..544ccaf08a 100644 --- a/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql +++ b/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql @@ -33,7 +33,14 @@ values (19, 1, 'Lorem Ipsum sit amet diri guru humu saguri alam apmach helum di (22, 1, 'Lorem Ipsum sit amet diri guru humu saguri alam apmach helum di gau', '2023-10-02 13:07:56.000000', 'Wing Wang Tala Tala Ting Tang', 1, 7, 6, 'DRAFT', null, '2023-10-02 09:08:40.000000'), (21, 1, 'Lorem Ipsum sit amet diri guru humu saguri alam apmach helum di gau', '2023-10-02 13:07:09.000000', - 'Ting Tang Wala Wala Bing Bang', 1, 7, 6, 'DRAFT', null, '2023-10-02 09:07:39.000000'); + 'Ting Tang Wala Wala Bing Bang', 1, 7, 6, 'DRAFT', null, '2023-10-02 09:07:39.000000'), + (40,1,'', '2024-04-04 13:45:13.000000','Wir wollen eine gute Mitarbeiterzufriedenheit.', 1, 1, 5, 'ONGOING', null,'2024-04-04 13:44:52.000000'), + (41,1,'','2024-04-04 13:59:06.511620','Das Projekt generiert 10000 CHF Umsatz',1,1,5,'ONGOING',null,'2024-04-04 13:59:06.523496'), + (42,1,'','2024-04-04 13:59:40.835896','Die Lehrlinge sollen Freude haben',1,1,4,'ONGOING',null,'2024-04-04 13:59:40.848992'), + (43,1,'','2024-04-04 14:00:05.586152','Der Firmenumsatz steigt',1,1,5,'ONGOING',null,'2024-04-04 14:00:05.588509'), + (44,1,'','2024-04-04 14:00:28.221906','Die Members sollen gerne zur Arbeit kommen',1,1,6,'ONGOING',null,'2024-04-04 14:00:28.229058'), + (45,1,'','2024-04-04 14:00:47.659884','Unsere Designer äussern sich zufrieden',1,1,8,'ONGOING',null,'2024-04-04 14:00:47.664414'), + (46,1,'','2024-04-04 14:00:57.485887','Unsere Designer kommen gerne zur Arbeit',1,1,8,'ONGOING',null,'2024-04-04 14:00:57.494192'); insert into key_result (id, version, baseline, description, modified_on, stretch_goal, title, created_by_id, objective_id, owner_id, unit, key_result_type, created_on, commit_zone, target_zone, @@ -123,7 +130,9 @@ values (20, 1, 0, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lore', '2023-10-02 13:15:22.000000', null, 'Clap of thunder bilge aft log crows nest landlubber or just lubber overhaul', 1, 11, 1, '', 'ordinal', - '2023-10-02 09:16:07.000000', 'This is the commit zone', 'This is the target zone', 'This is the stretch zone'); + '2023-10-02 09:16:07.000000', 'This is the commit zone', 'This is the target zone', 'This is the stretch zone'), + (40,1,50,'',null,70,'60% sind in der Membersumfrage zufrienden',1,40,1,'PERCENT','metric','2024-04-04 14:06:21.689768',null,null,null), + (41,1,20000,'',null,80000,'Wir erreichen einen Umsatz von 70000 CHF',1,46,1,'CHF','metric','2024-04-04 14:06:42.100353',null,null,null); insert into check_in (id, version, change_info, created_on, initiatives, modified_on, value_metric, created_by_id, key_result_id, @@ -171,7 +180,8 @@ values (21, 1, null, 1, 30, 2, 'ordinal', 'FAIL'), (32, 1, 'Lorem ipsum dolor sit amet, richi rogsi brokilon', '2023-10-02 08:50:44.059000', ' sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat', '2023-10-02 22:00:00.000000', - 13, 1, 31, 3, 'metric', null); + 13, 1, 31, 3, 'metric', null), + (40,1,'','2024-04-04 14:10:33.377726','','2024-04-04 14:10:33.377739',30000,1,41,7,'metric',null); insert into quarter (id, label, start_date, end_date) values (8, 'GJ 23/24-Q3', '2024-01-01', '2024-03-31'); @@ -190,4 +200,18 @@ insert into completed (id, version, objective_id, comment) values (1, 1, 15, 'Not successful because there were many events this month'), (2, 1, 19, 'Was not successful because we were too slow'), (3, 1, 18, 'Sadly we had not enough members to complete this objective'), - (4, 1, 20, 'Objective could be completed fast and easy'); \ No newline at end of file + (4, 1, 20, 'Objective could be completed fast and easy'); + +insert into alignment(id, aligned_objective_id, alignment_type, target_key_result_id, target_objective_id, version) +values (1, 4, 'objective', null, 6, 0), + (2, 3, 'objective', null, 6, 0), + (3, 8, 'objective', null, 3, 0), + (4, 9, 'keyResult', 8, null, 0), + (5, 10, 'keyResult', 5, null, 0), + (6, 5, 'keyResult', 4, null, 0), + (7, 6, 'keyResult', 3, null, 0), + (8, 41, 'objective', null, 40, 0), + (9, 42, 'objective', null, 40, 0), + (10, 43, 'keyResult', 40, null, 0), + (11, 44, 'objective', null, 42, 0), + (12, 45, 'keyResult', 41, null, 0); From 6d04917b485a0a40a20fd057583af5cb29a52640 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 4 Apr 2024 14:46:28 +0200 Subject: [PATCH 056/119] Add new DTOs to return compact data --- .../puzzle/okr/dto/alignment/AlignmentConnectionDto.java | 5 +++++ .../java/ch/puzzle/okr/dto/alignment/AlignmentLists.java | 7 +++++++ .../ch/puzzle/okr/dto/alignment/AlignmentObjectDto.java | 5 +++++ 3 files changed, 17 insertions(+) create mode 100644 backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentConnectionDto.java create mode 100644 backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentLists.java create mode 100644 backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentObjectDto.java diff --git a/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentConnectionDto.java b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentConnectionDto.java new file mode 100644 index 0000000000..a721058a0a --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentConnectionDto.java @@ -0,0 +1,5 @@ + +package ch.puzzle.okr.dto.alignment; + +public record AlignmentConnectionDto(Long alignedObjectiveId, Long targetObjectiveId, Long targetKeyResultId) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentLists.java b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentLists.java new file mode 100644 index 0000000000..4206fcde23 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentLists.java @@ -0,0 +1,7 @@ +package ch.puzzle.okr.dto.alignment; + +import java.util.List; + +public record AlignmentLists(List alignmentObjectDtoList, + List alignmentConnectionDtoList) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentObjectDto.java b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentObjectDto.java new file mode 100644 index 0000000000..b23c24f1d0 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/alignment/AlignmentObjectDto.java @@ -0,0 +1,5 @@ +package ch.puzzle.okr.dto.alignment; + +public record AlignmentObjectDto(Long objectId, String objectTitle, String objectTeamName, String objectState, + String objectType) { +} From b2b674d0966597a5dbde843d8f284ecca37128c2 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 4 Apr 2024 16:31:18 +0200 Subject: [PATCH 057/119] Add equals and hashCode for AlignmentView --- .../okr/models/alignment/AlignmentView.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java index 520ba22b2b..4ce090ffcb 100644 --- a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java +++ b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java @@ -4,6 +4,8 @@ import jakarta.persistence.Id; import org.hibernate.annotations.Immutable; +import java.util.Objects; + @Entity @Immutable public class AlignmentView { @@ -126,6 +128,27 @@ public void setRefType(String refType) { this.refType = refType; } + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + AlignmentView that = (AlignmentView) o; + return Objects.equals(uniqueId, that.uniqueId) && Objects.equals(id, that.id) + && Objects.equals(title, that.title) && Objects.equals(teamId, that.teamId) + && Objects.equals(teamName, that.teamName) && Objects.equals(quarterId, that.quarterId) + && Objects.equals(state, that.state) && Objects.equals(objectType, that.objectType) + && Objects.equals(connectionItem, that.connectionItem) && Objects.equals(refId, that.refId) + && Objects.equals(refType, that.refType); + } + + @Override + public int hashCode() { + return Objects.hash(uniqueId, id, title, teamId, teamName, quarterId, state, objectType, connectionItem, refId, + refType); + } + @Override public String toString() { return "AlignmentView{" + "uniqueId=" + uniqueId + ", id=" + id + ", title='" + title + '\'' + ", teamId=" From ef19004f488bd34a815440bba31e41ec896394a3 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 4 Apr 2024 16:32:03 +0200 Subject: [PATCH 058/119] Add alignment_view to db schema for testing --- .../V1_0_0__current-db-schema-for-testing.sql | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql index 19f8f23ee7..0960428497 100644 --- a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql +++ b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql @@ -227,3 +227,56 @@ create table if not exists team_organisation constraint fk_team_organisation_team foreign key (team_id) references team ); + +DROP VIEW IF EXISTS ALIGNMENT_VIEW; +CREATE VIEW ALIGNMENT_VIEW AS +SELECT + CONCAT(OA.ID, COALESCE(A.TARGET_OBJECTIVE_ID, A.TARGET_KEY_RESULT_ID)) AS UNIQUE_ID, + OA.ID AS ID, + OA.TITLE AS TITLE, + OTT.ID AS TEAM_ID, + OTT.NAME AS TEAM_NAME, + OA.QUARTER_ID AS QUARTER_ID, + OA.STATE AS STATE, + 'objective' AS OBJECT_TYPE, + 'source' AS CONNECTION_ITEM, + COALESCE(A.TARGET_OBJECTIVE_ID, A.TARGET_KEY_RESULT_ID) AS REF_ID, + A.ALIGNMENT_TYPE AS REF_TYPE +FROM ALIGNMENT A + LEFT JOIN OBJECTIVE OA ON OA.ID = A.ALIGNED_OBJECTIVE_ID + LEFT JOIN TEAM OTT ON OTT.ID = OA.TEAM_ID +UNION +SELECT + CONCAT(OT.ID, A.ALIGNED_OBJECTIVE_ID) AS UNIQUE_ID, + OT.ID AS ID, + OT.TITLE AS TITLE, + OTT.ID AS TEAM_ID, + OTT.NAME AS TEAM_NAME, + OT.QUARTER_ID AS QUARTER_ID, + OT.STATE AS STATE, + 'objective' AS OBJECT_TYPE, + 'target' AS CONNECTION_ITEM, + A.ALIGNED_OBJECTIVE_ID AS REF_ID, + 'objective' AS REF_TYPE +FROM ALIGNMENT A + LEFT JOIN OBJECTIVE OT ON OT.ID = A.TARGET_OBJECTIVE_ID + LEFT JOIN TEAM OTT ON OTT.ID = OT.TEAM_ID +WHERE ALIGNMENT_TYPE = 'objective' +UNION +SELECT + CONCAT(KRT.ID, A.ALIGNED_OBJECTIVE_ID) AS UNIQUE_ID, + KRT.ID AS ID, + KRT.TITLE AS TITLE, + OTT.ID AS TEAM_ID, + OTT.NAME AS TEAM_NAME, + O.QUARTER_ID AS QUARTER_ID, + NULL AS STATE, + 'keyResult' AS OBJECT_TYPE, + 'target' AS CONNECTION_ITEM, + A.ALIGNED_OBJECTIVE_ID AS REF_ID, + 'objective' AS REF_TYPE +FROM ALIGNMENT A + LEFT JOIN KEY_RESULT KRT ON KRT.ID = A.TARGET_KEY_RESULT_ID + LEFT JOIN OBJECTIVE O ON O.ID = KRT.OBJECTIVE_ID + LEFT JOIN TEAM OTT ON OTT.ID = O.TEAM_ID +WHERE ALIGNMENT_TYPE = 'keyResult'; \ No newline at end of file From fb338d5f5c971484bebe1150ffed0161de84cca0 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 5 Apr 2024 08:24:07 +0200 Subject: [PATCH 059/119] Implement function for getting Alignments by filters --- .../business/AlignmentBusinessService.java | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index 2ea6083c45..f19a4e8e4d 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -1,6 +1,9 @@ package ch.puzzle.okr.service.business; import ch.puzzle.okr.ErrorKey; +import ch.puzzle.okr.dto.alignment.AlignmentConnectionDto; +import ch.puzzle.okr.dto.alignment.AlignmentLists; +import ch.puzzle.okr.dto.alignment.AlignmentObjectDto; import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.Objective; @@ -159,17 +162,62 @@ private void validateAndDeleteAlignmentById(Long alignmentId) { alignmentPersistenceService.deleteById(alignmentId); } - public List getAlignmentsByFilters(Long quarterFilter, List teamFilter, - String objectiveFilter) { + public AlignmentLists getAlignmentsByFilters(Long quarterFilter, List teamFilter, String objectiveFilter) { alignmentValidationService.validateOnAlignmentGet(quarterFilter, teamFilter); + List targetAlignmentList = new ArrayList<>(); + List alignmentConnectionDtoList = new ArrayList<>(); + List alignmentObjectDtoList = new ArrayList<>(); + List alignmentViewList = alignmentViewPersistenceService .getAlignmentViewListByQuarterId(quarterFilter); DividedAlignmentViewLists dividedAlignmentViewLists = filterAlignmentViews(alignmentViewList, teamFilter, objectiveFilter); - return alignmentViewList; + List correctAlignments = dividedAlignmentViewLists.correctAlignments(); + List wrongAlignments = dividedAlignmentViewLists.wrongAlignments(); + + // If counterpart of the correct Alignment is in wrongAlignmentList, take it back + correctAlignments.forEach(alignment -> { + Optional matchingObject = wrongAlignments.stream() + .filter(view -> Objects.equals(view.getId(), alignment.getRefId()) + && Objects.equals(view.getObjectType(), alignment.getRefType()) + && Objects.equals(view.getRefId(), alignment.getId())) + .findFirst(); + + if (matchingObject.isPresent()) { + AlignmentView alignmentView = matchingObject.get(); + targetAlignmentList.add(alignmentView); + } + }); + + // Create a new list because correctAlignments has a fixed length and targetAlignmentList can't be added + List finalList = new ArrayList<>(correctAlignments); + if (!targetAlignmentList.isEmpty()) { + finalList.addAll(targetAlignmentList); + } + + // Create ConnectionDtoList for every connection + finalList.forEach(alignmentView -> { + if (Objects.equals(alignmentView.getConnectionItem(), "source")) { + if (Objects.equals(alignmentView.getRefType(), "objective")) { + alignmentConnectionDtoList + .add(new AlignmentConnectionDto(alignmentView.getId(), alignmentView.getRefId(), null)); + } else { + alignmentConnectionDtoList + .add(new AlignmentConnectionDto(alignmentView.getId(), null, alignmentView.getRefId())); + } + } + }); + + finalList.forEach(alignmentView -> alignmentObjectDtoList + .add(new AlignmentObjectDto(alignmentView.getId(), alignmentView.getTitle(), + alignmentView.getTeamName(), alignmentView.getState(), alignmentView.getObjectType()))); + + // Remove duplicated items + List alignmentObjectDtos = alignmentObjectDtoList.stream().distinct().toList(); + return new AlignmentLists(alignmentObjectDtos, alignmentConnectionDtoList); } protected DividedAlignmentViewLists filterAlignmentViews(List alignmentViewList, From ee52cf1fd0bdc723fab8ec498757a92e84d8460b Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 5 Apr 2024 11:40:42 +0200 Subject: [PATCH 060/119] Add cytoscape.js and @types/cytoscape --- frontend/package-lock.json | 28 ++++++++++++++++++++++++++-- frontend/package.json | 2 ++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 62f5abeef5..023d96e1d8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,6 +23,7 @@ "@ngx-translate/http-loader": "^8.0.0", "angular-oauth2-oidc": "^17.0.0", "bootstrap": "^5.3.2", + "cytoscape": "^3.28.1", "moment": "^2.30.1", "ngx-toastr": "^18.0.0", "rxjs": "^7.8.1", @@ -35,6 +36,7 @@ "@angular/compiler-cli": "^17.0.6", "@cypress/schematic": "^2.5.1", "@cypress/skip-test": "^2.6.1", + "@types/cytoscape": "^3.21.0", "@types/jest": "^29.5.11", "cypress": "^13.6.3", "cypress-real-events": "^1.11.0", @@ -5726,6 +5728,12 @@ "@types/node": "*" } }, + "node_modules/@types/cytoscape": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.21.0.tgz", + "integrity": "sha512-RN5SPiyVDpUP+LoOlxxlOYAMzkE7iuv3gA1jt3Hx2qTwArpZVPPdO+SI0hUj49OAn4QABR7JK9Gi0hibzGE0Aw==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.56.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", @@ -8289,6 +8297,18 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/cytoscape": { + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.28.1.tgz", + "integrity": "sha512-xyItz4O/4zp9/239wCcH8ZcFuuZooEeF8KHRmzjDfGdXsj3OG9MFSMA0pJE0uX3uCN/ygof6hHf4L7lst+JaDg==", + "dependencies": { + "heap": "^0.2.6", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -9753,6 +9773,11 @@ "node": ">= 0.4" } }, + "node_modules/heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==" + }, "node_modules/hosted-git-info": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", @@ -12870,8 +12895,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.debounce": { "version": "4.0.8", diff --git a/frontend/package.json b/frontend/package.json index 378772ff9f..8e0c3b5641 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "@ngx-translate/http-loader": "^8.0.0", "angular-oauth2-oidc": "^17.0.0", "bootstrap": "^5.3.2", + "cytoscape": "^3.28.1", "moment": "^2.30.1", "ngx-toastr": "^18.0.0", "rxjs": "^7.8.1", @@ -48,6 +49,7 @@ "@angular/compiler-cli": "^17.0.6", "@cypress/schematic": "^2.5.1", "@cypress/skip-test": "^2.6.1", + "@types/cytoscape": "^3.21.0", "@types/jest": "^29.5.11", "cypress": "^13.6.3", "cypress-real-events": "^1.11.0", From 41828d5613f257572cb3c73c2d15326b8c77beb1 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 5 Apr 2024 11:57:18 +0200 Subject: [PATCH 061/119] Implement tab-switch --- .../src/app/overview/overview.component.html | 50 +++++++++++++++---- .../src/app/overview/overview.component.scss | 43 ++++++++++++++++ .../src/app/overview/overview.component.ts | 10 ++++ 3 files changed, 94 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/overview/overview.component.html b/frontend/src/app/overview/overview.component.html index a8e1e40d7f..e1d67e316a 100644 --- a/frontend/src/app/overview/overview.component.html +++ b/frontend/src/app/overview/overview.component.html @@ -3,14 +3,46 @@
- - -
-

Kein Team ausgewählt

- +
+
+
+ Overview +
+
+ Diagramm +
+
+
+ +
+ + +
+

Kein Team ausgewählt

+ +
+
+ + +

Test

+
diff --git a/frontend/src/app/overview/overview.component.scss b/frontend/src/app/overview/overview.component.scss index 0f59a39431..13615c285c 100644 --- a/frontend/src/app/overview/overview.component.scss +++ b/frontend/src/app/overview/overview.component.scss @@ -13,3 +13,46 @@ .puzzle-logo { filter: invert(38%) sepia(31%) saturate(216%) hue-rotate(167deg) brightness(96%) contrast(85%); } + +.active { + border-left: #909090 1px solid; + border-top: #909090 1px solid; + border-right: #909090 1px solid; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + background: white; + color: black; +} + +.non-active { + color: #9c9c9c; + border-bottom: #909090 1px solid; +} + +.tab-title { + display: flex; + justify-content: center; + align-items: center; + height: 39px; + margin-bottom: 16px; +} + +.buffer { + border-bottom: #909090 1px solid; +} + +.tabfocus { + outline: none; + &:focus-visible { + border-radius: 5px; + border: 2px solid #1a4e83; + } +} + +.overview { + width: 87px; +} + +.diagram { + width: 100px; +} diff --git a/frontend/src/app/overview/overview.component.ts b/frontend/src/app/overview/overview.component.ts index 674346df80..e729777b14 100644 --- a/frontend/src/app/overview/overview.component.ts +++ b/frontend/src/app/overview/overview.component.ts @@ -18,6 +18,7 @@ export class OverviewComponent implements OnInit, OnDestroy { private destroyed$: ReplaySubject = new ReplaySubject(1); hasAdminAccess: ReplaySubject = new ReplaySubject(1); overviewPadding: Subject = new Subject(); + isOverview: boolean = true; constructor( private overviewService: OverviewService, @@ -81,4 +82,13 @@ export class OverviewComponent implements OnInit, OnDestroy { this.destroyed$.next(true); this.destroyed$.complete(); } + + switchPage(input: string) { + if (input == 'diagram') { + this.isOverview = false; + } else { + this.isOverview = true; + this.loadOverviewWithParams(); + } + } } From 08c9670651daa56153123ffa5d3d61acfba9f2b5 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 5 Apr 2024 14:24:21 +0200 Subject: [PATCH 062/119] Adjust uniqueId on AlignmentView for full uniqueness --- .../okr/models/alignment/AlignmentView.java | 18 +++++++++--------- .../migration/V2_1_3__createAlignmentView.sql | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java index 4ce090ffcb..6aab8b426c 100644 --- a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java +++ b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java @@ -11,7 +11,7 @@ public class AlignmentView { @Id - private Long uniqueId; + private String uniqueId; private Long id; private String title; private Long teamId; @@ -40,11 +40,11 @@ private AlignmentView(Builder builder) { setRefType(builder.refType); } - public Long getUniqueId() { + public String getUniqueId() { return uniqueId; } - public void setUniqueId(Long uniqueId) { + public void setUniqueId(String uniqueId) { this.uniqueId = uniqueId; } @@ -151,14 +151,14 @@ public int hashCode() { @Override public String toString() { - return "AlignmentView{" + "uniqueId=" + uniqueId + ", id=" + id + ", title='" + title + '\'' + ", teamId=" - + teamId + ", teamName='" + teamName + '\'' + ", quarterId=" + quarterId + ", state='" + state + '\'' - + ", objectType='" + objectType + '\'' + ", connectionItem='" + connectionItem + '\'' + ", refId=" - + refId + ", refType='" + refType + '\'' + '}'; + return "AlignmentView{" + "uniqueId='" + uniqueId + '\'' + ", id=" + id + ", title='" + title + '\'' + + ", teamId=" + teamId + ", teamName='" + teamName + '\'' + ", quarterId=" + quarterId + ", state='" + + state + '\'' + ", objectType='" + objectType + '\'' + ", connectionItem='" + connectionItem + '\'' + + ", refId=" + refId + ", refType='" + refType + '\'' + '}'; } public static final class Builder { - private Long uniqueId; + private String uniqueId; private Long id; private String title; private Long teamId; @@ -173,7 +173,7 @@ public static final class Builder { public Builder() { } - public Builder withUniqueId(Long val) { + public Builder withUniqueId(String val) { uniqueId = val; return this; } diff --git a/backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql b/backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql index 06f28a94a8..30c3267e38 100644 --- a/backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql +++ b/backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql @@ -1,7 +1,7 @@ DROP VIEW IF EXISTS alignment_view; CREATE VIEW alignment_view AS SELECT - concat(oa.id, coalesce(a.target_objective_id, a.target_key_result_id)) as unique_id, + concat(oa.id, coalesce(a.target_objective_id, a.target_key_result_id),'S',a.alignment_type) as unique_id, oa.id as id, oa.title as title, ott.id as team_id, @@ -19,7 +19,7 @@ FROM alignment a UNION SELECT - concat(ot.id, a.aligned_objective_id) as unique_id, + concat(ot.id, a.aligned_objective_id,'T','objective') as unique_id, ot.id as id, ot.title as title, ott.id as team_id, @@ -38,7 +38,7 @@ where alignment_type = 'objective' UNION SELECT - concat(krt.id, a.aligned_objective_id) as unique_id, + concat(krt.id, a.aligned_objective_id,'T','keyResult') as unique_id, krt.id as id, krt.title as title, ott.id as team_id, From 5cc5ad4c40f83906a5e3d438c5dd1d01dcce3cb5 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 5 Apr 2024 14:29:30 +0200 Subject: [PATCH 063/119] Add AlignmentService in and get alignment data from filters --- .../shared/services/alignment.service.spec.ts | 25 +++++++++++++++++++ .../app/shared/services/alignment.service.ts | 22 ++++++++++++++++ .../shared/types/model/AlignmentConnection.ts | 5 ++++ .../app/shared/types/model/AlignmentLists.ts | 7 ++++++ .../app/shared/types/model/AlignmentObject.ts | 7 ++++++ 5 files changed, 66 insertions(+) create mode 100644 frontend/src/app/shared/services/alignment.service.spec.ts create mode 100644 frontend/src/app/shared/services/alignment.service.ts create mode 100644 frontend/src/app/shared/types/model/AlignmentConnection.ts create mode 100644 frontend/src/app/shared/types/model/AlignmentLists.ts create mode 100644 frontend/src/app/shared/types/model/AlignmentObject.ts diff --git a/frontend/src/app/shared/services/alignment.service.spec.ts b/frontend/src/app/shared/services/alignment.service.spec.ts new file mode 100644 index 0000000000..3a884752d0 --- /dev/null +++ b/frontend/src/app/shared/services/alignment.service.spec.ts @@ -0,0 +1,25 @@ +import { TestBed } from '@angular/core/testing'; + +import { AlignmentService } from './alignment.service'; +import { HttpClientTestingModule } from "@angular/common/http/testing"; +import { HttpClient } from "@angular/common/http"; + +const httpClient = { + get: jest.fn(), +}; + +describe('AlignmentService', () => { + let service: AlignmentService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [{provide: HttpClient, useValue: httpClient}], + }).compileComponents(); + service = TestBed.inject(AlignmentService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/shared/services/alignment.service.ts b/frontend/src/app/shared/services/alignment.service.ts new file mode 100644 index 0000000000..1903f94ea5 --- /dev/null +++ b/frontend/src/app/shared/services/alignment.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { AlignmentLists } from '../types/model/AlignmentLists'; +import { optionalValue } from '../common'; + +@Injectable({ + providedIn: 'root', +}) +export class AlignmentService { + constructor(private httpClient: HttpClient) {} + + getAlignmentByFilter(quarterId?: number, teamIds?: number[], objectiveQuery?: string): Observable { + const params = optionalValue({ + teamFilter: teamIds, + quarterFilter: quarterId, + objectiveQuery: objectiveQuery, + }); + + return this.httpClient.get(`/api/v2/alignments/alignments`, { params: params }); + } +} diff --git a/frontend/src/app/shared/types/model/AlignmentConnection.ts b/frontend/src/app/shared/types/model/AlignmentConnection.ts new file mode 100644 index 0000000000..1e6d5a6305 --- /dev/null +++ b/frontend/src/app/shared/types/model/AlignmentConnection.ts @@ -0,0 +1,5 @@ +export interface AlignmentConnection { + alignedObjectiveId: number; + targetObjectiveId: number | null; + targetKeyResultId: number | null; +} diff --git a/frontend/src/app/shared/types/model/AlignmentLists.ts b/frontend/src/app/shared/types/model/AlignmentLists.ts new file mode 100644 index 0000000000..c5d41d7ef9 --- /dev/null +++ b/frontend/src/app/shared/types/model/AlignmentLists.ts @@ -0,0 +1,7 @@ +import { AlignmentObject } from './AlignmentObject'; +import { AlignmentConnection } from './AlignmentConnection'; + +export interface AlignmentLists { + alignmentObjectDtoList: AlignmentObject[]; + alignmentConnectionDtoList: AlignmentConnection[]; +} diff --git a/frontend/src/app/shared/types/model/AlignmentObject.ts b/frontend/src/app/shared/types/model/AlignmentObject.ts new file mode 100644 index 0000000000..320f5222d7 --- /dev/null +++ b/frontend/src/app/shared/types/model/AlignmentObject.ts @@ -0,0 +1,7 @@ +export interface AlignmentObject { + objectId: number; + objectTitle: string; + objectTeamName: string; + objectState: string | null; + objectType: string; +} From e3e9b2e9ff394f61691ccdf9251f6acced4e0c4b Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 5 Apr 2024 14:30:13 +0200 Subject: [PATCH 064/119] Load alignment data on filter change --- .../src/app/overview/overview.component.ts | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/overview/overview.component.ts b/frontend/src/app/overview/overview.component.ts index e729777b14..6aa16645f6 100644 --- a/frontend/src/app/overview/overview.component.ts +++ b/frontend/src/app/overview/overview.component.ts @@ -5,6 +5,8 @@ import { OverviewService } from '../shared/services/overview.service'; import { ActivatedRoute } from '@angular/router'; import { RefreshDataService } from '../shared/services/refresh-data.service'; import { getQueryString, getValueFromQuery, isMobileDevice, trackByFn } from '../shared/common'; +import { AlignmentService } from '../shared/services/alignment.service'; +import { AlignmentLists } from '../shared/types/model/AlignmentLists'; @Component({ selector: 'app-overview', @@ -14,14 +16,18 @@ import { getQueryString, getValueFromQuery, isMobileDevice, trackByFn } from '.. }) export class OverviewComponent implements OnInit, OnDestroy { overviewEntities$: Subject = new Subject(); + alignmentData$: Subject = new Subject(); + emptyAlignmentList: AlignmentLists = { alignmentObjectDtoList: [], alignmentConnectionDtoList: [] } as AlignmentLists; protected readonly trackByFn = trackByFn; private destroyed$: ReplaySubject = new ReplaySubject(1); hasAdminAccess: ReplaySubject = new ReplaySubject(1); overviewPadding: Subject = new Subject(); isOverview: boolean = true; + service!: OverviewService | AlignmentService; constructor( private overviewService: OverviewService, + private alignmentService: AlignmentService, private refreshDataService: RefreshDataService, private activatedRoute: ActivatedRoute, private changeDetector: ChangeDetectorRef, @@ -64,18 +70,32 @@ export class OverviewComponent implements OnInit, OnDestroy { } loadOverview(quarterId?: number, teamIds?: number[], objectiveQuery?: string) { - this.overviewService - .getOverview(quarterId, teamIds, objectiveQuery) - .pipe( - catchError(() => { - this.loadOverview(); - return EMPTY; - }), - ) - .subscribe((dashboard) => { - this.hasAdminAccess.next(dashboard.adminAccess); - this.overviewEntities$.next(dashboard.overviews); - }); + if (this.isOverview) { + this.overviewService + .getOverview(quarterId, teamIds, objectiveQuery) + .pipe( + catchError(() => { + this.loadOverview(); + return EMPTY; + }), + ) + .subscribe((dashboard) => { + this.hasAdminAccess.next(dashboard.adminAccess); + this.overviewEntities$.next(dashboard.overviews); + }); + } else { + this.alignmentService + .getAlignmentByFilter(quarterId, teamIds, objectiveQuery) + .pipe( + catchError(() => { + this.loadOverview(); + return EMPTY; + }), + ) + .subscribe((alignmentLists) => { + this.alignmentData$.next(alignmentLists); + }); + } } ngOnDestroy(): void { @@ -86,6 +106,7 @@ export class OverviewComponent implements OnInit, OnDestroy { switchPage(input: string) { if (input == 'diagram') { this.isOverview = false; + this.loadOverviewWithParams(); } else { this.isOverview = true; this.loadOverviewWithParams(); From cc1684c495bb89ee41b616c837dbc3d7d92ffac4 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 5 Apr 2024 16:05:28 +0200 Subject: [PATCH 065/119] Add new DiagramComponent and draw draft diagram with real data --- frontend/src/app/app.module.ts | 2 + .../src/app/diagram/diagram.component.html | 1 + .../src/app/diagram/diagram.component.scss | 5 + .../src/app/diagram/diagram.component.spec.ts | 16 +++ frontend/src/app/diagram/diagram.component.ts | 127 ++++++++++++++++++ .../src/app/overview/overview.component.html | 2 +- .../shared/services/alignment.service.spec.ts | 6 +- .../app/shared/services/alignment.service.ts | 2 +- 8 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 frontend/src/app/diagram/diagram.component.html create mode 100644 frontend/src/app/diagram/diagram.component.scss create mode 100644 frontend/src/app/diagram/diagram.component.spec.ts create mode 100644 frontend/src/app/diagram/diagram.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 59dab8cddb..3986b7b0d6 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -72,6 +72,7 @@ import { CdkDrag, CdkDragHandle, CdkDropList } from '@angular/cdk/drag-drop'; import { TeamManagementComponent } from './shared/dialog/team-management/team-management.component'; import { KeyresultDialogComponent } from './shared/dialog/keyresult-dialog/keyresult-dialog.component'; import { CustomizationService } from './shared/services/customization.service'; +import { DiagramComponent } from './diagram/diagram.component'; function initOauthFactory(configService: ConfigService, oauthService: OAuthService) { return async () => { @@ -134,6 +135,7 @@ export const MY_FORMATS = { ActionPlanComponent, TeamManagementComponent, KeyresultDialogComponent, + DiagramComponent, ], imports: [ CommonModule, diff --git a/frontend/src/app/diagram/diagram.component.html b/frontend/src/app/diagram/diagram.component.html new file mode 100644 index 0000000000..1192893b55 --- /dev/null +++ b/frontend/src/app/diagram/diagram.component.html @@ -0,0 +1 @@ +
diff --git a/frontend/src/app/diagram/diagram.component.scss b/frontend/src/app/diagram/diagram.component.scss new file mode 100644 index 0000000000..076021a5ca --- /dev/null +++ b/frontend/src/app/diagram/diagram.component.scss @@ -0,0 +1,5 @@ +#cy { + margin: 30px 0 30px 0; + width: calc(100vw - 60px); + height: calc(100vh - 300px); +} diff --git a/frontend/src/app/diagram/diagram.component.spec.ts b/frontend/src/app/diagram/diagram.component.spec.ts new file mode 100644 index 0000000000..b5b4ee19c8 --- /dev/null +++ b/frontend/src/app/diagram/diagram.component.spec.ts @@ -0,0 +1,16 @@ +import { DiagramComponent } from './diagram.component'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +describe('DiagramComponent', () => { + let component: DiagramComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + fixture = TestBed.createComponent(DiagramComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts new file mode 100644 index 0000000000..a83b87bc82 --- /dev/null +++ b/frontend/src/app/diagram/diagram.component.ts @@ -0,0 +1,127 @@ +import { AfterViewInit, Component, Input } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { AlignmentLists } from '../shared/types/model/AlignmentLists'; +import cytoscape from 'cytoscape'; + +@Component({ + selector: 'app-diagram', + templateUrl: './diagram.component.html', + styleUrl: './diagram.component.scss', +}) +export class DiagramComponent implements AfterViewInit { + private alignmentData$ = new BehaviorSubject({} as AlignmentLists); + cy!: cytoscape.Core; + + @Input() + get alignmentData(): BehaviorSubject { + return this.alignmentData$; + } + + set alignmentData(alignmentData: AlignmentLists) { + this.alignmentData$.next(alignmentData); + } + + ngAfterViewInit() { + this.alignmentData.subscribe((alignmentData: AlignmentLists): void => { + this.generateDiagram(alignmentData); + }); + } + + generateDiagram(alignmentData: AlignmentLists): void { + let alignmentElements: any[] = this.generateElements(alignmentData); + + this.cy = cytoscape({ + container: document.getElementById('cy'), + elements: alignmentElements, + + zoom: 1, + zoomingEnabled: true, + userZoomingEnabled: true, + + style: [ + { + selector: '[id^="Ob"]', + style: { + label: 'data(id)', + height: 160, + width: 160, + }, + }, + { + selector: '[id^="KR"]', + style: { + label: 'data(id)', + height: 120, + width: 120, + }, + }, + { + selector: 'edge', + style: { + width: 1, + 'line-color': '#000000', + 'target-arrow-color': '#000000', + 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier', + }, + }, + ], + + layout: { + name: 'cose', + }, + }); + } + + generateElements(alignmentData: AlignmentLists) { + let elements: any[] = []; + let edges: any[] = []; + alignmentData.alignmentObjectDtoList.forEach((alignmentObject) => { + if (alignmentObject.objectType == 'objective') { + let element = { + data: { + id: 'Ob' + alignmentObject.objectId, + label: alignmentObject.objectTitle, + }, + style: { + // SVG config + }, + }; + elements.push(element); + } else { + let element = { + data: { + id: 'KR' + alignmentObject.objectId, + label: alignmentObject.objectTitle, + }, + style: { + // SVG config + }, + }; + elements.push(element); + } + }); + + alignmentData.alignmentConnectionDtoList.forEach((alignmentConnection) => { + if (alignmentConnection.targetKeyResultId == null) { + let edge = { + data: { + source: 'Ob' + alignmentConnection.alignedObjectiveId, + target: 'Ob' + alignmentConnection.targetObjectiveId, + }, + }; + edges.push(edge); + } else { + let edge = { + data: { + source: 'Ob' + alignmentConnection.alignedObjectiveId, + target: 'KR' + alignmentConnection.targetKeyResultId, + }, + }; + edges.push(edge); + } + }); + + return elements.concat(edges); + } +} diff --git a/frontend/src/app/overview/overview.component.html b/frontend/src/app/overview/overview.component.html index e1d67e316a..c1a57e5bf9 100644 --- a/frontend/src/app/overview/overview.component.html +++ b/frontend/src/app/overview/overview.component.html @@ -43,6 +43,6 @@
-

Test

+
diff --git a/frontend/src/app/shared/services/alignment.service.spec.ts b/frontend/src/app/shared/services/alignment.service.spec.ts index 3a884752d0..e7778cf7b9 100644 --- a/frontend/src/app/shared/services/alignment.service.spec.ts +++ b/frontend/src/app/shared/services/alignment.service.spec.ts @@ -1,8 +1,8 @@ import { TestBed } from '@angular/core/testing'; import { AlignmentService } from './alignment.service'; -import { HttpClientTestingModule } from "@angular/common/http/testing"; -import { HttpClient } from "@angular/common/http"; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { HttpClient } from '@angular/common/http'; const httpClient = { get: jest.fn(), @@ -14,7 +14,7 @@ describe('AlignmentService', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], - providers: [{provide: HttpClient, useValue: httpClient}], + providers: [{ provide: HttpClient, useValue: httpClient }], }).compileComponents(); service = TestBed.inject(AlignmentService); }); diff --git a/frontend/src/app/shared/services/alignment.service.ts b/frontend/src/app/shared/services/alignment.service.ts index 1903f94ea5..36964b8457 100644 --- a/frontend/src/app/shared/services/alignment.service.ts +++ b/frontend/src/app/shared/services/alignment.service.ts @@ -17,6 +17,6 @@ export class AlignmentService { objectiveQuery: objectiveQuery, }); - return this.httpClient.get(`/api/v2/alignments/alignments`, { params: params }); + return this.httpClient.get(`/api/v2/alignments/alignmentLists`, { params: params }); } } From 79457850c9f88d4406d1a435e0886a296dde5f0c Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 5 Apr 2024 16:06:08 +0200 Subject: [PATCH 066/119] Split up method in AlignmentBusinessService and adjust uniqueId for full uniqueness --- .../business/AlignmentBusinessService.java | 62 +++++++++++-------- .../migration/V2_1_3__createAlignmentView.sql | 6 +- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index f19a4e8e4d..7d98f0b0c0 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -165,40 +165,22 @@ private void validateAndDeleteAlignmentById(Long alignmentId) { public AlignmentLists getAlignmentsByFilters(Long quarterFilter, List teamFilter, String objectiveFilter) { alignmentValidationService.validateOnAlignmentGet(quarterFilter, teamFilter); - List targetAlignmentList = new ArrayList<>(); - List alignmentConnectionDtoList = new ArrayList<>(); - List alignmentObjectDtoList = new ArrayList<>(); - List alignmentViewList = alignmentViewPersistenceService .getAlignmentViewListByQuarterId(quarterFilter); DividedAlignmentViewLists dividedAlignmentViewLists = filterAlignmentViews(alignmentViewList, teamFilter, objectiveFilter); - List correctAlignments = dividedAlignmentViewLists.correctAlignments(); - List wrongAlignments = dividedAlignmentViewLists.wrongAlignments(); - - // If counterpart of the correct Alignment is in wrongAlignmentList, take it back - correctAlignments.forEach(alignment -> { - Optional matchingObject = wrongAlignments.stream() - .filter(view -> Objects.equals(view.getId(), alignment.getRefId()) - && Objects.equals(view.getObjectType(), alignment.getRefType()) - && Objects.equals(view.getRefId(), alignment.getId())) - .findFirst(); + List finalList = getAlignmentCounterpart(dividedAlignmentViewLists); - if (matchingObject.isPresent()) { - AlignmentView alignmentView = matchingObject.get(); - targetAlignmentList.add(alignmentView); - } - }); + return generateAlignmentLists(finalList); + } - // Create a new list because correctAlignments has a fixed length and targetAlignmentList can't be added - List finalList = new ArrayList<>(correctAlignments); - if (!targetAlignmentList.isEmpty()) { - finalList.addAll(targetAlignmentList); - } + protected AlignmentLists generateAlignmentLists(List alignmentViewList) { + List alignmentConnectionDtoList = new ArrayList<>(); + List alignmentObjectDtoList = new ArrayList<>(); // Create ConnectionDtoList for every connection - finalList.forEach(alignmentView -> { + alignmentViewList.forEach(alignmentView -> { if (Objects.equals(alignmentView.getConnectionItem(), "source")) { if (Objects.equals(alignmentView.getRefType(), "objective")) { alignmentConnectionDtoList @@ -210,7 +192,7 @@ public AlignmentLists getAlignmentsByFilters(Long quarterFilter, List team } }); - finalList.forEach(alignmentView -> alignmentObjectDtoList + alignmentViewList.forEach(alignmentView -> alignmentObjectDtoList .add(new AlignmentObjectDto(alignmentView.getId(), alignmentView.getTitle(), alignmentView.getTeamName(), alignmentView.getState(), alignmentView.getObjectType()))); @@ -220,6 +202,34 @@ public AlignmentLists getAlignmentsByFilters(Long quarterFilter, List team return new AlignmentLists(alignmentObjectDtos, alignmentConnectionDtoList); } + protected List getAlignmentCounterpart(DividedAlignmentViewLists alignmentViewLists) { + List correctAlignments = alignmentViewLists.correctAlignments(); + List wrongAlignments = alignmentViewLists.wrongAlignments(); + List targetAlignmentList = new ArrayList<>(); + + // If counterpart of the correct Alignment is in wrongAlignmentList, take it back + correctAlignments.forEach(alignment -> { + Optional matchingObject = wrongAlignments.stream() + .filter(view -> Objects.equals(view.getId(), alignment.getRefId()) + && Objects.equals(view.getObjectType(), alignment.getRefType()) + && Objects.equals(view.getRefId(), alignment.getId()) + && !Objects.equals(view.getConnectionItem(), alignment.getConnectionItem())) + .findFirst(); + + if (matchingObject.isPresent()) { + AlignmentView alignmentView = matchingObject.get(); + targetAlignmentList.add(alignmentView); + } + }); + + // Create a new list because correctAlignments has a fixed length and targetAlignmentList can't be added + List finalList = new ArrayList<>(correctAlignments); + if (!targetAlignmentList.isEmpty()) { + finalList.addAll(targetAlignmentList); + } + return finalList; + } + protected DividedAlignmentViewLists filterAlignmentViews(List alignmentViewList, List teamFilter, String objectiveFilter) { List filteredList = alignmentViewList.stream() diff --git a/backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql b/backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql index 30c3267e38..aa037e62b2 100644 --- a/backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql +++ b/backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql @@ -1,7 +1,7 @@ DROP VIEW IF EXISTS alignment_view; CREATE VIEW alignment_view AS SELECT - concat(oa.id, coalesce(a.target_objective_id, a.target_key_result_id),'S',a.alignment_type) as unique_id, + concat(oa.id, coalesce(a.target_objective_id, a.target_key_result_id),'S','objective',a.alignment_type) as unique_id, oa.id as id, oa.title as title, ott.id as team_id, @@ -19,7 +19,7 @@ FROM alignment a UNION SELECT - concat(ot.id, a.aligned_objective_id,'T','objective') as unique_id, + concat(ot.id, a.aligned_objective_id,'T','objective','objective') as unique_id, ot.id as id, ot.title as title, ott.id as team_id, @@ -38,7 +38,7 @@ where alignment_type = 'objective' UNION SELECT - concat(krt.id, a.aligned_objective_id,'T','keyResult') as unique_id, + concat(krt.id, a.aligned_objective_id,'T','keyResult','keyResult') as unique_id, krt.id as id, krt.title as title, ott.id as team_id, From 10f74ccfafbc0355f3534dcfb72f81bec501e56f Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 5 Apr 2024 17:00:08 +0200 Subject: [PATCH 067/119] Remove unused check in wrongAlignments filter --- .../puzzle/okr/service/business/AlignmentBusinessService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index 7d98f0b0c0..3330ecd27c 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -212,8 +212,7 @@ protected List getAlignmentCounterpart(DividedAlignmentViewLists Optional matchingObject = wrongAlignments.stream() .filter(view -> Objects.equals(view.getId(), alignment.getRefId()) && Objects.equals(view.getObjectType(), alignment.getRefType()) - && Objects.equals(view.getRefId(), alignment.getId()) - && !Objects.equals(view.getConnectionItem(), alignment.getConnectionItem())) + && Objects.equals(view.getRefId(), alignment.getId())) .findFirst(); if (matchingObject.isPresent()) { From 2bb79e40c31676258230f7fe39a3b4d0653c359a Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 8 Apr 2024 13:26:53 +0200 Subject: [PATCH 068/119] Add check for right counterpart --- .../puzzle/okr/service/business/AlignmentBusinessService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index 3330ecd27c..3e8660df30 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -212,7 +212,8 @@ protected List getAlignmentCounterpart(DividedAlignmentViewLists Optional matchingObject = wrongAlignments.stream() .filter(view -> Objects.equals(view.getId(), alignment.getRefId()) && Objects.equals(view.getObjectType(), alignment.getRefType()) - && Objects.equals(view.getRefId(), alignment.getId())) + && Objects.equals(view.getRefId(), alignment.getId()) + && Objects.equals(view.getRefType(), alignment.getObjectType())) .findFirst(); if (matchingObject.isPresent()) { From fdc5d24fb3f3b3606661c77d8f66d9548b45030c Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 8 Apr 2024 13:29:54 +0200 Subject: [PATCH 069/119] Add SVG Styling for Diagram Nodes --- frontend/src/app/diagram/diagram.component.ts | 166 ++++++++-- frontend/src/app/diagram/svgGeneration.ts | 286 ++++++++++++++++++ 2 files changed, 427 insertions(+), 25 deletions(-) create mode 100644 frontend/src/app/diagram/svgGeneration.ts diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index a83b87bc82..187a6c7a26 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -1,7 +1,25 @@ import { AfterViewInit, Component, Input } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, forkJoin, map } from 'rxjs'; import { AlignmentLists } from '../shared/types/model/AlignmentLists'; import cytoscape from 'cytoscape'; +import { + generateKeyResultSVG, + generateNeutralKeyResultSVG, + generateObjectiveSVG, + getCommitIcon, + getDraftIcon, + getFailIcon, + getNotSuccessfulIcon, + getOnGoingIcon, + getStretchIcon, + getSuccessfulIcon, + getTargetIcon, +} from './svgGeneration'; +import { KeyresultService } from '../shared/services/keyresult.service'; +import { KeyResult } from '../shared/types/model/KeyResult'; +import { KeyResultMetric } from '../shared/types/model/KeyResultMetric'; +import { calculateCurrentPercentage } from '../shared/common'; +import { KeyResultOrdinal } from '../shared/types/model/KeyResultOrdinal'; @Component({ selector: 'app-diagram', @@ -11,6 +29,10 @@ import cytoscape from 'cytoscape'; export class DiagramComponent implements AfterViewInit { private alignmentData$ = new BehaviorSubject({} as AlignmentLists); cy!: cytoscape.Core; + diagramData: any[] = []; + noDiagramData: boolean = true; + + constructor(private keyResultService: KeyresultService) {} @Input() get alignmentData(): BehaviorSubject { @@ -23,16 +45,15 @@ export class DiagramComponent implements AfterViewInit { ngAfterViewInit() { this.alignmentData.subscribe((alignmentData: AlignmentLists): void => { - this.generateDiagram(alignmentData); + this.diagramData = []; + this.prepareDiagramData(alignmentData); }); } - generateDiagram(alignmentData: AlignmentLists): void { - let alignmentElements: any[] = this.generateElements(alignmentData); - + generateDiagram(): void { this.cy = cytoscape({ container: document.getElementById('cy'), - elements: alignmentElements, + elements: this.diagramData, zoom: 1, zoomingEnabled: true, @@ -42,7 +63,6 @@ export class DiagramComponent implements AfterViewInit { { selector: '[id^="Ob"]', style: { - label: 'data(id)', height: 160, width: 160, }, @@ -50,7 +70,6 @@ export class DiagramComponent implements AfterViewInit { { selector: '[id^="KR"]', style: { - label: 'data(id)', height: 120, width: 120, }, @@ -73,35 +92,91 @@ export class DiagramComponent implements AfterViewInit { }); } - generateElements(alignmentData: AlignmentLists) { - let elements: any[] = []; - let edges: any[] = []; + prepareDiagramData(alignmentData: AlignmentLists): void { + if (alignmentData.alignmentObjectDtoList.length == 0) { + this.diagramData = []; + this.noDiagramData = true; + } else { + this.noDiagramData = false; + this.generateElements(alignmentData); + } + } + + generateElements(alignmentData: AlignmentLists): void { + let observableArray: any[] = []; + let diagramElements: any[] = []; alignmentData.alignmentObjectDtoList.forEach((alignmentObject) => { if (alignmentObject.objectType == 'objective') { + let objectiveTitle = this.replaceUmlauts(alignmentObject.objectTitle); + let teamTitle = this.replaceUmlauts(alignmentObject.objectTeamName); let element = { data: { id: 'Ob' + alignmentObject.objectId, - label: alignmentObject.objectTitle, }, style: { - // SVG config + 'background-image': this.generateObjectiveSVG(objectiveTitle, teamTitle, alignmentObject.objectState!), }, }; - elements.push(element); + diagramElements.push(element); } else { - let element = { - data: { - id: 'KR' + alignmentObject.objectId, - label: alignmentObject.objectTitle, - }, - style: { - // SVG config - }, - }; - elements.push(element); + let observable = this.keyResultService.getFullKeyResult(alignmentObject.objectId).pipe( + map((keyResult: KeyResult) => { + if (keyResult.keyResultType == 'metric') { + let metricKeyResult = keyResult as KeyResultMetric; + let percentage = calculateCurrentPercentage(metricKeyResult); + + let keyResultState: string | undefined; + if (percentage < 30) { + keyResultState = 'FAIL'; + } else if (percentage < 70) { + keyResultState = 'COMMIT'; + } else if (percentage < 100) { + keyResultState = 'TARGET'; + } else if (percentage >= 100) { + keyResultState = 'STRETCH'; + } else { + keyResultState = undefined; + } + let keyResultTitle = this.replaceUmlauts(alignmentObject.objectTitle); + let teamTitle = this.replaceUmlauts(alignmentObject.objectTeamName); + let element = { + data: { + id: 'KR' + alignmentObject.objectId, + }, + style: { + 'background-image': this.generateKeyResultSVG(keyResultTitle, teamTitle, keyResultState), + }, + }; + diagramElements.push(element); + } else { + let ordinalKeyResult = keyResult as KeyResultOrdinal; + let keyResultState: string | undefined = ordinalKeyResult.lastCheckIn?.value.toString(); + + let keyResultTitle = this.replaceUmlauts(alignmentObject.objectTitle); + let teamTitle = this.replaceUmlauts(alignmentObject.objectTeamName); + let element = { + data: { + id: 'KR' + alignmentObject.objectId, + }, + style: { + 'background-image': this.generateKeyResultSVG(keyResultTitle, teamTitle, keyResultState), + }, + }; + diagramElements.push(element); + } + }), + ); + observableArray.push(observable); } }); + forkJoin(observableArray).subscribe(() => { + this.generateConnections(alignmentData, diagramElements); + }); + } + + generateConnections(alignmentData: AlignmentLists, diagramElements: any[]): void { + let edges: any[] = []; alignmentData.alignmentConnectionDtoList.forEach((alignmentConnection) => { if (alignmentConnection.targetKeyResultId == null) { let edge = { @@ -121,7 +196,48 @@ export class DiagramComponent implements AfterViewInit { edges.push(edge); } }); + this.diagramData = diagramElements.concat(edges); + this.generateDiagram(); + } + + replaceUmlauts(text: string): string { + text = text.replace(/\u00c4/g, 'Ae'); + text = text.replace(/\u00e4/g, 'ae'); + text = text.replace(/\u00dc/g, 'Ue'); + text = text.replace(/\u00fc/g, 'ue'); + text = text.replace(/\u00d6/g, 'Oe'); + text = text.replace(/\u00f6/g, 'oe'); + text = text.replace(/\u00df/g, 'ss'); + text = text.replace(/\u00B2/g, '^2'); + text = text.replace(/\u00B3/g, '^3'); + return text; + } + + generateObjectiveSVG(title: string, teamName: string, state: string): string { + switch (state) { + case 'ONGOING': + return generateObjectiveSVG(title, teamName, getOnGoingIcon); + case 'SUCCESSFUL': + return generateObjectiveSVG(title, teamName, getSuccessfulIcon); + case 'NOTSUCCESSFUL': + return generateObjectiveSVG(title, teamName, getNotSuccessfulIcon); + default: + return generateObjectiveSVG(title, teamName, getDraftIcon); + } + } - return elements.concat(edges); + generateKeyResultSVG(title: string, teamName: string, state: string | undefined): string { + switch (state) { + case 'FAIL': + return generateKeyResultSVG(title, teamName, getFailIcon, '#BA3838', 'white'); + case 'COMMIT': + return generateKeyResultSVG(title, teamName, getCommitIcon, '#FFD600', 'black'); + case 'TARGET': + return generateKeyResultSVG(title, teamName, getTargetIcon, '#1E8A29', 'black'); + case 'STRETCH': + return generateKeyResultSVG(title, teamName, getStretchIcon, '#1E5A96', 'white'); + default: + return generateNeutralKeyResultSVG(title, teamName); + } } } diff --git a/frontend/src/app/diagram/svgGeneration.ts b/frontend/src/app/diagram/svgGeneration.ts new file mode 100644 index 0000000000..3d67570fab --- /dev/null +++ b/frontend/src/app/diagram/svgGeneration.ts @@ -0,0 +1,286 @@ +export function generateObjectiveSVG(title: string, teamName: string, iconFunction: any) { + let svg = ` + + + + + + + + + + + + + ${iconFunction} + + + +
+

+ ${title} +

+ +
+
+ + +
+

${teamName}

+
+
+
+`; + + return 'data:image/svg+xml;base64,' + btoa(svg); +} + +export function generateKeyResultSVG( + title: string, + teamName: string, + iconFunction: any, + backgroundColor: any, + fontColor: any, +) { + let svg = ` + + + + + + + + + + + + ${iconFunction} + + +
+

+ ${title} +

+ +
+
+ + +
+

${teamName}

+
+
+
+ `; + + return 'data:image/svg+xml;base64,' + btoa(svg); +} + +export function generateNeutralKeyResultSVG(title: string, teamName: string) { + let svg = ` + + + + + + + + + + + + +
+

+ ${title} +

+ +
+
+ + +
+

${teamName}

+
+
+
+ `; + + return 'data:image/svg+xml;base64,' + btoa(svg); +} + +export function getDraftIcon() { + return ` + + + + + + + + `; +} + +export function getOnGoingIcon() { + return ` + + + + + + + + `; +} + +export function getSuccessfulIcon() { + return ` + + + + + + + + `; +} + +export function getNotSuccessfulIcon() { + return ` + + + + + + + + `; +} + +export function getFailIcon() { + return ` + + + + + + + + + + + + + + + +`; +} + +export function getCommitIcon() { + return ` + + + + + + + + + + + + + + + +`; +} + +export function getTargetIcon() { + return ` + + + + + + + + + + + + + + + +`; +} + +export function getStretchIcon() { + return ` + + + + + + + + + + + + + + + +`; +} From fb7a32922c6d0ed242e58d2fdabeeee4d0b19134 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 8 Apr 2024 13:30:16 +0200 Subject: [PATCH 070/119] Add user information when no alignments --- frontend/src/app/diagram/diagram.component.html | 7 ++++++- frontend/src/app/diagram/diagram.component.scss | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/diagram/diagram.component.html b/frontend/src/app/diagram/diagram.component.html index 1192893b55..570dbee884 100644 --- a/frontend/src/app/diagram/diagram.component.html +++ b/frontend/src/app/diagram/diagram.component.html @@ -1 +1,6 @@ -
+
+ +
+

Kein Alignment vorhanden

+ +
diff --git a/frontend/src/app/diagram/diagram.component.scss b/frontend/src/app/diagram/diagram.component.scss index 076021a5ca..025ee23f5c 100644 --- a/frontend/src/app/diagram/diagram.component.scss +++ b/frontend/src/app/diagram/diagram.component.scss @@ -3,3 +3,7 @@ width: calc(100vw - 60px); height: calc(100vh - 300px); } + +.puzzle-logo { + filter: invert(38%) sepia(31%) saturate(216%) hue-rotate(167deg) brightness(96%) contrast(85%); +} From 89510dd0c3e98ddbffcf7b6fac8b8403bd69c238 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 8 Apr 2024 13:52:20 +0200 Subject: [PATCH 071/119] Add diagram click handler --- frontend/src/app/diagram/diagram.component.ts | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index 187a6c7a26..098af0b495 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, Input } from '@angular/core'; +import { AfterViewInit, Component, Input, OnDestroy } from '@angular/core'; import { BehaviorSubject, forkJoin, map } from 'rxjs'; import { AlignmentLists } from '../shared/types/model/AlignmentLists'; import cytoscape from 'cytoscape'; @@ -20,19 +20,23 @@ import { KeyResult } from '../shared/types/model/KeyResult'; import { KeyResultMetric } from '../shared/types/model/KeyResultMetric'; import { calculateCurrentPercentage } from '../shared/common'; import { KeyResultOrdinal } from '../shared/types/model/KeyResultOrdinal'; +import { Router } from '@angular/router'; @Component({ selector: 'app-diagram', templateUrl: './diagram.component.html', styleUrl: './diagram.component.scss', }) -export class DiagramComponent implements AfterViewInit { +export class DiagramComponent implements AfterViewInit, OnDestroy { private alignmentData$ = new BehaviorSubject({} as AlignmentLists); cy!: cytoscape.Core; diagramData: any[] = []; noDiagramData: boolean = true; - constructor(private keyResultService: KeyresultService) {} + constructor( + private keyResultService: KeyresultService, + private router: Router, + ) {} @Input() get alignmentData(): BehaviorSubject { @@ -50,6 +54,14 @@ export class DiagramComponent implements AfterViewInit { }); } + ngOnDestroy() { + if (this.cy) { + this.cy.edges().remove(); + this.cy.nodes().remove(); + this.cy.removeAllListeners(); + } + } + generateDiagram(): void { this.cy = cytoscape({ container: document.getElementById('cy'), @@ -90,6 +102,30 @@ export class DiagramComponent implements AfterViewInit { name: 'cose', }, }); + + this.cy.on('tap', 'node', (evt: cytoscape.EventObject) => { + let node = evt.target; + node.style({ + 'border-width': 0, + }); + + let type: string = node.id().charAt(0) == 'O' ? 'objective' : 'keyresult'; + this.router.navigate([type.toLowerCase(), node.id().substring(2)]); + }); + + this.cy.on('mouseover', 'node', (evt: cytoscape.EventObject) => { + let node = evt.target; + node.style({ + 'border-color': '#1E5A96', + 'border-width': 2, + }); + }); + + this.cy.on('mouseout', 'node', (evt: cytoscape.EventObject) => { + evt.target.style({ + 'border-width': 0, + }); + }); } prepareDiagramData(alignmentData: AlignmentLists): void { From 91e704fbf852ca13342b1c454d10aec79da8463c Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Tue, 9 Apr 2024 09:26:30 +0200 Subject: [PATCH 072/119] Add missing quarter and move testdata to current quarter --- .../V2_0_99__newQuarterData.sql | 14 ++++---- .../h2-db/data-test-h2/V100_0_0__TestData.sql | 34 +++++++++++++++---- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql b/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql index 544ccaf08a..8fdaec29ab 100644 --- a/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql +++ b/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql @@ -34,13 +34,13 @@ values (19, 1, 'Lorem Ipsum sit amet diri guru humu saguri alam apmach helum di 'Wing Wang Tala Tala Ting Tang', 1, 7, 6, 'DRAFT', null, '2023-10-02 09:08:40.000000'), (21, 1, 'Lorem Ipsum sit amet diri guru humu saguri alam apmach helum di gau', '2023-10-02 13:07:09.000000', 'Ting Tang Wala Wala Bing Bang', 1, 7, 6, 'DRAFT', null, '2023-10-02 09:07:39.000000'), - (40,1,'', '2024-04-04 13:45:13.000000','Wir wollen eine gute Mitarbeiterzufriedenheit.', 1, 1, 5, 'ONGOING', null,'2024-04-04 13:44:52.000000'), - (41,1,'','2024-04-04 13:59:06.511620','Das Projekt generiert 10000 CHF Umsatz',1,1,5,'ONGOING',null,'2024-04-04 13:59:06.523496'), - (42,1,'','2024-04-04 13:59:40.835896','Die Lehrlinge sollen Freude haben',1,1,4,'ONGOING',null,'2024-04-04 13:59:40.848992'), - (43,1,'','2024-04-04 14:00:05.586152','Der Firmenumsatz steigt',1,1,5,'ONGOING',null,'2024-04-04 14:00:05.588509'), - (44,1,'','2024-04-04 14:00:28.221906','Die Members sollen gerne zur Arbeit kommen',1,1,6,'ONGOING',null,'2024-04-04 14:00:28.229058'), - (45,1,'','2024-04-04 14:00:47.659884','Unsere Designer äussern sich zufrieden',1,1,8,'ONGOING',null,'2024-04-04 14:00:47.664414'), - (46,1,'','2024-04-04 14:00:57.485887','Unsere Designer kommen gerne zur Arbeit',1,1,8,'ONGOING',null,'2024-04-04 14:00:57.494192'); + (40,1,'', '2024-04-04 13:45:13.000000','Wir wollen eine gute Mitarbeiterzufriedenheit.', 1, 9, 5, 'ONGOING', null,'2024-04-04 13:44:52.000000'), + (41,1,'','2024-04-04 13:59:06.511620','Das Projekt generiert 10000 CHF Umsatz',1,9,5,'ONGOING',null,'2024-04-04 13:59:06.523496'), + (42,1,'','2024-04-04 13:59:40.835896','Die Lehrlinge sollen Freude haben',1,9,4,'ONGOING',null,'2024-04-04 13:59:40.848992'), + (43,1,'','2024-04-04 14:00:05.586152','Der Firmenumsatz steigt',1,9,5,'ONGOING',null,'2024-04-04 14:00:05.588509'), + (44,1,'','2024-04-04 14:00:28.221906','Die Members sollen gerne zur Arbeit kommen',1,9,6,'ONGOING',null,'2024-04-04 14:00:28.229058'), + (45,1,'','2024-04-04 14:00:47.659884','Unsere Designer äussern sich zufrieden',1,9,8,'ONGOING',null,'2024-04-04 14:00:47.664414'), + (46,1,'','2024-04-04 14:00:57.485887','Unsere Designer kommen gerne zur Arbeit',1,9,8,'ONGOING',null,'2024-04-04 14:00:57.494192'); insert into key_result (id, version, baseline, description, modified_on, stretch_goal, title, created_by_id, objective_id, owner_id, unit, key_result_type, created_on, commit_zone, target_zone, diff --git a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql index c712d2532c..66c8a2395d 100644 --- a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql +++ b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql @@ -72,7 +72,14 @@ values (4, 1, '', '2023-07-25 08:17:51.309958', 66, 'Build a company culture tha null, '2023-07-25 08:39:45.772126'), (8,1, '', '2023-07-25 08:39:28.175703', 40, 'consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua', - 1, 2, 6, 'ONGOING', null, '2023-07-25 08:39:28.175703'); + 1, 2, 6, 'ONGOING', null, '2023-07-25 08:39:28.175703'), + (40,1,'', '2024-04-04 13:45:13.000000',40,'Wir wollen eine gute Mitarbeiterzufriedenheit.', 1, 9, 5, 'ONGOING', null,'2024-04-04 13:44:52.000000'), + (41,1,'','2024-04-04 13:59:06.511620',40,'Das Projekt generiert 10000 CHF Umsatz',1,9,5,'ONGOING',null,'2024-04-04 13:59:06.523496'), + (42,1,'','2024-04-04 13:59:40.835896',40,'Die Lehrlinge sollen Freude haben',1,9,4,'ONGOING',null,'2024-04-04 13:59:40.848992'), + (43,1,'','2024-04-04 14:00:05.586152',40,'Der Firmenumsatz steigt',1,9,5,'ONGOING',null,'2024-04-04 14:00:05.588509'), + (44,1,'','2024-04-04 14:00:28.221906',40,'Die Members sollen gerne zur Arbeit kommen',1,9,6,'ONGOING',null,'2024-04-04 14:00:28.229058'), + (45,1,'','2024-04-04 14:00:47.659884',40,'Unsere Designer äussern sich zufrieden',1,9,8,'ONGOING',null,'2024-04-04 14:00:47.664414'), + (46,1,'','2024-04-04 14:00:57.485887',40,'Unsere Designer kommen gerne zur Arbeit',1,9,8,'ONGOING',null,'2024-04-04 14:00:57.494192'); insert into key_result (id, version, baseline, description, modified_on, stretch_goal, title, created_by_id, objective_id, owner_id, key_result_type, created_on, unit, commit_zone, target_zone, stretch_zone) @@ -91,7 +98,9 @@ values (10,1, 465, '', '2023-07-25 08:23:02.273028', 60, 'Im Durchschnitt soll (19,1, 50, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lore', '2023-07-25 08:42:56.407125', 1, 'nsetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At ', 1, 8, 1, 'metric', '2023-07-25 08:42:56.407125', 'PERCENT', null, null, null), (17,1, 525, 'asdf', '2023-07-25 08:41:52.844903', 20000000, 'vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lore', 1, 9, 1, 'metric', '2023-07-25 08:41:52.844903', 'PERCENT', null, null, null), (9,1, 100, '', '2023-07-25 08:48:45.825328', 80, 'Die Member des BBT reduzieren Ihre Lautstärke um 20%', 1, 5, 1, 'metric', '2023-07-25 08:48:45.825328', 'PERCENT', null, null, null), - (18,1, 0, 'consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lore', '2023-07-25 08:42:24.779721', 1, 'Lorem', 1, 8, 1, 'metric', '2023-07-25 08:42:24.779721', 'PERCENT', null, null, null); + (18,1, 0, 'consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lore', '2023-07-25 08:42:24.779721', 1, 'Lorem', 1, 8, 1, 'metric', '2023-07-25 08:42:24.779721', 'PERCENT', null, null, null), + (40,1,50,'',null,70,'60% sind in der Membersumfrage zufrienden',1,40,1,'metric','2024-04-04 14:06:21.689768','PERCENT',null,null,null), + (41,1,20000,'',null,80000,'Wir erreichen einen Umsatz von 70000 CHF',1,46,1,'metric','2024-04-04 14:06:42.100353','CHF',null,null,null); insert into check_in (id, version, change_info, created_on, initiatives, modified_on, value_metric, created_by_id, key_result_id, confidence, check_in_type, zone) values (1,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam', '2023-07-25 08:44:13.865976', '', '2023-07-24 22:00:00.000000', 77, 1, 8, 5, 'metric', null), @@ -112,11 +121,24 @@ values (1,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam (17,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-25 08:49:32.030171', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-24 22:00:00.000000', 66.7, 1, 16, 5, 'metric', null), (18,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-25 08:49:56.975649', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-24 22:00:00.000000', 99, 1, 15, 5, 'metric', null), (19,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-25 08:50:19.024254', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-24 22:00:00.000000', 35, 1, 19, 5, 'metric', null), - (20,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-25 08:50:44.059020', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-24 22:00:00.000000', 0.5, 1, 18, 5, 'metric', null); + (20,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-25 08:50:44.059020', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores ', '2023-07-24 22:00:00.000000', 0.5, 1, 18, 5, 'metric', null), + (40,1,'','2024-04-04 14:10:33.377726','','2024-04-04 14:10:33.377739',30000,1,41,7,'metric',null); -insert into alignment (id, version, aligned_objective_id, alignment_type, target_key_result_id, target_objective_id) values - (1,1, 4, 'objective', null, 3), - (2,1, 9, 'keyResult', 8, null); +insert into alignment (id, aligned_objective_id, alignment_type, target_key_result_id, target_objective_id, version) values + (1, 4, 'objective', null, 3, 1), + (2, 9, 'keyResult', 8, null, 1), + (1, 4, 'objective', null, 6, 0), + (2, 3, 'objective', null, 6, 0), + (3, 8, 'objective', null, 3, 0), + (4, 9, 'keyResult', 8, null, 0), + (5, 10, 'keyResult', 5, null, 0), + (6, 5, 'keyResult', 4, null, 0), + (7, 6, 'keyResult', 3, null, 0), + (8, 41, 'objective', null, 40, 0), + (9, 42, 'objective', null, 40, 0), + (10, 43, 'keyResult', 40, null, 0), + (11, 44, 'objective', null, 42, 0), + (12, 45, 'keyResult', 41, null, 0); insert into completed (id, version, objective_id, comment) values (1,1, 4, 'Das hat geklappt'), From 8cb5d7e6899ba5b79a34f9a14bb4c9abc55bbc2e Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Tue, 9 Apr 2024 09:27:18 +0200 Subject: [PATCH 073/119] Fix bug when no KeyResult in AlignmentData --- .../src/app/diagram/diagram.component.html | 6 +- frontend/src/app/diagram/diagram.component.ts | 55 +++++++++++-------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/frontend/src/app/diagram/diagram.component.html b/frontend/src/app/diagram/diagram.component.html index 570dbee884..ebba733bc9 100644 --- a/frontend/src/app/diagram/diagram.component.html +++ b/frontend/src/app/diagram/diagram.component.html @@ -1,6 +1,6 @@ -
+
-
+

Kein Alignment vorhanden

- +
diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index 098af0b495..1ec82cf26a 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -1,5 +1,5 @@ import { AfterViewInit, Component, Input, OnDestroy } from '@angular/core'; -import { BehaviorSubject, forkJoin, map } from 'rxjs'; +import { forkJoin, map, Observable, Subject } from 'rxjs'; import { AlignmentLists } from '../shared/types/model/AlignmentLists'; import cytoscape from 'cytoscape'; import { @@ -28,10 +28,10 @@ import { Router } from '@angular/router'; styleUrl: './diagram.component.scss', }) export class DiagramComponent implements AfterViewInit, OnDestroy { - private alignmentData$ = new BehaviorSubject({} as AlignmentLists); + private alignmentData$ = new Subject(); cy!: cytoscape.Core; diagramData: any[] = []; - noDiagramData: boolean = true; + emptyDiagramData: boolean = false; constructor( private keyResultService: KeyresultService, @@ -39,7 +39,7 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { ) {} @Input() - get alignmentData(): BehaviorSubject { + get alignmentData(): Subject { return this.alignmentData$; } @@ -50,16 +50,13 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { ngAfterViewInit() { this.alignmentData.subscribe((alignmentData: AlignmentLists): void => { this.diagramData = []; + this.cleanUpDiagram(); this.prepareDiagramData(alignmentData); }); } ngOnDestroy() { - if (this.cy) { - this.cy.edges().remove(); - this.cy.nodes().remove(); - this.cy.removeAllListeners(); - } + this.cleanUpDiagram(); } generateDiagram(): void { @@ -130,10 +127,9 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { prepareDiagramData(alignmentData: AlignmentLists): void { if (alignmentData.alignmentObjectDtoList.length == 0) { - this.diagramData = []; - this.noDiagramData = true; + this.emptyDiagramData = true; } else { - this.noDiagramData = false; + this.emptyDiagramData = false; this.generateElements(alignmentData); } } @@ -143,17 +139,22 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { let diagramElements: any[] = []; alignmentData.alignmentObjectDtoList.forEach((alignmentObject) => { if (alignmentObject.objectType == 'objective') { - let objectiveTitle = this.replaceUmlauts(alignmentObject.objectTitle); - let teamTitle = this.replaceUmlauts(alignmentObject.objectTeamName); - let element = { - data: { - id: 'Ob' + alignmentObject.objectId, - }, - style: { - 'background-image': this.generateObjectiveSVG(objectiveTitle, teamTitle, alignmentObject.objectState!), - }, - }; - diagramElements.push(element); + let observable = new Observable((observer) => { + let objectiveTitle = this.replaceUmlauts(alignmentObject.objectTitle); + let teamTitle = this.replaceUmlauts(alignmentObject.objectTeamName); + let element = { + data: { + id: 'Ob' + alignmentObject.objectId, + }, + style: { + 'background-image': this.generateObjectiveSVG(objectiveTitle, teamTitle, alignmentObject.objectState!), + }, + }; + diagramElements.push(element); + observer.next(element); + observer.complete(); + }); + observableArray.push(observable); } else { let observable = this.keyResultService.getFullKeyResult(alignmentObject.objectId).pipe( map((keyResult: KeyResult) => { @@ -276,4 +277,12 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { return generateNeutralKeyResultSVG(title, teamName); } } + + cleanUpDiagram() { + if (this.cy) { + this.cy.edges().remove(); + this.cy.nodes().remove(); + this.cy.removeAllListeners(); + } + } } From 4ed68f9909a4819a7291794ca316e1baf843e332 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Tue, 9 Apr 2024 11:39:45 +0200 Subject: [PATCH 074/119] Write unit test for AlignmentBusinessService --- .../okr/models/alignment/AlignmentView.java | 6 +- .../AlignmentBusinessServiceTest.java | 244 ++++++++++++++++++ 2 files changed, 249 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java index 6aab8b426c..912839a6a3 100644 --- a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java +++ b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java @@ -170,7 +170,11 @@ public static final class Builder { private Long refId; private String refType; - public Builder() { + private Builder() { + } + + public static AlignmentView.Builder builder() { + return new AlignmentView.Builder(); } public Builder withUniqueId(String val) { diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java index 6737dd466b..1bbf239bb2 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -2,15 +2,18 @@ import ch.puzzle.okr.TestHelper; import ch.puzzle.okr.dto.ErrorDto; +import ch.puzzle.okr.dto.alignment.AlignmentLists; import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.Objective; import ch.puzzle.okr.models.alignment.Alignment; +import ch.puzzle.okr.models.alignment.AlignmentView; import ch.puzzle.okr.models.alignment.KeyResultAlignment; import ch.puzzle.okr.models.alignment.ObjectiveAlignment; import ch.puzzle.okr.models.keyresult.KeyResult; import ch.puzzle.okr.models.keyresult.KeyResultMetric; import ch.puzzle.okr.service.persistence.AlignmentPersistenceService; +import ch.puzzle.okr.service.persistence.AlignmentViewPersistenceService; import ch.puzzle.okr.service.persistence.KeyResultPersistenceService; import ch.puzzle.okr.service.persistence.ObjectivePersistenceService; import ch.puzzle.okr.service.validation.AlignmentValidationService; @@ -37,6 +40,8 @@ class AlignmentBusinessServiceTest { @Mock AlignmentPersistenceService alignmentPersistenceService; @Mock + AlignmentViewPersistenceService alignmentViewPersistenceService; + @Mock AlignmentValidationService validator; @InjectMocks private AlignmentBusinessService alignmentBusinessService; @@ -60,6 +65,23 @@ class AlignmentBusinessServiceTest { KeyResultAlignment keyResultAlignment = KeyResultAlignment.Builder.builder().withId(6L) .withAlignedObjective(objective3).withTargetKeyResult(metricKeyResult).build(); + AlignmentView alignmentView1 = AlignmentView.Builder.builder().withUniqueId("45TkeyResultkeyResult").withId(4L) + .withTitle("Antwortzeit für Supportanfragen um 33% verkürzen.").withTeamId(5L).withTeamName("Puzzle ITC") + .withQuarterId(2L).withObjectType("keyResult").withConnectionItem("target").withRefId(5L) + .withRefType("objective").build(); + AlignmentView alignmentView2 = AlignmentView.Builder.builder().withUniqueId("54SobjectivekeyResult").withId(5L) + .withTitle("Wir wollen das leiseste Team bei Puzzle sein.").withTeamId(4L).withTeamName("/BBT") + .withQuarterId(2L).withObjectType("objective").withConnectionItem("source").withRefId(4L) + .withRefType("keyResult").build(); + AlignmentView alignmentView3 = AlignmentView.Builder.builder().withUniqueId("4041Tobjectiveobjective").withId(40L) + .withTitle("Wir wollen eine gute Mitarbeiterzufriedenheit.").withTeamId(6L).withTeamName("LoremIpsum") + .withQuarterId(2L).withObjectType("objective").withConnectionItem("target").withRefId(41L) + .withRefType("objective").build(); + AlignmentView alignmentView4 = AlignmentView.Builder.builder().withUniqueId("4140Sobjectiveobjective").withId(41L) + .withTitle("Das Projekt generiert 10000 CHF Umsatz").withTeamId(6L).withTeamName("LoremIpsum") + .withQuarterId(2L).withObjectType("objective").withConnectionItem("source").withRefId(40L) + .withRefType("objective").build(); + @Test void shouldGetTargetAlignmentIdObjective() { // arrange @@ -261,4 +283,226 @@ void shouldDeleteByKeyResultId() { // assert verify(alignmentPersistenceService, times(1)).deleteById(keyResultAlignment.getId()); } + + @Test + void shouldReturnCorrectAlignmentData() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L), ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(5L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(4L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals(5L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(4L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + } + + @Test + void shouldReturnCorrectAlignmentDataWithMultipleTeamFilter() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L, 6L), ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(2, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(4, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(5L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals(5L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(4L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + assertEquals(41L, alignmentLists.alignmentConnectionDtoList().get(1).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(1).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(1).targetKeyResultId()); + } + + @Test + void shouldReturnCorrectAlignmentDataWhenTeamFilterHasLimitedMatch() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L), ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(5L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(4L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals(5L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(4L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoMatchingTeam() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(12L), ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + } + + @Test + void shouldReturnCorrectAlignmentDataWithObjectiveSearch() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L, 5L, 6L), + "leise"); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(5L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(4L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals(5L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(4L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoMatchingObjectiveFromObjectiveSearch() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L, 5L, 6L), + "Supportanfragen"); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoMatchingObjectiveSearch() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L, 5L, 6L), + "wird nicht vorkommen"); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoAlignmentViews() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)).thenReturn(List.of()); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L, 5L, 6L), ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + } + + @Test + void shouldReturnCorrectAlignmentListsWithComplexAlignments() { + AlignmentView alignmentView1 = AlignmentView.Builder.builder().withUniqueId("36TkeyResultkeyResult").withId(3L) + .withTitle("Steigern der URS um 25%").withTeamId(5L).withTeamName("Puzzle ITC").withQuarterId(2L) + .withObjectType("keyResult").withConnectionItem("target").withRefId(6L).withRefType("objective") + .build(); + AlignmentView alignmentView2 = AlignmentView.Builder.builder().withUniqueId("63SobjectivekeyResult").withId(6L) + .withTitle("Als BBT wollen wir den Arbeitsalltag der Members von Puzzle ITC erleichtern.") + .withTeamId(4L).withTeamName("/BBT").withQuarterId(2L).withObjectType("objective") + .withConnectionItem("source").withRefId(3L).withRefType("keyResult").build(); + AlignmentView alignmentView3 = AlignmentView.Builder.builder().withUniqueId("63Tobjectiveobjective").withId(6L) + .withTitle("Als BBT wollen wir den Arbeitsalltag der Members von Puzzle ITC erleichtern.") + .withTeamId(4L).withTeamName("/BBT").withQuarterId(2L).withObjectType("objective") + .withConnectionItem("target").withRefId(3L).withRefType("objective").build(); + AlignmentView alignmentView4 = AlignmentView.Builder.builder().withUniqueId("36Sobjectiveobjective").withId(3L) + .withTitle("Wir wollen die Kundenzufriedenheit steigern").withTeamId(4L).withTeamName("/BBT") + .withQuarterId(2L).withObjectType("objective").withConnectionItem("source").withRefId(6L) + .withRefType("objective").build(); + + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(5L), ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(3L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals("keyResult", alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(6L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(1).objectType()); + assertEquals(6L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(3L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + } + + @Test + void shouldCorrectFilterAlignmentViewListsWithAllCorrectData() { + AlignmentBusinessService.DividedAlignmentViewLists dividedAlignmentViewLists = alignmentBusinessService + .filterAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), + List.of(4L, 6L, 5L), ""); + + assertEquals(4, dividedAlignmentViewLists.correctAlignments().size()); + assertEquals(0, dividedAlignmentViewLists.wrongAlignments().size()); + assertEquals(4, dividedAlignmentViewLists.correctAlignments().get(0).getId()); + assertEquals(5, dividedAlignmentViewLists.correctAlignments().get(1).getId()); + assertEquals(40, dividedAlignmentViewLists.correctAlignments().get(2).getId()); + assertEquals(41, dividedAlignmentViewLists.correctAlignments().get(3).getId()); + } + + @Test + void shouldCorrectFilterAlignmentViewListsWithLimitedTeamFilter() { + AlignmentBusinessService.DividedAlignmentViewLists dividedAlignmentViewLists = alignmentBusinessService + .filterAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), + List.of(6L), ""); + + assertEquals(2, dividedAlignmentViewLists.correctAlignments().size()); + assertEquals(2, dividedAlignmentViewLists.wrongAlignments().size()); + assertEquals(40, dividedAlignmentViewLists.correctAlignments().get(0).getId()); + assertEquals(41, dividedAlignmentViewLists.correctAlignments().get(1).getId()); + assertEquals(4, dividedAlignmentViewLists.wrongAlignments().get(0).getId()); + assertEquals(5, dividedAlignmentViewLists.wrongAlignments().get(1).getId()); + } + + @Test + void shouldCorrectFilterAlignmentViewListsWithObjectiveSearch() { + AlignmentBusinessService.DividedAlignmentViewLists dividedAlignmentViewLists = alignmentBusinessService + .filterAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), + List.of(4L, 6L, 5L), "leise"); + + assertEquals(1, dividedAlignmentViewLists.correctAlignments().size()); + assertEquals(3, dividedAlignmentViewLists.wrongAlignments().size()); + assertEquals(5, dividedAlignmentViewLists.correctAlignments().get(0).getId()); + assertEquals(4, dividedAlignmentViewLists.wrongAlignments().get(0).getId()); + assertEquals(40, dividedAlignmentViewLists.wrongAlignments().get(1).getId()); + assertEquals(41, dividedAlignmentViewLists.wrongAlignments().get(2).getId()); + } + + @Test + void shouldReturnEmptyCorrectListWhenNoMatchingObjectiveSearch() { + AlignmentBusinessService.DividedAlignmentViewLists dividedAlignmentViewLists = alignmentBusinessService + .filterAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), + List.of(4L, 6L, 5L), "verk"); + + assertEquals(0, dividedAlignmentViewLists.correctAlignments().size()); + assertEquals(4, dividedAlignmentViewLists.wrongAlignments().size()); + assertEquals(4, dividedAlignmentViewLists.wrongAlignments().get(0).getId()); + assertEquals(5, dividedAlignmentViewLists.wrongAlignments().get(1).getId()); + assertEquals(40, dividedAlignmentViewLists.wrongAlignments().get(2).getId()); + assertEquals(41, dividedAlignmentViewLists.wrongAlignments().get(3).getId()); + } } From ee13bc46c56cebf8d969996f03c958ca274ff4cd Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Tue, 9 Apr 2024 13:10:10 +0200 Subject: [PATCH 075/119] Write unit test for AlignmentValidationService --- .../AlignmentValidationServiceTest.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java index 8d370e4bc0..6346b10f41 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java @@ -54,6 +54,7 @@ class AlignmentValidationServiceTest { .withTargetObjective(objective1).build(); KeyResultAlignment keyResultAlignment = KeyResultAlignment.Builder.builder().withId(6L) .withAlignedObjective(objective3).withTargetKeyResult(metricKeyResult).build(); + List emptyLongList = List.of(); @BeforeEach void setUp() { @@ -424,4 +425,40 @@ void validateOnDeleteShouldThrowExceptionIfAlignmentIdIsNull() { assertEquals(List.of(new ErrorDto("ATTRIBUTE_NULL", List.of("ID", "Alignment"))), exception.getErrors()); } + @Test + void validateOnAlignmentGetShouldBeSuccessfulWhenQuarterIdAndTeamFilterSet() { + validator.validateOnAlignmentGet(2L, List.of(4L, 5L)); + + verify(validator, times(1)).validateOnAlignmentGet(2L, List.of(4L, 5L)); + } + + @Test + void validateOnAlignmentGetShouldThrowExceptionWhenQuarterIdIsNull() { + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnAlignmentGet(null, emptyLongList)); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertEquals("ATTRIBUTE_NOT_SET", exception.getReason()); + assertEquals(List.of(new ErrorDto("ATTRIBUTE_NOT_SET", List.of("quarterId"))), exception.getErrors()); + } + + @Test + void validateOnAlignmentGetShouldThrowExceptionWhenTeamFilterIsNull() { + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnAlignmentGet(2L, null)); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertEquals("ATTRIBUTE_NOT_SET", exception.getReason()); + assertEquals(List.of(new ErrorDto("ATTRIBUTE_NOT_SET", List.of("teamFilter"))), exception.getErrors()); + } + + @Test + void validateOnAlignmentGetShouldThrowExceptionWhenTeamFilterIsEmpty() { + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> validator.validateOnAlignmentGet(2L, emptyLongList)); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + assertEquals("ATTRIBUTE_NOT_SET", exception.getReason()); + assertEquals(List.of(new ErrorDto("ATTRIBUTE_NOT_SET", List.of("teamFilter"))), exception.getErrors()); + } } From 0e869722030aa71f629f117537751ad64d1c7f7b Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Tue, 9 Apr 2024 13:10:24 +0200 Subject: [PATCH 076/119] Write integration tests for AlignmentBusinessService --- .../business/AlignmentBusinessServiceIT.java | 123 ++++++++++++++++++ .../AlignmentBusinessServiceTest.java | 2 +- 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java new file mode 100644 index 0000000000..e797d72d64 --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java @@ -0,0 +1,123 @@ +package ch.puzzle.okr.service.business; + +import ch.puzzle.okr.dto.alignment.AlignmentLists; +import ch.puzzle.okr.test.SpringIntegrationTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@SpringIntegrationTest +class AlignmentBusinessServiceIT { + @Autowired + private AlignmentBusinessService alignmentBusinessService; + + @Test + void shouldReturnCorrectAlignmentData() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(9L, List.of(5L, 6L), ""); + + assertEquals(6, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(4, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals("keyResult", alignmentLists.alignmentObjectDtoList().get(1).objectType()); + assertEquals(41L, alignmentLists.alignmentObjectDtoList().get(2).objectId()); + assertEquals(43L, alignmentLists.alignmentObjectDtoList().get(3).objectId()); + assertEquals(44L, alignmentLists.alignmentObjectDtoList().get(4).objectId()); + assertEquals(42L, alignmentLists.alignmentObjectDtoList().get(5).objectId()); + assertEquals(41L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertEquals(43L, alignmentLists.alignmentConnectionDtoList().get(1).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(1).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(1).targetObjectiveId()); + assertEquals(44L, alignmentLists.alignmentConnectionDtoList().get(2).alignedObjectiveId()); + assertEquals(42L, alignmentLists.alignmentConnectionDtoList().get(2).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(2).targetKeyResultId()); + assertEquals(42L, alignmentLists.alignmentConnectionDtoList().get(3).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(3).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(3).targetKeyResultId()); + } + + @Test + void shouldReturnCorrectAlignmentDataWhenLimitedTeamMatching() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(9L, List.of(6L), ""); + + assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(44L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(42L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(1).objectType()); + assertEquals(44L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(42L, alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + } + + @Test + void shouldReturnCorrectAlignmentDataWithObjectiveSearch() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(9L, List.of(4L, 5L, 6L, 8L), + "lehrling"); + + assertEquals(3, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(2, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(42L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(1).objectType()); + assertEquals(44L, alignmentLists.alignmentObjectDtoList().get(2).objectId()); + assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(2).objectType()); + assertEquals(42L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertEquals(44L, alignmentLists.alignmentConnectionDtoList().get(1).alignedObjectiveId()); + assertEquals(42L, alignmentLists.alignmentConnectionDtoList().get(1).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(1).targetKeyResultId()); + } + + @Test + void shouldReturnCorrectAlignmentDataWithKeyResultWhenMatchingObjectiveSearch() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(9L, List.of(4L, 5L, 6L, 8L), + "firmenums"); + + assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(43L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals("keyResult", alignmentLists.alignmentObjectDtoList().get(1).objectType()); + assertEquals(43L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenAlignments() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(3L, List.of(5L, 6L), ""); + + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoMatchingObjectiveSearch() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(9L, List.of(4L, 5L, 6L, 8L), + "spass"); + + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoMatchingQuarterFilter() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(311L, List.of(4L, 5L, 6L, 8L), + ""); + + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + } +} diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java index 1bbf239bb2..25f6c2c4a8 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -493,7 +493,7 @@ void shouldCorrectFilterAlignmentViewListsWithObjectiveSearch() { } @Test - void shouldReturnEmptyCorrectListWhenNoMatchingObjectiveSearch() { + void shouldCorrectFilterWhenNoMatchingObjectiveSearch() { AlignmentBusinessService.DividedAlignmentViewLists dividedAlignmentViewLists = alignmentBusinessService .filterAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), List.of(4L, 6L, 5L), "verk"); From 0ef90405f44dad7848c897d97cccc9c118252ead Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Tue, 9 Apr 2024 13:10:36 +0200 Subject: [PATCH 077/119] Adjust foreign key in testdata --- .../h2-db/data-test-h2/V100_0_0__TestData.sql | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql index 66c8a2395d..43341724ea 100644 --- a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql +++ b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql @@ -127,18 +127,18 @@ values (1,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam insert into alignment (id, aligned_objective_id, alignment_type, target_key_result_id, target_objective_id, version) values (1, 4, 'objective', null, 3, 1), (2, 9, 'keyResult', 8, null, 1), - (1, 4, 'objective', null, 6, 0), - (2, 3, 'objective', null, 6, 0), - (3, 8, 'objective', null, 3, 0), - (4, 9, 'keyResult', 8, null, 0), - (5, 10, 'keyResult', 5, null, 0), - (6, 5, 'keyResult', 4, null, 0), - (7, 6, 'keyResult', 3, null, 0), - (8, 41, 'objective', null, 40, 0), - (9, 42, 'objective', null, 40, 0), - (10, 43, 'keyResult', 40, null, 0), - (11, 44, 'objective', null, 42, 0), - (12, 45, 'keyResult', 41, null, 0); + (3, 4, 'objective', null, 6, 0), + (4, 3, 'objective', null, 6, 0), + (5, 8, 'objective', null, 3, 0), + (6, 9, 'keyResult', 8, null, 0), + (7, 10, 'keyResult', 5, null, 0), + (8, 5, 'keyResult', 4, null, 0), + (9, 6, 'keyResult', 3, null, 0), + (10, 41, 'objective', null, 40, 0), + (11, 42, 'objective', null, 40, 0), + (12, 43, 'keyResult', 40, null, 0), + (13, 44, 'objective', null, 42, 0), + (14, 45, 'keyResult', 41, null, 0); insert into completed (id, version, objective_id, comment) values (1,1, 4, 'Das hat geklappt'), From 2319f15291d85724f23ef3e5eb015148fb7f8085 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Tue, 9 Apr 2024 16:12:13 +0200 Subject: [PATCH 078/119] Write integration tests for AlignmentViewPersistenceService --- .../AlignmentViewPersistenceServiceIT.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceServiceIT.java diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceServiceIT.java new file mode 100644 index 0000000000..9d36c2ede3 --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentViewPersistenceServiceIT.java @@ -0,0 +1,53 @@ +package ch.puzzle.okr.service.persistence; + +import ch.puzzle.okr.models.alignment.AlignmentView; +import ch.puzzle.okr.test.SpringIntegrationTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringIntegrationTest +class AlignmentViewPersistenceServiceIT { + @Autowired + private AlignmentViewPersistenceService alignmentViewPersistenceService; + + private static final List expectedAlignmentViewIds = List.of(40L, 41L, 42L, 43L, 44L, 45L); + + private static final List expectedAlignmentViewTeamIds = List.of(4L, 5L, 6L, 8L); + + private static final List expectedAlignmentViewQuarterId = List.of(9L); + + @Test + void getAlignmentsByFiltersShouldReturnListOfAlignmentViews() { + List alignmentViewList = alignmentViewPersistenceService.getAlignmentViewListByQuarterId(9L); + + assertEquals(10, alignmentViewList.size()); + + assertThat(getAlignmentViewIds(alignmentViewList)).hasSameElementsAs(expectedAlignmentViewIds); + assertThat(getAlignmentViewTeamIds(alignmentViewList)).hasSameElementsAs(expectedAlignmentViewTeamIds); + assertThat(getAlignmentViewQuarterIds(alignmentViewList)).hasSameElementsAs(expectedAlignmentViewQuarterId); + } + + @Test + void getAlignmentsByFiltersShouldReturnEmptyListOfAlignmentViewsWhenQuarterNotExisting() { + List alignmentViewList = alignmentViewPersistenceService.getAlignmentViewListByQuarterId(311L); + + assertEquals(0, alignmentViewList.size()); + } + + private List getAlignmentViewIds(List alignmentViewIds) { + return alignmentViewIds.stream().map(AlignmentView::getId).toList(); + } + + private List getAlignmentViewTeamIds(List alignmentViewIds) { + return alignmentViewIds.stream().map(AlignmentView::getTeamId).toList(); + } + + private List getAlignmentViewQuarterIds(List alignmentViewIds) { + return alignmentViewIds.stream().map(AlignmentView::getQuarterId).toList(); + } +} From d977bfce07f9ae00ff2d4d6f6a2f0dc312bf6331 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Tue, 9 Apr 2024 16:12:57 +0200 Subject: [PATCH 079/119] Fix failing backend tests --- .../h2-db/data-test-h2/V100_0_0__TestData.sql | 26 +++++++++---------- .../V1_0_0__current-db-schema-for-testing.sql | 6 ++--- .../business/AlignmentBusinessServiceIT.java | 2 +- .../AlignmentPersistenceServiceIT.java | 6 ++--- .../CheckInPersistenceServiceIT.java | 2 +- .../ObjectivePersistenceServiceIT.java | 2 +- 6 files changed, 21 insertions(+), 23 deletions(-) diff --git a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql index 43341724ea..8b3131a332 100644 --- a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql +++ b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql @@ -125,20 +125,18 @@ values (1,1, 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam (40,1,'','2024-04-04 14:10:33.377726','','2024-04-04 14:10:33.377739',30000,1,41,7,'metric',null); insert into alignment (id, aligned_objective_id, alignment_type, target_key_result_id, target_objective_id, version) values - (1, 4, 'objective', null, 3, 1), - (2, 9, 'keyResult', 8, null, 1), - (3, 4, 'objective', null, 6, 0), - (4, 3, 'objective', null, 6, 0), - (5, 8, 'objective', null, 3, 0), - (6, 9, 'keyResult', 8, null, 0), - (7, 10, 'keyResult', 5, null, 0), - (8, 5, 'keyResult', 4, null, 0), - (9, 6, 'keyResult', 3, null, 0), - (10, 41, 'objective', null, 40, 0), - (11, 42, 'objective', null, 40, 0), - (12, 43, 'keyResult', 40, null, 0), - (13, 44, 'objective', null, 42, 0), - (14, 45, 'keyResult', 41, null, 0); + (1, 9, 'keyResult', 8, null, 1), + (2, 4, 'objective', null, 6, 0), + (3, 3, 'objective', null, 6, 0), + (4, 8, 'objective', null, 3, 0), + (5, 10, 'keyResult', 5, null, 0), + (6, 5, 'keyResult', 4, null, 0), + (7, 6, 'keyResult', 3, null, 0), + (8, 41, 'objective', null, 40, 0), + (9, 42, 'objective', null, 40, 0), + (10, 43, 'keyResult', 40, null, 0), + (11, 44, 'objective', null, 42, 0), + (12, 45, 'keyResult', 41, null, 0); insert into completed (id, version, objective_id, comment) values (1,1, 4, 'Das hat geklappt'), diff --git a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql index 0960428497..d4bcf19be6 100644 --- a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql +++ b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql @@ -231,7 +231,7 @@ create table if not exists team_organisation DROP VIEW IF EXISTS ALIGNMENT_VIEW; CREATE VIEW ALIGNMENT_VIEW AS SELECT - CONCAT(OA.ID, COALESCE(A.TARGET_OBJECTIVE_ID, A.TARGET_KEY_RESULT_ID)) AS UNIQUE_ID, + CONCAT(OA.ID, COALESCE(A.TARGET_OBJECTIVE_ID, A.TARGET_KEY_RESULT_ID),'S','objective',A.ALIGNMENT_TYPE) AS UNIQUE_ID, OA.ID AS ID, OA.TITLE AS TITLE, OTT.ID AS TEAM_ID, @@ -247,7 +247,7 @@ FROM ALIGNMENT A LEFT JOIN TEAM OTT ON OTT.ID = OA.TEAM_ID UNION SELECT - CONCAT(OT.ID, A.ALIGNED_OBJECTIVE_ID) AS UNIQUE_ID, + CONCAT(OT.ID, A.ALIGNED_OBJECTIVE_ID,'T','objective','objective') AS UNIQUE_ID, OT.ID AS ID, OT.TITLE AS TITLE, OTT.ID AS TEAM_ID, @@ -264,7 +264,7 @@ FROM ALIGNMENT A WHERE ALIGNMENT_TYPE = 'objective' UNION SELECT - CONCAT(KRT.ID, A.ALIGNED_OBJECTIVE_ID) AS UNIQUE_ID, + CONCAT(KRT.ID, A.ALIGNED_OBJECTIVE_ID,'T','keyResult','keyResult') AS UNIQUE_ID, KRT.ID AS ID, KRT.TITLE AS TITLE, OTT.ID AS TEAM_ID, diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java index e797d72d64..3da6a169c7 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java @@ -96,7 +96,7 @@ void shouldReturnCorrectAlignmentDataWithKeyResultWhenMatchingObjectiveSearch() } @Test - void shouldReturnEmptyAlignmentDataWhenAlignments() { + void shouldReturnEmptyAlignmentDataWhenNoAlignments() { AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(3L, List.of(5L, 6L), ""); assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java index 428c415aa1..34e2f56a20 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/AlignmentPersistenceServiceIT.java @@ -235,13 +235,13 @@ private void shouldDeleteOldAlignment(Long alignmentId) { } private void assertAlignment(ObjectiveAlignment objectiveAlignment) { - assertEquals(1L, objectiveAlignment.getId()); + assertEquals(4L, objectiveAlignment.getId()); assertEquals(3L, objectiveAlignment.getAlignmentTarget().getId()); - assertEquals(4L, objectiveAlignment.getAlignedObjective().getId()); + assertEquals(8L, objectiveAlignment.getAlignedObjective().getId()); } private void assertAlignment(KeyResultAlignment keyResultAlignment) { - assertEquals(2L, keyResultAlignment.getId()); + assertEquals(1L, keyResultAlignment.getId()); assertEquals(8L, keyResultAlignment.getAlignmentTarget().getId()); assertEquals(9L, keyResultAlignment.getAlignedObjective().getId()); } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/CheckInPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/CheckInPersistenceServiceIT.java index b985c482f5..ab366e16d5 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/CheckInPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/CheckInPersistenceServiceIT.java @@ -108,7 +108,7 @@ void updateKeyResultShouldThrowExceptionWhenAlreadyUpdated() { void getAllCheckInShouldReturnListOfAllCheckIns() { List checkIns = checkInPersistenceService.findAll(); - assertEquals(19, checkIns.size()); + assertEquals(20, checkIns.size()); } @Test diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java index bda4b4976a..343224c4b6 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/ObjectivePersistenceServiceIT.java @@ -68,7 +68,7 @@ void tearDown() { void findAllShouldReturnListOfObjectives() { List objectives = objectivePersistenceService.findAll(); - assertEquals(7, objectives.size()); + assertEquals(14, objectives.size()); } @Test From 38717c1a3b0c5e26101d42e054db276647facdb4 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Tue, 9 Apr 2024 16:42:12 +0200 Subject: [PATCH 080/119] Add current quarter id to url on application launch --- frontend/src/app/quarter-filter/quarter-filter.component.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/quarter-filter/quarter-filter.component.ts b/frontend/src/app/quarter-filter/quarter-filter.component.ts index 10168b7bab..d7c393b8e9 100644 --- a/frontend/src/app/quarter-filter/quarter-filter.component.ts +++ b/frontend/src/app/quarter-filter/quarter-filter.component.ts @@ -36,7 +36,9 @@ export class QuarterFilterComponent implements OnInit { if (quarterQuery !== undefined) { this.changeDisplayedQuarter(); } else { - this.refreshDataService.quarterFilterReady.next(); + this.router + .navigate([], { queryParams: { quarter: this.quarterId } }) + .then(() => this.refreshDataService.quarterFilterReady.next()); } } const quarterLabel = quarters.find((e) => e.id == this.quarterId)?.label || ''; From 303e70076efb0a62bff67fcb683ae476b79dfcc8 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Tue, 9 Apr 2024 17:01:37 +0200 Subject: [PATCH 081/119] Fix frontend e2e tests --- frontend/src/app/diagram/diagram.component.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/app/diagram/diagram.component.spec.ts b/frontend/src/app/diagram/diagram.component.spec.ts index b5b4ee19c8..0e24fe799b 100644 --- a/frontend/src/app/diagram/diagram.component.spec.ts +++ b/frontend/src/app/diagram/diagram.component.spec.ts @@ -1,11 +1,16 @@ import { DiagramComponent } from './diagram.component'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('DiagramComponent', () => { let component: DiagramComponent; let fixture: ComponentFixture; beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [DiagramComponent], + imports: [HttpClientTestingModule], + }); fixture = TestBed.createComponent(DiagramComponent); component = fixture.componentInstance; }); From 0074278b97fd0b019ebc900f547ee556f72c341e Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 10 Apr 2024 08:06:08 +0200 Subject: [PATCH 082/119] Fix frontend unit tests for quarter-filter --- .../src/app/quarter-filter/quarter-filter.component.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/quarter-filter/quarter-filter.component.spec.ts b/frontend/src/app/quarter-filter/quarter-filter.component.spec.ts index 9a578a6310..784bbd7669 100644 --- a/frontend/src/app/quarter-filter/quarter-filter.component.spec.ts +++ b/frontend/src/app/quarter-filter/quarter-filter.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { QuarterFilterComponent } from './quarter-filter.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { OverviewService } from '../shared/services/overview.service'; @@ -8,7 +8,6 @@ import { Quarter } from '../shared/types/model/Quarter'; import { QuarterService } from '../shared/services/quarter.service'; import { RouterTestingHarness, RouterTestingModule } from '@angular/router/testing'; import { FormsModule } from '@angular/forms'; -import { By } from '@angular/platform-browser'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -75,7 +74,8 @@ describe('QuarterFilterComponent', () => { fixture.detectChanges(); expect(component.quarterId).toBe(quarters[2].id); expect(await quarterSelect.getValueText()).toBe(quarters[2].label + ' Aktuell'); - expect(component.changeDisplayedQuarter).toHaveBeenCalledTimes(0); + expect(component.changeDisplayedQuarter).toHaveBeenCalledTimes(1); + expect(router.url).toBe('/?quarter=' + quarters[2].id); }); it('should set correct value in form according to route param', async () => { From 0779055053fbcb9437e048a8d2f2f2756b38ca48 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 10 Apr 2024 09:18:35 +0200 Subject: [PATCH 083/119] Add frontend e2e tests for diagram --- frontend/cypress/e2e/diagram.cy.ts | 76 +++++++++++++++++++ .../src/app/overview/overview.component.html | 2 + 2 files changed, 78 insertions(+) create mode 100644 frontend/cypress/e2e/diagram.cy.ts diff --git a/frontend/cypress/e2e/diagram.cy.ts b/frontend/cypress/e2e/diagram.cy.ts new file mode 100644 index 0000000000..4f6049fbe4 --- /dev/null +++ b/frontend/cypress/e2e/diagram.cy.ts @@ -0,0 +1,76 @@ +import * as users from '../fixtures/users.json'; + +describe('OKR diagram e2e tests', () => { + describe('tests via click', () => { + beforeEach(() => { + cy.loginAsUser(users.gl); + cy.visit('/?quarter=10'); + cy.getByTestId('add-objective').first().click(); + cy.fillOutObjective('An Objective for Testing', 'safe-draft', '10'); + }); + + it('Can switch to diagram with the tab-switch', () => { + cy.get('h1:contains(Puzzle ITC)').should('have.length', 1); + cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); + cy.contains('Overview'); + cy.contains('Diagramm'); + cy.contains('An Objective for Testing'); + + cy.getByTestId('diagramTab').first().click(); + + cy.contains('Kein Alignment vorhanden'); + cy.get('h1:contains(Puzzle ITC)').should('have.length', 0); + cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); + cy.getByTestId('objective').should('have.length', 0); + + cy.getByTestId('overviewTab').first().click(); + + cy.get('h1:contains(Puzzle ITC)').should('have.length', 1); + cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); + cy.contains('An Objective for Testing'); + }); + + it('Can switch to diagram and the filter stay the same', () => { + cy.get('h1:contains(Puzzle ITC)').should('have.length', 1); + cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); + cy.contains('Overview'); + cy.contains('Diagramm'); + cy.contains('An Objective for Testing'); + cy.getByTestId('quarterFilter').should('contain', 'GJ 24/25-Q1'); + cy.get('mat-chip:visible:contains("Puzzle ITC")') + .should('have.css', 'background-color') + .and('eq', 'rgb(30, 90, 150)'); + cy.get('mat-chip:visible:contains("/BBT")') + .should('have.css', 'background-color') + .and('eq', 'rgb(255, 255, 255)'); + + cy.get('mat-chip:visible:contains("/BBT")').click(); + cy.get('mat-chip:visible:contains("Puzzle ITC")').click(); + + cy.getByTestId('diagramTab').first().click(); + + cy.contains('Kein Alignment vorhanden'); + cy.get('h1:contains(Puzzle ITC)').should('have.length', 0); + cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); + cy.getByTestId('quarterFilter').should('contain', 'GJ 24/25-Q1'); + cy.getByTestId('objective').should('have.length', 0); + cy.get('mat-chip:visible:contains("/BBT")').should('have.css', 'background-color').and('eq', 'rgb(30, 90, 150)'); + cy.get('mat-chip:visible:contains("Puzzle ITC")') + .should('have.css', 'background-color') + .and('eq', 'rgb(255, 255, 255)'); + cy.get('mat-chip:visible:contains("Puzzle ITC")').click(); + + cy.getByTestId('quarterFilter').first().focus(); + cy.focused().realPress('ArrowDown'); + + cy.getByTestId('quarterFilter').should('contain', 'GJ 23/24-Q4'); + cy.getByTestId('overviewTab').first().click(); + + cy.get('mat-chip:visible:contains("/BBT")').should('have.css', 'background-color').and('eq', 'rgb(30, 90, 150)'); + cy.get('mat-chip:visible:contains("Puzzle ITC")') + .should('have.css', 'background-color') + .and('eq', 'rgb(30, 90, 150)'); + cy.getByTestId('quarterFilter').should('contain', 'GJ 23/24-Q4'); + }); + }); +}); diff --git a/frontend/src/app/overview/overview.component.html b/frontend/src/app/overview/overview.component.html index c1a57e5bf9..2880c2e621 100644 --- a/frontend/src/app/overview/overview.component.html +++ b/frontend/src/app/overview/overview.component.html @@ -8,6 +8,7 @@
Diagramm From ca199808586e9ae2118e4615b91f3aa0a590133a Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 10 Apr 2024 13:36:46 +0200 Subject: [PATCH 084/119] Rename function for replacing non ascii characters --- frontend/src/app/diagram/diagram.component.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index 1ec82cf26a..b0a2f332fd 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -140,8 +140,8 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { alignmentData.alignmentObjectDtoList.forEach((alignmentObject) => { if (alignmentObject.objectType == 'objective') { let observable = new Observable((observer) => { - let objectiveTitle = this.replaceUmlauts(alignmentObject.objectTitle); - let teamTitle = this.replaceUmlauts(alignmentObject.objectTeamName); + let objectiveTitle = this.replaceNonAsciiCharacters(alignmentObject.objectTitle); + let teamTitle = this.replaceNonAsciiCharacters(alignmentObject.objectTeamName); let element = { data: { id: 'Ob' + alignmentObject.objectId, @@ -174,8 +174,8 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { } else { keyResultState = undefined; } - let keyResultTitle = this.replaceUmlauts(alignmentObject.objectTitle); - let teamTitle = this.replaceUmlauts(alignmentObject.objectTeamName); + let keyResultTitle = this.replaceNonAsciiCharacters(alignmentObject.objectTitle); + let teamTitle = this.replaceNonAsciiCharacters(alignmentObject.objectTeamName); let element = { data: { id: 'KR' + alignmentObject.objectId, @@ -189,8 +189,8 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { let ordinalKeyResult = keyResult as KeyResultOrdinal; let keyResultState: string | undefined = ordinalKeyResult.lastCheckIn?.value.toString(); - let keyResultTitle = this.replaceUmlauts(alignmentObject.objectTitle); - let teamTitle = this.replaceUmlauts(alignmentObject.objectTeamName); + let keyResultTitle = this.replaceNonAsciiCharacters(alignmentObject.objectTitle); + let teamTitle = this.replaceNonAsciiCharacters(alignmentObject.objectTeamName); let element = { data: { id: 'KR' + alignmentObject.objectId, @@ -237,7 +237,7 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { this.generateDiagram(); } - replaceUmlauts(text: string): string { + replaceNonAsciiCharacters(text: string): string { text = text.replace(/\u00c4/g, 'Ae'); text = text.replace(/\u00e4/g, 'ae'); text = text.replace(/\u00dc/g, 'Ue'); From c2d33102404c0550268208b92590ffbe2e656dbe Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 10 Apr 2024 13:37:06 +0200 Subject: [PATCH 085/119] Write unit tests for DiagramComponent --- .../src/app/diagram/diagram.component.spec.ts | 193 ++++++++++++++++++ frontend/src/app/shared/testData.ts | 57 ++++++ 2 files changed, 250 insertions(+) diff --git a/frontend/src/app/diagram/diagram.component.spec.ts b/frontend/src/app/diagram/diagram.component.spec.ts index 0e24fe799b..a877edea1d 100644 --- a/frontend/src/app/diagram/diagram.component.spec.ts +++ b/frontend/src/app/diagram/diagram.component.spec.ts @@ -1,6 +1,17 @@ import { DiagramComponent } from './diagram.component'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { AlignmentLists } from '../shared/types/model/AlignmentLists'; +import { alignmentLists, alignmentListsKeyResult, keyResult, keyResultMetric } from '../shared/testData'; +import * as functions from './svgGeneration'; +import { getDraftIcon, getNotSuccessfulIcon, getOnGoingIcon, getSuccessfulIcon } from './svgGeneration'; +import { of } from 'rxjs'; +import { KeyresultService } from '../shared/services/keyresult.service'; +import { ParseUnitValuePipe } from '../shared/pipes/parse-unit-value/parse-unit-value.pipe'; + +const keyResultServiceMock = { + getFullKeyResult: jest.fn(), +}; describe('DiagramComponent', () => { let component: DiagramComponent; @@ -10,6 +21,7 @@ describe('DiagramComponent', () => { TestBed.configureTestingModule({ declarations: [DiagramComponent], imports: [HttpClientTestingModule], + providers: [{ provide: KeyresultService, useValue: keyResultServiceMock }, ParseUnitValuePipe], }); fixture = TestBed.createComponent(DiagramComponent); component = fixture.componentInstance; @@ -18,4 +30,185 @@ describe('DiagramComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should call cleanUpDiagram when ngOnDestroy gets called', () => { + jest.spyOn(component, 'cleanUpDiagram'); + + component.ngOnDestroy(); + expect(component.cleanUpDiagram).toHaveBeenCalled(); + }); + + it('should call generateElements if alignmentData is present', () => { + jest.spyOn(component, 'generateElements'); + + component.prepareDiagramData(alignmentLists); + expect(component.generateElements).toHaveBeenCalled(); + expect(component.emptyDiagramData).toBeFalsy(); + }); + + it('should not call generateElements if alignmentData is empty', () => { + jest.spyOn(component, 'generateElements'); + + let alignmentLists: AlignmentLists = { + alignmentObjectDtoList: [], + alignmentConnectionDtoList: [], + }; + + component.prepareDiagramData(alignmentLists); + expect(component.generateElements).not.toHaveBeenCalled(); + expect(component.emptyDiagramData).toBeTruthy(); + }); + + it('should call prepareDiagramData when Subject receives new data', () => { + jest.spyOn(component, 'cleanUpDiagram'); + jest.spyOn(component, 'prepareDiagramData'); + + component.ngAfterViewInit(); + component.alignmentData.next(alignmentLists); + + expect(component.cleanUpDiagram).toHaveBeenCalled(); + expect(component.prepareDiagramData).toHaveBeenCalledWith(alignmentLists); + }); + + it('should generate correct diagramData for Objectives', () => { + jest.spyOn(component, 'generateConnections'); + jest.spyOn(component, 'generateDiagram'); + jest.spyOn(component, 'generateObjectiveSVG').mockReturnValue('Test.svg'); + + let edge = { + data: { + source: 'Ob1', + target: 'Ob2', + }, + }; + let element1 = { + data: { + id: 'Ob1', + }, + style: { + 'background-image': 'Test.svg', + }, + }; + let element2 = { + data: { + id: 'Ob2', + }, + style: { + 'background-image': 'Test.svg', + }, + }; + + let diagramElements: any[] = [element1, element2]; + let edges: any[] = [edge]; + + component.generateElements(alignmentLists); + + expect(component.generateConnections).toHaveBeenCalled(); + expect(component.generateDiagram).toHaveBeenCalled(); + expect(component.diagramData).toEqual(diagramElements.concat(edges)); + }); + + it('should generate correct diagramData for KeyResult Metric', () => { + jest.spyOn(component, 'generateConnections'); + jest.spyOn(component, 'generateDiagram'); + jest.spyOn(component, 'generateObjectiveSVG').mockReturnValue('TestObjective.svg'); + jest.spyOn(component, 'generateKeyResultSVG').mockReturnValue('TestKeyResult.svg'); + jest.spyOn(keyResultServiceMock, 'getFullKeyResult').mockReturnValue(of(keyResultMetric)); + + let diagramData: any[] = getReturnedAlignmentDataKeyResult(); + + component.generateElements(alignmentListsKeyResult); + + expect(component.generateConnections).toHaveBeenCalled(); + expect(component.generateDiagram).toHaveBeenCalled(); + expect(component.diagramData).toEqual(diagramData); + }); + + it('should generate correct diagramData for KeyResult Ordinal', () => { + jest.spyOn(component, 'generateConnections'); + jest.spyOn(component, 'generateDiagram'); + jest.spyOn(component, 'generateObjectiveSVG').mockReturnValue('TestObjective.svg'); + jest.spyOn(component, 'generateKeyResultSVG').mockReturnValue('TestKeyResult.svg'); + jest.spyOn(keyResultServiceMock, 'getFullKeyResult').mockReturnValue(of(keyResult)); + + let diagramData: any[] = getReturnedAlignmentDataKeyResult(); + + component.generateElements(alignmentListsKeyResult); + + expect(component.generateConnections).toHaveBeenCalled(); + expect(component.generateDiagram).toHaveBeenCalled(); + expect(component.diagramData).toEqual(diagramData); + }); + + it('should replace correct non ascii characters', () => { + let specialText: string = + 'Die klügsten Schafe springen über den Zaun und rechnen 2², während die ängstlichen Mäuse sich in ihren Löchern verkriechen und das Gemüß folgend rechnen 3³. Östlich befindet sich Ägypten mit einem Überfluss an Sand.'; + + let correctedText: string = + 'Die kluegsten Schafe springen ueber den Zaun und rechnen 2^2, waehrend die aengstlichen Maeuse sich in ihren Loechern verkriechen und das Gemuess folgend rechnen 3^3. Oestlich befindet sich Aegypten mit einem Ueberfluss an Sand.'; + + expect(component.replaceNonAsciiCharacters(specialText)).toEqual(correctedText); + }); + + it('should generate correct SVGs for Objective', () => { + jest.spyOn(functions, 'generateObjectiveSVG'); + + component.generateObjectiveSVG('Title 1', 'Team name 1', 'ONGOING'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 1', 'Team name 1', getOnGoingIcon); + + component.generateObjectiveSVG('Title 2', 'Team name 2', 'SUCCESSFUL'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 2', 'Team name 2', getSuccessfulIcon); + + component.generateObjectiveSVG('Title 3', 'Team name 3', 'NOTSUCCESSFUL'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 3', 'Team name 3', getNotSuccessfulIcon); + + component.generateObjectiveSVG('Title 4', 'Team name 4', 'DRAFT'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 4', 'Team name 4', getDraftIcon); + }); + + it('should generate correct SVGs for KeyResult', () => { + jest.spyOn(functions, 'generateObjectiveSVG'); + + component.generateObjectiveSVG('Title 1', 'Team name 1', 'ONGOING'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 1', 'Team name 1', getOnGoingIcon); + + component.generateObjectiveSVG('Title 2', 'Team name 2', 'SUCCESSFUL'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 2', 'Team name 2', getSuccessfulIcon); + + component.generateObjectiveSVG('Title 3', 'Team name 3', 'NOTSUCCESSFUL'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 3', 'Team name 3', getNotSuccessfulIcon); + + component.generateObjectiveSVG('Title 4', 'Team name 4', 'DRAFT'); + expect(functions.generateObjectiveSVG).toHaveBeenCalledWith('Title 4', 'Team name 4', getDraftIcon); + }); }); + +function getReturnedAlignmentDataKeyResult(): any[] { + let edge = { + data: { + source: 'Ob3', + target: 'KR102', + }, + }; + let element1 = { + data: { + id: 'Ob3', + }, + style: { + 'background-image': 'TestObjective.svg', + }, + }; + let element2 = { + data: { + id: 'KR102', + }, + style: { + 'background-image': 'TestKeyResult.svg', + }, + }; + + let diagramElements: any[] = [element1, element2]; + let edges: any[] = [edge]; + + return diagramElements.concat(edges); +} diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index 65dac779cb..4639348559 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -19,6 +19,9 @@ import { Action } from './types/model/Action'; import { OrganisationState } from './types/enums/OrganisationState'; import { Organisation } from './types/model/Organisation'; import { Dashboard } from './types/model/Dashboard'; +import { AlignmentObject } from './types/model/AlignmentObject'; +import { AlignmentConnection } from './types/model/AlignmentConnection'; +import { AlignmentLists } from './types/model/AlignmentLists'; import { AlignmentPossibilityObject } from './types/model/AlignmentPossibilityObject'; import { AlignmentPossibility } from './types/model/AlignmentPossibility'; @@ -662,3 +665,57 @@ export const alignmentPossibility2: AlignmentPossibility = { teamName: 'We are cube', alignmentObjects: [alignmentObject1], }; + +export const alignmentObject1: AlignmentObject = { + objectId: 1, + objectTitle: 'Title 1', + objectTeamName: 'Example Team', + objectState: 'ONGOING', + objectType: 'objective', +}; + +export const alignmentObject2: AlignmentObject = { + objectId: 2, + objectTitle: 'Title 2', + objectTeamName: 'Example Team', + objectState: 'DRAFT', + objectType: 'objective', +}; + +export const alignmentObject3: AlignmentObject = { + objectId: 3, + objectTitle: 'Title 3', + objectTeamName: 'Example Team', + objectState: 'DRAFT', + objectType: 'objective', +}; + +export const alignmentObjectKeyResult: AlignmentObject = { + objectId: 102, + objectTitle: 'Title 1', + objectTeamName: 'Example Team', + objectState: null, + objectType: 'keyResult', +}; + +export const alignmentConnection: AlignmentConnection = { + alignedObjectiveId: 1, + targetObjectiveId: 2, + targetKeyResultId: null, +}; + +export const alignmentConnectionKeyResult: AlignmentConnection = { + alignedObjectiveId: 3, + targetObjectiveId: null, + targetKeyResultId: 102, +}; + +export const alignmentLists: AlignmentLists = { + alignmentObjectDtoList: [alignmentObject1, alignmentObject2], + alignmentConnectionDtoList: [alignmentConnection], +}; + +export const alignmentListsKeyResult: AlignmentLists = { + alignmentObjectDtoList: [alignmentObject3, alignmentObjectKeyResult], + alignmentConnectionDtoList: [alignmentConnectionKeyResult], +}; From b11c8926898726c0e630bab57c646b934fee2d08 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 10 Apr 2024 13:38:13 +0200 Subject: [PATCH 086/119] Add validation check for final AlignmentViewList on same amout of source and target elements --- backend/src/main/java/ch/puzzle/okr/ErrorKey.java | 2 +- .../business/AlignmentBusinessService.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/ch/puzzle/okr/ErrorKey.java b/backend/src/main/java/ch/puzzle/okr/ErrorKey.java index 47a11e39af..50d20d7df7 100644 --- a/backend/src/main/java/ch/puzzle/okr/ErrorKey.java +++ b/backend/src/main/java/ch/puzzle/okr/ErrorKey.java @@ -4,5 +4,5 @@ public enum ErrorKey { ATTRIBUTE_NULL, ATTRIBUTE_CHANGED, ATTRIBUTE_SET_FORBIDDEN, ATTRIBUTE_NOT_SET, ATTRIBUTE_CANNOT_CHANGE, ATTRIBUTE_MUST_BE_DRAFT, KEY_RESULT_CONVERSION, ALREADY_EXISTS_SAME_NAME, CONVERT_TOKEN, DATA_HAS_BEEN_UPDATED, MODEL_NULL, MODEL_WITH_ID_NOT_FOUND, NOT_AUTHORIZED_TO_READ, NOT_AUTHORIZED_TO_WRITE, NOT_AUTHORIZED_TO_DELETE, - TOKEN_NULL, NOT_LINK_YOURSELF, NOT_LINK_IN_SAME_TEAM, ALIGNMENT_ALREADY_EXISTS + TOKEN_NULL, NOT_LINK_YOURSELF, NOT_LINK_IN_SAME_TEAM, ALIGNMENT_ALREADY_EXISTS, ALIGNMENT_DATA_FAIL } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index 3e8660df30..0f947d4556 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -172,9 +172,24 @@ public AlignmentLists getAlignmentsByFilters(Long quarterFilter, List team List finalList = getAlignmentCounterpart(dividedAlignmentViewLists); + validateFinalList(finalList, quarterFilter, teamFilter, objectiveFilter); + return generateAlignmentLists(finalList); } + protected void validateFinalList(List finalList, Long quarterFilter, List teamFilter, + String objectiveFilter) { + List sourceList = finalList.stream() + .filter(alignmentView -> Objects.equals(alignmentView.getConnectionItem(), "source")).toList(); + List targetList = finalList.stream() + .filter(alignmentView -> Objects.equals(alignmentView.getConnectionItem(), "target")).toList(); + + if (sourceList.size() != targetList.size()) { + throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ALIGNMENT_DATA_FAIL, + List.of("alignmentData", quarterFilter, teamFilter, objectiveFilter)); + } + } + protected AlignmentLists generateAlignmentLists(List alignmentViewList) { List alignmentConnectionDtoList = new ArrayList<>(); List alignmentObjectDtoList = new ArrayList<>(); From f98d83af0e28d588666d0292dbcb576b74b1f34d Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 10 Apr 2024 13:38:34 +0200 Subject: [PATCH 087/119] Write unit tests for new validation check --- .../AlignmentBusinessServiceTest.java | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java index 25f6c2c4a8..86ffa85096 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -17,6 +17,7 @@ import ch.puzzle.okr.service.persistence.KeyResultPersistenceService; import ch.puzzle.okr.service.persistence.ObjectivePersistenceService; import ch.puzzle.okr.service.validation.AlignmentValidationService; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -505,4 +506,49 @@ void shouldCorrectFilterWhenNoMatchingObjectiveSearch() { assertEquals(40, dividedAlignmentViewLists.wrongAlignments().get(2).getId()); assertEquals(41, dividedAlignmentViewLists.wrongAlignments().get(3).getId()); } + + @Test + void shouldThrowErrorWhenPersistenceServiceReturnsIncorrectData() { + AlignmentView alignmentView5 = AlignmentView.Builder.builder().withUniqueId("23TkeyResultkeyResult").withId(20L) + .withTitle("Dies hat kein Gegenstück").withTeamId(5L).withTeamName("Puzzle ITC").withQuarterId(2L) + .withObjectType("keyResult").withConnectionItem("target").withRefId(37L).withRefType("objective") + .build(); + List finalList = List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4, + alignmentView5); + + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)).thenReturn(finalList); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> alignmentBusinessService.getAlignmentsByFilters(2L, List.of(5L), "")); + + List expectedErrors = List + .of(new ErrorDto("ALIGNMENT_DATA_FAIL", List.of("alignmentData", "2", "[5]", ""))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + Assertions.assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } + + @Test + void shouldNotThrowErrorWhenSameAmountOfSourceAndTarget() { + List finalList = List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4); + + assertDoesNotThrow(() -> alignmentBusinessService.validateFinalList(finalList, 2L, List.of(5L), "")); + } + + @Test + void shouldThrowErrorWhenNotSameAmountOfSourceAndTarget() { + List finalList = List.of(alignmentView1, alignmentView2, alignmentView3); + + OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, + () -> alignmentBusinessService.validateFinalList(finalList, 2L, List.of(5L), "")); + + List expectedErrors = List + .of(new ErrorDto("ALIGNMENT_DATA_FAIL", List.of("alignmentData", "2", "[5]", ""))); + + assertEquals(BAD_REQUEST, exception.getStatusCode()); + Assertions.assertThat(expectedErrors).hasSameElementsAs(exception.getErrors()); + assertTrue(TestHelper.getAllErrorKeys(expectedErrors).contains(exception.getReason())); + } } From df61df585da93c5c78b4d4bf31fc858371641efb Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 10 Apr 2024 13:44:36 +0200 Subject: [PATCH 088/119] Add translation for new backend error ALIGNMENT_DATA_FAIL --- frontend/src/assets/i18n/de.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json index a7353961ff..3721d7dd9f 100644 --- a/frontend/src/assets/i18n/de.json +++ b/frontend/src/assets/i18n/de.json @@ -56,7 +56,8 @@ "ILLEGAL_CHANGE_OBJECTIVE_QUARTER": "Element kann nicht in ein anderes Quartal verlegt werden.", "NOT_LINK_YOURSELF": "Das Objective kann nicht auf sich selbst zeigen.", "NOT_LINK_IN_SAME_TEAM": "Das Objective kann nicht auf ein Objekt im selben Team zeigen.", - "ALIGNMENT_ALREADY_EXISTS": "Es existiert bereits ein Alignment ausgehend vom Objective mit der ID {1}." + "ALIGNMENT_ALREADY_EXISTS": "Es existiert bereits ein Alignment ausgehend vom Objective mit der ID {1}.", + "ALIGNMENT_DATA_FAIL": "Es gab ein Problem bei der Zusammenstellung der Daten für das Diagramm mit den Filter Quartal: {1}, Team: {2} und ObjectiveQuery: {3}." }, "SUCCESS": { "TEAM": { From ac185300985692a236ea52f2b5f0c66a403a3635 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 11 Apr 2024 07:59:25 +0200 Subject: [PATCH 089/119] Add unit tests for AlignmentService --- .../shared/services/alignment.service.spec.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/frontend/src/app/shared/services/alignment.service.spec.ts b/frontend/src/app/shared/services/alignment.service.spec.ts index e7778cf7b9..c3d7f558bf 100644 --- a/frontend/src/app/shared/services/alignment.service.spec.ts +++ b/frontend/src/app/shared/services/alignment.service.spec.ts @@ -3,6 +3,8 @@ import { TestBed } from '@angular/core/testing'; import { AlignmentService } from './alignment.service'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { HttpClient } from '@angular/common/http'; +import { of } from 'rxjs'; +import { alignmentLists } from '../testData'; const httpClient = { get: jest.fn(), @@ -22,4 +24,42 @@ describe('AlignmentService', () => { it('should be created', () => { expect(service).toBeTruthy(); }); + + it('should set params of filter correctly without objectiveQuery', (done) => { + jest.spyOn(httpClient, 'get').mockReturnValue(of(alignmentLists)); + service.getAlignmentByFilter(2, [4, 5], '').subscribe((alignmentLists) => { + alignmentLists.alignmentObjectDtoList.forEach((object) => { + expect(object.objectType).toEqual('objective'); + expect(object.objectTeamName).toEqual('Example Team'); + }); + alignmentLists.alignmentConnectionDtoList.forEach((connection) => { + expect(connection.targetKeyResultId).toEqual(null); + expect(connection.alignedObjectiveId).toEqual(1); + expect(connection.targetObjectiveId).toEqual(2); + }); + done(); + }); + expect(httpClient.get).toHaveBeenCalledWith('/api/v2/alignments/alignmentLists', { + params: { quarterFilter: 2, teamFilter: [4, 5] }, + }); + }); + + it('should set params of filter correctly with objectiveQuery', (done) => { + jest.spyOn(httpClient, 'get').mockReturnValue(of(alignmentLists)); + service.getAlignmentByFilter(2, [4, 5], 'objective').subscribe((alignmentLists) => { + alignmentLists.alignmentObjectDtoList.forEach((object) => { + expect(object.objectType).toEqual('objective'); + expect(object.objectTeamName).toEqual('Example Team'); + }); + alignmentLists.alignmentConnectionDtoList.forEach((connection) => { + expect(connection.targetKeyResultId).toEqual(null); + expect(connection.alignedObjectiveId).toEqual(1); + expect(connection.targetObjectiveId).toEqual(2); + }); + done(); + }); + expect(httpClient.get).toHaveBeenCalledWith('/api/v2/alignments/alignmentLists', { + params: { objectiveQuery: 'objective', quarterFilter: 2, teamFilter: [4, 5] }, + }); + }); }); From 2879b33e530caf89d80e2ff94acb5b3537904ef2 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 11 Apr 2024 15:40:23 +0200 Subject: [PATCH 090/119] Adjust height of commit icon in node --- frontend/src/app/diagram/svgGeneration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/diagram/svgGeneration.ts b/frontend/src/app/diagram/svgGeneration.ts index 3d67570fab..c678a217fe 100644 --- a/frontend/src/app/diagram/svgGeneration.ts +++ b/frontend/src/app/diagram/svgGeneration.ts @@ -227,7 +227,7 @@ export function getFailIcon() { export function getCommitIcon() { return ` - + From dd789f37bb94517a072ad9e99fb08990dcbe7c7d Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 12 Apr 2024 08:05:21 +0200 Subject: [PATCH 091/119] Escape html tags in object title --- frontend/cypress/e2e/diagram.cy.ts | 2 ++ frontend/src/app/diagram/diagram.component.spec.ts | 4 ++-- frontend/src/app/diagram/diagram.component.ts | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/cypress/e2e/diagram.cy.ts b/frontend/cypress/e2e/diagram.cy.ts index 4f6049fbe4..dbc62c9414 100644 --- a/frontend/cypress/e2e/diagram.cy.ts +++ b/frontend/cypress/e2e/diagram.cy.ts @@ -64,6 +64,8 @@ describe('OKR diagram e2e tests', () => { cy.focused().realPress('ArrowDown'); cy.getByTestId('quarterFilter').should('contain', 'GJ 23/24-Q4'); + cy.get('canvas').should('have.length', 3); + cy.getByTestId('overviewTab').first().click(); cy.get('mat-chip:visible:contains("/BBT")').should('have.css', 'background-color').and('eq', 'rgb(30, 90, 150)'); diff --git a/frontend/src/app/diagram/diagram.component.spec.ts b/frontend/src/app/diagram/diagram.component.spec.ts index a877edea1d..f2f11b0f85 100644 --- a/frontend/src/app/diagram/diagram.component.spec.ts +++ b/frontend/src/app/diagram/diagram.component.spec.ts @@ -142,10 +142,10 @@ describe('DiagramComponent', () => { it('should replace correct non ascii characters', () => { let specialText: string = - 'Die klügsten Schafe springen über den Zaun und rechnen 2², während die ängstlichen Mäuse sich in ihren Löchern verkriechen und das Gemüß folgend rechnen 3³. Östlich befindet sich Ägypten mit einem Überfluss an Sand.'; + 'Die klügsten Schafe springen über den Zaun und rechnen 2², während die ängstlichen Mäuse sich in ihren Löchern verkriechen und das Gemüß folgend rechnen 3³. Östlich befindet sich Ägypten mit einem Überfluss an Sand. " test'; let correctedText: string = - 'Die kluegsten Schafe springen ueber den Zaun und rechnen 2^2, waehrend die aengstlichen Maeuse sich in ihren Loechern verkriechen und das Gemuess folgend rechnen 3^3. Oestlich befindet sich Aegypten mit einem Ueberfluss an Sand.'; + 'Die kluegsten Schafe springen ueber den Zaun und rechnen 2^2, waehrend die aengstlichen Maeuse sich in ihren Loechern verkriechen und das Gemuess folgend rechnen 3^3. Oestlich befindet sich Aegypten mit einem Ueberfluss an Sand. </svg> " test'; expect(component.replaceNonAsciiCharacters(specialText)).toEqual(correctedText); }); diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index b0a2f332fd..ab2bebe20f 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -247,6 +247,12 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { text = text.replace(/\u00df/g, 'ss'); text = text.replace(/\u00B2/g, '^2'); text = text.replace(/\u00B3/g, '^3'); + text = text.replace(/&/g, '&'); + text = text.replace(//g, '>'); + text = text.replace(/'/g, '''); + text = text.replace(/"/g, '"'); + return text; } From b78be09ba5014c0444d51d941401978376a92a8c Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 22 Apr 2024 16:47:28 +0200 Subject: [PATCH 092/119] Return empty list when empty teamfilter is set --- .../puzzle/okr/service/business/AlignmentBusinessService.java | 4 ++++ .../okr/service/validation/AlignmentValidationService.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index 0f947d4556..21762e15f2 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -165,6 +165,10 @@ private void validateAndDeleteAlignmentById(Long alignmentId) { public AlignmentLists getAlignmentsByFilters(Long quarterFilter, List teamFilter, String objectiveFilter) { alignmentValidationService.validateOnAlignmentGet(quarterFilter, teamFilter); + if (teamFilter.isEmpty()) { + return new AlignmentLists(List.of(), List.of()); + } + List alignmentViewList = alignmentViewPersistenceService .getAlignmentViewListByQuarterId(quarterFilter); DividedAlignmentViewLists dividedAlignmentViewLists = filterAlignmentViews(alignmentViewList, teamFilter, diff --git a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java index c37461d3b6..206a191e05 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java @@ -105,7 +105,7 @@ private void throwExceptionWhenAlignmentWithAlignedObjectiveAlreadyExists(Alignm public void validateOnAlignmentGet(Long quarterId, List teamFilter) { if (quarterId == null) { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NOT_SET, "quarterId"); - } else if (teamFilter == null || teamFilter.isEmpty()) { + } else if (teamFilter == null) { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NOT_SET, "teamFilter"); } } From ccd34b36ff8be5a9fac370bcddd0a766651890e1 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 22 Apr 2024 16:47:58 +0200 Subject: [PATCH 093/119] Do not reload diagram when closing dialog with cancel --- .../app/keyresult-detail/keyresult-detail.component.ts | 10 +++++++--- .../app/objective-detail/objective-detail.component.ts | 7 ++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/keyresult-detail/keyresult-detail.component.ts b/frontend/src/app/keyresult-detail/keyresult-detail.component.ts index 5326c6d430..bbaeb0415f 100644 --- a/frontend/src/app/keyresult-detail/keyresult-detail.component.ts +++ b/frontend/src/app/keyresult-detail/keyresult-detail.component.ts @@ -118,6 +118,8 @@ export class KeyresultDetailComponent implements OnInit { this.refreshDataService.markDataRefresh(); } else if (result?.closeState === CloseState.DELETED) { this.router.navigate(['']).then(() => this.refreshDataService.markDataRefresh()); + } else if (result == '') { + return; } else { this.loadKeyResult(this.keyResult$.getValue().id); } @@ -180,9 +182,11 @@ export class KeyresultDetailComponent implements OnInit { keyResult: this.keyResult$.getValue(), }, }); - dialogRef.afterClosed().subscribe(() => { - this.loadKeyResult(this.keyResult$.getValue().id); - this.refreshDataService.markDataRefresh(); + dialogRef.afterClosed().subscribe((result) => { + if (result != '' && result != undefined) { + this.loadKeyResult(this.keyResult$.getValue().id); + this.refreshDataService.markDataRefresh(); + } }); } diff --git a/frontend/src/app/objective-detail/objective-detail.component.ts b/frontend/src/app/objective-detail/objective-detail.component.ts index 40253b3588..5d3938219b 100644 --- a/frontend/src/app/objective-detail/objective-detail.component.ts +++ b/frontend/src/app/objective-detail/objective-detail.component.ts @@ -65,8 +65,11 @@ export class ObjectiveDetailComponent { .subscribe((result) => { if (result?.openNew) { this.openAddKeyResultDialog(); + } else if (result == '') { + return; + } else { + this.refreshDataService.markDataRefresh(); } - this.refreshDataService.markDataRefresh(); }); } @@ -86,6 +89,8 @@ export class ObjectiveDetailComponent { .subscribe((result) => { if (result.delete) { this.router.navigate(['']); + } else if (result == '') { + return; } else { this.loadObjective(this.objective$.value.id); } From af679ca2a395bf1f8fccfc9d77e34f87c6311e4f Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 22 Apr 2024 16:48:19 +0200 Subject: [PATCH 094/119] Do not change data when click already active diagram tab --- frontend/src/app/overview/overview.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/overview/overview.component.ts b/frontend/src/app/overview/overview.component.ts index 6aa16645f6..31f855cfa0 100644 --- a/frontend/src/app/overview/overview.component.ts +++ b/frontend/src/app/overview/overview.component.ts @@ -104,10 +104,10 @@ export class OverviewComponent implements OnInit, OnDestroy { } switchPage(input: string) { - if (input == 'diagram') { + if (input == 'diagram' && this.isOverview) { this.isOverview = false; this.loadOverviewWithParams(); - } else { + } else if (input == 'overview' && !this.isOverview) { this.isOverview = true; this.loadOverviewWithParams(); } From 7e94b3b7f0e5678bed5f3391ff9731107a1c4646 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 24 Apr 2024 15:09:24 +0200 Subject: [PATCH 095/119] Fix backend unit tests and e2e tests --- .../AlignmentValidationServiceTest.java | 10 +++------- frontend/cypress/e2e/checkIn.cy.ts | 16 ++++++++++++++++ frontend/cypress/e2e/scoring.cy.ts | 1 + .../check-in-form/check-in-form.component.ts | 4 +++- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java index 6346b10f41..27b297399e 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java @@ -25,6 +25,7 @@ import static ch.puzzle.okr.models.State.DRAFT; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.Mockito.*; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static ch.puzzle.okr.TestConstants.*; @@ -453,12 +454,7 @@ void validateOnAlignmentGetShouldThrowExceptionWhenTeamFilterIsNull() { } @Test - void validateOnAlignmentGetShouldThrowExceptionWhenTeamFilterIsEmpty() { - OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> validator.validateOnAlignmentGet(2L, emptyLongList)); - - assertEquals(BAD_REQUEST, exception.getStatusCode()); - assertEquals("ATTRIBUTE_NOT_SET", exception.getReason()); - assertEquals(List.of(new ErrorDto("ATTRIBUTE_NOT_SET", List.of("teamFilter"))), exception.getErrors()); + void validateOnAlignmentGetShouldNotThrowExceptionWhenTeamFilterIsEmpty() { + assertDoesNotThrow(() -> validator.validateOnAlignmentGet(2L, emptyLongList)); } } diff --git a/frontend/cypress/e2e/checkIn.cy.ts b/frontend/cypress/e2e/checkIn.cy.ts index e3df4724d0..33e5dfc368 100644 --- a/frontend/cypress/e2e/checkIn.cy.ts +++ b/frontend/cypress/e2e/checkIn.cy.ts @@ -32,6 +32,8 @@ describe('OKR Check-in e2e tests', () => { checkForDialogTextMetric(); cy.fillOutCheckInMetric(30, 6, 'We bought a new house', 'We have to buy more PCs'); + cy.wait(1000); + cy.contains('30%'); cy.contains('6/10'); cy.contains('Letztes Check-in (' + getCurrentDate() + ')'); @@ -62,6 +64,8 @@ describe('OKR Check-in e2e tests', () => { checkForDialogTextMetric(); cy.fillOutCheckInMetric(30, 0, 'We bought a new house', 'We have to buy more PCs'); + cy.wait(100); + cy.contains('30%'); cy.contains('6/10'); cy.contains('Letztes Check-in (' + getCurrentDate() + ')'); @@ -92,6 +96,8 @@ describe('OKR Check-in e2e tests', () => { checkForDialogTextMetric(); cy.fillOutCheckInMetric(5, 5, null, null); + cy.wait(500); + cy.contains('5%'); cy.contains('!'); cy.contains('5/10'); @@ -124,6 +130,8 @@ describe('OKR Check-in e2e tests', () => { checkForDialogTextOrdinal(); cy.fillOutCheckInOrdinal(1, 6, 'There is a new car', 'Buy now a new pool'); + cy.wait(500); + cy.contains('6/10'); cy.contains('There is a new car'); cy.contains('Letztes Check-in (' + getCurrentDate() + ')'); @@ -154,6 +162,8 @@ describe('OKR Check-in e2e tests', () => { cy.getByTestId('add-check-in').first().click(); cy.fillOutCheckInMetric(50, 6, 'This was a good idea', 'Will be difficult'); + cy.wait(500); + cy.getByTestId('show-all-checkins').click(); cy.wait(500); @@ -191,6 +201,9 @@ describe('OKR Check-in e2e tests', () => { cy.getByTestId('add-check-in').first().click(); cy.fillOutCheckInMetric(30, 5, 'Here we are', 'A cat would be great'); + + cy.wait(500); + cy.contains('Aktuell: CHF 30.-'); cy.getByTestId('show-all-checkins').click(); @@ -236,6 +249,9 @@ describe('OKR Check-in e2e tests', () => { cy.getByTestId('keyresult').contains('For editing ordinal checkin').click(); cy.getByTestId('add-check-in').first().click(); cy.fillOutCheckInOrdinal(0, 6, 'There is a new car', 'Buy now a new pool'); + + cy.wait(500); + cy.getByTestId('show-all-checkins').click(); cy.wait(500); diff --git a/frontend/cypress/e2e/scoring.cy.ts b/frontend/cypress/e2e/scoring.cy.ts index 1a22f91596..82d5cc6b72 100644 --- a/frontend/cypress/e2e/scoring.cy.ts +++ b/frontend/cypress/e2e/scoring.cy.ts @@ -78,6 +78,7 @@ describe('Scoring component e2e tests', () => { function setupMetricKR(baseline: number, stretchgoal: number, value: number) { cy.createMetricKeyresult('Metric scoring keyresult', String(baseline), String(stretchgoal)); + cy.wait(500); cy.getByTestId('keyresult').get(':contains("Metric scoring keyresult")').last().click(); cy.getByTestId('add-check-in').click(); cy.getByTestId('check-in-metric-value').clear().type(String(value)); diff --git a/frontend/src/app/shared/dialog/checkin/check-in-form/check-in-form.component.ts b/frontend/src/app/shared/dialog/checkin/check-in-form/check-in-form.component.ts index c6955bd251..0f43157a9e 100644 --- a/frontend/src/app/shared/dialog/checkin/check-in-form/check-in-form.component.ts +++ b/frontend/src/app/shared/dialog/checkin/check-in-form/check-in-form.component.ts @@ -98,7 +98,9 @@ export class CheckInFormComponent implements OnInit { this.checkInService.saveCheckIn(checkIn).subscribe(() => { this.actionService.updateActions(this.dialogForm.value.actionList!).subscribe(() => { - this.dialogRef.close(); + this.dialogRef.close({ + checkIn: checkIn + }); }); }); } From d135b88e274ede64f8e4c9d1aa08b8c160141cd4 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 24 Apr 2024 15:15:21 +0200 Subject: [PATCH 096/119] Remove reload of diagram when closing dialog --- .../src/app/objective-detail/objective-detail.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/objective-detail/objective-detail.component.ts b/frontend/src/app/objective-detail/objective-detail.component.ts index 5d3938219b..69fbc42ea0 100644 --- a/frontend/src/app/objective-detail/objective-detail.component.ts +++ b/frontend/src/app/objective-detail/objective-detail.component.ts @@ -65,7 +65,7 @@ export class ObjectiveDetailComponent { .subscribe((result) => { if (result?.openNew) { this.openAddKeyResultDialog(); - } else if (result == '') { + } else if (result == '' || result == undefined) { return; } else { this.refreshDataService.markDataRefresh(); @@ -89,7 +89,7 @@ export class ObjectiveDetailComponent { .subscribe((result) => { if (result.delete) { this.router.navigate(['']); - } else if (result == '') { + } else if (result == '' || result == undefined) { return; } else { this.loadObjective(this.objective$.value.id); From 8902ae6c1cfb892148409176973cd1ff9333e2de Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 29 Apr 2024 06:05:06 +0000 Subject: [PATCH 097/119] [FM] Automated formating frontend --- .../dialog/checkin/check-in-form/check-in-form.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/shared/dialog/checkin/check-in-form/check-in-form.component.ts b/frontend/src/app/shared/dialog/checkin/check-in-form/check-in-form.component.ts index 0f43157a9e..c164ab74eb 100644 --- a/frontend/src/app/shared/dialog/checkin/check-in-form/check-in-form.component.ts +++ b/frontend/src/app/shared/dialog/checkin/check-in-form/check-in-form.component.ts @@ -99,7 +99,7 @@ export class CheckInFormComponent implements OnInit { this.checkInService.saveCheckIn(checkIn).subscribe(() => { this.actionService.updateActions(this.dialogForm.value.actionList!).subscribe(() => { this.dialogRef.close({ - checkIn: checkIn + checkIn: checkIn, }); }); }); From a72f2c00f7452684f69f7488e6fbeac901767bec Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 29 Apr 2024 11:27:40 +0200 Subject: [PATCH 098/119] Use current quarter for alignments when no quarterFilter is set --- .../business/AlignmentBusinessService.java | 10 +++- .../AlignmentValidationService.java | 25 +++++++--- .../business/AlignmentBusinessServiceIT.java | 12 ++++- .../AlignmentBusinessServiceTest.java | 39 ++++++++++++++++ .../AlignmentValidationServiceTest.java | 46 +++++++++---------- .../quarter-filter.component.spec.ts | 6 +-- .../quarter-filter.component.ts | 4 +- 7 files changed, 100 insertions(+), 42 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index 21762e15f2..19747324a7 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -34,17 +34,20 @@ public class AlignmentBusinessService { private final ObjectivePersistenceService objectivePersistenceService; private final KeyResultPersistenceService keyResultPersistenceService; private final AlignmentViewPersistenceService alignmentViewPersistenceService; + private final QuarterBusinessService quarterBusinessService; public AlignmentBusinessService(AlignmentPersistenceService alignmentPersistenceService, AlignmentValidationService alignmentValidationService, ObjectivePersistenceService objectivePersistenceService, KeyResultPersistenceService keyResultPersistenceService, - AlignmentViewPersistenceService alignmentViewPersistenceService) { + AlignmentViewPersistenceService alignmentViewPersistenceService, + QuarterBusinessService quarterBusinessService) { this.alignmentPersistenceService = alignmentPersistenceService; this.alignmentValidationService = alignmentValidationService; this.objectivePersistenceService = objectivePersistenceService; this.keyResultPersistenceService = keyResultPersistenceService; this.alignmentViewPersistenceService = alignmentViewPersistenceService; + this.quarterBusinessService = quarterBusinessService; } protected record DividedAlignmentViewLists(List correctAlignments, @@ -163,6 +166,10 @@ private void validateAndDeleteAlignmentById(Long alignmentId) { } public AlignmentLists getAlignmentsByFilters(Long quarterFilter, List teamFilter, String objectiveFilter) { + if (Objects.isNull(quarterFilter)) { + quarterFilter = quarterBusinessService.getCurrentQuarter().getId(); + } + teamFilter = teamFilter == null ? List.of() : teamFilter; alignmentValidationService.validateOnAlignmentGet(quarterFilter, teamFilter); if (teamFilter.isEmpty()) { @@ -175,7 +182,6 @@ public AlignmentLists getAlignmentsByFilters(Long quarterFilter, List team objectiveFilter); List finalList = getAlignmentCounterpart(dividedAlignmentViewLists); - validateFinalList(finalList, quarterFilter, teamFilter, objectiveFilter); return generateAlignmentLists(finalList); diff --git a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java index 206a191e05..5107841af2 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java @@ -21,12 +21,16 @@ public class AlignmentValidationService private final AlignmentPersistenceService alignmentPersistenceService; private final TeamPersistenceService teamPersistenceService; + private final QuarterValidationService quarterValidationService; + private final TeamValidationService teamValidationService; - public AlignmentValidationService(AlignmentPersistenceService alignmentPersistenceService, - TeamPersistenceService teamPersistenceService) { + public AlignmentValidationService(AlignmentPersistenceService alignmentPersistenceService, TeamPersistenceService teamPersistenceService, + QuarterValidationService quarterValidationService, TeamValidationService teamValidationService) { super(alignmentPersistenceService); this.alignmentPersistenceService = alignmentPersistenceService; this.teamPersistenceService = teamPersistenceService; + this.quarterValidationService = quarterValidationService; + this.teamValidationService = teamValidationService; } @Override @@ -103,10 +107,17 @@ private void throwExceptionWhenAlignmentWithAlignedObjectiveAlreadyExists(Alignm } public void validateOnAlignmentGet(Long quarterId, List teamFilter) { - if (quarterId == null) { - throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NOT_SET, "quarterId"); - } else if (teamFilter == null) { - throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NOT_SET, "teamFilter"); - } + validateQuarter(quarterId); + teamFilter.forEach(this::validateTeam); + } + + public void validateTeam(Long id) { + teamValidationService.validateOnGet(id); + teamValidationService.doesEntityExist(id); + } + + public void validateQuarter(Long id) { + quarterValidationService.validateOnGet(id); + quarterValidationService.doesEntityExist(id); } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java index 3da6a169c7..6fd60fbec5 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java @@ -113,10 +113,18 @@ void shouldReturnEmptyAlignmentDataWhenNoMatchingObjectiveSearch() { } @Test - void shouldReturnEmptyAlignmentDataWhenNoMatchingQuarterFilter() { - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(311L, List.of(4L, 5L, 6L, 8L), + void shouldReturnCorrectAlignmentDataWhenEmptyQuarterFilter() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(null, List.of(4L, 5L, 6L, 8L), ""); + assertEquals(8, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(5, alignmentLists.alignmentConnectionDtoList().size()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenEmptyTeamFilter() { + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, null, ""); + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); } diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java index 86ffa85096..6babddba37 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -6,6 +6,7 @@ import ch.puzzle.okr.dto.alignment.AlignedEntityDto; import ch.puzzle.okr.exception.OkrResponseStatusException; import ch.puzzle.okr.models.Objective; +import ch.puzzle.okr.models.Quarter; import ch.puzzle.okr.models.alignment.Alignment; import ch.puzzle.okr.models.alignment.AlignmentView; import ch.puzzle.okr.models.alignment.KeyResultAlignment; @@ -43,6 +44,8 @@ class AlignmentBusinessServiceTest { @Mock AlignmentViewPersistenceService alignmentViewPersistenceService; @Mock + QuarterBusinessService quarterBusinessService; + @Mock AlignmentValidationService validator; @InjectMocks private AlignmentBusinessService alignmentBusinessService; @@ -65,6 +68,7 @@ class AlignmentBusinessServiceTest { .withAlignedObjective(objective2).withTargetObjective(objective1).build(); KeyResultAlignment keyResultAlignment = KeyResultAlignment.Builder.builder().withId(6L) .withAlignedObjective(objective3).withTargetKeyResult(metricKeyResult).build(); + Quarter quarter = Quarter.Builder.builder().withId(2L).withLabel("GJ 23/24-Q1").build(); AlignmentView alignmentView1 = AlignmentView.Builder.builder().withUniqueId("45TkeyResultkeyResult").withId(4L) .withTitle("Antwortzeit für Supportanfragen um 33% verkürzen.").withTeamId(5L).withTeamName("Puzzle ITC") @@ -355,6 +359,41 @@ void shouldReturnEmptyAlignmentDataWhenNoMatchingTeam() { assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); } + @Test + void shouldReturnEmptyAlignmentDataWhenNoTeamFilterProvided() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, null, ""); + + verify(alignmentViewPersistenceService, times(0)).getAlignmentViewListByQuarterId(2L); + verify(validator, times(1)).validateOnAlignmentGet(2L, List.of()); + assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); + } + + @Test + void shouldReturnEmptyAlignmentDataWhenNoQuarterFilterProvided() { + doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); + when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(any())) + .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); + when(quarterBusinessService.getCurrentQuarter()).thenReturn(quarter); + + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(null, List.of(4L, 6L), ""); + + verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); + verify(quarterBusinessService, times(1)).getCurrentQuarter(); + assertEquals(2, alignmentLists.alignmentConnectionDtoList().size()); + assertEquals(4, alignmentLists.alignmentObjectDtoList().size()); + assertEquals(5L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); + assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); + assertEquals(5L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); + assertEquals(4L, alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); + assertEquals(41L, alignmentLists.alignmentConnectionDtoList().get(1).alignedObjectiveId()); + assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(1).targetObjectiveId()); + assertNull(alignmentLists.alignmentConnectionDtoList().get(1).targetKeyResultId()); + } + @Test void shouldReturnCorrectAlignmentDataWithObjectiveSearch() { doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); diff --git a/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java index 27b297399e..80c1803f2c 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/validation/AlignmentValidationServiceTest.java @@ -25,7 +25,6 @@ import static ch.puzzle.okr.models.State.DRAFT; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.Mockito.*; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static ch.puzzle.okr.TestConstants.*; @@ -37,6 +36,10 @@ class AlignmentValidationServiceTest { AlignmentPersistenceService alignmentPersistenceService; @Mock TeamPersistenceService teamPersistenceService; + @Mock + QuarterValidationService quarterValidationService; + @Mock + TeamValidationService teamValidationService; @Spy @InjectMocks private AlignmentValidationService validator; @@ -55,7 +58,9 @@ class AlignmentValidationServiceTest { .withTargetObjective(objective1).build(); KeyResultAlignment keyResultAlignment = KeyResultAlignment.Builder.builder().withId(6L) .withAlignedObjective(objective3).withTargetKeyResult(metricKeyResult).build(); - List emptyLongList = List.of(); + Long quarterId = 1L; + Long teamId = 1L; + List teamIds = List.of(1L, 2L, 3L, 4L); @BeforeEach void setUp() { @@ -427,34 +432,25 @@ void validateOnDeleteShouldThrowExceptionIfAlignmentIdIsNull() { } @Test - void validateOnAlignmentGetShouldBeSuccessfulWhenQuarterIdAndTeamFilterSet() { - validator.validateOnAlignmentGet(2L, List.of(4L, 5L)); - - verify(validator, times(1)).validateOnAlignmentGet(2L, List.of(4L, 5L)); - } - - @Test - void validateOnAlignmentGetShouldThrowExceptionWhenQuarterIdIsNull() { - OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> validator.validateOnAlignmentGet(null, emptyLongList)); - - assertEquals(BAD_REQUEST, exception.getStatusCode()); - assertEquals("ATTRIBUTE_NOT_SET", exception.getReason()); - assertEquals(List.of(new ErrorDto("ATTRIBUTE_NOT_SET", List.of("quarterId"))), exception.getErrors()); + void validateOnGetShouldCallQuarterValidator() { + validator.validateQuarter(quarterId); + verify(quarterValidationService, times(1)).validateOnGet(quarterId); + verify(quarterValidationService, times(1)).doesEntityExist(quarterId); } @Test - void validateOnAlignmentGetShouldThrowExceptionWhenTeamFilterIsNull() { - OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> validator.validateOnAlignmentGet(2L, null)); - - assertEquals(BAD_REQUEST, exception.getStatusCode()); - assertEquals("ATTRIBUTE_NOT_SET", exception.getReason()); - assertEquals(List.of(new ErrorDto("ATTRIBUTE_NOT_SET", List.of("teamFilter"))), exception.getErrors()); + void validateOnGetShouldCallTeamValidator() { + validator.validateTeam(teamId); + verify(teamValidationService, times(1)).validateOnGet(teamId); + verify(teamValidationService, times(1)).doesEntityExist(teamId); } @Test - void validateOnAlignmentGetShouldNotThrowExceptionWhenTeamFilterIsEmpty() { - assertDoesNotThrow(() -> validator.validateOnAlignmentGet(2L, emptyLongList)); + void validateOnGetShouldCallQuarterValidatorAndTeamValidator() { + validator.validateOnAlignmentGet(quarterId, teamIds); + verify(quarterValidationService, times(1)).validateOnGet(quarterId); + verify(quarterValidationService, times(1)).doesEntityExist(quarterId); + verify(teamValidationService, times(teamIds.size())).validateOnGet(anyLong()); + verify(teamValidationService, times(teamIds.size())).doesEntityExist(anyLong()); } } diff --git a/frontend/src/app/quarter-filter/quarter-filter.component.spec.ts b/frontend/src/app/quarter-filter/quarter-filter.component.spec.ts index 784bbd7669..9a578a6310 100644 --- a/frontend/src/app/quarter-filter/quarter-filter.component.spec.ts +++ b/frontend/src/app/quarter-filter/quarter-filter.component.spec.ts @@ -1,4 +1,4 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed, tick } from '@angular/core/testing'; import { QuarterFilterComponent } from './quarter-filter.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { OverviewService } from '../shared/services/overview.service'; @@ -8,6 +8,7 @@ import { Quarter } from '../shared/types/model/Quarter'; import { QuarterService } from '../shared/services/quarter.service'; import { RouterTestingHarness, RouterTestingModule } from '@angular/router/testing'; import { FormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -74,8 +75,7 @@ describe('QuarterFilterComponent', () => { fixture.detectChanges(); expect(component.quarterId).toBe(quarters[2].id); expect(await quarterSelect.getValueText()).toBe(quarters[2].label + ' Aktuell'); - expect(component.changeDisplayedQuarter).toHaveBeenCalledTimes(1); - expect(router.url).toBe('/?quarter=' + quarters[2].id); + expect(component.changeDisplayedQuarter).toHaveBeenCalledTimes(0); }); it('should set correct value in form according to route param', async () => { diff --git a/frontend/src/app/quarter-filter/quarter-filter.component.ts b/frontend/src/app/quarter-filter/quarter-filter.component.ts index d7c393b8e9..10168b7bab 100644 --- a/frontend/src/app/quarter-filter/quarter-filter.component.ts +++ b/frontend/src/app/quarter-filter/quarter-filter.component.ts @@ -36,9 +36,7 @@ export class QuarterFilterComponent implements OnInit { if (quarterQuery !== undefined) { this.changeDisplayedQuarter(); } else { - this.router - .navigate([], { queryParams: { quarter: this.quarterId } }) - .then(() => this.refreshDataService.quarterFilterReady.next()); + this.refreshDataService.quarterFilterReady.next(); } } const quarterLabel = quarters.find((e) => e.id == this.quarterId)?.label || ''; From b6bfdd8803a5042983e15d01ea9284a53755b514 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 6 May 2024 14:05:59 +0200 Subject: [PATCH 099/119] Rename diagramm to network and k to kr --- frontend/cypress/e2e/diagram.cy.ts | 4 ++-- frontend/src/app/overview/overview.component.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/cypress/e2e/diagram.cy.ts b/frontend/cypress/e2e/diagram.cy.ts index dbc62c9414..c708d76c36 100644 --- a/frontend/cypress/e2e/diagram.cy.ts +++ b/frontend/cypress/e2e/diagram.cy.ts @@ -13,7 +13,7 @@ describe('OKR diagram e2e tests', () => { cy.get('h1:contains(Puzzle ITC)').should('have.length', 1); cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); cy.contains('Overview'); - cy.contains('Diagramm'); + cy.contains('Network'); cy.contains('An Objective for Testing'); cy.getByTestId('diagramTab').first().click(); @@ -34,7 +34,7 @@ describe('OKR diagram e2e tests', () => { cy.get('h1:contains(Puzzle ITC)').should('have.length', 1); cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); cy.contains('Overview'); - cy.contains('Diagramm'); + cy.contains('Network'); cy.contains('An Objective for Testing'); cy.getByTestId('quarterFilter').should('contain', 'GJ 24/25-Q1'); cy.get('mat-chip:visible:contains("Puzzle ITC")') diff --git a/frontend/src/app/overview/overview.component.html b/frontend/src/app/overview/overview.component.html index 2880c2e621..bafff3494d 100644 --- a/frontend/src/app/overview/overview.component.html +++ b/frontend/src/app/overview/overview.component.html @@ -25,7 +25,7 @@ [attr.data-testId]="'diagramTab'" tabindex="0" > - Diagramm + Network
From 9fe9e7de456f1ff339363dadef0285e792aa36b9 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 6 May 2024 15:50:24 +0200 Subject: [PATCH 100/119] Add wheel sensitivity for diagram --- frontend/src/app/diagram/diagram.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index ab2bebe20f..de922607b8 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -67,6 +67,7 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { zoom: 1, zoomingEnabled: true, userZoomingEnabled: true, + wheelSensitivity: 0.3, style: [ { From 99f769633a649a743ea2d9319faa6e3b644709d2 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 23 May 2024 12:51:55 +0200 Subject: [PATCH 101/119] Keep diagram on tab switch alive --- frontend/src/app/diagram/diagram.component.ts | 10 +++++-- .../src/app/overview/overview.component.html | 30 ++++++++++--------- .../src/app/overview/overview.component.scss | 4 +++ 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index de922607b8..b641736ab3 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -32,6 +32,7 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { cy!: cytoscape.Core; diagramData: any[] = []; emptyDiagramData: boolean = false; + alignmentDataCache: AlignmentLists | null = null; constructor( private keyResultService: KeyresultService, @@ -49,9 +50,12 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { ngAfterViewInit() { this.alignmentData.subscribe((alignmentData: AlignmentLists): void => { - this.diagramData = []; - this.cleanUpDiagram(); - this.prepareDiagramData(alignmentData); + if (JSON.stringify(this.alignmentDataCache) !== JSON.stringify(alignmentData)) { + this.alignmentDataCache = alignmentData; + this.diagramData = []; + this.cleanUpDiagram(); + this.prepareDiagramData(alignmentData); + } }); } diff --git a/frontend/src/app/overview/overview.component.html b/frontend/src/app/overview/overview.component.html index bafff3494d..bdb255d991 100644 --- a/frontend/src/app/overview/overview.component.html +++ b/frontend/src/app/overview/overview.component.html @@ -31,20 +31,22 @@ -
- - -
-

Kein Team ausgewählt

- +
+
+ + +
+

Kein Team ausgewählt

+ +
-
- - - +
+ +
+
diff --git a/frontend/src/app/overview/overview.component.scss b/frontend/src/app/overview/overview.component.scss index 13615c285c..f327dbff74 100644 --- a/frontend/src/app/overview/overview.component.scss +++ b/frontend/src/app/overview/overview.component.scss @@ -56,3 +56,7 @@ .diagram { width: 100px; } + +.hidden { + display: none; +} From eeca6732169646c66fc3b4fd6a05c722f92be4b1 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 23 May 2024 13:26:28 +0200 Subject: [PATCH 102/119] Remove icon on kr bubble --- frontend/src/app/diagram/diagram.component.ts | 12 +-- frontend/src/app/diagram/svgGeneration.ts | 92 +------------------ 2 files changed, 6 insertions(+), 98 deletions(-) diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index b641736ab3..998318b0f4 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -6,14 +6,10 @@ import { generateKeyResultSVG, generateNeutralKeyResultSVG, generateObjectiveSVG, - getCommitIcon, getDraftIcon, - getFailIcon, getNotSuccessfulIcon, getOnGoingIcon, - getStretchIcon, getSuccessfulIcon, - getTargetIcon, } from './svgGeneration'; import { KeyresultService } from '../shared/services/keyresult.service'; import { KeyResult } from '../shared/types/model/KeyResult'; @@ -277,13 +273,13 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { generateKeyResultSVG(title: string, teamName: string, state: string | undefined): string { switch (state) { case 'FAIL': - return generateKeyResultSVG(title, teamName, getFailIcon, '#BA3838', 'white'); + return generateKeyResultSVG(title, teamName, '#BA3838', 'white'); case 'COMMIT': - return generateKeyResultSVG(title, teamName, getCommitIcon, '#FFD600', 'black'); + return generateKeyResultSVG(title, teamName, '#FFD600', 'black'); case 'TARGET': - return generateKeyResultSVG(title, teamName, getTargetIcon, '#1E8A29', 'black'); + return generateKeyResultSVG(title, teamName, '#1E8A29', 'black'); case 'STRETCH': - return generateKeyResultSVG(title, teamName, getStretchIcon, '#1E5A96', 'white'); + return generateKeyResultSVG(title, teamName, '#1E5A96', 'white'); default: return generateNeutralKeyResultSVG(title, teamName); } diff --git a/frontend/src/app/diagram/svgGeneration.ts b/frontend/src/app/diagram/svgGeneration.ts index c678a217fe..f4c03e89ae 100644 --- a/frontend/src/app/diagram/svgGeneration.ts +++ b/frontend/src/app/diagram/svgGeneration.ts @@ -51,13 +51,7 @@ export function generateObjectiveSVG(title: string, teamName: string, iconFuncti return 'data:image/svg+xml;base64,' + btoa(svg); } -export function generateKeyResultSVG( - title: string, - teamName: string, - iconFunction: any, - backgroundColor: any, - fontColor: any, -) { +export function generateKeyResultSVG(title: string, teamName: string, backgroundColor: any, fontColor: any) { let svg = ` @@ -70,9 +64,7 @@ export function generateKeyResultSVG( - ${iconFunction} - - +

@@ -204,83 +196,3 @@ export function getNotSuccessfulIcon() { `; } - -export function getFailIcon() { - return ` - - - - - - - - - - - - - - - -`; -} - -export function getCommitIcon() { - return ` - - - - - - - - - - - - - - - -`; -} - -export function getTargetIcon() { - return ` - - - - - - - - - - - - - - - -`; -} - -export function getStretchIcon() { - return ` - - - - - - - - - - - - - - - -`; -} From 9fb6cb8ab6822e5bd96d9de828c6041dbc34973f Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 23 May 2024 13:27:41 +0200 Subject: [PATCH 103/119] Adjust quarter fk in newquarterdata --- .../V2_0_99__newQuarterData.sql | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql b/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql index 8fdaec29ab..9aee34b4e2 100644 --- a/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql +++ b/backend/src/main/resources/db/data-migration/V2_0_99__newQuarterData.sql @@ -34,13 +34,13 @@ values (19, 1, 'Lorem Ipsum sit amet diri guru humu saguri alam apmach helum di 'Wing Wang Tala Tala Ting Tang', 1, 7, 6, 'DRAFT', null, '2023-10-02 09:08:40.000000'), (21, 1, 'Lorem Ipsum sit amet diri guru humu saguri alam apmach helum di gau', '2023-10-02 13:07:09.000000', 'Ting Tang Wala Wala Bing Bang', 1, 7, 6, 'DRAFT', null, '2023-10-02 09:07:39.000000'), - (40,1,'', '2024-04-04 13:45:13.000000','Wir wollen eine gute Mitarbeiterzufriedenheit.', 1, 9, 5, 'ONGOING', null,'2024-04-04 13:44:52.000000'), - (41,1,'','2024-04-04 13:59:06.511620','Das Projekt generiert 10000 CHF Umsatz',1,9,5,'ONGOING',null,'2024-04-04 13:59:06.523496'), - (42,1,'','2024-04-04 13:59:40.835896','Die Lehrlinge sollen Freude haben',1,9,4,'ONGOING',null,'2024-04-04 13:59:40.848992'), - (43,1,'','2024-04-04 14:00:05.586152','Der Firmenumsatz steigt',1,9,5,'ONGOING',null,'2024-04-04 14:00:05.588509'), - (44,1,'','2024-04-04 14:00:28.221906','Die Members sollen gerne zur Arbeit kommen',1,9,6,'ONGOING',null,'2024-04-04 14:00:28.229058'), - (45,1,'','2024-04-04 14:00:47.659884','Unsere Designer äussern sich zufrieden',1,9,8,'ONGOING',null,'2024-04-04 14:00:47.664414'), - (46,1,'','2024-04-04 14:00:57.485887','Unsere Designer kommen gerne zur Arbeit',1,9,8,'ONGOING',null,'2024-04-04 14:00:57.494192'); + (40,1,'', '2024-04-04 13:45:13.000000','Wir wollen eine gute Mitarbeiterzufriedenheit.', 1, 6, 5, 'ONGOING', null,'2024-04-04 13:44:52.000000'), + (41,1,'','2024-04-04 13:59:06.511620','Das Projekt generiert 10000 CHF Umsatz',1,6,5,'ONGOING',null,'2024-04-04 13:59:06.523496'), + (42,1,'','2024-04-04 13:59:40.835896','Die Lehrlinge sollen Freude haben',1,6,4,'ONGOING',null,'2024-04-04 13:59:40.848992'), + (43,1,'','2024-04-04 14:00:05.586152','Der Firmenumsatz steigt',1,6,5,'ONGOING',null,'2024-04-04 14:00:05.588509'), + (44,1,'','2024-04-04 14:00:28.221906','Die Members sollen gerne zur Arbeit kommen',1,6,6,'ONGOING',null,'2024-04-04 14:00:28.229058'), + (45,1,'','2024-04-04 14:00:47.659884','Unsere Designer äussern sich zufrieden',1,6,8,'ONGOING',null,'2024-04-04 14:00:47.664414'), + (46,1,'','2024-04-04 14:00:57.485887','Unsere Designer kommen gerne zur Arbeit',1,6,8,'ONGOING',null,'2024-04-04 14:00:57.494192'); insert into key_result (id, version, baseline, description, modified_on, stretch_goal, title, created_by_id, objective_id, owner_id, unit, key_result_type, created_on, commit_zone, target_zone, @@ -210,8 +210,8 @@ values (1, 4, 'objective', null, 6, 0), (5, 10, 'keyResult', 5, null, 0), (6, 5, 'keyResult', 4, null, 0), (7, 6, 'keyResult', 3, null, 0), - (8, 41, 'objective', null, 40, 0), +-- (8, 41, 'objective', null, 40, 0), (9, 42, 'objective', null, 40, 0), - (10, 43, 'keyResult', 40, null, 0), + (10, 43, 'keyResult', 41, null, 0), (11, 44, 'objective', null, 42, 0), - (12, 45, 'keyResult', 41, null, 0); + (12, 45, 'keyResult', 40, null, 0); From 52f67f2ab8be0497da82385ae89b932bec943039 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 30 May 2024 15:36:09 +0200 Subject: [PATCH 104/119] Adjust testdata after rebase --- .../objective-form.component.spec.ts | 22 +++++++++---------- frontend/src/app/shared/testData.ts | 10 ++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index c117fcb782..f35c5c6993 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -11,11 +11,11 @@ import { ReactiveFormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ObjectiveService } from '../../services/objective.service'; import { - alignmentObject1, - alignmentObject2, - alignmentObject3, alignmentPossibility1, alignmentPossibility2, + alignmentPossibilityObject1, + alignmentPossibilityObject2, + alignmentPossibilityObject3, objective, objectiveWithAlignment, quarter, @@ -230,7 +230,7 @@ describe('ObjectiveDialogComponent', () => { description: 'Test description', quarter: 0, team: 0, - alignment: alignmentObject2, + alignment: alignmentPossibilityObject2, createKeyResults: false, }); @@ -258,7 +258,7 @@ describe('ObjectiveDialogComponent', () => { description: 'Test description', quarter: 0, team: 0, - alignment: alignmentObject3, + alignment: alignmentPossibilityObject3, createKeyResults: false, }); @@ -316,7 +316,7 @@ describe('ObjectiveDialogComponent', () => { description: 'Test description', quarter: 1, team: 1, - alignment: alignmentObject3, + alignment: alignmentPossibilityObject3, createKeyResults: false, }); @@ -376,7 +376,7 @@ describe('ObjectiveDialogComponent', () => { expect(rawFormValue.description).toBe(objectiveWithAlignment.description); expect(rawFormValue.team).toBe(objectiveWithAlignment.teamId); expect(rawFormValue.quarter).toBe(objectiveWithAlignment.quarterId); - expect(rawFormValue.alignment).toBe(alignmentObject2); + expect(rawFormValue.alignment).toBe(alignmentPossibilityObject2); }); it('should return correct value if allowed to save to backlog', async () => { @@ -552,7 +552,7 @@ describe('ObjectiveDialogComponent', () => { let modifiedAlignmentPossibility: AlignmentPossibility = { teamId: 1, teamName: 'Puzzle ITC', - alignmentObjects: [alignmentObject3], + alignmentObjects: [alignmentPossibilityObject3], }; expect(component.filteredAlignmentOptions$.getValue()).toEqual([modifiedAlignmentPossibility]); @@ -563,7 +563,7 @@ describe('ObjectiveDialogComponent', () => { modifiedAlignmentPossibility = { teamId: 1, teamName: 'Puzzle ITC', - alignmentObjects: [alignmentObject2, alignmentObject3], + alignmentObjects: [alignmentPossibilityObject2, alignmentPossibilityObject3], }; expect(component.filteredAlignmentOptions$.getValue()).toEqual([modifiedAlignmentPossibility]); @@ -575,12 +575,12 @@ describe('ObjectiveDialogComponent', () => { { teamId: 1, teamName: 'Puzzle ITC', - alignmentObjects: [alignmentObject3], + alignmentObjects: [alignmentPossibilityObject3], }, { teamId: 2, teamName: 'We are cube', - alignmentObjects: [alignmentObject1], + alignmentObjects: [alignmentPossibilityObject1], }, ]; expect(component.filteredAlignmentOptions$.getValue()).toEqual(modifiedAlignmentPossibilities); diff --git a/frontend/src/app/shared/testData.ts b/frontend/src/app/shared/testData.ts index 4639348559..52e16248d0 100644 --- a/frontend/src/app/shared/testData.ts +++ b/frontend/src/app/shared/testData.ts @@ -636,19 +636,19 @@ export const keyResultActions: KeyResultMetric = { writeable: true, }; -export const alignmentObject1: AlignmentPossibilityObject = { +export const alignmentPossibilityObject1: AlignmentPossibilityObject = { objectId: 1, objectTitle: 'We want to increase the income puzzle buy', objectType: 'objective', }; -export const alignmentObject2: AlignmentPossibilityObject = { +export const alignmentPossibilityObject2: AlignmentPossibilityObject = { objectId: 2, objectTitle: 'Our office has more plants for', objectType: 'objective', }; -export const alignmentObject3: AlignmentPossibilityObject = { +export const alignmentPossibilityObject3: AlignmentPossibilityObject = { objectId: 1, objectTitle: 'We buy 3 palms puzzle', objectType: 'keyResult', @@ -657,13 +657,13 @@ export const alignmentObject3: AlignmentPossibilityObject = { export const alignmentPossibility1: AlignmentPossibility = { teamId: 1, teamName: 'Puzzle ITC', - alignmentObjects: [alignmentObject2, alignmentObject3], + alignmentObjects: [alignmentPossibilityObject2, alignmentPossibilityObject3], }; export const alignmentPossibility2: AlignmentPossibility = { teamId: 2, teamName: 'We are cube', - alignmentObjects: [alignmentObject1], + alignmentObjects: [alignmentPossibilityObject1], }; export const alignmentObject1: AlignmentObject = { From 4f91761b5ec0a2948042fc74dbfb52414a5ca404 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 31 May 2024 08:27:47 +0200 Subject: [PATCH 105/119] Adjust testdata after rebase --- frontend/cypress/e2e/diagram.cy.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/cypress/e2e/diagram.cy.ts b/frontend/cypress/e2e/diagram.cy.ts index c708d76c36..76e53bb336 100644 --- a/frontend/cypress/e2e/diagram.cy.ts +++ b/frontend/cypress/e2e/diagram.cy.ts @@ -19,15 +19,16 @@ describe('OKR diagram e2e tests', () => { cy.getByTestId('diagramTab').first().click(); cy.contains('Kein Alignment vorhanden'); - cy.get('h1:contains(Puzzle ITC)').should('have.length', 0); + cy.get('h1:visible:contains(Puzzle ITC)').should('have.length', 0); cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); - cy.getByTestId('objective').should('have.length', 0); + cy.getByTestId('objective').should('not.be.visible'); cy.getByTestId('overviewTab').first().click(); cy.get('h1:contains(Puzzle ITC)').should('have.length', 1); cy.get('mat-chip:visible:contains("Puzzle ITC")').should('have.length', 1); cy.contains('An Objective for Testing'); + cy.getByTestId('objective').should('be.visible'); }); it('Can switch to diagram and the filter stay the same', () => { From 534105effd71d65f2b01b124a89e3ccfa40f11c8 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 31 May 2024 14:12:40 +0200 Subject: [PATCH 106/119] Change KR state on checkin edit --- frontend/src/app/diagram/diagram.component.ts | 42 ++++++++++++------- .../keyresult-detail.component.ts | 2 +- .../objective-detail.component.ts | 2 +- .../app/overview/overview.component.spec.ts | 2 +- .../src/app/overview/overview.component.ts | 19 +++++++-- .../check-in-history-dialog.component.ts | 6 ++- .../shared/services/refresh-data.service.ts | 6 +-- 7 files changed, 52 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index 998318b0f4..16ab0527cd 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -17,6 +17,8 @@ import { KeyResultMetric } from '../shared/types/model/KeyResultMetric'; import { calculateCurrentPercentage } from '../shared/common'; import { KeyResultOrdinal } from '../shared/types/model/KeyResultOrdinal'; import { Router } from '@angular/router'; +import { AlignmentObject } from '../shared/types/model/AlignmentObject'; +import { AlignmentConnection } from '../shared/types/model/AlignmentConnection'; @Component({ selector: 'app-diagram', @@ -24,7 +26,7 @@ import { Router } from '@angular/router'; styleUrl: './diagram.component.scss', }) export class DiagramComponent implements AfterViewInit, OnDestroy { - private alignmentData$ = new Subject(); + private alignmentData$: Subject = new Subject(); cy!: cytoscape.Core; diagramData: any[] = []; emptyDiagramData: boolean = false; @@ -46,7 +48,18 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { ngAfterViewInit() { this.alignmentData.subscribe((alignmentData: AlignmentLists): void => { - if (JSON.stringify(this.alignmentDataCache) !== JSON.stringify(alignmentData)) { + let lastItem: AlignmentObject = + alignmentData.alignmentObjectDtoList[alignmentData.alignmentObjectDtoList.length - 1]; + + let shouldUpdate: boolean = + lastItem?.objectTitle === 'reload' + ? lastItem?.objectType === 'true' + : JSON.stringify(this.alignmentDataCache) !== JSON.stringify(alignmentData); + + if (shouldUpdate) { + if (lastItem?.objectTitle === 'reload') { + alignmentData.alignmentObjectDtoList.pop(); + } this.alignmentDataCache = alignmentData; this.diagramData = []; this.cleanUpDiagram(); @@ -138,11 +151,11 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { generateElements(alignmentData: AlignmentLists): void { let observableArray: any[] = []; let diagramElements: any[] = []; - alignmentData.alignmentObjectDtoList.forEach((alignmentObject) => { + alignmentData.alignmentObjectDtoList.forEach((alignmentObject: AlignmentObject) => { if (alignmentObject.objectType == 'objective') { - let observable = new Observable((observer) => { - let objectiveTitle = this.replaceNonAsciiCharacters(alignmentObject.objectTitle); - let teamTitle = this.replaceNonAsciiCharacters(alignmentObject.objectTeamName); + let observable: Observable = new Observable((observer) => { + let objectiveTitle: string = this.replaceNonAsciiCharacters(alignmentObject.objectTitle); + let teamTitle: string = this.replaceNonAsciiCharacters(alignmentObject.objectTeamName); let element = { data: { id: 'Ob' + alignmentObject.objectId, @@ -157,11 +170,14 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { }); observableArray.push(observable); } else { - let observable = this.keyResultService.getFullKeyResult(alignmentObject.objectId).pipe( + let observable: Observable = this.keyResultService.getFullKeyResult(alignmentObject.objectId).pipe( map((keyResult: KeyResult) => { + let keyResultTitle: string = this.replaceNonAsciiCharacters(alignmentObject.objectTitle); + let teamTitle: string = this.replaceNonAsciiCharacters(alignmentObject.objectTeamName); + if (keyResult.keyResultType == 'metric') { - let metricKeyResult = keyResult as KeyResultMetric; - let percentage = calculateCurrentPercentage(metricKeyResult); + let metricKeyResult: KeyResultMetric = keyResult as KeyResultMetric; + let percentage: number = calculateCurrentPercentage(metricKeyResult); let keyResultState: string | undefined; if (percentage < 30) { @@ -175,8 +191,6 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { } else { keyResultState = undefined; } - let keyResultTitle = this.replaceNonAsciiCharacters(alignmentObject.objectTitle); - let teamTitle = this.replaceNonAsciiCharacters(alignmentObject.objectTeamName); let element = { data: { id: 'KR' + alignmentObject.objectId, @@ -187,11 +201,9 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { }; diagramElements.push(element); } else { - let ordinalKeyResult = keyResult as KeyResultOrdinal; + let ordinalKeyResult: KeyResultOrdinal = keyResult as KeyResultOrdinal; let keyResultState: string | undefined = ordinalKeyResult.lastCheckIn?.value.toString(); - let keyResultTitle = this.replaceNonAsciiCharacters(alignmentObject.objectTitle); - let teamTitle = this.replaceNonAsciiCharacters(alignmentObject.objectTeamName); let element = { data: { id: 'KR' + alignmentObject.objectId, @@ -215,7 +227,7 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { generateConnections(alignmentData: AlignmentLists, diagramElements: any[]): void { let edges: any[] = []; - alignmentData.alignmentConnectionDtoList.forEach((alignmentConnection) => { + alignmentData.alignmentConnectionDtoList.forEach((alignmentConnection: AlignmentConnection) => { if (alignmentConnection.targetKeyResultId == null) { let edge = { data: { diff --git a/frontend/src/app/keyresult-detail/keyresult-detail.component.ts b/frontend/src/app/keyresult-detail/keyresult-detail.component.ts index bbaeb0415f..79d03c2f54 100644 --- a/frontend/src/app/keyresult-detail/keyresult-detail.component.ts +++ b/frontend/src/app/keyresult-detail/keyresult-detail.component.ts @@ -185,7 +185,7 @@ export class KeyresultDetailComponent implements OnInit { dialogRef.afterClosed().subscribe((result) => { if (result != '' && result != undefined) { this.loadKeyResult(this.keyResult$.getValue().id); - this.refreshDataService.markDataRefresh(); + this.refreshDataService.markDataRefresh(true); } }); } diff --git a/frontend/src/app/objective-detail/objective-detail.component.ts b/frontend/src/app/objective-detail/objective-detail.component.ts index 69fbc42ea0..03bd8735e3 100644 --- a/frontend/src/app/objective-detail/objective-detail.component.ts +++ b/frontend/src/app/objective-detail/objective-detail.component.ts @@ -87,7 +87,7 @@ export class ObjectiveDetailComponent { }) .afterClosed() .subscribe((result) => { - if (result.delete) { + if (result && result.delete) { this.router.navigate(['']); } else if (result == '' || result == undefined) { return; diff --git a/frontend/src/app/overview/overview.component.spec.ts b/frontend/src/app/overview/overview.component.spec.ts index 4599cb1e60..e38f09d652 100644 --- a/frontend/src/app/overview/overview.component.spec.ts +++ b/frontend/src/app/overview/overview.component.spec.ts @@ -112,7 +112,7 @@ describe('OverviewComponent', () => { routerHarness.detectChanges(); component.loadOverviewWithParams(); expect(overviewService.getOverview).toHaveBeenCalledWith(quarterParam, teamsParam, objectiveQueryParam); - expect(component.loadOverview).toHaveBeenCalledWith(quarterParam, teamsParam, objectiveQueryParam); + expect(component.loadOverview).toHaveBeenCalledWith(quarterParam, teamsParam, objectiveQueryParam, undefined); }, ); diff --git a/frontend/src/app/overview/overview.component.ts b/frontend/src/app/overview/overview.component.ts index 31f855cfa0..44956431a2 100644 --- a/frontend/src/app/overview/overview.component.ts +++ b/frontend/src/app/overview/overview.component.ts @@ -7,6 +7,7 @@ import { RefreshDataService } from '../shared/services/refresh-data.service'; import { getQueryString, getValueFromQuery, isMobileDevice, trackByFn } from '../shared/common'; import { AlignmentService } from '../shared/services/alignment.service'; import { AlignmentLists } from '../shared/types/model/AlignmentLists'; +import { AlignmentObject } from '../shared/types/model/AlignmentObject'; @Component({ selector: 'app-overview', @@ -34,7 +35,7 @@ export class OverviewComponent implements OnInit, OnDestroy { ) { this.refreshDataService.reloadOverviewSubject .pipe(takeUntil(this.destroyed$)) - .subscribe(() => this.loadOverviewWithParams()); + .subscribe((reload: boolean | null | undefined) => this.loadOverviewWithParams(reload)); combineLatest([ refreshDataService.teamFilterReady.asObservable(), @@ -58,7 +59,7 @@ export class OverviewComponent implements OnInit, OnDestroy { } } - loadOverviewWithParams() { + loadOverviewWithParams(reload?: boolean | null) { const quarterQuery = this.activatedRoute.snapshot.queryParams['quarter']; const teamQuery = this.activatedRoute.snapshot.queryParams['teams']; const objectiveQuery = this.activatedRoute.snapshot.queryParams['objectiveQuery']; @@ -66,10 +67,10 @@ export class OverviewComponent implements OnInit, OnDestroy { const teamIds = getValueFromQuery(teamQuery); const quarterId = getValueFromQuery(quarterQuery)[0]; const objectiveQueryString = getQueryString(objectiveQuery); - this.loadOverview(quarterId, teamIds, objectiveQueryString); + this.loadOverview(quarterId, teamIds, objectiveQueryString, reload); } - loadOverview(quarterId?: number, teamIds?: number[], objectiveQuery?: string) { + loadOverview(quarterId?: number, teamIds?: number[], objectiveQuery?: string, reload?: boolean | null) { if (this.isOverview) { this.overviewService .getOverview(quarterId, teamIds, objectiveQuery) @@ -93,6 +94,16 @@ export class OverviewComponent implements OnInit, OnDestroy { }), ) .subscribe((alignmentLists) => { + if (reload != null) { + let kuchen: AlignmentObject = { + objectId: 0, + objectTitle: 'reload', + objectType: reload.toString(), + objectTeamName: '', + objectState: null, + }; + alignmentLists.alignmentObjectDtoList.push(kuchen); + } this.alignmentData$.next(alignmentLists); }); } diff --git a/frontend/src/app/shared/dialog/check-in-history-dialog/check-in-history-dialog.component.ts b/frontend/src/app/shared/dialog/check-in-history-dialog/check-in-history-dialog.component.ts index 8add8dad5d..a8880f8908 100644 --- a/frontend/src/app/shared/dialog/check-in-history-dialog/check-in-history-dialog.component.ts +++ b/frontend/src/app/shared/dialog/check-in-history-dialog/check-in-history-dialog.component.ts @@ -57,9 +57,11 @@ export class CheckInHistoryDialogComponent implements OnInit { maxHeight: dialogConfig.maxHeight, maxWidth: dialogConfig.maxWidth, }); - dialogRef.afterClosed().subscribe(() => { + dialogRef.afterClosed().subscribe((result) => { this.loadCheckInHistory(); - this.refreshDataService.markDataRefresh(); + if (result != '' && result != undefined) { + this.refreshDataService.markDataRefresh(true); + } }); } diff --git a/frontend/src/app/shared/services/refresh-data.service.ts b/frontend/src/app/shared/services/refresh-data.service.ts index d9f994e4c9..e04f27a711 100644 --- a/frontend/src/app/shared/services/refresh-data.service.ts +++ b/frontend/src/app/shared/services/refresh-data.service.ts @@ -6,14 +6,14 @@ import { DEFAULT_HEADER_HEIGHT_PX } from '../constantLibary'; providedIn: 'root', }) export class RefreshDataService { - public reloadOverviewSubject: Subject = new Subject(); + public reloadOverviewSubject: Subject = new Subject(); public quarterFilterReady: Subject = new Subject(); public teamFilterReady: Subject = new Subject(); public okrBannerHeightSubject: BehaviorSubject = new BehaviorSubject(DEFAULT_HEADER_HEIGHT_PX); - markDataRefresh() { - this.reloadOverviewSubject.next(); + markDataRefresh(reload?: boolean | null) { + this.reloadOverviewSubject.next(reload); } } From a702edf4799bddb749bfa2e7a95ac5f4c11de40c Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 3 Jun 2024 10:23:14 +0200 Subject: [PATCH 107/119] Use another method to convert svg --- frontend/src/app/diagram/diagram.component.ts | 42 +++++++------------ frontend/src/app/diagram/svgGeneration.ts | 9 ++-- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index 16ab0527cd..48501386dd 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -154,14 +154,16 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { alignmentData.alignmentObjectDtoList.forEach((alignmentObject: AlignmentObject) => { if (alignmentObject.objectType == 'objective') { let observable: Observable = new Observable((observer) => { - let objectiveTitle: string = this.replaceNonAsciiCharacters(alignmentObject.objectTitle); - let teamTitle: string = this.replaceNonAsciiCharacters(alignmentObject.objectTeamName); let element = { data: { id: 'Ob' + alignmentObject.objectId, }, style: { - 'background-image': this.generateObjectiveSVG(objectiveTitle, teamTitle, alignmentObject.objectState!), + 'background-image': this.generateObjectiveSVG( + alignmentObject.objectTitle, + alignmentObject.objectTeamName, + alignmentObject.objectState!, + ), }, }; diagramElements.push(element); @@ -172,9 +174,6 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { } else { let observable: Observable = this.keyResultService.getFullKeyResult(alignmentObject.objectId).pipe( map((keyResult: KeyResult) => { - let keyResultTitle: string = this.replaceNonAsciiCharacters(alignmentObject.objectTitle); - let teamTitle: string = this.replaceNonAsciiCharacters(alignmentObject.objectTeamName); - if (keyResult.keyResultType == 'metric') { let metricKeyResult: KeyResultMetric = keyResult as KeyResultMetric; let percentage: number = calculateCurrentPercentage(metricKeyResult); @@ -196,7 +195,11 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { id: 'KR' + alignmentObject.objectId, }, style: { - 'background-image': this.generateKeyResultSVG(keyResultTitle, teamTitle, keyResultState), + 'background-image': this.generateKeyResultSVG( + alignmentObject.objectTitle, + alignmentObject.objectTeamName, + keyResultState, + ), }, }; diagramElements.push(element); @@ -209,7 +212,11 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { id: 'KR' + alignmentObject.objectId, }, style: { - 'background-image': this.generateKeyResultSVG(keyResultTitle, teamTitle, keyResultState), + 'background-image': this.generateKeyResultSVG( + alignmentObject.objectTitle, + alignmentObject.objectTeamName, + keyResultState, + ), }, }; diagramElements.push(element); @@ -250,25 +257,6 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { this.generateDiagram(); } - replaceNonAsciiCharacters(text: string): string { - text = text.replace(/\u00c4/g, 'Ae'); - text = text.replace(/\u00e4/g, 'ae'); - text = text.replace(/\u00dc/g, 'Ue'); - text = text.replace(/\u00fc/g, 'ue'); - text = text.replace(/\u00d6/g, 'Oe'); - text = text.replace(/\u00f6/g, 'oe'); - text = text.replace(/\u00df/g, 'ss'); - text = text.replace(/\u00B2/g, '^2'); - text = text.replace(/\u00B3/g, '^3'); - text = text.replace(/&/g, '&'); - text = text.replace(//g, '>'); - text = text.replace(/'/g, '''); - text = text.replace(/"/g, '"'); - - return text; - } - generateObjectiveSVG(title: string, teamName: string, state: string): string { switch (state) { case 'ONGOING': diff --git a/frontend/src/app/diagram/svgGeneration.ts b/frontend/src/app/diagram/svgGeneration.ts index f4c03e89ae..8049c4db6a 100644 --- a/frontend/src/app/diagram/svgGeneration.ts +++ b/frontend/src/app/diagram/svgGeneration.ts @@ -48,7 +48,8 @@ export function generateObjectiveSVG(title: string, teamName: string, iconFuncti `; - return 'data:image/svg+xml;base64,' + btoa(svg); + let blob: Blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }); + return URL.createObjectURL(blob); } export function generateKeyResultSVG(title: string, teamName: string, backgroundColor: any, fontColor: any) { @@ -97,7 +98,8 @@ export function generateKeyResultSVG(title: string, teamName: string, background `; - return 'data:image/svg+xml;base64,' + btoa(svg); + let blob: Blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }); + return URL.createObjectURL(blob); } export function generateNeutralKeyResultSVG(title: string, teamName: string) { @@ -146,7 +148,8 @@ export function generateNeutralKeyResultSVG(title: string, teamName: string) { `; - return 'data:image/svg+xml;base64,' + btoa(svg); + let blob: Blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }); + return URL.createObjectURL(blob); } export function getDraftIcon() { From 1b7baba897d4917141812ebe153938efb7564231 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Mon, 3 Jun 2024 11:16:02 +0200 Subject: [PATCH 108/119] Fix failing tests --- frontend/src/app/diagram/diagram.component.spec.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/frontend/src/app/diagram/diagram.component.spec.ts b/frontend/src/app/diagram/diagram.component.spec.ts index f2f11b0f85..b914df59c7 100644 --- a/frontend/src/app/diagram/diagram.component.spec.ts +++ b/frontend/src/app/diagram/diagram.component.spec.ts @@ -24,6 +24,7 @@ describe('DiagramComponent', () => { providers: [{ provide: KeyresultService, useValue: keyResultServiceMock }, ParseUnitValuePipe], }); fixture = TestBed.createComponent(DiagramComponent); + URL.createObjectURL = jest.fn(); component = fixture.componentInstance; }); @@ -140,16 +141,6 @@ describe('DiagramComponent', () => { expect(component.diagramData).toEqual(diagramData); }); - it('should replace correct non ascii characters', () => { - let specialText: string = - 'Die klügsten Schafe springen über den Zaun und rechnen 2², während die ängstlichen Mäuse sich in ihren Löchern verkriechen und das Gemüß folgend rechnen 3³. Östlich befindet sich Ägypten mit einem Überfluss an Sand. " test'; - - let correctedText: string = - 'Die kluegsten Schafe springen ueber den Zaun und rechnen 2^2, waehrend die aengstlichen Maeuse sich in ihren Loechern verkriechen und das Gemuess folgend rechnen 3^3. Oestlich befindet sich Aegypten mit einem Ueberfluss an Sand. </svg> " test'; - - expect(component.replaceNonAsciiCharacters(specialText)).toEqual(correctedText); - }); - it('should generate correct SVGs for Objective', () => { jest.spyOn(functions, 'generateObjectiveSVG'); From 5c91e5ef632872f1d8b7dc1d19ba55fdde745590 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 3 Jun 2024 12:07:05 +0000 Subject: [PATCH 109/119] [FM] Automated formating backend --- .../okr/service/validation/AlignmentValidationService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java index 5107841af2..3c9238ac15 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java @@ -24,8 +24,9 @@ public class AlignmentValidationService private final QuarterValidationService quarterValidationService; private final TeamValidationService teamValidationService; - public AlignmentValidationService(AlignmentPersistenceService alignmentPersistenceService, TeamPersistenceService teamPersistenceService, - QuarterValidationService quarterValidationService, TeamValidationService teamValidationService) { + public AlignmentValidationService(AlignmentPersistenceService alignmentPersistenceService, + TeamPersistenceService teamPersistenceService, QuarterValidationService quarterValidationService, + TeamValidationService teamValidationService) { super(alignmentPersistenceService); this.alignmentPersistenceService = alignmentPersistenceService; this.teamPersistenceService = teamPersistenceService; From 8436030e2b0db9b5204c88e8abdee8b97ac0a5bd Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 5 Jun 2024 15:49:57 +0200 Subject: [PATCH 110/119] Add hyphens to svg bubble on word break --- frontend/src/app/diagram/svgGeneration.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/diagram/svgGeneration.ts b/frontend/src/app/diagram/svgGeneration.ts index 8049c4db6a..e08baf0ed2 100644 --- a/frontend/src/app/diagram/svgGeneration.ts +++ b/frontend/src/app/diagram/svgGeneration.ts @@ -18,7 +18,7 @@ export function generateObjectiveSVG(title: string, teamName: string, iconFuncti

-

+

${title}

@@ -68,7 +69,7 @@ export function generateKeyResultSVG(title: string, teamName: string, background
-

+

${title}

@@ -118,7 +120,7 @@ export function generateNeutralKeyResultSVG(title: string, teamName: string) {
-

+

${title}

From 21bbd7626e6b30dfc3bef9774fa5309c4e36d46e Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 5 Jun 2024 15:50:06 +0200 Subject: [PATCH 111/119] Clean up code --- .../okr/service/business/AlignmentBusinessService.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index 19747324a7..500b6cfebd 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -169,7 +169,8 @@ public AlignmentLists getAlignmentsByFilters(Long quarterFilter, List team if (Objects.isNull(quarterFilter)) { quarterFilter = quarterBusinessService.getCurrentQuarter().getId(); } - teamFilter = teamFilter == null ? List.of() : teamFilter; + + teamFilter = Objects.requireNonNullElse(teamFilter, List.of()); alignmentValidationService.validateOnAlignmentGet(quarterFilter, teamFilter); if (teamFilter.isEmpty()) { @@ -221,10 +222,7 @@ protected AlignmentLists generateAlignmentLists(List alignmentVie .add(new AlignmentObjectDto(alignmentView.getId(), alignmentView.getTitle(), alignmentView.getTeamName(), alignmentView.getState(), alignmentView.getObjectType()))); - // Remove duplicated items - List alignmentObjectDtos = alignmentObjectDtoList.stream().distinct().toList(); - - return new AlignmentLists(alignmentObjectDtos, alignmentConnectionDtoList); + return new AlignmentLists(alignmentObjectDtoList.stream().distinct().toList(), alignmentConnectionDtoList); } protected List getAlignmentCounterpart(DividedAlignmentViewLists alignmentViewLists) { From 5fc5d36c278c26e29521ee156df47f64fdf57a3b Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 6 Jun 2024 08:39:59 +0200 Subject: [PATCH 112/119] Fix sonar code smells --- .../business/AlignmentBusinessServiceIT.java | 16 +++++++++------- frontend/src/app/diagram/diagram.component.ts | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java index 6fd60fbec5..7371d10369 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java @@ -15,6 +15,8 @@ class AlignmentBusinessServiceIT { @Autowired private AlignmentBusinessService alignmentBusinessService; + private final String OBJECTIVE = "objective"; + @Test void shouldReturnCorrectAlignmentData() { AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(9L, List.of(5L, 6L), ""); @@ -22,7 +24,7 @@ void shouldReturnCorrectAlignmentData() { assertEquals(6, alignmentLists.alignmentObjectDtoList().size()); assertEquals(4, alignmentLists.alignmentConnectionDtoList().size()); assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); - assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(0).objectType()); assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); assertEquals("keyResult", alignmentLists.alignmentObjectDtoList().get(1).objectType()); assertEquals(41L, alignmentLists.alignmentObjectDtoList().get(2).objectId()); @@ -50,9 +52,9 @@ void shouldReturnCorrectAlignmentDataWhenLimitedTeamMatching() { assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); assertEquals(44L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); - assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(0).objectType()); assertEquals(42L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); - assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(1).objectType()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(1).objectType()); assertEquals(44L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); assertEquals(42L, alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); @@ -66,11 +68,11 @@ void shouldReturnCorrectAlignmentDataWithObjectiveSearch() { assertEquals(3, alignmentLists.alignmentObjectDtoList().size()); assertEquals(2, alignmentLists.alignmentConnectionDtoList().size()); assertEquals(42L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); - assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(0).objectType()); assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); - assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(1).objectType()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(1).objectType()); assertEquals(44L, alignmentLists.alignmentObjectDtoList().get(2).objectId()); - assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(2).objectType()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(2).objectType()); assertEquals(42L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); assertEquals(40L, alignmentLists.alignmentConnectionDtoList().get(0).targetObjectiveId()); assertNull(alignmentLists.alignmentConnectionDtoList().get(0).targetKeyResultId()); @@ -87,7 +89,7 @@ void shouldReturnCorrectAlignmentDataWithKeyResultWhenMatchingObjectiveSearch() assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); assertEquals(43L, alignmentLists.alignmentObjectDtoList().get(0).objectId()); - assertEquals("objective", alignmentLists.alignmentObjectDtoList().get(0).objectType()); + assertEquals(OBJECTIVE, alignmentLists.alignmentObjectDtoList().get(0).objectType()); assertEquals(40L, alignmentLists.alignmentObjectDtoList().get(1).objectId()); assertEquals("keyResult", alignmentLists.alignmentObjectDtoList().get(1).objectType()); assertEquals(43L, alignmentLists.alignmentConnectionDtoList().get(0).alignedObjectiveId()); diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index 48501386dd..6085c69ba0 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -1,5 +1,5 @@ import { AfterViewInit, Component, Input, OnDestroy } from '@angular/core'; -import { forkJoin, map, Observable, Subject } from 'rxjs'; +import { map, Observable, Subject, zip } from 'rxjs'; import { AlignmentLists } from '../shared/types/model/AlignmentLists'; import cytoscape from 'cytoscape'; import { @@ -227,7 +227,7 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { } }); - forkJoin(observableArray).subscribe(() => { + zip(observableArray).subscribe(() => { this.generateConnections(alignmentData, diagramElements); }); } From 73a17d577872dc8ec4734d01efe512b9442e9adb Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 6 Jun 2024 09:27:55 +0200 Subject: [PATCH 113/119] Refactor code --- .../main/java/ch/puzzle/okr/Constants.java | 2 + .../okr/controller/AlignmentController.java | 41 +++ .../okr/models/alignment/AlignmentView.java | 71 ++--- .../business/AlignmentBusinessService.java | 293 +++++++++++------- .../business/ObjectiveBusinessService.java | 80 +++-- .../AlignmentValidationService.java | 6 +- .../V1_0_0__current-db-schema-for-testing.sql | 18 +- ...ew.sql => V2_1_4__createAlignmentView.sql} | 18 +- .../okr/controller/AlignmentControllerIT.java | 72 +++++ .../business/AlignmentBusinessServiceIT.java | 18 +- .../AlignmentBusinessServiceTest.java | 118 +++---- .../src/app/diagram/diagram.component.html | 4 +- .../src/app/diagram/diagram.component.spec.ts | 18 +- frontend/src/app/diagram/diagram.component.ts | 137 ++++---- .../src/app/overview/overview.component.html | 2 +- .../src/app/overview/overview.component.ts | 11 +- .../objective-form.component.spec.ts | 4 +- 17 files changed, 560 insertions(+), 353 deletions(-) create mode 100644 backend/src/main/java/ch/puzzle/okr/controller/AlignmentController.java rename backend/src/main/resources/db/migration/{V2_1_3__createAlignmentView.sql => V2_1_4__createAlignmentView.sql} (79%) create mode 100644 backend/src/test/java/ch/puzzle/okr/controller/AlignmentControllerIT.java diff --git a/backend/src/main/java/ch/puzzle/okr/Constants.java b/backend/src/main/java/ch/puzzle/okr/Constants.java index 5dccc96d07..197d197e85 100644 --- a/backend/src/main/java/ch/puzzle/okr/Constants.java +++ b/backend/src/main/java/ch/puzzle/okr/Constants.java @@ -7,12 +7,14 @@ private Constants() { public static final String KEY_RESULT_TYPE_METRIC = "metric"; public static final String KEY_RESULT_TYPE_ORDINAL = "ordinal"; public static final String OBJECTIVE = "Objective"; + public static final String OBJECTIVE_LOWERCASE = "objective"; public static final String STATE_DRAFT = "Draft"; public static final String KEY_RESULT = "KeyResult"; public static final String CHECK_IN = "Check-in"; public static final String ACTION = "Action"; public static final String ALIGNMENT = "Alignment"; public static final String ALIGNMENT_VIEW = "AlignmentView"; + public static final String ALIGNED_OBJECTIVE_ID = "alignedObjectiveId"; public static final String COMPLETED = "Completed"; public static final String ORGANISATION = "Organisation"; public static final String QUARTER = "Quarter"; diff --git a/backend/src/main/java/ch/puzzle/okr/controller/AlignmentController.java b/backend/src/main/java/ch/puzzle/okr/controller/AlignmentController.java new file mode 100644 index 0000000000..d4d307fac0 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/controller/AlignmentController.java @@ -0,0 +1,41 @@ +package ch.puzzle.okr.controller; + +import ch.puzzle.okr.dto.alignment.AlignmentLists; +import ch.puzzle.okr.service.business.AlignmentBusinessService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("api/v2/alignments") +public class AlignmentController { + private final AlignmentBusinessService alignmentBusinessService; + + public AlignmentController(AlignmentBusinessService alignmentBusinessService) { + this.alignmentBusinessService = alignmentBusinessService; + } + + @Operation(summary = "Get AlignmentLists from filter", description = "Get a list of AlignmentObjects with all AlignmentConnections, which match current quarter, team and objective filter") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Returned AlignmentLists, which match current filters", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = AlignmentLists.class)) }), + @ApiResponse(responseCode = "400", description = "Can't generate AlignmentLists from current filters", content = @Content) }) + @GetMapping("/alignmentLists") + public ResponseEntity getAlignments( + @RequestParam(required = false, defaultValue = "", name = "teamFilter") List teamFilter, + @RequestParam(required = false, defaultValue = "", name = "quarterFilter") Long quarterFilter, + @RequestParam(required = false, defaultValue = "", name = "objectiveQuery") String objectiveQuery) { + return ResponseEntity.status(HttpStatus.OK) + .body(alignmentBusinessService.getAlignmentListsByFilters(quarterFilter, teamFilter, objectiveQuery)); + } +} diff --git a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java index 912839a6a3..6fd0ea825c 100644 --- a/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java +++ b/backend/src/main/java/ch/puzzle/okr/models/alignment/AlignmentView.java @@ -19,9 +19,9 @@ public class AlignmentView { private Long quarterId; private String state; private String objectType; - private String connectionItem; - private Long refId; - private String refType; + private String connectionRole; + private Long counterpartId; + private String counterpartType; public AlignmentView() { } @@ -35,13 +35,9 @@ private AlignmentView(Builder builder) { setQuarterId(builder.quarterId); setState(builder.state); setObjectType(builder.objectType); - setConnectionItem(builder.connectionItem); - setRefId(builder.refId); - setRefType(builder.refType); - } - - public String getUniqueId() { - return uniqueId; + setConnectionRole(builder.connectionRole); + setCounterpartId(builder.counterpartId); + setCounterpartType(builder.counterpartType); } public void setUniqueId(String uniqueId) { @@ -104,28 +100,28 @@ public void setObjectType(String objectType) { this.objectType = objectType; } - public String getConnectionItem() { - return connectionItem; + public String getConnectionRole() { + return connectionRole; } - public void setConnectionItem(String connectionItem) { - this.connectionItem = connectionItem; + public void setConnectionRole(String connectionItem) { + this.connectionRole = connectionItem; } - public Long getRefId() { - return refId; + public Long getCounterpartId() { + return counterpartId; } - public void setRefId(Long refId) { - this.refId = refId; + public void setCounterpartId(Long refId) { + this.counterpartId = refId; } - public String getRefType() { - return refType; + public String getCounterpartType() { + return counterpartType; } - public void setRefType(String refType) { - this.refType = refType; + public void setCounterpartType(String refType) { + this.counterpartType = refType; } @Override @@ -139,22 +135,23 @@ public boolean equals(Object o) { && Objects.equals(title, that.title) && Objects.equals(teamId, that.teamId) && Objects.equals(teamName, that.teamName) && Objects.equals(quarterId, that.quarterId) && Objects.equals(state, that.state) && Objects.equals(objectType, that.objectType) - && Objects.equals(connectionItem, that.connectionItem) && Objects.equals(refId, that.refId) - && Objects.equals(refType, that.refType); + && Objects.equals(connectionRole, that.connectionRole) + && Objects.equals(counterpartId, that.counterpartId) + && Objects.equals(counterpartType, that.counterpartType); } @Override public int hashCode() { - return Objects.hash(uniqueId, id, title, teamId, teamName, quarterId, state, objectType, connectionItem, refId, - refType); + return Objects.hash(uniqueId, id, title, teamId, teamName, quarterId, state, objectType, connectionRole, + counterpartId, counterpartType); } @Override public String toString() { return "AlignmentView{" + "uniqueId='" + uniqueId + '\'' + ", id=" + id + ", title='" + title + '\'' + ", teamId=" + teamId + ", teamName='" + teamName + '\'' + ", quarterId=" + quarterId + ", state='" - + state + '\'' + ", objectType='" + objectType + '\'' + ", connectionItem='" + connectionItem + '\'' - + ", refId=" + refId + ", refType='" + refType + '\'' + '}'; + + state + '\'' + ", objectType='" + objectType + '\'' + ", connectionItem='" + connectionRole + '\'' + + ", refId=" + counterpartId + ", refType='" + counterpartType + '\'' + '}'; } public static final class Builder { @@ -166,9 +163,9 @@ public static final class Builder { private Long quarterId; private String state; private String objectType; - private String connectionItem; - private Long refId; - private String refType; + private String connectionRole; + private Long counterpartId; + private String counterpartType; private Builder() { } @@ -217,18 +214,18 @@ public Builder withObjectType(String val) { return this; } - public Builder withConnectionItem(String val) { - connectionItem = val; + public Builder withConnectionRole(String val) { + connectionRole = val; return this; } - public Builder withRefId(Long val) { - refId = val; + public Builder withCounterpartId(Long val) { + counterpartId = val; return this; } - public Builder withRefType(String val) { - refType = val; + public Builder withCounterpartType(String val) { + counterpartType = val; return this; } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java index 500b6cfebd..a9c51e229a 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/AlignmentBusinessService.java @@ -26,6 +26,8 @@ import java.util.Objects; import java.util.Optional; +import static ch.puzzle.okr.Constants.OBJECTIVE_LOWERCASE; + @Service public class AlignmentBusinessService { @@ -50,8 +52,8 @@ public AlignmentBusinessService(AlignmentPersistenceService alignmentPersistence this.quarterBusinessService = quarterBusinessService; } - protected record DividedAlignmentViewLists(List correctAlignments, - List wrongAlignments) { + protected record DividedAlignmentViewLists(List filterMatchingAlignments, + List nonMatchingAlignments) { } public AlignedEntityDto getTargetIdByAlignedObjectiveId(Long alignedObjectiveId) { @@ -67,6 +69,10 @@ public AlignedEntityDto getTargetIdByAlignedObjectiveId(Long alignedObjectiveId) } public void createEntity(Objective alignedObjective) { + validateOnCreateAndSaveAlignment(alignedObjective); + } + + private void validateOnCreateAndSaveAlignment(Objective alignedObjective) { Alignment alignment = buildAlignmentModel(alignedObjective, 0); alignmentValidationService.validateOnCreate(alignment); alignmentPersistenceService.save(alignment); @@ -74,36 +80,36 @@ public void createEntity(Objective alignedObjective) { public void updateEntity(Long objectiveId, Objective objective) { Alignment savedAlignment = alignmentPersistenceService.findByAlignedObjectiveId(objectiveId); - if (savedAlignment == null) { - createEntity(objective); + validateOnCreateAndSaveAlignment(objective); } else { - handleExistingAlignment(objective, savedAlignment); + if (objective.getAlignedEntity() == null) { + validateOnDeleteAndDeleteById(savedAlignment.getId()); + } else { + Alignment alignment = buildAlignmentModel(objective, savedAlignment.getVersion()); + validateOnUpdateAndRecreateOrSaveAlignment(alignment, savedAlignment); + } } } - private void handleExistingAlignment(Objective objective, Alignment savedAlignment) { - if (objective.getAlignedEntity() == null) { - validateAndDeleteAlignmentById(savedAlignment.getId()); + private void validateOnUpdateAndRecreateOrSaveAlignment(Alignment alignment, Alignment savedAlignment) { + if (isAlignmentTypeChange(alignment, savedAlignment)) { + validateOnUpdateAndRecreateAlignment(savedAlignment.getId(), alignment); } else { - validateAndUpdateAlignment(objective, savedAlignment); + validateOnUpdateAndSaveAlignment(savedAlignment.getId(), alignment); } } - private void validateAndUpdateAlignment(Objective objective, Alignment savedAlignment) { - Alignment alignment = buildAlignmentModel(objective, savedAlignment.getVersion()); - - alignment.setId(savedAlignment.getId()); - alignmentValidationService.validateOnUpdate(savedAlignment.getId(), alignment); - updateAlignment(savedAlignment, alignment); + private void validateOnUpdateAndRecreateAlignment(Long id, Alignment alignment) { + alignment.setId(id); + alignmentValidationService.validateOnUpdate(id, alignment); + alignmentPersistenceService.recreateEntity(id, alignment); } - private void updateAlignment(Alignment savedAlignment, Alignment alignment) { - if (isAlignmentTypeChange(alignment, savedAlignment)) { - alignmentPersistenceService.recreateEntity(savedAlignment.getId(), alignment); - } else { - alignmentPersistenceService.save(alignment); - } + private void validateOnUpdateAndSaveAlignment(Long id, Alignment alignment) { + alignment.setId(id); + alignmentValidationService.validateOnUpdate(id, alignment); + alignmentPersistenceService.save(alignment); } public Alignment buildAlignmentModel(Objective alignedObjective, int version) { @@ -119,8 +125,10 @@ public Alignment buildAlignmentModel(Objective alignedObjective, int version) { Long entityId = alignedObjective.getAlignedEntity().id(); KeyResult targetKeyResult = keyResultPersistenceService.findById(entityId); - return KeyResultAlignment.Builder.builder().withAlignedObjective(alignedObjective) - .withTargetKeyResult(targetKeyResult).withVersion(version).build(); + return KeyResultAlignment.Builder.builder() // + .withAlignedObjective(alignedObjective) // + .withTargetKeyResult(targetKeyResult) // + .withVersion(version).build(); } else { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NOT_SET, List.of("alignedEntity", alignedObjective.getAlignedEntity())); @@ -133,43 +141,42 @@ public boolean isAlignmentTypeChange(Alignment alignment, Alignment savedAlignme } public void updateKeyResultIdOnIdChange(Long oldKeyResultId, KeyResult keyResult) { - List keyResultAlignmentList = alignmentPersistenceService - .findByKeyResultAlignmentId(oldKeyResultId); - keyResultAlignmentList.forEach(alignment -> { - alignment.setAlignmentTarget(keyResult); - alignmentValidationService.validateOnUpdate(alignment.getId(), alignment); - alignmentPersistenceService.save(alignment); - }); + alignmentPersistenceService.findByKeyResultAlignmentId(oldKeyResultId) + .forEach(alignment -> validateOnUpdateAndSaveAlignment(keyResult, alignment)); + } + + private void validateOnUpdateAndSaveAlignment(KeyResult keyResult, KeyResultAlignment alignment) { + alignment.setAlignmentTarget(keyResult); + alignmentValidationService.validateOnUpdate(alignment.getId(), alignment); + alignmentPersistenceService.save(alignment); } public void deleteAlignmentByObjectiveId(Long objectiveId) { + ensureAlignmentIdIsNotNull(objectiveId); + alignmentPersistenceService.findByObjectiveAlignmentId(objectiveId) + .forEach(objectiveAlignment -> validateOnDeleteAndDeleteById(objectiveAlignment.getId())); + } + + private void ensureAlignmentIdIsNotNull(Long objectiveId) { Alignment alignment = alignmentPersistenceService.findByAlignedObjectiveId(objectiveId); if (alignment != null) { - validateAndDeleteAlignmentById(alignment.getId()); + validateOnDeleteAndDeleteById(alignment.getId()); } - List objectiveAlignmentList = alignmentPersistenceService - .findByObjectiveAlignmentId(objectiveId); - objectiveAlignmentList - .forEach(objectiveAlignment -> validateAndDeleteAlignmentById(objectiveAlignment.getId())); } - public void deleteAlignmentByKeyResultId(Long keyResultId) { - List keyResultAlignmentList = alignmentPersistenceService - .findByKeyResultAlignmentId(keyResultId); - keyResultAlignmentList - .forEach(keyResultAlignment -> validateAndDeleteAlignmentById(keyResultAlignment.getId())); + private void validateOnDeleteAndDeleteById(Long id) { + alignmentValidationService.validateOnDelete(id); + alignmentPersistenceService.deleteById(id); } - private void validateAndDeleteAlignmentById(Long alignmentId) { - alignmentValidationService.validateOnDelete(alignmentId); - alignmentPersistenceService.deleteById(alignmentId); + public void deleteAlignmentByKeyResultId(Long keyResultId) { + alignmentPersistenceService.findByKeyResultAlignmentId(keyResultId) + .forEach(keyResultAlignment -> validateOnDeleteAndDeleteById(keyResultAlignment.getId())); } - public AlignmentLists getAlignmentsByFilters(Long quarterFilter, List teamFilter, String objectiveFilter) { - if (Objects.isNull(quarterFilter)) { - quarterFilter = quarterBusinessService.getCurrentQuarter().getId(); - } - + public AlignmentLists getAlignmentListsByFilters(Long quarterFilter, List teamFilter, + String objectiveFilter) { + quarterFilter = quarterFilter(quarterFilter); teamFilter = Objects.requireNonNullElse(teamFilter, List.of()); alignmentValidationService.validateOnAlignmentGet(quarterFilter, teamFilter); @@ -177,23 +184,38 @@ public AlignmentLists getAlignmentsByFilters(Long quarterFilter, List team return new AlignmentLists(List.of(), List.of()); } - List alignmentViewList = alignmentViewPersistenceService - .getAlignmentViewListByQuarterId(quarterFilter); - DividedAlignmentViewLists dividedAlignmentViewLists = filterAlignmentViews(alignmentViewList, teamFilter, + List correctAlignmentViewList = correctAlignmentViewList(quarterFilter, teamFilter, objectiveFilter); + sourceAndTargetListsEqualSameSize(correctAlignmentViewList, quarterFilter, teamFilter, objectiveFilter); + return generateAlignmentLists(correctAlignmentViewList); + } - List finalList = getAlignmentCounterpart(dividedAlignmentViewLists); - validateFinalList(finalList, quarterFilter, teamFilter, objectiveFilter); - - return generateAlignmentLists(finalList); + private Long quarterFilter(Long quarterFilter) { + if (Objects.isNull(quarterFilter)) { + return quarterBusinessService.getCurrentQuarter().getId(); + } + return quarterFilter; } - protected void validateFinalList(List finalList, Long quarterFilter, List teamFilter, + private List correctAlignmentViewList(Long quarterFilter, List teamFilter, String objectiveFilter) { - List sourceList = finalList.stream() - .filter(alignmentView -> Objects.equals(alignmentView.getConnectionItem(), "source")).toList(); - List targetList = finalList.stream() - .filter(alignmentView -> Objects.equals(alignmentView.getConnectionItem(), "target")).toList(); + List alignmentViewListByQuarter = alignmentViewPersistenceService + .getAlignmentViewListByQuarterId(quarterFilter); + + DividedAlignmentViewLists dividedAlignmentViewLists = filterAndDivideAlignmentViews(alignmentViewListByQuarter, + teamFilter, objectiveFilter); + return getAlignmentCounterpart(dividedAlignmentViewLists); + } + + protected void sourceAndTargetListsEqualSameSize(List finalList, Long quarterFilter, + List teamFilter, String objectiveFilter) { + List sourceList = finalList.stream() // + .filter(alignmentView -> Objects.equals(alignmentView.getConnectionRole(), "source")) // + .toList(); + + List targetList = finalList.stream() // + .filter(alignmentView -> Objects.equals(alignmentView.getConnectionRole(), "target")) // + .toList(); if (sourceList.size() != targetList.size()) { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ALIGNMENT_DATA_FAIL, @@ -202,72 +224,123 @@ protected void validateFinalList(List finalList, Long quarterFilt } protected AlignmentLists generateAlignmentLists(List alignmentViewList) { - List alignmentConnectionDtoList = new ArrayList<>(); + List distictObjectDtoList = createDistinctAlignmentObjectDtoList(alignmentViewList); + List alignmentConnectionDtoList = createAlignmentConnectionDtoListFromConnections( + alignmentViewList); + + return new AlignmentLists(distictObjectDtoList, alignmentConnectionDtoList); + } + + private List createDistinctAlignmentObjectDtoList(List alignmentViewList) { List alignmentObjectDtoList = new ArrayList<>(); + alignmentViewList.forEach(alignmentView -> alignmentObjectDtoList.add(new AlignmentObjectDto( // + alignmentView.getId(), // + alignmentView.getTitle(), // + alignmentView.getTeamName(), // + alignmentView.getState(), // + alignmentView.getObjectType()))); + + return alignmentObjectDtoList.stream() // + .distinct() // + .toList(); + } - // Create ConnectionDtoList for every connection + private List createAlignmentConnectionDtoListFromConnections( + List alignmentViewList) { + List alignmentConnectionDtoList = new ArrayList<>(); alignmentViewList.forEach(alignmentView -> { - if (Objects.equals(alignmentView.getConnectionItem(), "source")) { - if (Objects.equals(alignmentView.getRefType(), "objective")) { - alignmentConnectionDtoList - .add(new AlignmentConnectionDto(alignmentView.getId(), alignmentView.getRefId(), null)); + if (Objects.equals(alignmentView.getConnectionRole(), "source")) { + if (Objects.equals(alignmentView.getCounterpartType(), OBJECTIVE_LOWERCASE)) { + alignmentConnectionDtoList.add(new AlignmentConnectionDto( // + alignmentView.getId(), alignmentView.getCounterpartId(), null)); } else { - alignmentConnectionDtoList - .add(new AlignmentConnectionDto(alignmentView.getId(), null, alignmentView.getRefId())); + alignmentConnectionDtoList.add(new AlignmentConnectionDto( // + alignmentView.getId(), null, alignmentView.getCounterpartId())); } } }); - - alignmentViewList.forEach(alignmentView -> alignmentObjectDtoList - .add(new AlignmentObjectDto(alignmentView.getId(), alignmentView.getTitle(), - alignmentView.getTeamName(), alignmentView.getState(), alignmentView.getObjectType()))); - - return new AlignmentLists(alignmentObjectDtoList.stream().distinct().toList(), alignmentConnectionDtoList); + return alignmentConnectionDtoList; } protected List getAlignmentCounterpart(DividedAlignmentViewLists alignmentViewLists) { - List correctAlignments = alignmentViewLists.correctAlignments(); - List wrongAlignments = alignmentViewLists.wrongAlignments(); - List targetAlignmentList = new ArrayList<>(); - - // If counterpart of the correct Alignment is in wrongAlignmentList, take it back - correctAlignments.forEach(alignment -> { - Optional matchingObject = wrongAlignments.stream() - .filter(view -> Objects.equals(view.getId(), alignment.getRefId()) - && Objects.equals(view.getObjectType(), alignment.getRefType()) - && Objects.equals(view.getRefId(), alignment.getId()) - && Objects.equals(view.getRefType(), alignment.getObjectType())) - .findFirst(); - - if (matchingObject.isPresent()) { - AlignmentView alignmentView = matchingObject.get(); - targetAlignmentList.add(alignmentView); - } + List nonMatchingAlignments = alignmentViewLists.nonMatchingAlignments(); + List filterMatchingAlignments = alignmentViewLists.filterMatchingAlignments(); + List correctAlignmentViewList = correctAlignmentViewList(filterMatchingAlignments, + nonMatchingAlignments); + return createFinalAlignmentViewList(filterMatchingAlignments, correctAlignmentViewList); + } + + private List correctAlignmentViewList(List filterMatchingAlignments, + List nonMatchingAlignments) { + List correctAlignmentViewList = new ArrayList<>(); + filterMatchingAlignments.forEach(alignment -> { + Optional matchingObject = findMatchingAlignmentInList(nonMatchingAlignments, alignment); + matchingObject.map(correctAlignmentViewList::add); }); + return correctAlignmentViewList; + } - // Create a new list because correctAlignments has a fixed length and targetAlignmentList can't be added - List finalList = new ArrayList<>(correctAlignments); - if (!targetAlignmentList.isEmpty()) { - finalList.addAll(targetAlignmentList); + private Optional findMatchingAlignmentInList(List alignmentList, + AlignmentView alignment) { + return alignmentList.stream().filter(view -> isMatching(alignment, view)).findFirst(); + } + + private boolean isMatching(AlignmentView firstAlignment, AlignmentView secondAlignment) { + return Objects.equals(secondAlignment.getId(), firstAlignment.getCounterpartId()) + && Objects.equals(secondAlignment.getObjectType(), firstAlignment.getCounterpartType()) + && Objects.equals(secondAlignment.getCounterpartId(), firstAlignment.getId()) + && Objects.equals(secondAlignment.getCounterpartType(), firstAlignment.getObjectType()); + } + + private List createFinalAlignmentViewList(List filterMatchingAlignments, + List correctAlignmentViewList) { + List finalAlignmentViewList = new ArrayList<>(filterMatchingAlignments); + if (!correctAlignmentViewList.isEmpty()) { + finalAlignmentViewList.addAll(correctAlignmentViewList); } - return finalList; + return finalAlignmentViewList; + } + + protected DividedAlignmentViewLists filterAndDivideAlignmentViews(List alignmentViewList, + List teamFilter, String objectiveFilter) { + List filterMatchingAlignments = filterAlignmentListByTeamAndObjective(alignmentViewList, + teamFilter, objectiveFilter); + List nonMatchingAlignments = filterNonMatchingAlignments(alignmentViewList, + filterMatchingAlignments); + + return new DividedAlignmentViewLists(filterMatchingAlignments, nonMatchingAlignments); } - protected DividedAlignmentViewLists filterAlignmentViews(List alignmentViewList, + private List filterAlignmentListByTeamAndObjective(List alignmentViewList, List teamFilter, String objectiveFilter) { - List filteredList = alignmentViewList.stream() - .filter(alignmentView -> teamFilter.contains(alignmentView.getTeamId())).toList(); - - boolean isObjectiveFilterDefined = StringUtils.isNotBlank(objectiveFilter); - if (isObjectiveFilterDefined) { - filteredList = filteredList.stream() - .filter(alignmentView -> Objects.equals(alignmentView.getObjectType(), "objective") - && alignmentView.getTitle().toLowerCase().contains(objectiveFilter.toLowerCase())) - .toList(); + List filteredList = filterByTeam(alignmentViewList, teamFilter); + if (StringUtils.isNotBlank(objectiveFilter)) { + filteredList = filterByObjective(filteredList, objectiveFilter); } - List correctAlignments = filteredList; - List wrongAlignments = alignmentViewList.stream() - .filter(alignmentView -> !correctAlignments.contains(alignmentView)).toList(); - return new DividedAlignmentViewLists(correctAlignments, wrongAlignments); + return filteredList; + } + + private List filterByTeam(List alignmentViewList, List teamFilter) { + return alignmentViewList.stream() // + .filter(alignmentView -> teamFilter.contains(alignmentView.getTeamId())) // + .toList(); + } + + private List filterByObjective(List filteredList, String objectiveFilter) { + return filteredList.stream() // + .filter(alignmentView -> isObjectiveAndMatchesFilter(alignmentView, objectiveFilter)) // + .toList(); + } + + private static boolean isObjectiveAndMatchesFilter(AlignmentView alignmentView, String objectiveFilter) { + return Objects.equals(alignmentView.getObjectType(), OBJECTIVE_LOWERCASE) + && alignmentView.getTitle().toLowerCase().contains(objectiveFilter.toLowerCase()); + } + + private List filterNonMatchingAlignments(List alignmentViewList, + List nonMatchingAlignments) { + return alignmentViewList.stream() // + .filter(alignmentView -> !nonMatchingAlignments.contains(alignmentView)) // + .toList(); } } diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java index 7157f6ef85..01797dc100 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java @@ -56,13 +56,22 @@ public List getAlignmentPossibilities(Long quarterId) { validator.validateOnGet(quarterId); List objectivesByQuarter = objectivePersistenceService.findObjectiveByQuarterId(quarterId); - List alignmentDtoList = new ArrayList<>(); + List teamList = getTeamsFromObjectives(objectivesByQuarter); + + return createAlignmentDtoForEveryTeam(teamList, objectivesByQuarter); + } - List teamList = objectivesByQuarter.stream() // + private List getTeamsFromObjectives(List objectiveList) { + return objectiveList.stream() // .map(Objective::getTeam) // .distinct() // .sorted(Comparator.comparing(Team::getName)) // .toList(); + } + + private List createAlignmentDtoForEveryTeam(List teamList, + List objectivesByQuarter) { + List alignmentDtoList = new ArrayList<>(); teamList.forEach(team -> { List filteredObjectiveList = objectivesByQuarter.stream() @@ -70,7 +79,6 @@ public List getAlignmentPossibilities(Long quarterId) { .sorted(Comparator.comparing(Objective::getTitle)).toList(); List alignmentObjectDtoList = generateAlignmentObjects(filteredObjectiveList); - AlignmentDto alignmentDto = new AlignmentDto(team.getId(), team.getName(), alignmentObjectDtoList); alignmentDtoList.add(alignmentDto); }); @@ -113,6 +121,25 @@ public List getEntitiesByTeamId(Long id) { @Transactional public Objective updateEntity(Long id, Objective objective, AuthorizationUser authorizationUser) { Objective savedObjective = objectivePersistenceService.findById(id); + Objective updatedObjective = updateObjectiveWithSavedAttrs(objective, savedObjective, authorizationUser); + + validator.validateOnUpdate(id, updatedObjective); + savedObjective = objectivePersistenceService.save(updatedObjective); + handleAlignedEntity(id, savedObjective, updatedObjective); + return savedObjective; + } + + private void handleAlignedEntity(Long id, Objective savedObjective, Objective updatedObjective) { + AlignedEntityDto alignedEntity = alignmentBusinessService.getTargetIdByAlignedObjectiveId(savedObjective.getId()); + if ((updatedObjective.getAlignedEntity() != null) + || updatedObjective.getAlignedEntity() == null && alignedEntity != null) { + savedObjective.setAlignedEntity(updatedObjective.getAlignedEntity()); + alignmentBusinessService.updateEntity(id, savedObjective); + } + } + + private Objective updateObjectiveWithSavedAttrs(Objective objective, Objective savedObjective, + AuthorizationUser authorizationUser) { objective.setCreatedBy(savedObjective.getCreatedBy()); objective.setCreatedOn(savedObjective.getCreatedOn()); objective.setModifiedBy(authorizationUser.user()); @@ -123,15 +150,7 @@ public Objective updateEntity(Long id, Objective objective, AuthorizationUser au not = " NOT "; } logger.debug("quarter has changed and is{}changeable, {}", not, objective); - validator.validateOnUpdate(id, objective); - savedObjective = objectivePersistenceService.save(objective); - AlignedEntityDto alignedEntity = alignmentBusinessService - .getTargetIdByAlignedObjectiveId(savedObjective.getId()); - if ((objective.getAlignedEntity() != null) || objective.getAlignedEntity() == null && alignedEntity != null) { - savedObjective.setAlignedEntity(objective.getAlignedEntity()); - alignmentBusinessService.updateEntity(id, savedObjective); - } - return savedObjective; + return objective; } public boolean isImUsed(Objective objective) { @@ -169,23 +188,34 @@ public Objective duplicateObjective(Long id, Objective objective, AuthorizationU Objective duplicatedObjective = createEntity(objective, authorizationUser); List keyResultsOfDuplicatedObjective = keyResultBusinessService.getAllKeyResultsByObjective(id); for (KeyResult keyResult : keyResultsOfDuplicatedObjective) { - if (keyResult.getKeyResultType().equals(KEY_RESULT_TYPE_METRIC)) { - KeyResult keyResultMetric = KeyResultMetric.Builder.builder().withObjective(duplicatedObjective) - .withTitle(keyResult.getTitle()).withDescription(keyResult.getDescription()) - .withOwner(keyResult.getOwner()).withUnit(((KeyResultMetric) keyResult).getUnit()) - .withBaseline(0D).withStretchGoal(1D).build(); - keyResultBusinessService.createEntity(keyResultMetric, authorizationUser); - } else if (keyResult.getKeyResultType().equals(KEY_RESULT_TYPE_ORDINAL)) { - KeyResult keyResultOrdinal = KeyResultOrdinal.Builder.builder().withObjective(duplicatedObjective) - .withTitle(keyResult.getTitle()).withDescription(keyResult.getDescription()) - .withOwner(keyResult.getOwner()).withCommitZone("-").withTargetZone("-").withStretchZone("-") - .build(); - keyResultBusinessService.createEntity(keyResultOrdinal, authorizationUser); - } + createKeyResult(keyResult, duplicatedObjective, authorizationUser); } return duplicatedObjective; } + private void createKeyResult(KeyResult keyResult, Objective objective, AuthorizationUser authorizationUser) { + if (keyResult.getKeyResultType().equals(KEY_RESULT_TYPE_METRIC)) { + createMetricKeyResult(keyResult, objective, authorizationUser); + } else if (keyResult.getKeyResultType().equals(KEY_RESULT_TYPE_ORDINAL)) { + createOrdinalKeyResult(keyResult, objective, authorizationUser); + } + } + + private void createMetricKeyResult(KeyResult keyResult, Objective objective, AuthorizationUser authorizationUser) { + KeyResult keyResultMetric = KeyResultMetric.Builder.builder().withObjective(objective) + .withTitle(keyResult.getTitle()).withDescription(keyResult.getDescription()) + .withOwner(keyResult.getOwner()).withUnit(((KeyResultMetric) keyResult).getUnit()).withBaseline(0D) + .withStretchGoal(1D).build(); + keyResultBusinessService.createEntity(keyResultMetric, authorizationUser); + } + + private void createOrdinalKeyResult(KeyResult keyResult, Objective objective, AuthorizationUser authorizationUser) { + KeyResult keyResultOrdinal = KeyResultOrdinal.Builder.builder().withObjective(objective) + .withTitle(keyResult.getTitle()).withDescription(keyResult.getDescription()) + .withOwner(keyResult.getOwner()).withCommitZone("-").withTargetZone("-").withStretchZone("-").build(); + keyResultBusinessService.createEntity(keyResultOrdinal, authorizationUser); + } + @Transactional public void deleteEntityById(Long id) { validator.validateOnDelete(id); diff --git a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java index 3c9238ac15..ef126e12fc 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/validation/AlignmentValidationService.java @@ -15,6 +15,8 @@ import java.util.List; import java.util.Objects; +import static ch.puzzle.okr.Constants.ALIGNED_OBJECTIVE_ID; + @Service public class AlignmentValidationService extends ValidationBase { @@ -58,7 +60,7 @@ public void validateOnUpdate(Long id, Alignment model) { private void throwExceptionWhenAlignmentObjectIsNull(Alignment model) { if (model.getAlignedObjective() == null) { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, - List.of("alignedObjectiveId")); + List.of(ALIGNED_OBJECTIVE_ID)); } else if (model instanceof ObjectiveAlignment objectiveAlignment) { if (objectiveAlignment.getAlignmentTarget() == null) { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ATTRIBUTE_NULL, @@ -103,7 +105,7 @@ private void throwExceptionWhenAlignedIdIsSameAsTargetId(Alignment model) { private void throwExceptionWhenAlignmentWithAlignedObjectiveAlreadyExists(Alignment model) { if (this.alignmentPersistenceService.findByAlignedObjectiveId(model.getAlignedObjective().getId()) != null) { throw new OkrResponseStatusException(HttpStatus.BAD_REQUEST, ErrorKey.ALIGNMENT_ALREADY_EXISTS, - List.of("alignedObjectiveId", model.getAlignedObjective().getId())); + List.of(ALIGNED_OBJECTIVE_ID, model.getAlignedObjective().getId())); } } diff --git a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql index d4bcf19be6..acb9f76816 100644 --- a/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql +++ b/backend/src/main/resources/db/h2-db/database-h2-schema/V1_0_0__current-db-schema-for-testing.sql @@ -239,9 +239,9 @@ SELECT OA.QUARTER_ID AS QUARTER_ID, OA.STATE AS STATE, 'objective' AS OBJECT_TYPE, - 'source' AS CONNECTION_ITEM, - COALESCE(A.TARGET_OBJECTIVE_ID, A.TARGET_KEY_RESULT_ID) AS REF_ID, - A.ALIGNMENT_TYPE AS REF_TYPE + 'source' AS CONNECTION_ROLE, + COALESCE(A.TARGET_OBJECTIVE_ID, A.TARGET_KEY_RESULT_ID) AS COUNTERPART_ID, + A.ALIGNMENT_TYPE AS COUNTERPART_TYPE FROM ALIGNMENT A LEFT JOIN OBJECTIVE OA ON OA.ID = A.ALIGNED_OBJECTIVE_ID LEFT JOIN TEAM OTT ON OTT.ID = OA.TEAM_ID @@ -255,9 +255,9 @@ SELECT OT.QUARTER_ID AS QUARTER_ID, OT.STATE AS STATE, 'objective' AS OBJECT_TYPE, - 'target' AS CONNECTION_ITEM, - A.ALIGNED_OBJECTIVE_ID AS REF_ID, - 'objective' AS REF_TYPE + 'target' AS CONNECTION_ROLE, + A.ALIGNED_OBJECTIVE_ID AS COUNTERPART_ID, + 'objective' AS COUNTERPART_TYPE FROM ALIGNMENT A LEFT JOIN OBJECTIVE OT ON OT.ID = A.TARGET_OBJECTIVE_ID LEFT JOIN TEAM OTT ON OTT.ID = OT.TEAM_ID @@ -272,9 +272,9 @@ SELECT O.QUARTER_ID AS QUARTER_ID, NULL AS STATE, 'keyResult' AS OBJECT_TYPE, - 'target' AS CONNECTION_ITEM, - A.ALIGNED_OBJECTIVE_ID AS REF_ID, - 'objective' AS REF_TYPE + 'target' AS CONNECTION_ROLE, + A.ALIGNED_OBJECTIVE_ID AS COUNTERPART_ID, + 'objective' AS COUNTERPART_TYPE FROM ALIGNMENT A LEFT JOIN KEY_RESULT KRT ON KRT.ID = A.TARGET_KEY_RESULT_ID LEFT JOIN OBJECTIVE O ON O.ID = KRT.OBJECTIVE_ID diff --git a/backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql b/backend/src/main/resources/db/migration/V2_1_4__createAlignmentView.sql similarity index 79% rename from backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql rename to backend/src/main/resources/db/migration/V2_1_4__createAlignmentView.sql index aa037e62b2..2cced34040 100644 --- a/backend/src/main/resources/db/migration/V2_1_3__createAlignmentView.sql +++ b/backend/src/main/resources/db/migration/V2_1_4__createAlignmentView.sql @@ -9,9 +9,9 @@ SELECT oa.quarter_id as quarter_id, oa.state as state, 'objective' as object_type, - 'source' as connection_item, - coalesce(a.target_objective_id, a.target_key_result_id) as ref_id, - a.alignment_type as ref_type + 'source' as connection_role, + coalesce(a.target_objective_id, a.target_key_result_id) as counterpart_id, + a.alignment_type as counterpart_type FROM alignment a LEFT JOIN objective oa ON oa.id = a.aligned_objective_id LEFT JOIN team ott ON ott.id = oa.team_id @@ -27,9 +27,9 @@ SELECT ot.quarter_id as quarter_id, ot.state as state, 'objective' as object_type, - 'target' as connection_item, - a.aligned_objective_id as ref_id, - 'objective' as ref_type + 'target' as connection_role, + a.aligned_objective_id as counterpart_id, + 'objective' as counterpart_type FROM alignment a LEFT JOIN objective ot ON ot.id = a.target_objective_id LEFT JOIN team ott ON ott.id = ot.team_id @@ -46,9 +46,9 @@ SELECT o.quarter_id as quarter_id, null as state, 'keyResult' as object_type, - 'target' as connection_item, - a.aligned_objective_id as ref_id, - 'objective' as ref_type + 'target' as connection_role, + a.aligned_objective_id as counterpart_id, + 'objective' as counterpart_type FROM alignment a LEFT JOIN key_result krt ON krt.id = a.target_key_result_id LEFT JOIN objective o ON o.id = krt.objective_id diff --git a/backend/src/test/java/ch/puzzle/okr/controller/AlignmentControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/AlignmentControllerIT.java new file mode 100644 index 0000000000..33dd701e76 --- /dev/null +++ b/backend/src/test/java/ch/puzzle/okr/controller/AlignmentControllerIT.java @@ -0,0 +1,72 @@ +package ch.puzzle.okr.controller; + +import ch.puzzle.okr.dto.alignment.AlignmentConnectionDto; +import ch.puzzle.okr.dto.alignment.AlignmentLists; +import ch.puzzle.okr.dto.alignment.AlignmentObjectDto; +import ch.puzzle.okr.service.business.AlignmentBusinessService; +import org.hamcrest.Matchers; +import org.hamcrest.core.Is; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.BDDMockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.List; + +import static ch.puzzle.okr.TestConstants.TEAM_PUZZLE; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +@WithMockUser(value = "spring") +@ExtendWith(MockitoExtension.class) +@WebMvcTest(AlignmentController.class) +class AlignmentControllerIT { + @Autowired + private MockMvc mvc; + @MockBean + private AlignmentBusinessService alignmentBusinessService; + + private static final String OBJECTIVE = "objective"; + private static final String ONGOING = "ONGOING"; + static AlignmentObjectDto alignmentObjectDto1 = new AlignmentObjectDto(3L, "Title of first Objective", TEAM_PUZZLE, + ONGOING, OBJECTIVE); + static AlignmentObjectDto alignmentObjectDto2 = new AlignmentObjectDto(4L, "Title of second Objective", "BBT", + ONGOING, OBJECTIVE); + static AlignmentConnectionDto alignmentConnectionDto = new AlignmentConnectionDto(4L, 3L, null); + + static AlignmentLists alignmentLists = new AlignmentLists(List.of(alignmentObjectDto1, alignmentObjectDto2), + List.of(alignmentConnectionDto)); + + @Test + void shouldReturnCorrectAlignmentData() throws Exception { + BDDMockito.given(alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 5L, 8L), "")) + .willReturn(alignmentLists); + + mvc.perform(get("/api/v2/alignments/alignmentLists?quarterFilter=2&teamFilter=4,5,8") + .contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(jsonPath("$.alignmentObjectDtoList", Matchers.hasSize(2))) + .andExpect(jsonPath("$.alignmentObjectDtoList[1].objectId", Is.is(4))) + .andExpect(jsonPath("$.alignmentConnectionDtoList[0].alignedObjectiveId", Is.is(4))) + .andExpect(jsonPath("$.alignmentConnectionDtoList[0].targetObjectiveId", Is.is(3))); + } + + @Test + void shouldReturnCorrectAlignmentDataWithObjectiveSearch() throws Exception { + BDDMockito.given(alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 5L, 8L), "secon")) + .willReturn(alignmentLists); + + mvc.perform(get("/api/v2/alignments/alignmentLists?quarterFilter=2&teamFilter=4,5,8&objectiveQuery=secon") + .contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(jsonPath("$.alignmentObjectDtoList", Matchers.hasSize(2))) + .andExpect(jsonPath("$.alignmentObjectDtoList[1].objectId", Is.is(4))) + .andExpect(jsonPath("$.alignmentConnectionDtoList[0].alignedObjectiveId", Is.is(4))) + .andExpect(jsonPath("$.alignmentConnectionDtoList[0].targetObjectiveId", Is.is(3))); + } +} diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java index 7371d10369..e4b9bf7e79 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceIT.java @@ -19,7 +19,7 @@ class AlignmentBusinessServiceIT { @Test void shouldReturnCorrectAlignmentData() { - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(9L, List.of(5L, 6L), ""); + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(9L, List.of(5L, 6L), ""); assertEquals(6, alignmentLists.alignmentObjectDtoList().size()); assertEquals(4, alignmentLists.alignmentConnectionDtoList().size()); @@ -47,7 +47,7 @@ void shouldReturnCorrectAlignmentData() { @Test void shouldReturnCorrectAlignmentDataWhenLimitedTeamMatching() { - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(9L, List.of(6L), ""); + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(9L, List.of(6L), ""); assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); @@ -62,7 +62,7 @@ void shouldReturnCorrectAlignmentDataWhenLimitedTeamMatching() { @Test void shouldReturnCorrectAlignmentDataWithObjectiveSearch() { - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(9L, List.of(4L, 5L, 6L, 8L), + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(9L, List.of(4L, 5L, 6L, 8L), "lehrling"); assertEquals(3, alignmentLists.alignmentObjectDtoList().size()); @@ -83,7 +83,7 @@ void shouldReturnCorrectAlignmentDataWithObjectiveSearch() { @Test void shouldReturnCorrectAlignmentDataWithKeyResultWhenMatchingObjectiveSearch() { - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(9L, List.of(4L, 5L, 6L, 8L), + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(9L, List.of(4L, 5L, 6L, 8L), "firmenums"); assertEquals(2, alignmentLists.alignmentObjectDtoList().size()); @@ -99,7 +99,7 @@ void shouldReturnCorrectAlignmentDataWithKeyResultWhenMatchingObjectiveSearch() @Test void shouldReturnEmptyAlignmentDataWhenNoAlignments() { - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(3L, List.of(5L, 6L), ""); + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(3L, List.of(5L, 6L), ""); assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); @@ -107,7 +107,7 @@ void shouldReturnEmptyAlignmentDataWhenNoAlignments() { @Test void shouldReturnEmptyAlignmentDataWhenNoMatchingObjectiveSearch() { - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(9L, List.of(4L, 5L, 6L, 8L), + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(9L, List.of(4L, 5L, 6L, 8L), "spass"); assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); @@ -116,8 +116,8 @@ void shouldReturnEmptyAlignmentDataWhenNoMatchingObjectiveSearch() { @Test void shouldReturnCorrectAlignmentDataWhenEmptyQuarterFilter() { - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(null, List.of(4L, 5L, 6L, 8L), - ""); + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(null, + List.of(4L, 5L, 6L, 8L), ""); assertEquals(8, alignmentLists.alignmentObjectDtoList().size()); assertEquals(5, alignmentLists.alignmentConnectionDtoList().size()); @@ -125,7 +125,7 @@ void shouldReturnCorrectAlignmentDataWhenEmptyQuarterFilter() { @Test void shouldReturnEmptyAlignmentDataWhenEmptyTeamFilter() { - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, null, ""); + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, null, ""); assertEquals(0, alignmentLists.alignmentObjectDtoList().size()); assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); diff --git a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java index 6babddba37..7e81b66335 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java +++ b/backend/src/test/java/ch/puzzle/okr/service/business/AlignmentBusinessServiceTest.java @@ -72,20 +72,20 @@ class AlignmentBusinessServiceTest { AlignmentView alignmentView1 = AlignmentView.Builder.builder().withUniqueId("45TkeyResultkeyResult").withId(4L) .withTitle("Antwortzeit für Supportanfragen um 33% verkürzen.").withTeamId(5L).withTeamName("Puzzle ITC") - .withQuarterId(2L).withObjectType("keyResult").withConnectionItem("target").withRefId(5L) - .withRefType("objective").build(); + .withQuarterId(2L).withObjectType("keyResult").withConnectionRole("target").withCounterpartId(5L) + .withCounterpartType("objective").build(); AlignmentView alignmentView2 = AlignmentView.Builder.builder().withUniqueId("54SobjectivekeyResult").withId(5L) .withTitle("Wir wollen das leiseste Team bei Puzzle sein.").withTeamId(4L).withTeamName("/BBT") - .withQuarterId(2L).withObjectType("objective").withConnectionItem("source").withRefId(4L) - .withRefType("keyResult").build(); + .withQuarterId(2L).withObjectType("objective").withConnectionRole("source").withCounterpartId(4L) + .withCounterpartType("keyResult").build(); AlignmentView alignmentView3 = AlignmentView.Builder.builder().withUniqueId("4041Tobjectiveobjective").withId(40L) .withTitle("Wir wollen eine gute Mitarbeiterzufriedenheit.").withTeamId(6L).withTeamName("LoremIpsum") - .withQuarterId(2L).withObjectType("objective").withConnectionItem("target").withRefId(41L) - .withRefType("objective").build(); + .withQuarterId(2L).withObjectType("objective").withConnectionRole("target").withCounterpartId(41L) + .withCounterpartType("objective").build(); AlignmentView alignmentView4 = AlignmentView.Builder.builder().withUniqueId("4140Sobjectiveobjective").withId(41L) .withTitle("Das Projekt generiert 10000 CHF Umsatz").withTeamId(6L).withTeamName("LoremIpsum") - .withQuarterId(2L).withObjectType("objective").withConnectionItem("source").withRefId(40L) - .withRefType("objective").build(); + .withQuarterId(2L).withObjectType("objective").withConnectionRole("source").withCounterpartId(40L) + .withCounterpartType("objective").build(); @Test void shouldGetTargetAlignmentIdObjective() { @@ -295,7 +295,7 @@ void shouldReturnCorrectAlignmentData() { when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) .thenReturn(List.of(alignmentView1, alignmentView2)); - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L), ""); + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L), ""); verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); @@ -313,7 +313,7 @@ void shouldReturnCorrectAlignmentDataWithMultipleTeamFilter() { when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L, 6L), ""); + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 6L), ""); verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); assertEquals(2, alignmentLists.alignmentConnectionDtoList().size()); @@ -334,7 +334,7 @@ void shouldReturnCorrectAlignmentDataWhenTeamFilterHasLimitedMatch() { when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L), ""); + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L), ""); verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); @@ -352,7 +352,7 @@ void shouldReturnEmptyAlignmentDataWhenNoMatchingTeam() { when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(12L), ""); + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(12L), ""); verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); @@ -363,7 +363,7 @@ void shouldReturnEmptyAlignmentDataWhenNoMatchingTeam() { void shouldReturnEmptyAlignmentDataWhenNoTeamFilterProvided() { doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, null, ""); + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, null, ""); verify(alignmentViewPersistenceService, times(0)).getAlignmentViewListByQuarterId(2L); verify(validator, times(1)).validateOnAlignmentGet(2L, List.of()); @@ -378,7 +378,7 @@ void shouldReturnEmptyAlignmentDataWhenNoQuarterFilterProvided() { .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); when(quarterBusinessService.getCurrentQuarter()).thenReturn(quarter); - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(null, List.of(4L, 6L), ""); + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(null, List.of(4L, 6L), ""); verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); verify(quarterBusinessService, times(1)).getCurrentQuarter(); @@ -400,7 +400,7 @@ void shouldReturnCorrectAlignmentDataWithObjectiveSearch() { when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L, 5L, 6L), + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 5L, 6L), "leise"); verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); @@ -419,7 +419,7 @@ void shouldReturnEmptyAlignmentDataWhenNoMatchingObjectiveFromObjectiveSearch() when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L, 5L, 6L), + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 5L, 6L), "Supportanfragen"); verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); @@ -433,7 +433,7 @@ void shouldReturnEmptyAlignmentDataWhenNoMatchingObjectiveSearch() { when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L, 5L, 6L), + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 5L, 6L), "wird nicht vorkommen"); verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); @@ -446,7 +446,8 @@ void shouldReturnEmptyAlignmentDataWhenNoAlignmentViews() { doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)).thenReturn(List.of()); - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(4L, 5L, 6L), ""); + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(4L, 5L, 6L), + ""); verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); assertEquals(0, alignmentLists.alignmentConnectionDtoList().size()); @@ -457,26 +458,26 @@ void shouldReturnEmptyAlignmentDataWhenNoAlignmentViews() { void shouldReturnCorrectAlignmentListsWithComplexAlignments() { AlignmentView alignmentView1 = AlignmentView.Builder.builder().withUniqueId("36TkeyResultkeyResult").withId(3L) .withTitle("Steigern der URS um 25%").withTeamId(5L).withTeamName("Puzzle ITC").withQuarterId(2L) - .withObjectType("keyResult").withConnectionItem("target").withRefId(6L).withRefType("objective") - .build(); + .withObjectType("keyResult").withConnectionRole("target").withCounterpartId(6L) + .withCounterpartType("objective").build(); AlignmentView alignmentView2 = AlignmentView.Builder.builder().withUniqueId("63SobjectivekeyResult").withId(6L) .withTitle("Als BBT wollen wir den Arbeitsalltag der Members von Puzzle ITC erleichtern.") .withTeamId(4L).withTeamName("/BBT").withQuarterId(2L).withObjectType("objective") - .withConnectionItem("source").withRefId(3L).withRefType("keyResult").build(); + .withConnectionRole("source").withCounterpartId(3L).withCounterpartType("keyResult").build(); AlignmentView alignmentView3 = AlignmentView.Builder.builder().withUniqueId("63Tobjectiveobjective").withId(6L) .withTitle("Als BBT wollen wir den Arbeitsalltag der Members von Puzzle ITC erleichtern.") .withTeamId(4L).withTeamName("/BBT").withQuarterId(2L).withObjectType("objective") - .withConnectionItem("target").withRefId(3L).withRefType("objective").build(); + .withConnectionRole("target").withCounterpartId(3L).withCounterpartType("objective").build(); AlignmentView alignmentView4 = AlignmentView.Builder.builder().withUniqueId("36Sobjectiveobjective").withId(3L) .withTitle("Wir wollen die Kundenzufriedenheit steigern").withTeamId(4L).withTeamName("/BBT") - .withQuarterId(2L).withObjectType("objective").withConnectionItem("source").withRefId(6L) - .withRefType("objective").build(); + .withQuarterId(2L).withObjectType("objective").withConnectionRole("source").withCounterpartId(6L) + .withCounterpartType("objective").build(); doNothing().when(validator).validateOnAlignmentGet(anyLong(), anyList()); when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)) .thenReturn(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4)); - AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentsByFilters(2L, List.of(5L), ""); + AlignmentLists alignmentLists = alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(5L), ""); verify(alignmentViewPersistenceService, times(1)).getAlignmentViewListByQuarterId(2L); assertEquals(1, alignmentLists.alignmentConnectionDtoList().size()); @@ -493,65 +494,65 @@ void shouldReturnCorrectAlignmentListsWithComplexAlignments() { @Test void shouldCorrectFilterAlignmentViewListsWithAllCorrectData() { AlignmentBusinessService.DividedAlignmentViewLists dividedAlignmentViewLists = alignmentBusinessService - .filterAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), + .filterAndDivideAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), List.of(4L, 6L, 5L), ""); - assertEquals(4, dividedAlignmentViewLists.correctAlignments().size()); - assertEquals(0, dividedAlignmentViewLists.wrongAlignments().size()); - assertEquals(4, dividedAlignmentViewLists.correctAlignments().get(0).getId()); - assertEquals(5, dividedAlignmentViewLists.correctAlignments().get(1).getId()); - assertEquals(40, dividedAlignmentViewLists.correctAlignments().get(2).getId()); - assertEquals(41, dividedAlignmentViewLists.correctAlignments().get(3).getId()); + assertEquals(4, dividedAlignmentViewLists.filterMatchingAlignments().size()); + assertEquals(0, dividedAlignmentViewLists.nonMatchingAlignments().size()); + assertEquals(4, dividedAlignmentViewLists.filterMatchingAlignments().get(0).getId()); + assertEquals(5, dividedAlignmentViewLists.filterMatchingAlignments().get(1).getId()); + assertEquals(40, dividedAlignmentViewLists.filterMatchingAlignments().get(2).getId()); + assertEquals(41, dividedAlignmentViewLists.filterMatchingAlignments().get(3).getId()); } @Test void shouldCorrectFilterAlignmentViewListsWithLimitedTeamFilter() { AlignmentBusinessService.DividedAlignmentViewLists dividedAlignmentViewLists = alignmentBusinessService - .filterAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), + .filterAndDivideAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), List.of(6L), ""); - assertEquals(2, dividedAlignmentViewLists.correctAlignments().size()); - assertEquals(2, dividedAlignmentViewLists.wrongAlignments().size()); - assertEquals(40, dividedAlignmentViewLists.correctAlignments().get(0).getId()); - assertEquals(41, dividedAlignmentViewLists.correctAlignments().get(1).getId()); - assertEquals(4, dividedAlignmentViewLists.wrongAlignments().get(0).getId()); - assertEquals(5, dividedAlignmentViewLists.wrongAlignments().get(1).getId()); + assertEquals(2, dividedAlignmentViewLists.filterMatchingAlignments().size()); + assertEquals(2, dividedAlignmentViewLists.nonMatchingAlignments().size()); + assertEquals(40, dividedAlignmentViewLists.filterMatchingAlignments().get(0).getId()); + assertEquals(41, dividedAlignmentViewLists.filterMatchingAlignments().get(1).getId()); + assertEquals(4, dividedAlignmentViewLists.nonMatchingAlignments().get(0).getId()); + assertEquals(5, dividedAlignmentViewLists.nonMatchingAlignments().get(1).getId()); } @Test void shouldCorrectFilterAlignmentViewListsWithObjectiveSearch() { AlignmentBusinessService.DividedAlignmentViewLists dividedAlignmentViewLists = alignmentBusinessService - .filterAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), + .filterAndDivideAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), List.of(4L, 6L, 5L), "leise"); - assertEquals(1, dividedAlignmentViewLists.correctAlignments().size()); - assertEquals(3, dividedAlignmentViewLists.wrongAlignments().size()); - assertEquals(5, dividedAlignmentViewLists.correctAlignments().get(0).getId()); - assertEquals(4, dividedAlignmentViewLists.wrongAlignments().get(0).getId()); - assertEquals(40, dividedAlignmentViewLists.wrongAlignments().get(1).getId()); - assertEquals(41, dividedAlignmentViewLists.wrongAlignments().get(2).getId()); + assertEquals(1, dividedAlignmentViewLists.filterMatchingAlignments().size()); + assertEquals(3, dividedAlignmentViewLists.nonMatchingAlignments().size()); + assertEquals(5, dividedAlignmentViewLists.filterMatchingAlignments().get(0).getId()); + assertEquals(4, dividedAlignmentViewLists.nonMatchingAlignments().get(0).getId()); + assertEquals(40, dividedAlignmentViewLists.nonMatchingAlignments().get(1).getId()); + assertEquals(41, dividedAlignmentViewLists.nonMatchingAlignments().get(2).getId()); } @Test void shouldCorrectFilterWhenNoMatchingObjectiveSearch() { AlignmentBusinessService.DividedAlignmentViewLists dividedAlignmentViewLists = alignmentBusinessService - .filterAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), + .filterAndDivideAlignmentViews(List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4), List.of(4L, 6L, 5L), "verk"); - assertEquals(0, dividedAlignmentViewLists.correctAlignments().size()); - assertEquals(4, dividedAlignmentViewLists.wrongAlignments().size()); - assertEquals(4, dividedAlignmentViewLists.wrongAlignments().get(0).getId()); - assertEquals(5, dividedAlignmentViewLists.wrongAlignments().get(1).getId()); - assertEquals(40, dividedAlignmentViewLists.wrongAlignments().get(2).getId()); - assertEquals(41, dividedAlignmentViewLists.wrongAlignments().get(3).getId()); + assertEquals(0, dividedAlignmentViewLists.filterMatchingAlignments().size()); + assertEquals(4, dividedAlignmentViewLists.nonMatchingAlignments().size()); + assertEquals(4, dividedAlignmentViewLists.nonMatchingAlignments().get(0).getId()); + assertEquals(5, dividedAlignmentViewLists.nonMatchingAlignments().get(1).getId()); + assertEquals(40, dividedAlignmentViewLists.nonMatchingAlignments().get(2).getId()); + assertEquals(41, dividedAlignmentViewLists.nonMatchingAlignments().get(3).getId()); } @Test void shouldThrowErrorWhenPersistenceServiceReturnsIncorrectData() { AlignmentView alignmentView5 = AlignmentView.Builder.builder().withUniqueId("23TkeyResultkeyResult").withId(20L) .withTitle("Dies hat kein Gegenstück").withTeamId(5L).withTeamName("Puzzle ITC").withQuarterId(2L) - .withObjectType("keyResult").withConnectionItem("target").withRefId(37L).withRefType("objective") - .build(); + .withObjectType("keyResult").withConnectionRole("target").withCounterpartId(37L) + .withCounterpartType("objective").build(); List finalList = List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4, alignmentView5); @@ -559,7 +560,7 @@ void shouldThrowErrorWhenPersistenceServiceReturnsIncorrectData() { when(alignmentViewPersistenceService.getAlignmentViewListByQuarterId(2L)).thenReturn(finalList); OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> alignmentBusinessService.getAlignmentsByFilters(2L, List.of(5L), "")); + () -> alignmentBusinessService.getAlignmentListsByFilters(2L, List.of(5L), "")); List expectedErrors = List .of(new ErrorDto("ALIGNMENT_DATA_FAIL", List.of("alignmentData", "2", "[5]", ""))); @@ -573,7 +574,8 @@ void shouldThrowErrorWhenPersistenceServiceReturnsIncorrectData() { void shouldNotThrowErrorWhenSameAmountOfSourceAndTarget() { List finalList = List.of(alignmentView1, alignmentView2, alignmentView3, alignmentView4); - assertDoesNotThrow(() -> alignmentBusinessService.validateFinalList(finalList, 2L, List.of(5L), "")); + assertDoesNotThrow( + () -> alignmentBusinessService.sourceAndTargetListsEqualSameSize(finalList, 2L, List.of(5L), "")); } @Test @@ -581,7 +583,7 @@ void shouldThrowErrorWhenNotSameAmountOfSourceAndTarget() { List finalList = List.of(alignmentView1, alignmentView2, alignmentView3); OkrResponseStatusException exception = assertThrows(OkrResponseStatusException.class, - () -> alignmentBusinessService.validateFinalList(finalList, 2L, List.of(5L), "")); + () -> alignmentBusinessService.sourceAndTargetListsEqualSameSize(finalList, 2L, List.of(5L), "")); List expectedErrors = List .of(new ErrorDto("ALIGNMENT_DATA_FAIL", List.of("alignmentData", "2", "[5]", ""))); diff --git a/frontend/src/app/diagram/diagram.component.html b/frontend/src/app/diagram/diagram.component.html index ebba733bc9..e6594ae99e 100644 --- a/frontend/src/app/diagram/diagram.component.html +++ b/frontend/src/app/diagram/diagram.component.html @@ -1,6 +1,6 @@ -
+
-
+

Kein Alignment vorhanden

diff --git a/frontend/src/app/diagram/diagram.component.spec.ts b/frontend/src/app/diagram/diagram.component.spec.ts index b914df59c7..2a9d737409 100644 --- a/frontend/src/app/diagram/diagram.component.spec.ts +++ b/frontend/src/app/diagram/diagram.component.spec.ts @@ -40,15 +40,15 @@ describe('DiagramComponent', () => { }); it('should call generateElements if alignmentData is present', () => { - jest.spyOn(component, 'generateElements'); + jest.spyOn(component, 'generateNodes'); component.prepareDiagramData(alignmentLists); - expect(component.generateElements).toHaveBeenCalled(); - expect(component.emptyDiagramData).toBeFalsy(); + expect(component.generateNodes).toHaveBeenCalled(); + expect(component.noAlignmentData).toBeFalsy(); }); it('should not call generateElements if alignmentData is empty', () => { - jest.spyOn(component, 'generateElements'); + jest.spyOn(component, 'generateNodes'); let alignmentLists: AlignmentLists = { alignmentObjectDtoList: [], @@ -56,8 +56,8 @@ describe('DiagramComponent', () => { }; component.prepareDiagramData(alignmentLists); - expect(component.generateElements).not.toHaveBeenCalled(); - expect(component.emptyDiagramData).toBeTruthy(); + expect(component.generateNodes).not.toHaveBeenCalled(); + expect(component.noAlignmentData).toBeTruthy(); }); it('should call prepareDiagramData when Subject receives new data', () => { @@ -102,7 +102,7 @@ describe('DiagramComponent', () => { let diagramElements: any[] = [element1, element2]; let edges: any[] = [edge]; - component.generateElements(alignmentLists); + component.generateNodes(alignmentLists); expect(component.generateConnections).toHaveBeenCalled(); expect(component.generateDiagram).toHaveBeenCalled(); @@ -118,7 +118,7 @@ describe('DiagramComponent', () => { let diagramData: any[] = getReturnedAlignmentDataKeyResult(); - component.generateElements(alignmentListsKeyResult); + component.generateNodes(alignmentListsKeyResult); expect(component.generateConnections).toHaveBeenCalled(); expect(component.generateDiagram).toHaveBeenCalled(); @@ -134,7 +134,7 @@ describe('DiagramComponent', () => { let diagramData: any[] = getReturnedAlignmentDataKeyResult(); - component.generateElements(alignmentListsKeyResult); + component.generateNodes(alignmentListsKeyResult); expect(component.generateConnections).toHaveBeenCalled(); expect(component.generateDiagram).toHaveBeenCalled(); diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index 6085c69ba0..7eefa8a76c 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -29,7 +29,7 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { private alignmentData$: Subject = new Subject(); cy!: cytoscape.Core; diagramData: any[] = []; - emptyDiagramData: boolean = false; + noAlignmentData: boolean = false; alignmentDataCache: AlignmentLists | null = null; constructor( @@ -46,18 +46,18 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { this.alignmentData$.next(alignmentData); } - ngAfterViewInit() { + ngAfterViewInit(): void { this.alignmentData.subscribe((alignmentData: AlignmentLists): void => { - let lastItem: AlignmentObject = + let lastAlignmentItem: AlignmentObject = alignmentData.alignmentObjectDtoList[alignmentData.alignmentObjectDtoList.length - 1]; - let shouldUpdate: boolean = - lastItem?.objectTitle === 'reload' - ? lastItem?.objectType === 'true' + let needsUpdate: boolean = + lastAlignmentItem?.objectTitle === 'reload' + ? lastAlignmentItem?.objectType === 'true' : JSON.stringify(this.alignmentDataCache) !== JSON.stringify(alignmentData); - if (shouldUpdate) { - if (lastItem?.objectTitle === 'reload') { + if (needsUpdate) { + if (lastAlignmentItem?.objectTitle === 'reload') { alignmentData.alignmentObjectDtoList.pop(); } this.alignmentDataCache = alignmentData; @@ -68,7 +68,7 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { }); } - ngOnDestroy() { + ngOnDestroy(): void { this.cleanUpDiagram(); } @@ -141,20 +141,20 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { prepareDiagramData(alignmentData: AlignmentLists): void { if (alignmentData.alignmentObjectDtoList.length == 0) { - this.emptyDiagramData = true; + this.noAlignmentData = true; } else { - this.emptyDiagramData = false; - this.generateElements(alignmentData); + this.noAlignmentData = false; + this.generateNodes(alignmentData); } } - generateElements(alignmentData: AlignmentLists): void { + generateNodes(alignmentData: AlignmentLists): void { let observableArray: any[] = []; let diagramElements: any[] = []; alignmentData.alignmentObjectDtoList.forEach((alignmentObject: AlignmentObject) => { if (alignmentObject.objectType == 'objective') { let observable: Observable = new Observable((observer) => { - let element = { + let node = { data: { id: 'Ob' + alignmentObject.objectId, }, @@ -166,61 +166,26 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { ), }, }; - diagramElements.push(element); - observer.next(element); + diagramElements.push(node); + observer.next(node); observer.complete(); }); observableArray.push(observable); } else { let observable: Observable = this.keyResultService.getFullKeyResult(alignmentObject.objectId).pipe( map((keyResult: KeyResult) => { + let keyResultState: string | undefined; + if (keyResult.keyResultType == 'metric') { let metricKeyResult: KeyResultMetric = keyResult as KeyResultMetric; let percentage: number = calculateCurrentPercentage(metricKeyResult); - - let keyResultState: string | undefined; - if (percentage < 30) { - keyResultState = 'FAIL'; - } else if (percentage < 70) { - keyResultState = 'COMMIT'; - } else if (percentage < 100) { - keyResultState = 'TARGET'; - } else if (percentage >= 100) { - keyResultState = 'STRETCH'; - } else { - keyResultState = undefined; - } - let element = { - data: { - id: 'KR' + alignmentObject.objectId, - }, - style: { - 'background-image': this.generateKeyResultSVG( - alignmentObject.objectTitle, - alignmentObject.objectTeamName, - keyResultState, - ), - }, - }; - diagramElements.push(element); + keyResultState = this.generateMetricKeyResultState(percentage); } else { let ordinalKeyResult: KeyResultOrdinal = keyResult as KeyResultOrdinal; - let keyResultState: string | undefined = ordinalKeyResult.lastCheckIn?.value.toString(); - - let element = { - data: { - id: 'KR' + alignmentObject.objectId, - }, - style: { - 'background-image': this.generateKeyResultSVG( - alignmentObject.objectTitle, - alignmentObject.objectTeamName, - keyResultState, - ), - }, - }; - diagramElements.push(element); + keyResultState = ordinalKeyResult.lastCheckIn?.value.toString(); } + let element = this.generateKeyResultElement(alignmentObject, keyResultState); + diagramElements.push(element); }), ); observableArray.push(observable); @@ -232,26 +197,50 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { }); } + generateMetricKeyResultState(percentage: number): string | undefined { + let keyResultState: string | undefined; + if (percentage < 30) { + keyResultState = 'FAIL'; + } else if (percentage < 70) { + keyResultState = 'COMMIT'; + } else if (percentage < 100) { + keyResultState = 'TARGET'; + } else if (percentage >= 100) { + keyResultState = 'STRETCH'; + } else { + keyResultState = undefined; + } + return keyResultState; + } + + generateKeyResultElement(alignmentObject: AlignmentObject, keyResultState: string | undefined) { + return { + data: { + id: 'KR' + alignmentObject.objectId, + }, + style: { + 'background-image': this.generateKeyResultSVG( + alignmentObject.objectTitle, + alignmentObject.objectTeamName, + keyResultState, + ), + }, + }; + } + generateConnections(alignmentData: AlignmentLists, diagramElements: any[]): void { let edges: any[] = []; alignmentData.alignmentConnectionDtoList.forEach((alignmentConnection: AlignmentConnection) => { - if (alignmentConnection.targetKeyResultId == null) { - let edge = { - data: { - source: 'Ob' + alignmentConnection.alignedObjectiveId, - target: 'Ob' + alignmentConnection.targetObjectiveId, - }, - }; - edges.push(edge); - } else { - let edge = { - data: { - source: 'Ob' + alignmentConnection.alignedObjectiveId, - target: 'KR' + alignmentConnection.targetKeyResultId, - }, - }; - edges.push(edge); - } + let edge = { + data: { + source: 'Ob' + alignmentConnection.alignedObjectiveId, + target: + alignmentConnection.targetKeyResultId == null + ? 'Ob' + alignmentConnection.targetObjectiveId + : 'KR' + alignmentConnection.targetKeyResultId, + }, + }; + edges.push(edge); }); this.diagramData = diagramElements.concat(edges); this.generateDiagram(); diff --git a/frontend/src/app/overview/overview.component.html b/frontend/src/app/overview/overview.component.html index bdb255d991..f4929da899 100644 --- a/frontend/src/app/overview/overview.component.html +++ b/frontend/src/app/overview/overview.component.html @@ -46,7 +46,7 @@
- +
diff --git a/frontend/src/app/overview/overview.component.ts b/frontend/src/app/overview/overview.component.ts index 44956431a2..25ac21ff80 100644 --- a/frontend/src/app/overview/overview.component.ts +++ b/frontend/src/app/overview/overview.component.ts @@ -17,14 +17,13 @@ import { AlignmentObject } from '../shared/types/model/AlignmentObject'; }) export class OverviewComponent implements OnInit, OnDestroy { overviewEntities$: Subject = new Subject(); - alignmentData$: Subject = new Subject(); + alignmentLists$: Subject = new Subject(); emptyAlignmentList: AlignmentLists = { alignmentObjectDtoList: [], alignmentConnectionDtoList: [] } as AlignmentLists; protected readonly trackByFn = trackByFn; private destroyed$: ReplaySubject = new ReplaySubject(1); hasAdminAccess: ReplaySubject = new ReplaySubject(1); overviewPadding: Subject = new Subject(); isOverview: boolean = true; - service!: OverviewService | AlignmentService; constructor( private overviewService: OverviewService, @@ -93,18 +92,18 @@ export class OverviewComponent implements OnInit, OnDestroy { return EMPTY; }), ) - .subscribe((alignmentLists) => { + .subscribe((alignmentLists: AlignmentLists) => { if (reload != null) { - let kuchen: AlignmentObject = { + let alignmentObjectReload: AlignmentObject = { objectId: 0, objectTitle: 'reload', objectType: reload.toString(), objectTeamName: '', objectState: null, }; - alignmentLists.alignmentObjectDtoList.push(kuchen); + alignmentLists.alignmentObjectDtoList.push(alignmentObjectReload); } - this.alignmentData$.next(alignmentLists); + this.alignmentLists$.next(alignmentLists); }); } } diff --git a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts index f35c5c6993..0f98cd7a76 100644 --- a/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts +++ b/frontend/src/app/shared/dialog/objective-dialog/objective-form.component.spec.ts @@ -531,7 +531,7 @@ describe('ObjectiveDialogComponent', () => { expect(component.alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); - expect(component.objectiveForm.getRawValue().alignment).toEqual(alignmentObject2); + expect(component.objectiveForm.getRawValue().alignment).toEqual(alignmentPossibilityObject2); }); it('should load existing keyResult alignment to objectiveForm', async () => { @@ -541,7 +541,7 @@ describe('ObjectiveDialogComponent', () => { expect(component.alignmentPossibilities).toStrictEqual([alignmentPossibility1, alignmentPossibility2]); expect(component.filteredAlignmentOptions$.getValue()).toEqual([alignmentPossibility1, alignmentPossibility2]); - expect(component.objectiveForm.getRawValue().alignment).toEqual(alignmentObject3); + expect(component.objectiveForm.getRawValue().alignment).toEqual(alignmentPossibilityObject3); }); it('should filter correct alignment possibilities', async () => { From 1a359742513ec5a0347740de6c4a6402e5f6fc71 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 6 Jun 2024 13:35:19 +0200 Subject: [PATCH 114/119] Adjust height of diagram --- frontend/src/app/diagram/diagram.component.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/diagram/diagram.component.scss b/frontend/src/app/diagram/diagram.component.scss index 025ee23f5c..e19ddd7398 100644 --- a/frontend/src/app/diagram/diagram.component.scss +++ b/frontend/src/app/diagram/diagram.component.scss @@ -1,7 +1,7 @@ #cy { - margin: 30px 0 30px 0; + margin-top: 30px; width: calc(100vw - 60px); - height: calc(100vh - 300px); + height: calc(100vh - 360px); } .puzzle-logo { From d4cba313e13ae29b3ff139e74b051315830421f3 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 13 Jun 2024 07:08:52 +0000 Subject: [PATCH 115/119] [FM] Automated formating backend --- .../puzzle/okr/service/business/ObjectiveBusinessService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java index 01797dc100..00940f98e1 100644 --- a/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java +++ b/backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java @@ -130,7 +130,8 @@ public Objective updateEntity(Long id, Objective objective, AuthorizationUser au } private void handleAlignedEntity(Long id, Objective savedObjective, Objective updatedObjective) { - AlignedEntityDto alignedEntity = alignmentBusinessService.getTargetIdByAlignedObjectiveId(savedObjective.getId()); + AlignedEntityDto alignedEntity = alignmentBusinessService + .getTargetIdByAlignedObjectiveId(savedObjective.getId()); if ((updatedObjective.getAlignedEntity() != null) || updatedObjective.getAlignedEntity() == null && alignedEntity != null) { savedObjective.setAlignedEntity(updatedObjective.getAlignedEntity()); From 3de5fda1c0a82404778c070a1299f4004fa1c1fc Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Thu, 13 Jun 2024 09:44:11 +0200 Subject: [PATCH 116/119] Rename variable --- frontend/src/app/diagram/diagram.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index 7eefa8a76c..1fa6bc43d7 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -51,12 +51,12 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { let lastAlignmentItem: AlignmentObject = alignmentData.alignmentObjectDtoList[alignmentData.alignmentObjectDtoList.length - 1]; - let needsUpdate: boolean = + let diagramReloadRequired: boolean = lastAlignmentItem?.objectTitle === 'reload' ? lastAlignmentItem?.objectType === 'true' : JSON.stringify(this.alignmentDataCache) !== JSON.stringify(alignmentData); - if (needsUpdate) { + if (diagramReloadRequired) { if (lastAlignmentItem?.objectTitle === 'reload') { alignmentData.alignmentObjectDtoList.pop(); } From 89de0e41789be8d0670e56d35bb669995ec0ac7d Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Fri, 14 Jun 2024 13:38:33 +0200 Subject: [PATCH 117/119] Refactor frontend code --- .../src/app/diagram/diagram.component.html | 7 +- .../src/app/diagram/diagram.component.spec.ts | 3 +- frontend/src/app/diagram/diagram.component.ts | 59 +++++++------- .../objective-detail.component.ts | 4 +- .../app/overview/overview.component.spec.ts | 28 ++++++- .../src/app/overview/overview.component.ts | 76 ++++++++++--------- .../app/shared/types/enums/ObjectiveState.ts | 6 ++ 7 files changed, 109 insertions(+), 74 deletions(-) create mode 100644 frontend/src/app/shared/types/enums/ObjectiveState.ts diff --git a/frontend/src/app/diagram/diagram.component.html b/frontend/src/app/diagram/diagram.component.html index e6594ae99e..2491b38045 100644 --- a/frontend/src/app/diagram/diagram.component.html +++ b/frontend/src/app/diagram/diagram.component.html @@ -1,6 +1,9 @@ -
+
-
+

Kein Alignment vorhanden

diff --git a/frontend/src/app/diagram/diagram.component.spec.ts b/frontend/src/app/diagram/diagram.component.spec.ts index 2a9d737409..4b6a6b0285 100644 --- a/frontend/src/app/diagram/diagram.component.spec.ts +++ b/frontend/src/app/diagram/diagram.component.spec.ts @@ -44,7 +44,7 @@ describe('DiagramComponent', () => { component.prepareDiagramData(alignmentLists); expect(component.generateNodes).toHaveBeenCalled(); - expect(component.noAlignmentData).toBeFalsy(); + expect(component.alignmentDataCache?.alignmentObjectDtoList.length).not.toEqual(0); }); it('should not call generateElements if alignmentData is empty', () => { @@ -57,7 +57,6 @@ describe('DiagramComponent', () => { component.prepareDiagramData(alignmentLists); expect(component.generateNodes).not.toHaveBeenCalled(); - expect(component.noAlignmentData).toBeTruthy(); }); it('should call prepareDiagramData when Subject receives new data', () => { diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index 1fa6bc43d7..4883cb248b 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -1,5 +1,5 @@ import { AfterViewInit, Component, Input, OnDestroy } from '@angular/core'; -import { map, Observable, Subject, zip } from 'rxjs'; +import { map, Observable, of, Subject, zip } from 'rxjs'; import { AlignmentLists } from '../shared/types/model/AlignmentLists'; import cytoscape from 'cytoscape'; import { @@ -19,6 +19,8 @@ import { KeyResultOrdinal } from '../shared/types/model/KeyResultOrdinal'; import { Router } from '@angular/router'; import { AlignmentObject } from '../shared/types/model/AlignmentObject'; import { AlignmentConnection } from '../shared/types/model/AlignmentConnection'; +import { Zone } from '../shared/types/enums/Zone'; +import { ObjectiveState } from '../shared/types/enums/ObjectiveState'; @Component({ selector: 'app-diagram', @@ -29,7 +31,6 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { private alignmentData$: Subject = new Subject(); cy!: cytoscape.Core; diagramData: any[] = []; - noAlignmentData: boolean = false; alignmentDataCache: AlignmentLists | null = null; constructor( @@ -51,7 +52,7 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { let lastAlignmentItem: AlignmentObject = alignmentData.alignmentObjectDtoList[alignmentData.alignmentObjectDtoList.length - 1]; - let diagramReloadRequired: boolean = + const diagramReloadRequired: boolean = lastAlignmentItem?.objectTitle === 'reload' ? lastAlignmentItem?.objectType === 'true' : JSON.stringify(this.alignmentDataCache) !== JSON.stringify(alignmentData); @@ -70,6 +71,7 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { ngOnDestroy(): void { this.cleanUpDiagram(); + this.alignmentData.unsubscribe(); } generateDiagram(): void { @@ -140,10 +142,7 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { } prepareDiagramData(alignmentData: AlignmentLists): void { - if (alignmentData.alignmentObjectDtoList.length == 0) { - this.noAlignmentData = true; - } else { - this.noAlignmentData = false; + if (alignmentData.alignmentObjectDtoList.length != 0) { this.generateNodes(alignmentData); } } @@ -153,24 +152,20 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { let diagramElements: any[] = []; alignmentData.alignmentObjectDtoList.forEach((alignmentObject: AlignmentObject) => { if (alignmentObject.objectType == 'objective') { - let observable: Observable = new Observable((observer) => { - let node = { - data: { - id: 'Ob' + alignmentObject.objectId, - }, - style: { - 'background-image': this.generateObjectiveSVG( - alignmentObject.objectTitle, - alignmentObject.objectTeamName, - alignmentObject.objectState!, - ), - }, - }; - diagramElements.push(node); - observer.next(node); - observer.complete(); - }); - observableArray.push(observable); + let node = { + data: { + id: 'Ob' + alignmentObject.objectId, + }, + style: { + 'background-image': this.generateObjectiveSVG( + alignmentObject.objectTitle, + alignmentObject.objectTeamName, + alignmentObject.objectState!, + ), + }, + }; + diagramElements.push(node); + observableArray.push(of(node)); } else { let observable: Observable = this.keyResultService.getFullKeyResult(alignmentObject.objectId).pipe( map((keyResult: KeyResult) => { @@ -248,11 +243,11 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { generateObjectiveSVG(title: string, teamName: string, state: string): string { switch (state) { - case 'ONGOING': + case ObjectiveState.ONGOING: return generateObjectiveSVG(title, teamName, getOnGoingIcon); - case 'SUCCESSFUL': + case ObjectiveState.SUCCESSFUL: return generateObjectiveSVG(title, teamName, getSuccessfulIcon); - case 'NOTSUCCESSFUL': + case ObjectiveState.NOTSUCCESSFUL: return generateObjectiveSVG(title, teamName, getNotSuccessfulIcon); default: return generateObjectiveSVG(title, teamName, getDraftIcon); @@ -261,13 +256,13 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { generateKeyResultSVG(title: string, teamName: string, state: string | undefined): string { switch (state) { - case 'FAIL': + case Zone.FAIL: return generateKeyResultSVG(title, teamName, '#BA3838', 'white'); - case 'COMMIT': + case Zone.COMMIT: return generateKeyResultSVG(title, teamName, '#FFD600', 'black'); - case 'TARGET': + case Zone.TARGET: return generateKeyResultSVG(title, teamName, '#1E8A29', 'black'); - case 'STRETCH': + case Zone.STRETCH: return generateKeyResultSVG(title, teamName, '#1E5A96', 'white'); default: return generateNeutralKeyResultSVG(title, teamName); diff --git a/frontend/src/app/objective-detail/objective-detail.component.ts b/frontend/src/app/objective-detail/objective-detail.component.ts index 03bd8735e3..747bd70ea0 100644 --- a/frontend/src/app/objective-detail/objective-detail.component.ts +++ b/frontend/src/app/objective-detail/objective-detail.component.ts @@ -65,11 +65,9 @@ export class ObjectiveDetailComponent { .subscribe((result) => { if (result?.openNew) { this.openAddKeyResultDialog(); - } else if (result == '' || result == undefined) { return; - } else { - this.refreshDataService.markDataRefresh(); } + this.refreshDataService.markDataRefresh(); }); } diff --git a/frontend/src/app/overview/overview.component.spec.ts b/frontend/src/app/overview/overview.component.spec.ts index e38f09d652..c047057d24 100644 --- a/frontend/src/app/overview/overview.component.spec.ts +++ b/frontend/src/app/overview/overview.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { OverviewComponent } from './overview.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { overViewEntity1 } from '../shared/testData'; +import { alignmentLists, overViewEntity1 } from '../shared/testData'; import { BehaviorSubject, of, Subject } from 'rxjs'; import { OverviewService } from '../shared/services/overview.service'; import { AppRoutingModule } from '../app-routing.module'; @@ -16,11 +16,16 @@ import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { AlignmentService } from '../shared/services/alignment.service'; const overviewService = { getOverview: jest.fn(), }; +const alignmentService = { + getAlignmentByFilter: jest.fn(), +}; + const authGuardMock = () => { return Promise.resolve(true); }; @@ -53,6 +58,10 @@ describe('OverviewComponent', () => { provide: OverviewService, useValue: overviewService, }, + { + provide: AlignmentService, + useValue: alignmentService, + }, { provide: authGuard, useValue: authGuardMock, @@ -132,6 +141,23 @@ describe('OverviewComponent', () => { expect(component.loadOverview).toHaveBeenLastCalledWith(); }); + it('should call overviewService on overview', async () => { + jest.spyOn(overviewService, 'getOverview'); + component.isOverview = true; + + component.loadOverview(3, [5, 6], '', null); + expect(overviewService.getOverview).toHaveBeenCalled(); + }); + + it('should call alignmentService on diagram', async () => { + jest.spyOn(alignmentService, 'getAlignmentByFilter').mockReturnValue(of(alignmentLists)); + component.isOverview = false; + fixture.detectChanges(); + + component.loadOverview(3, [5, 6], '', null); + expect(alignmentService.getAlignmentByFilter).toHaveBeenCalled(); + }); + function markFiltersAsReady() { refreshDataServiceMock.quarterFilterReady.next(null); refreshDataServiceMock.teamFilterReady.next(null); diff --git a/frontend/src/app/overview/overview.component.ts b/frontend/src/app/overview/overview.component.ts index 25ac21ff80..cc97822235 100644 --- a/frontend/src/app/overview/overview.component.ts +++ b/frontend/src/app/overview/overview.component.ts @@ -69,45 +69,53 @@ export class OverviewComponent implements OnInit, OnDestroy { this.loadOverview(quarterId, teamIds, objectiveQueryString, reload); } - loadOverview(quarterId?: number, teamIds?: number[], objectiveQuery?: string, reload?: boolean | null) { + loadOverview(quarterId?: number, teamIds?: number[], objectiveQuery?: string, reload?: boolean | null): void { if (this.isOverview) { - this.overviewService - .getOverview(quarterId, teamIds, objectiveQuery) - .pipe( - catchError(() => { - this.loadOverview(); - return EMPTY; - }), - ) - .subscribe((dashboard) => { - this.hasAdminAccess.next(dashboard.adminAccess); - this.overviewEntities$.next(dashboard.overviews); - }); + this.loadOverviewData(quarterId, teamIds, objectiveQuery); } else { - this.alignmentService - .getAlignmentByFilter(quarterId, teamIds, objectiveQuery) - .pipe( - catchError(() => { - this.loadOverview(); - return EMPTY; - }), - ) - .subscribe((alignmentLists: AlignmentLists) => { - if (reload != null) { - let alignmentObjectReload: AlignmentObject = { - objectId: 0, - objectTitle: 'reload', - objectType: reload.toString(), - objectTeamName: '', - objectState: null, - }; - alignmentLists.alignmentObjectDtoList.push(alignmentObjectReload); - } - this.alignmentLists$.next(alignmentLists); - }); + this.loadAlignmentData(quarterId, teamIds, objectiveQuery, reload); } } + loadOverviewData(quarterId?: number, teamIds?: number[], objectiveQuery?: string): void { + this.overviewService + .getOverview(quarterId, teamIds, objectiveQuery) + .pipe( + catchError(() => { + this.loadOverview(); + return EMPTY; + }), + ) + .subscribe((dashboard) => { + this.hasAdminAccess.next(dashboard.adminAccess); + this.overviewEntities$.next(dashboard.overviews); + }); + } + + loadAlignmentData(quarterId?: number, teamIds?: number[], objectiveQuery?: string, reload?: boolean | null): void { + this.alignmentService + .getAlignmentByFilter(quarterId, teamIds, objectiveQuery) + .pipe( + catchError(() => { + this.loadOverview(); + return EMPTY; + }), + ) + .subscribe((alignmentLists: AlignmentLists) => { + if (reload != null) { + let alignmentObjectReload: AlignmentObject = { + objectId: 0, + objectTitle: 'reload', + objectType: reload.toString(), + objectTeamName: '', + objectState: null, + }; + alignmentLists.alignmentObjectDtoList.push(alignmentObjectReload); + } + this.alignmentLists$.next(alignmentLists); + }); + } + ngOnDestroy(): void { this.destroyed$.next(true); this.destroyed$.complete(); diff --git a/frontend/src/app/shared/types/enums/ObjectiveState.ts b/frontend/src/app/shared/types/enums/ObjectiveState.ts new file mode 100644 index 0000000000..342fa5a49d --- /dev/null +++ b/frontend/src/app/shared/types/enums/ObjectiveState.ts @@ -0,0 +1,6 @@ +export enum ObjectiveState { + DRAFT = 'DRAFT', + ONGOING = 'ONGOING', + SUCCESSFUL = 'SUCCESSFUL', + NOTSUCCESSFUL = 'NOTSUCCESSFUL', +} From 6b9a56b907d3f33aab0cddb88bcd2097d32a0987 Mon Sep 17 00:00:00 2001 From: Lias Kleisa Date: Wed, 19 Jun 2024 14:36:25 +0200 Subject: [PATCH 118/119] Use second refresh subject --- frontend/cypress/e2e/checkIn.cy.ts | 16 ------- frontend/cypress/e2e/scoring.cy.ts | 1 - .../src/app/diagram/diagram.component.html | 4 +- .../src/app/diagram/diagram.component.spec.ts | 5 +- frontend/src/app/diagram/diagram.component.ts | 47 ++++++++----------- .../src/app/overview/overview.component.html | 2 +- .../app/overview/overview.component.spec.ts | 6 +-- .../src/app/overview/overview.component.ts | 24 +++------- .../shared/services/refresh-data.service.ts | 6 ++- 9 files changed, 37 insertions(+), 74 deletions(-) diff --git a/frontend/cypress/e2e/checkIn.cy.ts b/frontend/cypress/e2e/checkIn.cy.ts index 33e5dfc368..e3df4724d0 100644 --- a/frontend/cypress/e2e/checkIn.cy.ts +++ b/frontend/cypress/e2e/checkIn.cy.ts @@ -32,8 +32,6 @@ describe('OKR Check-in e2e tests', () => { checkForDialogTextMetric(); cy.fillOutCheckInMetric(30, 6, 'We bought a new house', 'We have to buy more PCs'); - cy.wait(1000); - cy.contains('30%'); cy.contains('6/10'); cy.contains('Letztes Check-in (' + getCurrentDate() + ')'); @@ -64,8 +62,6 @@ describe('OKR Check-in e2e tests', () => { checkForDialogTextMetric(); cy.fillOutCheckInMetric(30, 0, 'We bought a new house', 'We have to buy more PCs'); - cy.wait(100); - cy.contains('30%'); cy.contains('6/10'); cy.contains('Letztes Check-in (' + getCurrentDate() + ')'); @@ -96,8 +92,6 @@ describe('OKR Check-in e2e tests', () => { checkForDialogTextMetric(); cy.fillOutCheckInMetric(5, 5, null, null); - cy.wait(500); - cy.contains('5%'); cy.contains('!'); cy.contains('5/10'); @@ -130,8 +124,6 @@ describe('OKR Check-in e2e tests', () => { checkForDialogTextOrdinal(); cy.fillOutCheckInOrdinal(1, 6, 'There is a new car', 'Buy now a new pool'); - cy.wait(500); - cy.contains('6/10'); cy.contains('There is a new car'); cy.contains('Letztes Check-in (' + getCurrentDate() + ')'); @@ -162,8 +154,6 @@ describe('OKR Check-in e2e tests', () => { cy.getByTestId('add-check-in').first().click(); cy.fillOutCheckInMetric(50, 6, 'This was a good idea', 'Will be difficult'); - cy.wait(500); - cy.getByTestId('show-all-checkins').click(); cy.wait(500); @@ -201,9 +191,6 @@ describe('OKR Check-in e2e tests', () => { cy.getByTestId('add-check-in').first().click(); cy.fillOutCheckInMetric(30, 5, 'Here we are', 'A cat would be great'); - - cy.wait(500); - cy.contains('Aktuell: CHF 30.-'); cy.getByTestId('show-all-checkins').click(); @@ -249,9 +236,6 @@ describe('OKR Check-in e2e tests', () => { cy.getByTestId('keyresult').contains('For editing ordinal checkin').click(); cy.getByTestId('add-check-in').first().click(); cy.fillOutCheckInOrdinal(0, 6, 'There is a new car', 'Buy now a new pool'); - - cy.wait(500); - cy.getByTestId('show-all-checkins').click(); cy.wait(500); diff --git a/frontend/cypress/e2e/scoring.cy.ts b/frontend/cypress/e2e/scoring.cy.ts index 82d5cc6b72..1a22f91596 100644 --- a/frontend/cypress/e2e/scoring.cy.ts +++ b/frontend/cypress/e2e/scoring.cy.ts @@ -78,7 +78,6 @@ describe('Scoring component e2e tests', () => { function setupMetricKR(baseline: number, stretchgoal: number, value: number) { cy.createMetricKeyresult('Metric scoring keyresult', String(baseline), String(stretchgoal)); - cy.wait(500); cy.getByTestId('keyresult').get(':contains("Metric scoring keyresult")').last().click(); cy.getByTestId('add-check-in').click(); cy.getByTestId('check-in-metric-value').clear().type(String(value)); diff --git a/frontend/src/app/diagram/diagram.component.html b/frontend/src/app/diagram/diagram.component.html index 2491b38045..9e34e3ff55 100644 --- a/frontend/src/app/diagram/diagram.component.html +++ b/frontend/src/app/diagram/diagram.component.html @@ -1,7 +1,7 @@ -
+

Kein Alignment vorhanden

diff --git a/frontend/src/app/diagram/diagram.component.spec.ts b/frontend/src/app/diagram/diagram.component.spec.ts index 4b6a6b0285..dda3c6e3aa 100644 --- a/frontend/src/app/diagram/diagram.component.spec.ts +++ b/frontend/src/app/diagram/diagram.component.spec.ts @@ -64,7 +64,7 @@ describe('DiagramComponent', () => { jest.spyOn(component, 'prepareDiagramData'); component.ngAfterViewInit(); - component.alignmentData.next(alignmentLists); + component.alignmentData$.next(alignmentLists); expect(component.cleanUpDiagram).toHaveBeenCalled(); expect(component.prepareDiagramData).toHaveBeenCalledWith(alignmentLists); @@ -104,7 +104,6 @@ describe('DiagramComponent', () => { component.generateNodes(alignmentLists); expect(component.generateConnections).toHaveBeenCalled(); - expect(component.generateDiagram).toHaveBeenCalled(); expect(component.diagramData).toEqual(diagramElements.concat(edges)); }); @@ -120,7 +119,6 @@ describe('DiagramComponent', () => { component.generateNodes(alignmentListsKeyResult); expect(component.generateConnections).toHaveBeenCalled(); - expect(component.generateDiagram).toHaveBeenCalled(); expect(component.diagramData).toEqual(diagramData); }); @@ -136,7 +134,6 @@ describe('DiagramComponent', () => { component.generateNodes(alignmentListsKeyResult); expect(component.generateConnections).toHaveBeenCalled(); - expect(component.generateDiagram).toHaveBeenCalled(); expect(component.diagramData).toEqual(diagramData); }); diff --git a/frontend/src/app/diagram/diagram.component.ts b/frontend/src/app/diagram/diagram.component.ts index 4883cb248b..6d35f71bad 100644 --- a/frontend/src/app/diagram/diagram.component.ts +++ b/frontend/src/app/diagram/diagram.component.ts @@ -21,6 +21,7 @@ import { AlignmentObject } from '../shared/types/model/AlignmentObject'; import { AlignmentConnection } from '../shared/types/model/AlignmentConnection'; import { Zone } from '../shared/types/enums/Zone'; import { ObjectiveState } from '../shared/types/enums/ObjectiveState'; +import { RefreshDataService } from '../shared/services/refresh-data.service'; @Component({ selector: 'app-diagram', @@ -28,39 +29,27 @@ import { ObjectiveState } from '../shared/types/enums/ObjectiveState'; styleUrl: './diagram.component.scss', }) export class DiagramComponent implements AfterViewInit, OnDestroy { - private alignmentData$: Subject = new Subject(); + @Input() + public alignmentData$: Subject = new Subject(); cy!: cytoscape.Core; diagramData: any[] = []; alignmentDataCache: AlignmentLists | null = null; + reloadRequired: boolean | null | undefined = false; constructor( private keyResultService: KeyresultService, + private refreshDataService: RefreshDataService, private router: Router, ) {} - @Input() - get alignmentData(): Subject { - return this.alignmentData$; - } - - set alignmentData(alignmentData: AlignmentLists) { - this.alignmentData$.next(alignmentData); - } - ngAfterViewInit(): void { - this.alignmentData.subscribe((alignmentData: AlignmentLists): void => { - let lastAlignmentItem: AlignmentObject = - alignmentData.alignmentObjectDtoList[alignmentData.alignmentObjectDtoList.length - 1]; - - const diagramReloadRequired: boolean = - lastAlignmentItem?.objectTitle === 'reload' - ? lastAlignmentItem?.objectType === 'true' - : JSON.stringify(this.alignmentDataCache) !== JSON.stringify(alignmentData); + this.refreshDataService.reloadAlignmentSubject.subscribe((value: boolean | null | undefined): void => { + this.reloadRequired = value; + }); - if (diagramReloadRequired) { - if (lastAlignmentItem?.objectTitle === 'reload') { - alignmentData.alignmentObjectDtoList.pop(); - } + this.alignmentData$.subscribe((alignmentData: AlignmentLists): void => { + if (this.reloadRequired == true || JSON.stringify(this.alignmentDataCache) !== JSON.stringify(alignmentData)) { + this.reloadRequired = undefined; this.alignmentDataCache = alignmentData; this.diagramData = []; this.cleanUpDiagram(); @@ -71,7 +60,8 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { ngOnDestroy(): void { this.cleanUpDiagram(); - this.alignmentData.unsubscribe(); + this.alignmentData$.unsubscribe(); + this.refreshDataService.reloadAlignmentSubject.unsubscribe(); } generateDiagram(): void { @@ -187,8 +177,8 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { } }); - zip(observableArray).subscribe(() => { - this.generateConnections(alignmentData, diagramElements); + zip(observableArray).subscribe(async () => { + await this.generateConnections(alignmentData, diagramElements); }); } @@ -223,7 +213,7 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { }; } - generateConnections(alignmentData: AlignmentLists, diagramElements: any[]): void { + async generateConnections(alignmentData: AlignmentLists, diagramElements: any[]) { let edges: any[] = []; alignmentData.alignmentConnectionDtoList.forEach((alignmentConnection: AlignmentConnection) => { let edge = { @@ -238,7 +228,10 @@ export class DiagramComponent implements AfterViewInit, OnDestroy { edges.push(edge); }); this.diagramData = diagramElements.concat(edges); - this.generateDiagram(); + + // Sometimes the DOM Element #cy is not ready when cytoscape tries to generate the diagram + // To avoid this, we use here a setTimeout() + setTimeout(() => this.generateDiagram(), 0); } generateObjectiveSVG(title: string, teamName: string, state: string): string { diff --git a/frontend/src/app/overview/overview.component.html b/frontend/src/app/overview/overview.component.html index f4929da899..5c46bf9c35 100644 --- a/frontend/src/app/overview/overview.component.html +++ b/frontend/src/app/overview/overview.component.html @@ -46,7 +46,7 @@
- +
diff --git a/frontend/src/app/overview/overview.component.spec.ts b/frontend/src/app/overview/overview.component.spec.ts index c047057d24..6f9e20117e 100644 --- a/frontend/src/app/overview/overview.component.spec.ts +++ b/frontend/src/app/overview/overview.component.spec.ts @@ -121,7 +121,7 @@ describe('OverviewComponent', () => { routerHarness.detectChanges(); component.loadOverviewWithParams(); expect(overviewService.getOverview).toHaveBeenCalledWith(quarterParam, teamsParam, objectiveQueryParam); - expect(component.loadOverview).toHaveBeenCalledWith(quarterParam, teamsParam, objectiveQueryParam, undefined); + expect(component.loadOverview).toHaveBeenCalledWith(quarterParam, teamsParam, objectiveQueryParam); }, ); @@ -145,7 +145,7 @@ describe('OverviewComponent', () => { jest.spyOn(overviewService, 'getOverview'); component.isOverview = true; - component.loadOverview(3, [5, 6], '', null); + component.loadOverview(3, [5, 6], ''); expect(overviewService.getOverview).toHaveBeenCalled(); }); @@ -154,7 +154,7 @@ describe('OverviewComponent', () => { component.isOverview = false; fixture.detectChanges(); - component.loadOverview(3, [5, 6], '', null); + component.loadOverview(3, [5, 6], ''); expect(alignmentService.getAlignmentByFilter).toHaveBeenCalled(); }); diff --git a/frontend/src/app/overview/overview.component.ts b/frontend/src/app/overview/overview.component.ts index cc97822235..43116692ce 100644 --- a/frontend/src/app/overview/overview.component.ts +++ b/frontend/src/app/overview/overview.component.ts @@ -7,7 +7,6 @@ import { RefreshDataService } from '../shared/services/refresh-data.service'; import { getQueryString, getValueFromQuery, isMobileDevice, trackByFn } from '../shared/common'; import { AlignmentService } from '../shared/services/alignment.service'; import { AlignmentLists } from '../shared/types/model/AlignmentLists'; -import { AlignmentObject } from '../shared/types/model/AlignmentObject'; @Component({ selector: 'app-overview', @@ -18,7 +17,6 @@ import { AlignmentObject } from '../shared/types/model/AlignmentObject'; export class OverviewComponent implements OnInit, OnDestroy { overviewEntities$: Subject = new Subject(); alignmentLists$: Subject = new Subject(); - emptyAlignmentList: AlignmentLists = { alignmentObjectDtoList: [], alignmentConnectionDtoList: [] } as AlignmentLists; protected readonly trackByFn = trackByFn; private destroyed$: ReplaySubject = new ReplaySubject(1); hasAdminAccess: ReplaySubject = new ReplaySubject(1); @@ -34,7 +32,7 @@ export class OverviewComponent implements OnInit, OnDestroy { ) { this.refreshDataService.reloadOverviewSubject .pipe(takeUntil(this.destroyed$)) - .subscribe((reload: boolean | null | undefined) => this.loadOverviewWithParams(reload)); + .subscribe(() => this.loadOverviewWithParams()); combineLatest([ refreshDataService.teamFilterReady.asObservable(), @@ -58,7 +56,7 @@ export class OverviewComponent implements OnInit, OnDestroy { } } - loadOverviewWithParams(reload?: boolean | null) { + loadOverviewWithParams() { const quarterQuery = this.activatedRoute.snapshot.queryParams['quarter']; const teamQuery = this.activatedRoute.snapshot.queryParams['teams']; const objectiveQuery = this.activatedRoute.snapshot.queryParams['objectiveQuery']; @@ -66,14 +64,14 @@ export class OverviewComponent implements OnInit, OnDestroy { const teamIds = getValueFromQuery(teamQuery); const quarterId = getValueFromQuery(quarterQuery)[0]; const objectiveQueryString = getQueryString(objectiveQuery); - this.loadOverview(quarterId, teamIds, objectiveQueryString, reload); + this.loadOverview(quarterId, teamIds, objectiveQueryString); } - loadOverview(quarterId?: number, teamIds?: number[], objectiveQuery?: string, reload?: boolean | null): void { + loadOverview(quarterId?: number, teamIds?: number[], objectiveQuery?: string): void { if (this.isOverview) { this.loadOverviewData(quarterId, teamIds, objectiveQuery); } else { - this.loadAlignmentData(quarterId, teamIds, objectiveQuery, reload); + this.loadAlignmentData(quarterId, teamIds, objectiveQuery); } } @@ -92,7 +90,7 @@ export class OverviewComponent implements OnInit, OnDestroy { }); } - loadAlignmentData(quarterId?: number, teamIds?: number[], objectiveQuery?: string, reload?: boolean | null): void { + loadAlignmentData(quarterId?: number, teamIds?: number[], objectiveQuery?: string): void { this.alignmentService .getAlignmentByFilter(quarterId, teamIds, objectiveQuery) .pipe( @@ -102,16 +100,6 @@ export class OverviewComponent implements OnInit, OnDestroy { }), ) .subscribe((alignmentLists: AlignmentLists) => { - if (reload != null) { - let alignmentObjectReload: AlignmentObject = { - objectId: 0, - objectTitle: 'reload', - objectType: reload.toString(), - objectTeamName: '', - objectState: null, - }; - alignmentLists.alignmentObjectDtoList.push(alignmentObjectReload); - } this.alignmentLists$.next(alignmentLists); }); } diff --git a/frontend/src/app/shared/services/refresh-data.service.ts b/frontend/src/app/shared/services/refresh-data.service.ts index e04f27a711..637eb78ba8 100644 --- a/frontend/src/app/shared/services/refresh-data.service.ts +++ b/frontend/src/app/shared/services/refresh-data.service.ts @@ -6,7 +6,8 @@ import { DEFAULT_HEADER_HEIGHT_PX } from '../constantLibary'; providedIn: 'root', }) export class RefreshDataService { - public reloadOverviewSubject: Subject = new Subject(); + public reloadOverviewSubject: Subject = new Subject(); + public reloadAlignmentSubject: Subject = new Subject(); public quarterFilterReady: Subject = new Subject(); public teamFilterReady: Subject = new Subject(); @@ -14,6 +15,7 @@ export class RefreshDataService { public okrBannerHeightSubject: BehaviorSubject = new BehaviorSubject(DEFAULT_HEADER_HEIGHT_PX); markDataRefresh(reload?: boolean | null) { - this.reloadOverviewSubject.next(reload); + this.reloadOverviewSubject.next(); + this.reloadAlignmentSubject.next(reload); } } From bec586ca4c29716587a9344d9776c162687099f7 Mon Sep 17 00:00:00 2001 From: Nevio Di Gennaro Date: Thu, 21 Nov 2024 14:24:57 +0100 Subject: [PATCH 119/119] add newer quarters to testdata and change CSP to allow image blobs --- backend/src/main/java/ch/puzzle/okr/SecurityConfig.java | 5 +++-- .../resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/ch/puzzle/okr/SecurityConfig.java b/backend/src/main/java/ch/puzzle/okr/SecurityConfig.java index 7bd87b4c2f..5b2cdf77c9 100644 --- a/backend/src/main/java/ch/puzzle/okr/SecurityConfig.java +++ b/backend/src/main/java/ch/puzzle/okr/SecurityConfig.java @@ -47,8 +47,9 @@ private HttpSecurity setHeaders(HttpSecurity http) throws Exception { + "script-src 'self' 'unsafe-inline';" + " style-src 'self' 'unsafe-inline';" + " object-src 'none';" + " base-uri 'self';" + " connect-src 'self' https://sso.puzzle.ch http://localhost:8544;" - + " font-src 'self';" + " frame-src 'self';" + " img-src 'self' data: ;" - + " manifest-src 'self';" + " media-src 'self';" + " worker-src 'none';")) + + " font-src 'self';" + " frame-src 'self';" + + " img-src 'self' data: blob:;" + " manifest-src 'self';" + + " media-src 'self';" + " worker-src 'none';")) .crossOriginEmbedderPolicy(coepCustomizer -> coepCustomizer .policy(CrossOriginEmbedderPolicyHeaderWriter.CrossOriginEmbedderPolicy.REQUIRE_CORP)) .crossOriginOpenerPolicy(coopCustomizer -> coopCustomizer diff --git a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql index 8b3131a332..50741ef6b3 100644 --- a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql +++ b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql @@ -42,6 +42,8 @@ values (1, 'GJ 22/23-Q4', '2023-04-01', '2023-06-30'), (8, 'GJ 23/24-Q3', '2024-01-01', '2024-03-31'), (9, 'GJ 23/24-Q4', '2024-04-01', '2024-06-30'), (10, 'GJ 24/25-Q1', '2024-07-01', '2024-09-30'), + (11, 'GJ 24/25-Q2', '2024-10-01', '2024-12-31'), + (199, 'Backlog', null, null); insert into team (id, version, name)