diff --git a/frontend/cypress/e2e/objective.cy.ts b/frontend/cypress/e2e/objective.cy.ts
index 83eb98496e..afca9c4c76 100644
--- a/frontend/cypress/e2e/objective.cy.ts
+++ b/frontend/cypress/e2e/objective.cy.ts
@@ -129,6 +129,10 @@ describe('OKR Objective e2e tests', () => {
.contains('Objective wiedereröffnen')
.click();
+ cy.contains('Objective wiedereröffnen');
+ cy.contains('Soll dieses Objective wiedereröffnet werden?');
+ cy.getByTestId('confirm-yes').click();
+
cy.getByTestId('objective')
.filter(':contains("This objective will be reopened after")')
.last()
@@ -154,7 +158,7 @@ describe('OKR Objective e2e tests', () => {
.tabForward();
cy.contains('Objective als Draft speichern');
cy.contains('Soll dieses Objective als Draft gespeichert werden?');
- cy.focused().click().wait(500);
+ cy.getByTestId('confirm-yes').click();
cy.getByTestId('objective')
.filter(':contains("This objective will be returned to draft state")')
diff --git a/frontend/cypress/e2e/tab.cy.ts b/frontend/cypress/e2e/tab.cy.ts
index 7909c48142..343f1b979d 100644
--- a/frontend/cypress/e2e/tab.cy.ts
+++ b/frontend/cypress/e2e/tab.cy.ts
@@ -72,7 +72,7 @@ describe('Tab workflow tests', () => {
}
function openKeyresultDetail() {
- cy.get('.objective').first().focus();
+ cy.get("[src='assets/icons/ongoing-icon.svg']").parentsUntil('#objective-column').last().focus();
cy.tabForwardUntil('[data-testId="key-result"]');
cy.focused().contains('Fail');
cy.focused().contains('Commit');
@@ -212,13 +212,17 @@ describe('Tab workflow tests', () => {
editInputFields('Edited by Cypress too');
cy.tabForward();
cy.tabForward();
+ cy.focused().contains('Speichern');
cy.realPress('Enter');
- cy.contains('Edited by Cypress');
+ cy.focused().invoke('attr', 'data-testid').should('contain', 'three-dot-menu');
+ cy.focused().parentsUntil('#objective-column').last().contains('Edited by Cypress');
});
it('Duplicate objective with tab', () => {
openThreeDotMenu();
cy.realPress('ArrowDown');
+ cy.realPress('ArrowDown');
+ cy.realPress('ArrowDown');
cy.focused().contains('Objective duplizieren');
cy.realPress('Enter');
cy.wait(500);
@@ -242,7 +246,6 @@ describe('Tab workflow tests', () => {
it('Complete objective dialog with tab', () => {
openThreeDotMenu();
cy.realPress('ArrowDown');
- cy.realPress('ArrowDown');
cy.focused().contains('Objective abschliessen');
cy.realPress('Enter');
cy.wait(500);
diff --git a/frontend/src/app/components/objective/ObjectiveMenuActions.ts b/frontend/src/app/components/objective/ObjectiveMenuActions.ts
new file mode 100644
index 0000000000..a33b090e23
--- /dev/null
+++ b/frontend/src/app/components/objective/ObjectiveMenuActions.ts
@@ -0,0 +1,77 @@
+import { DialogService } from '../../services/dialog.service';
+import { Objective } from '../../shared/types/model/Objective';
+import { RefreshDataService } from '../../services/refresh-data.service';
+import { ObjectiveMin } from '../../shared/types/model/ObjectiveMin';
+import { ObjectiveFormComponent } from '../../shared/dialog/objective-dialog/objective-form.component';
+import { CompleteDialogComponent } from '../../shared/dialog/complete-dialog/complete-dialog.component';
+import {
+ ObjectiveMenuAction,
+ ObjectiveMenuAfterAction,
+ ObjectiveMenuEntry,
+} from '../../services/objective-menu-actions.service';
+import { ObjectiveMenuAfterActions } from './ObjectiveMenuAfterActions';
+
+export class ObjectiveMenuActions {
+ constructor(
+ private readonly dialogService: DialogService,
+ private readonly refreshDataService: RefreshDataService,
+ private readonly afterActions: ObjectiveMenuAfterActions,
+ ) {}
+
+ releaseFromQuarterAction(objective: ObjectiveMin): ObjectiveMenuEntry {
+ const action: ObjectiveMenuAction = () => this.dialogService.openConfirmDialog('CONFIRMATION.RELEASE');
+ const afterAction: ObjectiveMenuAfterAction = (objective, dialogResult) =>
+ this.afterActions.releaseFromQuarter(objective);
+ return { displayName: 'Objective veröffentlichen', action: action, afterAction: afterAction };
+ }
+
+ releaseFromBacklogAction(objective: ObjectiveMin): ObjectiveMenuEntry {
+ const config = { data: { objective: { objectiveId: objective.id }, action: 'releaseBacklog' } };
+ const action: ObjectiveMenuAction = () => this.dialogService.open(ObjectiveFormComponent, config);
+ const afterAction: ObjectiveMenuAfterAction = () => this.refreshDataService.markDataRefresh();
+ return { displayName: 'Objective veröffentlichen', action: action, afterAction };
+ }
+
+ editObjectiveAction(objective: ObjectiveMin): ObjectiveMenuEntry {
+ const config = { data: { objective: { objectiveId: objective.id } } };
+ const action: ObjectiveMenuAction = () => this.dialogService.open(ObjectiveFormComponent, config);
+ const afterAction: ObjectiveMenuAfterAction = () => {
+ this.refreshDataService.markDataRefresh();
+ };
+ return { displayName: 'Objective bearbeiten', action: action, afterAction: afterAction };
+ }
+
+ duplicateObjectiveAction(objective: ObjectiveMin): ObjectiveMenuEntry {
+ const config = { data: { objective: { objectiveId: objective.id }, action: 'duplicate' } };
+ const action: ObjectiveMenuAction = () => this.dialogService.open(ObjectiveFormComponent, config);
+ const afterAction: ObjectiveMenuAfterAction = () => this.refreshDataService.markDataRefresh();
+ return { displayName: 'Objective duplizieren', action: action, afterAction: afterAction };
+ }
+
+ completeObjectiveAction(objective: ObjectiveMin): ObjectiveMenuEntry {
+ const config = {
+ data: { objectiveTitle: objective.title },
+ };
+ const action: ObjectiveMenuAction = () => this.dialogService.open(CompleteDialogComponent, config);
+ const afterAction: ObjectiveMenuAfterAction = (obj: Objective, result: any) =>
+ this.afterActions.completeObjective(obj, result);
+
+ return { displayName: 'Objective abschliessen', action: action, afterAction: afterAction };
+ }
+
+ objectiveBackToDraft(): ObjectiveMenuEntry {
+ const action: ObjectiveMenuAction = () => this.dialogService.openConfirmDialog('CONFIRMATION.TO_DRAFT');
+ const afterAction: ObjectiveMenuAfterAction = (obj: Objective, result: any) =>
+ this.afterActions.objectiveBackToDraft(obj);
+
+ return { displayName: 'Objective als Draft speichern', action: action, afterAction: afterAction };
+ }
+
+ objectiveReopen(): ObjectiveMenuEntry {
+ const action: ObjectiveMenuAction = () => this.dialogService.openConfirmDialog('CONFIRMATION.REOPEN');
+ const afterAction: ObjectiveMenuAfterAction = (obj: Objective, result: any) =>
+ this.afterActions.objectiveReopen(obj);
+
+ return { displayName: 'Objective wiedereröffnen', action: action, afterAction: afterAction };
+ }
+}
diff --git a/frontend/src/app/components/objective/ObjectiveMenuAfterActions.ts b/frontend/src/app/components/objective/ObjectiveMenuAfterActions.ts
new file mode 100644
index 0000000000..5691305fa9
--- /dev/null
+++ b/frontend/src/app/components/objective/ObjectiveMenuAfterActions.ts
@@ -0,0 +1,50 @@
+import { Objective } from '../../shared/types/model/Objective';
+import { State } from '../../shared/types/enums/State';
+import { Completed } from '../../shared/types/model/Completed';
+import { ObjectiveService } from '../../services/objective.service';
+import { RefreshDataService } from '../../services/refresh-data.service';
+
+export class ObjectiveMenuAfterActions {
+ constructor(
+ private readonly objectiveService: ObjectiveService,
+ private readonly refreshDataService: RefreshDataService,
+ ) {}
+
+ completeObjective(objective: Objective, result: { endState: string; comment: string | null; objective: any }) {
+ objective.state = result.endState as State;
+ const completed: Completed = {
+ id: null,
+ version: objective.version,
+ objective: objective,
+ comment: result.comment,
+ };
+ this.objectiveService.updateObjective(objective).subscribe(() => {
+ this.objectiveService.createCompleted(completed).subscribe(() => {
+ this.refreshDataService.markDataRefresh();
+ });
+ });
+ }
+
+ releaseFromQuarter(objective: Objective) {
+ objective.state = 'ONGOING' as State;
+ this.objectiveService.updateObjective(objective).subscribe(() => {
+ this.refreshDataService.markDataRefresh();
+ });
+ }
+
+ objectiveBackToDraft(objective: Objective) {
+ objective.state = 'DRAFT' as State;
+ this.objectiveService.updateObjective(objective).subscribe(() => {
+ this.refreshDataService.markDataRefresh();
+ });
+ }
+
+ objectiveReopen(objective: Objective) {
+ objective.state = 'ONGOING' as State;
+ this.objectiveService.updateObjective(objective).subscribe(() => {
+ this.objectiveService.deleteCompleted(objective.id).subscribe(() => {
+ this.refreshDataService.markDataRefresh();
+ });
+ });
+ }
+}
diff --git a/frontend/src/app/components/objective/objective.component.html b/frontend/src/app/components/objective/objective.component.html
index 48fd01a194..71cd720191 100644
--- a/frontend/src/app/components/objective/objective.component.html
+++ b/frontend/src/app/components/objective/objective.component.html
@@ -1,81 +1,82 @@
-
-
-
-
-
+
+
+
+
+
+ {{ objective.title }}
+
+
- {{ objective.title }}
+ (keydown.enter)="$event.stopPropagation()"
+ [attr.data-testId]="'three-dot-menu'"
+ >
+
+
-
-
-
+
-
-
-
+
+
+
+
-
-
-
-
+
+
+
diff --git a/frontend/src/app/components/objective/objective.component.spec.ts b/frontend/src/app/components/objective/objective.component.spec.ts
index c429f95f77..79385ab23c 100644
--- a/frontend/src/app/components/objective/objective.component.spec.ts
+++ b/frontend/src/app/components/objective/objective.component.spec.ts
@@ -7,23 +7,19 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { By } from '@angular/platform-browser';
import { State } from '../../shared/types/enums/State';
-import { RouterTestingModule } from '@angular/router/testing';
import { OverviewService } from '../../services/overview.service';
-import { objective, objectiveMin } from '../../shared/testData';
+import { objectiveMin } from '../../shared/testData';
import { MatMenuHarness } from '@angular/material/menu/testing';
import { KeyresultComponent } from '../keyresult/keyresult.component';
import { MatDialogModule } from '@angular/material/dialog';
-import { HttpClientTestingModule } from '@angular/common/http/testing';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ScoringComponent } from '../../shared/custom/scoring/scoring.component';
import { ConfidenceComponent } from '../confidence/confidence.component';
import { ReactiveFormsModule } from '@angular/forms';
+// @ts-ignore
import * as de from '../../../assets/i18n/de.json';
import { TranslateTestingModule } from 'ngx-translate-testing';
-import { ObjectiveMin } from '../../shared/types/model/ObjectiveMin';
-import { MenuEntry } from '../../shared/types/menu-entry';
-import { of } from 'rxjs';
import { ObjectiveService } from '../../services/objective.service';
const overviewServiceMock = {
@@ -31,12 +27,9 @@ const overviewServiceMock = {
};
const objectiveServiceMock = {
- getFullObjective(objectiveMin: ObjectiveMin) {
- let ongoingObjective = objective;
- ongoingObjective.state = State.ONGOING;
- return of(ongoingObjective);
- },
+ getFullObjective: jest.fn(),
};
+
describe('ObjectiveColumnComponent', () => {
let component: ObjectiveComponent;
let fixture: ComponentFixture
;
@@ -50,9 +43,7 @@ describe('ObjectiveColumnComponent', () => {
MatMenuModule,
MatCardModule,
NoopAnimationsModule,
- RouterTestingModule,
MatDialogModule,
- HttpClientTestingModule,
MatIconModule,
MatTooltipModule,
ReactiveFormsModule,
@@ -114,46 +105,4 @@ describe('ObjectiveColumnComponent', () => {
const button = fixture.debugElement.query(By.css('[data-testId="add-keyResult"]'));
expect(button).toBeFalsy();
});
-
- it('Correct method should be called when back to draft is clicked', () => {
- jest.spyOn(component, 'objectiveBackToDraft');
- component.objective$.next(objectiveMin);
- fixture.detectChanges();
- const menuEntry: MenuEntry =
- component.getOngoingMenuActions()[
- component
- .getOngoingMenuActions()
- .map((menuAction) => menuAction.action)
- .indexOf('todraft')
- ];
- component.handleDialogResult(menuEntry, { endState: '', comment: null, objective: objective });
- fixture.detectChanges();
- expect(component.objectiveBackToDraft).toHaveBeenCalled();
- });
-
- test('Should set isBacklogQuarter right', async () => {
- expect(component.isBacklogQuarter).toBeFalsy();
-
- objectiveMin.quarter.label = 'Backlog';
-
- component.objective = objectiveMin;
- fixture.detectChanges();
- component.ngOnInit();
-
- expect(component.isBacklogQuarter).toBeTruthy();
- });
-
- test('Should return correct menu entries when backlog', async () => {
- objectiveMin.quarter.label = 'Backlog';
- component.objective = objectiveMin;
- fixture.detectChanges();
- component.ngOnInit();
-
- let menuActions = component.getDraftMenuActions();
-
- expect(menuActions.length).toEqual(3);
- expect(menuActions[0].displayName).toEqual('Objective bearbeiten');
- expect(menuActions[1].displayName).toEqual('Objective duplizieren');
- expect(menuActions[2].displayName).toEqual('Objective veröffentlichen');
- });
});
diff --git a/frontend/src/app/components/objective/objective.component.ts b/frontend/src/app/components/objective/objective.component.ts
index 94ea0f3b01..d67984a57b 100644
--- a/frontend/src/app/components/objective/objective.component.ts
+++ b/frontend/src/app/components/objective/objective.component.ts
@@ -1,264 +1,86 @@
-import { ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
-import { MenuEntry } from '../../shared/types/menu-entry';
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { ObjectiveMin } from '../../shared/types/model/ObjectiveMin';
import { Router } from '@angular/router';
-import { ObjectiveFormComponent } from '../../shared/dialog/objective-dialog/objective-form.component';
-import { BehaviorSubject } from 'rxjs';
+import { map, ReplaySubject, take } from 'rxjs';
import { RefreshDataService } from '../../services/refresh-data.service';
-import { State } from '../../shared/types/enums/State';
import { ObjectiveService } from '../../services/objective.service';
-import { ConfirmDialogComponent } from '../../shared/dialog/confirm-dialog/confirm-dialog.component';
-import { CompleteDialogComponent } from '../../shared/dialog/complete-dialog/complete-dialog.component';
-import { Completed } from '../../shared/types/model/Completed';
-import { Objective } from '../../shared/types/model/Objective';
import { trackByFn } from '../../shared/common';
import { KeyresultDialogComponent } from '../keyresult-dialog/keyresult-dialog.component';
import { TranslateService } from '@ngx-translate/core';
-import { GJ_REGEX_PATTERN } from '../../shared/constantLibary';
import { DialogService } from '../../services/dialog.service';
+import { ObjectiveMenuActionsService, ObjectiveMenuEntry } from '../../services/objective-menu-actions.service';
+import { State } from '../../shared/types/enums/State';
+import { MatMenuTrigger } from '@angular/material/menu';
@Component({
selector: 'app-objective-column',
templateUrl: './objective.component.html',
styleUrls: ['./objective.component.scss'],
- changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class ObjectiveComponent implements OnInit {
- @Input()
- isWritable!: boolean;
-
- menuEntries: MenuEntry[] = [];
- isComplete: boolean = false;
- isBacklogQuarter: boolean = false;
+export class ObjectiveComponent {
+ @Input() isWritable!: boolean;
+ public objective$ = new ReplaySubject();
+ menuEntries = this.objective$.pipe(map((objective) => this.objectiveMenuActionsService.getMenu(objective)));
protected readonly trackByFn = trackByFn;
- @ViewChild('menuButton') private menuButton!: ElementRef;
+ @ViewChild(MatMenuTrigger) trigger: MatMenuTrigger | undefined;
constructor(
- private dialogService: DialogService,
- private router: Router,
- private refreshDataService: RefreshDataService,
- private objectiveService: ObjectiveService,
- private translate: TranslateService,
+ private readonly dialogService: DialogService,
+ private readonly router: Router,
+ private readonly refreshDataService: RefreshDataService,
+ private readonly objectiveService: ObjectiveService,
+ private readonly translate: TranslateService,
+ private readonly objectiveMenuActionsService: ObjectiveMenuActionsService,
) {}
- @Input()
- set objective(objective: ObjectiveMin) {
+ @Input() set objective(objective: ObjectiveMin) {
this.objective$.next(objective);
}
- public objective$ = new BehaviorSubject({} as ObjectiveMin);
-
- ngOnInit() {
- if (this.objective$.value.state.includes('successful') || this.objective$.value.state.includes('not-successful')) {
- this.isComplete = true;
- }
- if (!GJ_REGEX_PATTERN.test(this.objective$.value.quarter.label)) {
- this.isBacklogQuarter = true;
- }
+ getStateTooltip(stateString: string): string {
+ const state = this.getStateByValue(stateString);
+ return this.translate.instant('INFORMATION.OBJECTIVE_STATE_TOOLTIP', { state: state });
}
- formatObjectiveState(state: string): string {
- const lastIndex = state.lastIndexOf('-');
- if (lastIndex !== -1) {
- return state.substring(0, lastIndex).toUpperCase();
- } else {
- return state.toUpperCase();
- }
- }
-
- getStateTooltip(): string {
- return this.translate.instant('INFORMATION.OBJECTIVE_STATE_TOOLTIP');
- }
-
- getMenu(): void {
- if (this.objective$.value.state.includes('successful') || this.objective$.value.state.includes('not-successful')) {
- this.menuEntries = this.getCompletedMenuActions();
- } else {
- if (this.objective$.value.state === State.ONGOING) {
- this.menuEntries = this.getOngoingMenuActions();
- } else {
- this.menuEntries = this.getDraftMenuActions();
- }
- }
- }
-
- getOngoingMenuActions() {
- return [
- ...this.getDefaultMenuActions(),
- ...[
- {
- displayName: 'Objective abschliessen',
- action: 'complete',
- dialog: { dialog: CompleteDialogComponent, data: { objectiveTitle: this.objective$.value.title } },
- },
- {
- displayName: 'Objective als Draft speichern',
- action: 'todraft',
- dialog: {
- dialog: ConfirmDialogComponent,
- data: {
- title: this.translate.instant('CONFIRMATION.TO_DRAFT.TITLE'),
- text: this.translate.instant('CONFIRMATION.TO_DRAFT.TEXT'),
- },
- },
- },
- ],
- ];
- }
-
- getDraftMenuActions() {
- const action = this.isBacklogQuarter ? 'releaseBacklog' : 'release';
- let menuEntries = {
- displayName: 'Objective veröffentlichen',
- action: action,
- dialog: {
- dialog: this.isBacklogQuarter ? ObjectiveFormComponent : ConfirmDialogComponent,
- data: {
- title: this.translate.instant('CONFIRMATION.RELEASE.TITLE'),
- text: this.translate.instant('CONFIRMATION.RELEASE.TEXT'),
- action: action,
- objectiveId: this.isBacklogQuarter ? this.objective$.value.id : undefined,
- },
- },
- };
-
- return [...this.getDefaultMenuActions(), menuEntries];
- }
-
- getDefaultMenuActions() {
- return [
- {
- displayName: 'Objective bearbeiten',
- dialog: { dialog: ObjectiveFormComponent, data: { objectiveId: this.objective$.value.id } },
- },
- {
- displayName: 'Objective duplizieren',
- action: 'duplicate',
- dialog: { dialog: ObjectiveFormComponent, data: { objectiveId: this.objective$.value.id } },
- },
- ];
- }
-
- getCompletedMenuActions() {
- return [
- { displayName: 'Objective wiedereröffnen', action: 'reopen' },
- {
- displayName: 'Objective duplizieren',
- action: 'duplicate',
- dialog: { dialog: ObjectiveFormComponent, data: { objectiveId: this.objective$.value.id } },
- },
- ];
- }
-
- redirect(menuEntry: MenuEntry) {
- if (menuEntry.dialog) {
- const matDialogRef = this.dialogService.open(menuEntry.dialog.dialog, {
- data: {
- title: menuEntry.dialog.data.title,
- action: menuEntry.action,
- text: menuEntry.dialog.data.text,
- objective: menuEntry.dialog.data,
- objectiveTitle: menuEntry.dialog.data.objectiveTitle,
- },
- ...((menuEntry.action == 'release' || menuEntry.action == 'todraft') && { width: 'auto' }),
- });
- matDialogRef.afterClosed().subscribe((result) => {
- this.menuButton.nativeElement.focus();
- if (result) {
- this.handleDialogResult(menuEntry, result);
- }
- });
- } else {
- this.reopenRedirect(menuEntry);
- }
- }
-
- handleDialogResult(menuEntry: MenuEntry, result: { endState: string; comment: string | null; objective: any }) {
- if (menuEntry.action) {
- this.objectiveService.getFullObjective(this.objective$.value.id).subscribe((objective) => {
- if (menuEntry.action == 'complete') {
- this.completeObjective(objective, result);
- } else if (menuEntry.action == 'release') {
- this.releaseObjective(objective);
- } else if (menuEntry.action == 'duplicate') {
- this.refreshDataService.markDataRefresh();
- } else if (menuEntry.action == 'releaseBacklog') {
- this.refreshDataService.markDataRefresh();
- } else if (menuEntry.action == 'todraft') {
- this.objectiveBackToDraft(objective);
- }
- });
- } else {
- if (result?.objective) {
- this.refreshDataService.markDataRefresh();
- }
- }
- }
-
- completeObjective(objective: Objective, result: { endState: string; comment: string | null; objective: any }) {
- objective.state = result.endState as State;
- const completed: Completed = {
- id: null,
- version: objective.version,
- objective: objective,
- comment: result.comment,
- };
- this.objectiveService.updateObjective(objective).subscribe(() => {
- this.objectiveService.createCompleted(completed).subscribe(() => {
- this.isComplete = true;
- this.refreshDataService.markDataRefresh();
- });
- });
- }
-
- releaseObjective(objective: Objective) {
- objective.state = 'ONGOING' as State;
- this.objectiveService.updateObjective(objective).subscribe(() => {
- this.refreshDataService.markDataRefresh();
- });
- }
-
- objectiveBackToDraft(objective: Objective) {
- objective.state = 'DRAFT' as State;
- this.objectiveService.updateObjective(objective).subscribe(() => {
- this.refreshDataService.markDataRefresh();
- });
- }
-
- reopenRedirect(menuEntry: MenuEntry) {
- if (menuEntry.action === 'reopen') {
- this.objectiveService.getFullObjective(this.objective$.value.id).subscribe((objective) => {
- objective.state = 'ONGOING' as State;
- this.objectiveService.updateObjective(objective).subscribe(() => {
- this.objectiveService.deleteCompleted(objective.id).subscribe(() => {
- this.isComplete = false;
- this.refreshDataService.markDataRefresh();
- });
+ redirect(menuEntry: ObjectiveMenuEntry, objectiveMin: ObjectiveMin) {
+ const matDialogRef = menuEntry.action();
+ matDialogRef
+ .afterClosed()
+ .pipe(take(1))
+ .subscribe((result) => {
+ this.objectiveService.getFullObjective(objectiveMin.id).subscribe((objective) => {
+ menuEntry.afterAction(objective, result);
+ this.trigger?.focus();
});
});
- } else {
- this.router.navigate([menuEntry.route!]);
- }
}
- openObjectiveDetail() {
- this.router.navigate(['details/objective', this.objective$.value.id]);
+ openObjectiveDetail(objectiveId: number) {
+ this.router.navigate(['details/objective', objectiveId]);
}
- openAddKeyResultDialog() {
+ openAddKeyResultDialog(objective: ObjectiveMin) {
this.dialogService
.open(KeyresultDialogComponent, {
data: {
- objective: this.objective$.value,
+ objective: objective,
keyResult: null,
},
})
.afterClosed()
.subscribe((result) => {
if (result?.openNew) {
- this.openAddKeyResultDialog();
+ this.openAddKeyResultDialog(objective);
}
this.refreshDataService.markDataRefresh();
});
}
+
+ isObjectiveComplete(objective: ObjectiveMin): boolean {
+ return objective.state == State.SUCCESSFUL || objective.state == State.NOTSUCCESSFUL;
+ }
+
+ getStateByValue(value: string): string {
+ return Object.keys(State).find((key) => State[key as keyof typeof State] === value) ?? '';
+ }
}
diff --git a/frontend/src/app/services/dialog.service.ts b/frontend/src/app/services/dialog.service.ts
index a1236133ea..964ce85571 100644
--- a/frontend/src/app/services/dialog.service.ts
+++ b/frontend/src/app/services/dialog.service.ts
@@ -18,6 +18,7 @@ export class DialogService {
DIALOG_CONFIG: MatDialogConfig = {
maxWidth: '100vw', // Used to override the default maxWidth of angular material dialog
+ restoreFocus: true,
autoFocus: 'first-tabbable',
};
diff --git a/frontend/src/app/services/objective-menu-actions.service.spec.ts b/frontend/src/app/services/objective-menu-actions.service.spec.ts
new file mode 100644
index 0000000000..75124354cc
--- /dev/null
+++ b/frontend/src/app/services/objective-menu-actions.service.spec.ts
@@ -0,0 +1,87 @@
+import { TestBed } from '@angular/core/testing';
+
+import { ObjectiveMenuActionsService } from './objective-menu-actions.service';
+import { TranslateModule, TranslateService } from '@ngx-translate/core';
+import { provideRouter } from '@angular/router';
+import { provideHttpClient } from '@angular/common/http';
+import { provideHttpClientTesting } from '@angular/common/http/testing';
+import { ObjectiveMin } from '../shared/types/model/ObjectiveMin';
+import { State } from '../shared/types/enums/State';
+import { objectiveMin } from '../shared/testData';
+
+describe('ObjectiveMenuActionsService', () => {
+ let service: ObjectiveMenuActionsService;
+ let specificMenuEntriesSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot()],
+ providers: [TranslateService, provideRouter([]), provideHttpClient(), provideHttpClientTesting()],
+ });
+ service = TestBed.inject(ObjectiveMenuActionsService);
+
+ specificMenuEntriesSpy = jest.spyOn(service as any, 'getSpecificMenuEntries');
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ describe('getMenu', () => {
+ it('should return default and specific menu entries for an ongoing objective', () => {
+ let spyOn = jest.spyOn(service as any, 'getOngoingMenuActions');
+
+ let objectiveMinLocal: ObjectiveMin = objectiveMin;
+ objectiveMinLocal.state = State.ONGOING;
+ service.getMenu(objectiveMinLocal);
+ expect(spyOn).toHaveBeenCalledTimes(1);
+ });
+
+ it('should return draft menu entries for a draft objective', () => {
+ let spyOn = jest.spyOn(service as any, 'getDraftMenuActions');
+
+ let objectiveMinLocal: ObjectiveMin = objectiveMin;
+ objectiveMinLocal.state = State.DRAFT;
+ service.getMenu(objectiveMinLocal);
+ expect(spyOn).toHaveBeenCalledTimes(1);
+ });
+
+ it('should return completed menu entries for a successful objective', () => {
+ let spyOn = jest.spyOn(service as any, 'getCompletedMenuActions');
+
+ let objectiveMinLocal: ObjectiveMin = objectiveMin;
+ objectiveMinLocal.state = State.SUCCESSFUL;
+ service.getMenu(objectiveMinLocal);
+ expect(spyOn).toHaveBeenCalledTimes(1);
+ });
+
+ it('should return completed menu entries for a non-successful objective', () => {
+ let spyOn = jest.spyOn(service as any, 'getCompletedMenuActions');
+
+ let objectiveMinLocal: ObjectiveMin = objectiveMin;
+ objectiveMinLocal.state = State.NOTSUCCESSFUL;
+ service.getMenu(objectiveMinLocal);
+ expect(spyOn).toHaveBeenCalledTimes(1);
+ });
+ afterEach(() => {
+ expect(specificMenuEntriesSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('getReleaseAction', () => {
+ it('should return release from backlog action for an objective in backlog quarter', () => {
+ jest.spyOn(service as any, 'isInBacklogQuarter').mockReturnValue(true);
+ let spyOn = jest.spyOn(service as any, 'isInBacklogQuarter').mockReturnValue(true);
+ // @ts-expect-error
+ service.getReleaseAction(objectiveMin);
+ expect(spyOn).toHaveBeenCalledTimes(1);
+ });
+
+ it('should return release from quarter action for an objective in non-backlog quarter', () => {
+ let spyOn = jest.spyOn(service as any, 'isInBacklogQuarter').mockReturnValue(false);
+ // @ts-expect-error
+ service.getReleaseAction(objectiveMin);
+ expect(spyOn).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/frontend/src/app/services/objective-menu-actions.service.ts b/frontend/src/app/services/objective-menu-actions.service.ts
new file mode 100644
index 0000000000..5dd1c2b285
--- /dev/null
+++ b/frontend/src/app/services/objective-menu-actions.service.ts
@@ -0,0 +1,85 @@
+import { Injectable } from '@angular/core';
+import { DialogService } from './dialog.service';
+import { MatDialogRef } from '@angular/material/dialog';
+import { ObjectiveMin } from '../shared/types/model/ObjectiveMin';
+import { State } from '../shared/types/enums/State';
+import { ObjectiveMenuAfterActions } from '../components/objective/ObjectiveMenuAfterActions';
+import { ObjectiveService } from './objective.service';
+import { RefreshDataService } from './refresh-data.service';
+import { Objective } from '../shared/types/model/Objective';
+import { ObjectiveMenuActions } from '../components/objective/ObjectiveMenuActions';
+import { GJ_REGEX_PATTERN } from '../shared/constantLibary';
+
+export type ObjectiveMenuAction = () => MatDialogRef;
+export type ObjectiveMenuAfterAction = (objective: Objective, dialogResult: any) => any;
+
+export interface ObjectiveMenuEntry {
+ displayName: string;
+ action: ObjectiveMenuAction;
+ afterAction: ObjectiveMenuAfterAction;
+}
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ObjectiveMenuActionsService {
+ afterActions: ObjectiveMenuAfterActions;
+ actions: ObjectiveMenuActions;
+ constructor(
+ dialogService: DialogService,
+ objectiveService: ObjectiveService,
+ refreshDataService: RefreshDataService,
+ ) {
+ this.afterActions = new ObjectiveMenuAfterActions(objectiveService, refreshDataService);
+ this.actions = new ObjectiveMenuActions(dialogService, refreshDataService, this.afterActions);
+ }
+
+ getMenu(objective: ObjectiveMin): ObjectiveMenuEntry[] {
+ return [...this.getSpecificMenuEntries(objective), ...this.getDefaultActions(objective)];
+ }
+
+ private getSpecificMenuEntries(objective: ObjectiveMin): ObjectiveMenuEntry[] {
+ if (this.isObjectiveComplete(objective)) {
+ return this.getCompletedMenuActions(objective);
+ } else if (objective.state === State.ONGOING) {
+ return this.getOngoingMenuActions(objective);
+ } else if (objective.state === State.DRAFT) {
+ return this.getDraftMenuActions(objective);
+ }
+ throw new Error('Objective invalid');
+ }
+
+ private getDefaultActions(objective: ObjectiveMin): ObjectiveMenuEntry[] {
+ return [this.actions.duplicateObjectiveAction(objective)];
+ }
+
+ private getDraftMenuActions(objective: ObjectiveMin): ObjectiveMenuEntry[] {
+ return [this.actions.editObjectiveAction(objective), this.getReleaseAction(objective)];
+ }
+
+ private getOngoingMenuActions(objective: ObjectiveMin): ObjectiveMenuEntry[] {
+ return [
+ this.actions.editObjectiveAction(objective),
+ this.actions.completeObjectiveAction(objective),
+ this.actions.objectiveBackToDraft(),
+ ];
+ }
+
+ private getCompletedMenuActions(objective: ObjectiveMin): ObjectiveMenuEntry[] {
+ return [this.actions.objectiveReopen()];
+ }
+
+ private getReleaseAction(objective: ObjectiveMin): ObjectiveMenuEntry {
+ return this.isInBacklogQuarter(objective)
+ ? this.actions.releaseFromBacklogAction(objective)
+ : this.actions.releaseFromQuarterAction(objective);
+ }
+
+ private isObjectiveComplete(objective: ObjectiveMin): boolean {
+ return objective.state == State.SUCCESSFUL || objective.state == State.NOTSUCCESSFUL;
+ }
+
+ private isInBacklogQuarter(objective: ObjectiveMin) {
+ return !GJ_REGEX_PATTERN.test(objective.quarter.label);
+ }
+}
diff --git a/frontend/src/assets/i18n/de.json b/frontend/src/assets/i18n/de.json
index f723866d85..be9c8d61a4 100644
--- a/frontend/src/assets/i18n/de.json
+++ b/frontend/src/assets/i18n/de.json
@@ -23,16 +23,17 @@
"ordinal": "Ordinal"
},
"INFORMATION": {
- "OBJECTIVE_STATE_TOOLTIP": "Der Status dieses Objectives ist",
- "DELETE_TEAM": "Möchtest du dieses Team wirklich löschen? Zugehörige Objectives werden dadurch in allen Quartalen ebenfalls gelöscht!",
- "DELETE_OBJECTIVE": "Möchtest du dieses Objective wirklich löschen? Zugehörige Key Results werden dadurch ebenfalls gelöscht!",
- "DELETE_KEY_RESULT": "Möchtest du dieses Key Result wirklich löschen? Zugehörige Check-ins werden dadurch ebenfalls gelöscht!"
+ "OBJECTIVE_STATE_TOOLTIP": "Der Status dieses Objectives ist: {{state}}"
},
"CONFIRMATION": {
"DRAFT_CREATE": {
"TITLE": "Check-in im Draft-Status",
"TEXT": "Dein Objective befindet sich noch im DRAFT Status. Möchtest du das Check-in trotzdem erfassen?"
},
+ "REOPEN": {
+ "TITLE": "Objective wiedereröffnen",
+ "TEXT": "Soll dieses Objective wiedereröffnet werden?"
+ },
"RELEASE": {
"TITLE": "Objective veröffentlichen",
"TEXT": "Soll dieses Objective veröffentlicht werden?"