Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/624 refactor objective menu #1078

Merged
merged 44 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7467830
Fix/fix git history (#1125)
kcinay055679 Nov 4, 2024
7c0e846
jar is now debuggable
kcinay055679 Oct 15, 2024
56fa137
add jar debug dev tools only on profile
kcinay055679 Oct 15, 2024
5f1c3eb
rename intelij config and change log level of spring to debug in stag…
kcinay055679 Oct 17, 2024
bca6c95
rename folder
kcinay055679 Oct 18, 2024
9d90b90
update docker compose file
kcinay055679 Oct 18, 2024
783ddaa
use external profile to disable formatter
kcinay055679 Oct 18, 2024
11dc0a9
clean up
kcinay055679 Oct 18, 2024
0624f47
refactor dialog calls with config by introducing a service
kcinay055679 Oct 18, 2024
f697191
refactor frontend tests related to dialogService
kcinay055679 Oct 21, 2024
f7af3f4
remove backend/pom.xml from branch
kcinay055679 Oct 24, 2024
159e2c2
introduce new objective menu action service to simplify dialog creati…
kcinay055679 Oct 23, 2024
30e958d
refactor objective
kcinay055679 Oct 23, 2024
12663d5
reimplement generic menu
kcinay055679 Oct 23, 2024
c05c734
use pipe with map to generate menu entries
kcinay055679 Oct 23, 2024
234fc14
introduce afterAction property
kcinay055679 Oct 23, 2024
69591cd
add ObjectiveAfterActionFactory
kcinay055679 Oct 23, 2024
424045d
introduce objectiveMenuAction file
kcinay055679 Oct 23, 2024
3ddc34d
refactor objective status tooltip
kcinay055679 Oct 23, 2024
2f89f2e
clean up objective menu action service
kcinay055679 Oct 24, 2024
f011cb9
rename objective actions file
kcinay055679 Oct 25, 2024
942df1e
Try to fix frontend unit tests
ManuelMoeri Oct 28, 2024
44690e2
restore staging.properties
kcinay055679 Nov 4, 2024
88a7202
fix angular build
kcinay055679 Nov 5, 2024
b79af1f
place functions in related files instead of common.ts
kcinay055679 Nov 5, 2024
8b31a44
restore angular config files
kcinay055679 Nov 5, 2024
fdb1318
fix action service tests
kcinay055679 Nov 5, 2024
f569af5
add test for actions service
kcinay055679 Nov 5, 2024
2822b9c
clean up
kcinay055679 Nov 5, 2024
254775f
display correct options for draft and use replay subject
kcinay055679 Nov 5, 2024
56445eb
fix check-in tests
kcinay055679 Nov 5, 2024
9797b4f
fix objective backlog tests
kcinay055679 Nov 5, 2024
55d2b86
fix objective e2e tests
kcinay055679 Nov 5, 2024
1808502
fix tabbing tests
kcinay055679 Nov 5, 2024
4b8a8fe
fix tabbing tests
kcinay055679 Nov 5, 2024
65ccc9c
schtibitz e2e strategy from pulfer
kcinay055679 Nov 5, 2024
704a919
remove console.log
kcinay055679 Nov 5, 2024
a5713a0
restore focus
kcinay055679 Nov 5, 2024
0afd272
restore old frontend-test-action
kcinay055679 Nov 5, 2024
f07168f
clean pup
kcinay055679 Nov 7, 2024
72369bb
remove application.staging from pr
kcinay055679 Nov 7, 2024
1cbb427
clean up tests of objective
kcinay055679 Nov 7, 2024
9b33d7f
fix e2e tests
kcinay055679 Nov 7, 2024
81f094e
check for focues of threedot menu after dialog closes
kcinay055679 Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
kcinay055679 marked this conversation as resolved.
Show resolved Hide resolved
const action: ObjectiveMenuAction = () => this.dialogService.openConfirmDialog('CONFIRMATION.RELEASE');
const afterAction: ObjectiveMenuAfterAction = (objective, dialogResult) =>
kcinay055679 marked this conversation as resolved.
Show resolved Hide resolved
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) =>
kcinay055679 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading