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 <[email protected]>
  • Loading branch information
kcinay055679 and ManuelMoeri authored Nov 8, 2024
1 parent 1d8d99f commit f3a8271
Show file tree
Hide file tree
Showing 11 changed files with 438 additions and 358 deletions.
6 changes: 5 additions & 1 deletion frontend/cypress/e2e/objective.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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")')
Expand Down
9 changes: 6 additions & 3 deletions frontend/cypress/e2e/tab.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
77 changes: 77 additions & 0 deletions frontend/src/app/components/objective/ObjectiveMenuActions.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
}
50 changes: 50 additions & 0 deletions frontend/src/app/components/objective/ObjectiveMenuAfterActions.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
}
}
151 changes: 76 additions & 75 deletions frontend/src/app/components/objective/objective.component.html
Original file line number Diff line number Diff line change
@@ -1,81 +1,82 @@
<div
(click)="openObjectiveDetail()"
(keydown.enter)="openObjectiveDetail()"
*ngIf="objective$ | async as objective"
[attr.data-testId]="'objective'"
class="objective rounded-3 bg-white w-100 cursor-pointer focus-outline"
tabindex="0"
>
<div class="row mx-1">
<section class="d-flex mb-3 mt-3 justify-content-between pe-0">
<section class="d-flex gap-2 align-items-start fit-content objective-title">
<img
<ng-container *ngIf="objective$ | async as objective">
<div
(click)="openObjectiveDetail(objective.id)"
(keydown.enter)="openObjectiveDetail(objective.id)"
[attr.data-testId]="'objective'"
class="objective rounded-3 bg-white w-100 cursor-pointer focus-outline"
tabindex="0"
>
<div class="row mx-1">
<section class="d-flex mb-3 mt-3 justify-content-between pe-0">
<section class="d-flex gap-2 align-items-start fit-content objective-title">
<img
(click)="$event.stopPropagation()"
[attr.data-testId]="'objective-state'"
[src]="'assets/icons/' + objective.state"
alt="The objectives state"
class="icon"
matTooltip="{{ getStateTooltip(objective.state) }}"
matTooltipPosition="above"
/>
<h2 class="title fit-content">{{ objective.title }}</h2>
</section>
<button
#menuButton
*ngIf="isWritable"
class="icon-button three-dot-menu focus-outline"
[matMenuTriggerFor]="objectiveMenu"
(click)="$event.stopPropagation()"
[attr.data-testId]="'objective-state'"
[src]="'assets/icons/' + objective.state"
alt="The objectives state"
class="icon"
matTooltip="{{ getStateTooltip() + ' ' + formatObjectiveState(objective.state) }}"
matTooltipPosition="above"
/>
<h2 class="title fit-content">{{ objective.title }}</h2>
(keydown.enter)="$event.stopPropagation()"
[attr.data-testId]="'three-dot-menu'"
>
<img src="../assets/icons/three-dot-menu-icon.svg" alt="menu icon" class="text-white menu-scale" />
</button>
</section>
<button
#menuButton
*ngIf="isWritable"
class="icon-button three-dot-menu focus-outline"
[matMenuTriggerFor]="objectiveMenu"
(click)="$event.stopPropagation(); getMenu()"
(keydown.enter)="$event.stopPropagation()"
[attr.data-testId]="'three-dot-menu'"
>
<img src="../assets/icons/three-dot-menu-icon.svg" alt="menu icon" class="text-white menu-scale" />
</button>
</section>

<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"
(click)="$event.stopPropagation()"
(keydown.enter)="$event.stopPropagation()"
[keyResult]="keyResult"
[attr.data-testId]="'keyresult'"
></app-keyresult>
</div>
<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"
(click)="$event.stopPropagation()"
(keydown.enter)="$event.stopPropagation()"
[keyResult]="keyResult"
[attr.data-testId]="'keyresult'"
></app-keyresult>
</div>

<section class="p-0 py-2 m-0">
<button
*ngIf="!isComplete && isWritable"
mat-button
color="primary"
class="fw-bold px-0 pe-2 ms-2"
[attr.data-testId]="'add-keyResult'"
(click)="openAddKeyResultDialog(); $event.stopPropagation()"
(keydown.enter)="$event.stopPropagation()"
>
<span class="d-flex align-items-center add-text">
<img alt="Add key-result button" class="add-cross-button" src="../../../assets/icons/new-icon.svg" />
Key Result hinzufügen
</span>
</button>
</section>
<section class="p-0 py-2 m-0">
<button
*ngIf="!isObjectiveComplete(this.objective) && isWritable"
mat-button
color="primary"
class="fw-bold px-0 pe-2 ms-2"
[attr.data-testId]="'add-keyResult'"
(click)="openAddKeyResultDialog(objective); $event.stopPropagation()"
(keydown.enter)="$event.stopPropagation()"
>
<span class="d-flex align-items-center add-text">
<img alt="Add key-result button" class="add-cross-button" src="../../../assets/icons/new-icon.svg" />
Key Result hinzufügen
</span>
</button>
</section>
</div>
</div>
</div>
<mat-menu
#objectiveMenu="matMenu"
[class]="'objective-three-dot-menu'"
class="pt-2 pb-2"
xPosition="before"
yPosition="below"
>
<button
(click)="redirect(menuEntry)"
*ngFor="let menuEntry of menuEntries"
[attr.data-testId]="'objective-menu'"
class="objective-menu-option"
mat-menu-item
<mat-menu
#objectiveMenu="matMenu"
[class]="'objective-three-dot-menu'"
class="pt-2 pb-2"
xPosition="before"
yPosition="below"
>
{{ menuEntry.displayName }}
</button>
</mat-menu>
<button
(click)="redirect(menuEntry, objective)"
*ngFor="let menuEntry of menuEntries | async"
[attr.data-testId]="'objective-menu'"
class="objective-menu-option"
mat-menu-item
>
{{ menuEntry.displayName }}
</button>
</mat-menu>
</ng-container>
Loading

0 comments on commit f3a8271

Please sign in to comment.