Skip to content

Commit

Permalink
Feature/624 refactor objective menu (#1078)
Browse files Browse the repository at this point in the history
* Fix/fix git history (#1125)

* fix history

* fix hist

* jar is now debuggable

* add jar debug dev tools only on profile

* rename intelij config and change log level of spring to debug in staging config

* rename folder

* update docker compose file

* use external profile to disable formatter

* clean up

* refactor dialog calls with config by introducing a service

* refactor frontend tests related to dialogService

* remove backend/pom.xml from branch

* introduce new objective menu action service to simplify dialog creation of dialog actions

* refactor objective

* reimplement generic menu

* use pipe with map to generate menu entries

* introduce afterAction property

* add ObjectiveAfterActionFactory

* introduce objectiveMenuAction file

* refactor objective status tooltip

* clean up objective menu action service

* rename objective actions file

* Try to fix frontend unit tests

* restore staging.properties

* fix angular build

* place functions in related files instead of common.ts

* restore angular config files

* fix action service tests

* add test for actions service

* clean up

* display correct options for draft and use replay subject

* fix check-in tests

* fix objective backlog tests

* fix objective e2e tests

* fix tabbing tests

* fix tabbing tests

* schtibitz e2e strategy from pulfer

* remove console.log

* restore focus

* restore old frontend-test-action

* clean pup

* remove application.staging from pr

* clean up tests of objective

* fix e2e tests

* check for focues of threedot menu after dialog closes

---------

Co-authored-by: Manuel <moeri@puzzle.ch>
  • Loading branch information
kcinay055679 and ManuelMoeri committed Nov 15, 2024
1 parent d66e449 commit f96719d
Show file tree
Hide file tree
Showing 5 changed files with 26 additions and 227 deletions.
2 changes: 1 addition & 1 deletion frontend/cypress/e2e/objective.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,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")')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
>
<div class="row mx-1">
<section class="d-flex mb-3 mt-3 justify-content-between pe-0">
<div class="d-flex gap-2 align-items-start fit-content objective-title">
<section class="d-flex gap-2 align-items-start fit-content objective-title">
<img
(click)="$event.stopPropagation()"
[attr.data-testId]="'objective-state'"
Expand All @@ -19,7 +19,7 @@
matTooltipPosition="above"
/>
<h2 class="title fit-content">{{ objective.title }}</h2>
</div>
</section>
<button
#menuButton
*ngIf="isWritable"
Expand All @@ -33,7 +33,7 @@ <h2 class="title fit-content">{{ objective.title }}</h2>
</button>
</section>

<section class="d-flex px-3 gap-3 flex-column">
<div class="d-flex px-3 gap-3 flex-column">
<app-keyresult
*ngFor="let keyResult of objective.keyResults; trackBy: trackByFn"
class="border-0 p-0"
Expand All @@ -42,7 +42,7 @@ <h2 class="title fit-content">{{ objective.title }}</h2>
[keyResult]="keyResult"
[attr.data-testId]="'keyresult'"
></app-keyresult>
</section>
</div>

<section class="p-0 py-2 m-0">
<button
Expand Down
206 changes: 20 additions & 186 deletions frontend/src/app/components/objective/objective.component.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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 { ObjectiveService } from '../../services/objective.service';
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',
Expand All @@ -24,11 +25,12 @@ export class ObjectiveComponent {
@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) {
Expand All @@ -40,183 +42,15 @@ export class ObjectiveComponent {
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();
});
});
}
Expand All @@ -225,7 +59,7 @@ export class ObjectiveComponent {
this.router.navigate(['details/objective', objectiveId]);
}

openAddKeyResultDialog() {
openAddKeyResultDialog(objective: ObjectiveMin) {
this.dialogService
.open(KeyresultDialogComponent, {
data: {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/services/dialog.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};

Expand Down
36 changes: 0 additions & 36 deletions frontend/src/assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,42 +65,6 @@
}
}
},
"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?"
},
"RELEASE": {
"TITLE": "Objective veröffentlichen",
"TEXT": "Soll dieses Objective veröffentlicht werden?"
},
"TO_DRAFT": {
"TITLE": "Objective als Draft speichern",
"TEXT": "Soll dieses Objective als Draft gespeichert werden?"
},
"DELETE": {
"ACTION":{
"TITLE": "Löschen bestätigen",
"TEXT": "Möchtest du die Action wirklich löschen?"
},
"TEAM":{
"TITLE": "Team löschen",
"TEXT": "Möchtest du das Team '{{team}}' wirklich löschen? Zugehörige Objectives werden dadurch in allen Quartalen ebenfalls gelöscht!"
},
"OBJECTIVE":{
"TITLE": "Objective löschen",
"TEXT": "Möchtest du dieses Objective wirklich löschen? Zugehörige Key Results werden dadurch ebenfalls gelöscht!"
},
"KEYRESULT":{
"TITLE": "Key Result löschen",
"TEXT": "Möchtest du dieses Key Result wirklich löschen? Zugehörige Check-ins werden dadurch ebenfalls gelöscht!"
},
"USER_FROM_TEAM":{
"TITLE": "Mitglied entfernen",
"TEXT": "Möchtest du {{user}} wirklich aus dem Team '{{team}}' entfernen?"
}
}
},
"ERROR": {
"UNAUTHORIZED": "Du bist nicht autorisiert, um das Objekt mit der Id {1} zu öffnen.",
"NOT_FOUND": "Das Objekt '{0}' mit der Id {1} konnte nicht gefunden werden.",
Expand Down

0 comments on commit f96719d

Please sign in to comment.