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], +};