Skip to content

Commit e9ff46a

Browse files
committed
Implement typeahead
1 parent d04f222 commit e9ff46a

File tree

8 files changed

+192
-77
lines changed

8 files changed

+192
-77
lines changed

backend/src/main/java/ch/puzzle/okr/controller/ObjectiveController.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package ch.puzzle.okr.controller;
22

3-
import ch.puzzle.okr.dto.ObjectiveAlignmentsDto;
3+
import ch.puzzle.okr.dto.AlignmentDto;
44
import ch.puzzle.okr.dto.ObjectiveDto;
55
import ch.puzzle.okr.mapper.ObjectiveMapper;
66
import ch.puzzle.okr.models.Objective;
@@ -48,11 +48,11 @@ public ResponseEntity<ObjectiveDto> getObjective(
4848
@Operation(summary = "Get Alignment possibilities", description = "Get all possibilities to create an Alignment")
4949
@ApiResponses(value = {
5050
@ApiResponse(responseCode = "200", description = "Returned all Alignment possibilities for an Objective", content = {
51-
@Content(mediaType = "application/json", schema = @Schema(implementation = ObjectiveAlignmentsDto.class)) }),
51+
@Content(mediaType = "application/json", schema = @Schema(implementation = AlignmentDto.class)) }),
5252
@ApiResponse(responseCode = "401", description = "Not authorized to get Alignment possibilities", content = @Content),
5353
@ApiResponse(responseCode = "404", description = "Did not find any possibilities to create an Alignment", content = @Content) })
5454
@GetMapping("/alignmentPossibilities/{quarterId}")
55-
public ResponseEntity<List<ObjectiveAlignmentsDto>> getAlignmentPossibilities(
55+
public ResponseEntity<List<AlignmentDto>> getAlignmentPossibilities(
5656
@Parameter(description = "The Quarter ID for getting Alignment possibilities.", required = true) @PathVariable Long quarterId) {
5757
return ResponseEntity.status(HttpStatus.OK)
5858
.body(objectiveAuthorizationService.getAlignmentPossibilities(quarterId));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package ch.puzzle.okr.dto;
2+
3+
import java.util.List;
4+
5+
public record AlignmentDto(Long teamId, String teamName, List<AlignmentObjectDto> alignmentObjectDtos) {
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package ch.puzzle.okr.dto;
2+
3+
public record AlignmentObjectDto(Long objectId, String objectTitle, String objectType) {
4+
}

backend/src/main/java/ch/puzzle/okr/service/authorization/ObjectiveAuthorizationService.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package ch.puzzle.okr.service.authorization;
22

3-
import ch.puzzle.okr.dto.ObjectiveAlignmentsDto;
3+
import ch.puzzle.okr.dto.AlignmentDto;
44
import ch.puzzle.okr.models.Objective;
55
import ch.puzzle.okr.models.authorization.AuthorizationUser;
66
import ch.puzzle.okr.service.business.ObjectiveBusinessService;
@@ -22,7 +22,7 @@ public Objective duplicateEntity(Long id, Objective objective) {
2222
return getBusinessService().duplicateObjective(id, objective, authorizationUser);
2323
}
2424

25-
public List<ObjectiveAlignmentsDto> getAlignmentPossibilities(Long quarterId) {
25+
public List<AlignmentDto> getAlignmentPossibilities(Long quarterId) {
2626
return getBusinessService().getAlignmentPossibilities(quarterId);
2727
}
2828

backend/src/main/java/ch/puzzle/okr/service/business/ObjectiveBusinessService.java

+32-19
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package ch.puzzle.okr.service.business;
22

3-
import ch.puzzle.okr.dto.ObjectiveAlignmentsDto;
4-
import ch.puzzle.okr.dto.keyresult.KeyResultAlignmentsDto;
3+
import ch.puzzle.okr.dto.AlignmentDto;
4+
import ch.puzzle.okr.dto.AlignmentObjectDto;
55
import ch.puzzle.okr.models.Objective;
6+
import ch.puzzle.okr.models.Team;
67
import ch.puzzle.okr.models.authorization.AuthorizationUser;
78
import ch.puzzle.okr.models.keyresult.KeyResult;
89
import ch.puzzle.okr.models.keyresult.KeyResultMetric;
@@ -16,9 +17,7 @@
1617
import org.springframework.stereotype.Service;
1718

1819
import java.time.LocalDateTime;
19-
import java.util.ArrayList;
20-
import java.util.List;
21-
import java.util.Objects;
20+
import java.util.*;
2221

2322
import static ch.puzzle.okr.Constants.KEY_RESULT_TYPE_METRIC;
2423
import static ch.puzzle.okr.Constants.KEY_RESULT_TYPE_ORDINAL;
@@ -52,26 +51,40 @@ public Objective getEntityById(Long id) {
5251
return objective;
5352
}
5453

55-
public List<ObjectiveAlignmentsDto> getAlignmentPossibilities(Long quarterId) {
54+
public List<AlignmentDto> getAlignmentPossibilities(Long quarterId) {
5655
validator.validateOnGet(quarterId);
5756

5857
List<Objective> objectivesByQuarter = objectivePersistenceService.findObjectiveByQuarterId(quarterId);
59-
List<ObjectiveAlignmentsDto> objectiveAlignmentsDtos = new ArrayList<>();
60-
61-
objectivesByQuarter.forEach(objective -> {
62-
List<KeyResult> keyResults = keyResultBusinessService.getAllKeyResultsByObjective(objective.getId());
63-
List<KeyResultAlignmentsDto> keyResultAlignmentsDtos = new ArrayList<>();
64-
keyResults.forEach(keyResult -> {
65-
KeyResultAlignmentsDto keyResultAlignmentsDto = new KeyResultAlignmentsDto(keyResult.getId(),
66-
"K - " + keyResult.getTitle());
67-
keyResultAlignmentsDtos.add(keyResultAlignmentsDto);
58+
List<AlignmentDto> alignmentDtoList = new ArrayList<>();
59+
60+
List<Team> teamList = new ArrayList<>();
61+
objectivesByQuarter.forEach(objective -> teamList.add(objective.getTeam()));
62+
Set<Team> set = new HashSet<>(teamList);
63+
teamList.clear();
64+
teamList.addAll(set);
65+
66+
teamList.forEach(team -> {
67+
List<Objective> filteredObjectiveList = objectivesByQuarter.stream()
68+
.filter(objective -> objective.getTeam().equals(team)).toList();
69+
List<AlignmentObjectDto> alignmentObjectDtos = new ArrayList<>();
70+
71+
filteredObjectiveList.forEach(objective -> {
72+
AlignmentObjectDto objectiveDto = new AlignmentObjectDto(objective.getId(),
73+
"O - " + objective.getTitle(), "objective");
74+
alignmentObjectDtos.add(objectiveDto);
75+
76+
List<KeyResult> keyResults = keyResultBusinessService.getAllKeyResultsByObjective(objective.getId());
77+
keyResults.forEach(keyResult -> {
78+
AlignmentObjectDto keyResultDto = new AlignmentObjectDto(keyResult.getId(),
79+
"KR - " + keyResult.getTitle(), "keyResult");
80+
alignmentObjectDtos.add(keyResultDto);
81+
});
6882
});
69-
ObjectiveAlignmentsDto objectiveAlignmentsDto = new ObjectiveAlignmentsDto(objective.getId(),
70-
"O - " + objective.getTitle(), keyResultAlignmentsDtos);
71-
objectiveAlignmentsDtos.add(objectiveAlignmentsDto);
83+
AlignmentDto alignmentDto = new AlignmentDto(team.getId(), team.getName(), alignmentObjectDtos);
84+
alignmentDtoList.add(alignmentDto);
7285
});
7386

74-
return objectiveAlignmentsDtos;
87+
return alignmentDtoList;
7588
}
7689

7790
public List<Objective> getEntitiesByTeamId(Long id) {

frontend/src/app/shared/dialog/objective-dialog/objective-form.component.html

+20-18
Original file line numberDiff line numberDiff line change
@@ -68,25 +68,27 @@
6868
</div>
6969

7070
<div class="d-flex flex-column gap-2 col-6">
71-
<label class="text-black" for="alignment">Bezug (optional)</label>
72-
<select
73-
class="custom-select bg-white select-width"
71+
<label class="text-black">Bezug (optional)</label>
72+
<input
73+
#input
74+
type="text"
7475
formControlName="alignment"
75-
id="alignment"
76-
(change)="changeFirstAlignmentPossibility()"
77-
[attr.data-testId]="'alignmentSelect'"
78-
>
79-
<ng-container *ngFor="let alignment of alignmentPossibilities$ | async; let i = index">
80-
<option [value]="'O' + alignment.objectiveId">
81-
{{ alignment.objectiveTitle }}
82-
</option>
83-
<ng-container *ngFor="let keyResult of alignment.keyResultAlignmentsDtos; let i = index">
84-
<option [value]="'K' + keyResult.keyResultId">
85-
{{ keyResult.keyResultTitle }}
86-
</option>
87-
</ng-container>
88-
</ng-container>
89-
</select>
76+
class="custom-select bg-white select-width"
77+
placeholder="Bezug wählen"
78+
[matAutocomplete]="auto"
79+
(input)="filter()"
80+
(focus)="filter(); input.select()"
81+
[value]="displayedValue"
82+
/>
83+
<mat-autocomplete requireSelection #auto="matAutocomplete" [displayWith]="displayWith">
84+
@for (group of filteredOptions; track group) {
85+
<mat-optgroup [label]="group.teamName">
86+
@for (name of group.alignmentObjects; track name) {
87+
<mat-option [value]="name">{{ name.objectTitle }}</mat-option>
88+
}
89+
</mat-optgroup>
90+
}
91+
</mat-autocomplete>
9092
</div>
9193
</div>
9294
<mat-checkbox

frontend/src/app/shared/dialog/objective-dialog/objective-form.component.ts

+120-30
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core';
1+
import { ChangeDetectionStrategy, Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core';
22
import { FormControl, FormGroup, Validators } from '@angular/forms';
33
import { Quarter } from '../../types/model/Quarter';
44
import { TeamService } from '../../services/team.service';
@@ -24,12 +24,15 @@ import { AlignmentPossibility } from '../../types/model/AlignmentPossibility';
2424
changeDetection: ChangeDetectionStrategy.OnPush,
2525
})
2626
export class ObjectiveFormComponent implements OnInit {
27+
@ViewChild('input') input!: ElementRef<HTMLInputElement>;
28+
filteredOptions: AlignmentPossibility[] = [];
29+
2730
objectiveForm = new FormGroup({
2831
title: new FormControl<string>('', [Validators.required, Validators.minLength(2), Validators.maxLength(250)]),
2932
description: new FormControl<string>('', [Validators.maxLength(4096)]),
3033
quarter: new FormControl<number>(0, [Validators.required]),
3134
team: new FormControl<number>({ value: 0, disabled: true }, [Validators.required]),
32-
alignment: new FormControl<string>(''),
35+
alignment: new FormControl<AlignmentPossibility | null>(null),
3336
createKeyResults: new FormControl<boolean>(false),
3437
});
3538
quarters$: Observable<Quarter[]> = of([]);
@@ -65,6 +68,19 @@ export class ObjectiveFormComponent implements OnInit {
6568
onSubmit(submitType: any): void {
6669
const value = this.objectiveForm.getRawValue();
6770
const state = this.data.objective.objectiveId == null ? submitType : this.state;
71+
72+
let alignmentEntity: string | null = '';
73+
let alignment: any = value.alignment;
74+
if (alignment) {
75+
if (alignment?.objectiveId) {
76+
alignmentEntity = 'O' + alignment.objectiveId;
77+
} else {
78+
alignmentEntity = 'K' + alignment.keyResultId;
79+
}
80+
} else {
81+
alignmentEntity = null;
82+
}
83+
6884
let objectiveDTO: Objective = {
6985
id: this.data.objective.objectiveId,
7086
version: this.version,
@@ -73,7 +89,7 @@ export class ObjectiveFormComponent implements OnInit {
7389
title: value.title,
7490
teamId: value.team,
7591
state: state,
76-
alignedEntityId: value.alignment == 'Onull' ? null : value.alignment,
92+
alignedEntityId: alignmentEntity,
7793
} as unknown as Objective;
7894

7995
const submitFunction = this.getSubmitFunction(objectiveDTO.id, objectiveDTO);
@@ -106,14 +122,15 @@ export class ObjectiveFormComponent implements OnInit {
106122
this.teams$.subscribe((value) => {
107123
this.currentTeam.next(value.filter((team) => team.id == teamId)[0]);
108124
});
109-
this.generateAlignmentPossibilities(quarterId);
125+
this.generateAlignmentPossibilities(quarterId, objective, teamId!);
110126

111127
this.objectiveForm.patchValue({
112128
title: objective.title,
113129
description: objective.description,
114130
team: teamId,
115131
quarter: quarterId,
116-
alignment: objective.alignedEntityId ? objective.alignedEntityId : 'Onull',
132+
// alignment: null,
133+
// alignment: objective.alignedEntityId ? objective.alignedEntityId : 'Onull',
117134
});
118135
});
119136
}
@@ -238,47 +255,120 @@ export class ObjectiveFormComponent implements OnInit {
238255
return GJ_REGEX_PATTERN.test(label);
239256
}
240257

241-
generateAlignmentPossibilities(quarterId: number) {
258+
generateAlignmentPossibilities(quarterId: number, objective: Objective | null, teamId: number | null) {
242259
this.alignmentPossibilities$ = this.objectiveService.getAlignmentPossibilities(quarterId);
243260
this.alignmentPossibilities$.subscribe((value: AlignmentPossibility[]) => {
244-
if (this.objective?.id) {
245-
value = value.filter((item: AlignmentPossibility) => !(item.objectiveId == this.objective!.id));
261+
if (teamId) {
262+
value = value.filter((item: AlignmentPossibility) => !(item.teamId == teamId));
246263
}
247-
let firstSelectOption = {
248-
objectiveId: null,
249-
objectiveTitle: 'Kein Alignment',
250-
keyResultAlignmentsDtos: [],
251-
};
252-
if (value.length != 0) {
253-
if (this.objective?.alignedEntityId) {
254-
if (value[0].objectiveTitle == 'Bitte wählen') {
255-
value.splice(0, 1);
264+
// let firstSelectOption = {
265+
// objectiveId: null,
266+
// objectiveTitle: 'Kein Alignment',
267+
// keyResultAlignmentsDtos: [],
268+
// };
269+
// if (value.length != 0) {
270+
// if (this.objective?.alignedEntityId) {
271+
// if (value[0].objectiveTitle == 'Bitte wählen') {
272+
// value.splice(0, 1);
273+
// }
274+
// } else {
275+
// firstSelectOption.objectiveTitle = 'Bitte wählen';
276+
// }
277+
// }
278+
// value.unshift(firstSelectOption);
279+
280+
if (objective) {
281+
let alignment = objective.alignedEntityId;
282+
if (alignment) {
283+
let alignmentType = alignment.charAt(0);
284+
let alignmentId = parseInt(alignment.substring(1));
285+
if (alignmentType == 'O') {
286+
let element =
287+
value.find((ap) => ap.alignmentObjects.find((apObjective) => apObjective.objectId == alignmentId)) ||
288+
null;
289+
this.objectiveForm.patchValue({
290+
alignment: element,
291+
});
256292
}
257-
} else {
258-
firstSelectOption.objectiveTitle = 'Bitte wählen';
293+
// else {
294+
// for (let objectiveAlignment of value) {
295+
// let keyResult = objectiveAlignment.keyResultAlignmentsDtos.find((kr) => kr.keyResultId == alignmentId);
296+
// if (keyResult) {
297+
// // TODO change this to keyresult
298+
// this.objectiveForm.patchValue({
299+
// alignment: objectiveAlignment,
300+
// });
301+
// }
302+
// }
303+
// }
259304
}
305+
} else {
306+
this.objectiveForm.patchValue({
307+
alignment: null,
308+
});
260309
}
261-
value.unshift(firstSelectOption);
310+
311+
this.filteredOptions = value.slice();
262312
this.alignmentPossibilities$ = of(value);
263313
});
264314
}
265315

266316
updateAlignments() {
267-
this.generateAlignmentPossibilities(this.objectiveForm.value.quarter!);
268-
this.objectiveForm.patchValue({
269-
alignment: 'Onull',
317+
this.currentTeam.subscribe((team) => {
318+
this.generateAlignmentPossibilities(this.objectiveForm.value.quarter!, null, team.id);
270319
});
320+
// this.objectiveForm.patchValue({
321+
// alignment: 'Onull',
322+
// });
271323
}
272324

273-
changeFirstAlignmentPossibility() {
274-
this.alignmentPossibilities$.subscribe((value: AlignmentPossibility[]) => {
275-
let element: AlignmentPossibility = value[0];
276-
element.objectiveTitle = 'Kein Alignment';
277-
value.splice(0, 1);
278-
value.unshift(element);
279-
this.alignmentPossibilities$ = of(value);
325+
// changeFirstAlignmentPossibility() {
326+
// this.alignmentPossibilities$.subscribe((value: AlignmentPossibility[]) => {
327+
// let element: AlignmentPossibility = value[0];
328+
// element.objectiveTitle = 'Kein Alignment';
329+
// value.splice(0, 1);
330+
// value.unshift(element);
331+
// this.alignmentPossibilities$ = of(value);
332+
// });
333+
// }
334+
335+
filter() {
336+
let filterValue = this.input.nativeElement.value.toLowerCase();
337+
this.alignmentPossibilities$.subscribe((value) => {
338+
// this.filteredOptions = value.filter((o) => o.objectiveTitle.toLowerCase().includes(filterValue));
339+
this.filteredOptions = value.filter(
340+
(o) => o.teamName.toLowerCase().includes(filterValue),
341+
// ||
342+
// o.alignmentObjects.some((object) => object.objectTitle.toLowerCase().includes(filterValue))
343+
344+
// o.objectiveTitle.toLowerCase().includes(filterValue) || // Check if objectiveTitle includes the filterValue
345+
// o.keyResultAlignmentsDtos.some((kr) => kr.keyResultTitle.toLowerCase().includes(filterValue)), // Check if any keyResultTitle includes the filterValue
346+
);
347+
console.log(this.filteredOptions);
280348
});
281349
}
282350

351+
displayWith(value: any): string {
352+
return value;
353+
// if (value) {
354+
// if (value.objectiveId) {
355+
// return value.objectiveTitle;
356+
// } else {
357+
// return value.keyResultTitle;
358+
// }
359+
// } else {
360+
// return 'Bitte wählen';
361+
// }
362+
}
363+
364+
get displayedValue(): string {
365+
if (this.input) {
366+
const inputValue = this.input.nativeElement.value;
367+
return inputValue.length > 40 ? inputValue.slice(0, 40) + '...' : inputValue;
368+
} else {
369+
return '';
370+
}
371+
}
372+
283373
protected readonly getQuarterLabel = getQuarterLabel;
284374
}

0 commit comments

Comments
 (0)