Skip to content

Commit 9c66d05

Browse files
authored
Merge branch 'staging' into feat/status-updates
2 parents daec462 + 8ab7c02 commit 9c66d05

File tree

31 files changed

+2027
-133
lines changed

31 files changed

+2027
-133
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<div class="flex items-center gap-3 mb-4 pb-3">
2+
<i-tabler name="clipboard-check" class="!size-6 text-primary" />
3+
<h2 class="text-xl font-semibold">Evaluate Release Candidate</h2>
4+
</div>
5+
<div class="p-6">
6+
<!-- Header with Icon -->
7+
8+
<div class="flex flex-col gap-4">
9+
<p class="text-muted-color">
10+
You are marking release candidate "{{ data.releaseName }}" as
11+
<span [class]="data.isWorking ? 'text-green-600 font-semibold' : 'text-red-600 font-semibold'">
12+
<i [class]="data.isWorking ? 'pi pi-check mr-1' : 'pi pi-times mr-1'"></i>
13+
{{ data.isWorking ? 'Working' : 'Broken' }}
14+
</span>
15+
</p>
16+
17+
<form [formGroup]="evaluationForm" class="space-y-4">
18+
<div>
19+
<label for="comment" class="block text-sm font-medium mb-2"> Comment <span class="text-muted-color">(optional, max 500 characters)</span> </label>
20+
<textarea
21+
id="comment"
22+
pTextarea
23+
formControlName="comment"
24+
placeholder="Add your comments about this release candidate..."
25+
rows="4"
26+
class="w-full"
27+
maxlength="500"
28+
></textarea>
29+
<div class="text-right text-sm text-muted-color mt-1">{{ evaluationForm.get('comment')?.value?.length || 0 }}/500</div>
30+
</div>
31+
</form>
32+
33+
<div class="flex justify-between w-full mt-4">
34+
<p-button label="Cancel" severity="secondary" [text]="true" (onClick)="cancel()" />
35+
<p-button label="Submit Evaluation" severity="primary" (onClick)="submit()" />
36+
</div>
37+
</div>
38+
</div>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Component, inject } from '@angular/core';
2+
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
3+
import { ButtonModule } from 'primeng/button';
4+
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
5+
import { TextareaModule } from 'primeng/textarea';
6+
import { provideTablerIcons, TablerIconComponent } from 'angular-tabler-icons';
7+
import { IconClipboardCheck, IconX } from 'angular-tabler-icons/icons';
8+
9+
export interface ReleaseEvaluationDialogData {
10+
releaseName: string;
11+
isWorking: boolean;
12+
comment?: string;
13+
}
14+
15+
export interface ReleaseEvaluationDialogResult {
16+
isWorking: boolean;
17+
comment: string;
18+
}
19+
20+
@Component({
21+
selector: 'app-release-evaluation-dialog',
22+
imports: [ReactiveFormsModule, TextareaModule, ButtonModule, TablerIconComponent],
23+
providers: [
24+
provideTablerIcons({
25+
IconClipboardCheck,
26+
IconX,
27+
}),
28+
],
29+
templateUrl: './release-evaluation-dialog.component.html',
30+
})
31+
export class ReleaseEvaluationDialogComponent {
32+
private ref = inject(DynamicDialogRef);
33+
private config = inject(DynamicDialogConfig);
34+
35+
data: ReleaseEvaluationDialogData = this.config.data;
36+
37+
evaluationForm = new FormGroup({
38+
comment: new FormControl(this.data.comment ?? '', [Validators.maxLength(500)]),
39+
});
40+
41+
submit() {
42+
if (this.evaluationForm.valid) {
43+
const comment = this.evaluationForm.get('comment')?.value || '';
44+
45+
const result: ReleaseEvaluationDialogResult = {
46+
isWorking: this.data.isWorking,
47+
comment: comment,
48+
};
49+
50+
this.ref.close(result);
51+
}
52+
}
53+
54+
cancel() {
55+
this.ref.close();
56+
}
57+
}

client/src/app/components/profile-nav-section/profile-nav-section.component.html

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
11
<div class="flex flex-col gap-3 items-center">
22
@if (isLoggedIn()) {
3-
<p-button class="flex gap-3 items-center cursor-pointer" (click)="profileMenu.toggle($event)">
4-
<p-avatar class="w-7 h-7 flex-shrink-0" shape="circle" size="normal" [image]="getProfilePictureUrl()" />
5-
<span class="hidden md:block text-lg truncate overflow-hidden whitespace-nowrap max-w-[130px]">{{ fullName() }}</span>
3+
<p-button class="flex gap-3 items-center cursor-pointer" (click)="profileMenu.toggle($event)" size="small">
4+
<p-avatar class="size-5 flex-shrink-0" shape="circle" size="normal" [image]="getProfilePictureUrl()" />
5+
<span class="hidden md:block text-md runcate overflow-hidden whitespace-nowrap max-w-[130px]">{{ fullName() }}</span>
66
</p-button>
77
} @else {
8-
<p-button (click)="login()" class="rounded-xl flex items-center justify-center gap-2 w-full">
9-
<i-tabler name="login" class="!size-7 !stroke-1"></i-tabler>
8+
<p-button (click)="login()" class="rounded-xl flex items-center justify-center gap-2 w-full" size="small">
9+
<i-tabler name="login" class="!size-5 !stroke-1"></i-tabler>
1010
<span class="hidden md:block">Sign In</span>
1111
</p-button>
1212
}
1313
</div>
1414

1515
<p-popover #profileMenu>
16-
<div class="flex flex-col items-start gap-2 p-2 min-w-[10rem]">
16+
<div class="flex flex-col items-start gap-2">
1717
<!-- Settings Button -->
18-
<p-button [routerLink]="'/settings'" severity="secondary" text>
19-
<i-tabler name="settings" class="!size-6 !stroke-1"></i-tabler>
18+
<p-button [routerLink]="'/settings'" severity="secondary" text size="small">
19+
<i-tabler name="settings" class="!size-5 !stroke-1"></i-tabler>
2020
<span>Settings</span>
2121
</p-button>
2222

2323
<!-- Sign Out Button -->
24-
<p-button (click)="logout()" severity="secondary" text>
25-
<i-tabler name="logout" class="!size-6 !stroke-1 !text-red-500 !hover:text-red-700"></i-tabler>
24+
<p-button (click)="logout()" severity="secondary" text size="small">
25+
<i-tabler name="logout" class="!size-5 !stroke-1 !text-red-500 !hover:text-red-700"></i-tabler>
2626
<span>Sign Out</span>
2727
</p-button>
2828
</div>

client/src/app/core/modules/openapi/schemas.gen.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,11 @@ export const ReleaseEvaluationDtoSchema = {
537537
isWorking: {
538538
type: 'boolean',
539539
},
540+
comment: {
541+
type: 'string',
542+
maxLength: 500,
543+
minLength: 0,
544+
},
540545
},
541546
} as const;
542547

@@ -631,6 +636,9 @@ export const ReleaseCandidateEvaluationDtoSchema = {
631636
isWorking: {
632637
type: 'boolean',
633638
},
639+
comment: {
640+
type: 'string',
641+
},
634642
},
635643
required: ['user'],
636644
} as const;

client/src/app/core/modules/openapi/types.gen.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export type ReleaseNameDto = {
173173
export type ReleaseEvaluationDto = {
174174
name?: string;
175175
isWorking?: boolean;
176+
comment?: string;
176177
};
177178

178179
export type BranchInfoDto = {
@@ -205,6 +206,7 @@ export type ReleaseCandidateDeploymentDto = {
205206
export type ReleaseCandidateEvaluationDto = {
206207
user: UserInfoDto;
207208
isWorking?: boolean;
209+
comment?: string;
208210
};
209211

210212
export type ReleaseDto = {

client/src/app/pages/main-layout/main-layout.component.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
<div class="px-6 py-1.5 flex items-center justify-between bg-primary">
33
<div class="flex items-center gap-6">
44
<div class="flex items-center gap-2 cursor-pointer" styleClass="p-0" routerLink="/">
5-
<div class="size-12">
6-
<app-helios-icon size="2.5rem" class="rounded-xl w-8 cursor-pointer" />
5+
<div class="size-9">
6+
<app-helios-icon size="2rem" class="rounded-xl w-8 cursor-pointer" />
77
</div>
88
<div class="text-white text-2xl font-semibold">Helios</div>
99
</div>
@@ -13,18 +13,18 @@
1313
}
1414
</div>
1515
<div class="flex items-center gap-1">
16-
<p-button (click)="toggleDarkMode()">
16+
<p-button (click)="toggleDarkMode()" size="small">
1717
@if (isDarkModeEnabled()) {
18-
<i-tabler name="sun" />
18+
<i-tabler name="sun" class="!size-4" />
1919
} @else {
20-
<i-tabler name="moon" />
20+
<i-tabler name="moon" class="!size-4" />
2121
}
2222
</p-button>
2323

2424
<app-profile-nav-section [isExpanded]="true" />
2525
</div>
2626
</div>
27-
<div class="flex flex-grow h-[calc(100vh-3.75rem)]">
27+
<div class="flex flex-grow h-[calc(100vh-3rem)]">
2828
<!-- Fixed Navigation Bar -->
2929
<div class="h-full sticky top-3 flex">
3030
<app-navigation-bar [repositoryId]="repositoryId()"></app-navigation-bar>

client/src/app/pages/release-candidate-details/release-candidate-details.component.html

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,24 @@ <h2 class="text-2xl flex items-center gap-2 flex-wrap">
7474
}
7575

7676
<p-buttongroup>
77-
<p-button label="Working" severity="success" [disabled]="hasUserEvaluatedTo(true)" (onClick)="evaluateReleaseCandidate(true)">
77+
<p-button
78+
label="Working"
79+
severity="success"
80+
(onClick)="evaluateReleaseCandidate(true)"
81+
[ngClass]="{
82+
'opacity-50': hasUserEvaluatedTo(true),
83+
}"
84+
>
7885
<i-tabler name="check" class="!size-5" />
7986
</p-button>
80-
<p-button label="Broken" severity="danger" [disabled]="hasUserEvaluatedTo(false)" (onClick)="evaluateReleaseCandidate(false)">
87+
<p-button
88+
label="Broken"
89+
severity="danger"
90+
(onClick)="evaluateReleaseCandidate(false)"
91+
[ngClass]="{
92+
'opacity-50': hasUserEvaluatedTo(false),
93+
}"
94+
>
8195
<i-tabler name="x" class="!size-5" />
8296
</p-button>
8397
</p-buttongroup>
@@ -86,7 +100,7 @@ <h2 class="text-2xl flex items-center gap-2 flex-wrap">
86100
<div class="text-muted-color text-sm uppercase font-semibold tracking-wider mb-1">Reviews</div>
87101
<div>
88102
@for (evaluation of releaseCandidate.evaluations; track evaluation.user.id) {
89-
<div class="relative inline-block pt-2 pr-1">
103+
<div class="relative inline-block pt-2 pr-1" [pTooltip]="getEvaluationTooltip(evaluation)" tooltipPosition="top">
90104
<p-avatar [image]="evaluation.user.avatarUrl" class="size-7" shape="circle" />
91105
<div class="absolute top-0 right-0">
92106
@if (evaluation.isWorking) {
@@ -95,6 +109,11 @@ <h2 class="text-2xl flex items-center gap-2 flex-wrap">
95109
<i-tabler name="x" class="!size-3 text-white bg-red-500 rounded-full" />
96110
}
97111
</div>
112+
@if (evaluation.comment && evaluation.comment.trim() !== '') {
113+
<div class="absolute bottom-0 -left-1">
114+
<i-tabler name="message-circle" class="!size-3 text-white bg-blue-500 rounded-full p-0.5" />
115+
</div>
116+
}
98117
</div>
99118
}
100119
</div>

client/src/app/pages/release-candidate-details/release-candidate-details.component.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,32 @@ import { ConfirmationService, MessageService } from 'primeng/api';
2222
import { KeycloakService } from '@app/core/services/keycloak/keycloak.service';
2323
import { PermissionService } from '@app/core/services/permission.service';
2424
import { ActivatedRoute, Router } from '@angular/router';
25-
import { ReleaseInfoDetailsDto } from '@app/core/modules/openapi';
25+
import { ReleaseEvaluationDto, ReleaseInfoDetailsDto } from '@app/core/modules/openapi';
2626
import { MarkdownPipe } from '@app/core/modules/markdown/markdown.pipe';
2727
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
2828
import { TextareaModule } from 'primeng/textarea';
29-
import { SlicePipe } from '@angular/common';
29+
import { InputTextModule } from 'primeng/inputtext';
30+
import { NgClass, SlicePipe } from '@angular/common';
3031
import { provideTablerIcons, TablerIconComponent } from 'angular-tabler-icons';
31-
import { IconBrandGithub, IconCheck, IconUpload, IconExternalLink, IconGitCommit, IconPencil, IconPlus, IconTrash, IconUser, IconX } from 'angular-tabler-icons/icons';
32+
import {
33+
IconBrandGithub,
34+
IconCheck,
35+
IconUpload,
36+
IconExternalLink,
37+
IconGitCommit,
38+
IconMessageCircle,
39+
IconPencil,
40+
IconPlus,
41+
IconTrash,
42+
IconUser,
43+
IconX,
44+
} from 'angular-tabler-icons/icons';
45+
import { DialogService } from 'primeng/dynamicdialog';
46+
import {
47+
ReleaseEvaluationDialogComponent,
48+
ReleaseEvaluationDialogData,
49+
ReleaseEvaluationDialogResult,
50+
} from '@app/components/dialogs/release-evaluation-dialog/release-evaluation-dialog.component';
3251
import { PublishDraftReleaseConfirmationComponent } from '@app/components/dialogs/publish-draft-release-confirmation/publish-draft-release-confirmation.component';
3352

3453
@Component({
@@ -48,6 +67,8 @@ import { PublishDraftReleaseConfirmationComponent } from '@app/components/dialog
4867
ReactiveFormsModule,
4968
TextareaModule,
5069
PublishDraftReleaseConfirmationComponent,
70+
InputTextModule,
71+
NgClass,
5172
],
5273
providers: [
5374
provideTablerIcons({
@@ -61,7 +82,9 @@ import { PublishDraftReleaseConfirmationComponent } from '@app/components/dialog
6182
IconPlus,
6283
IconPencil,
6384
IconBrandGithub,
85+
IconMessageCircle,
6486
}),
87+
DialogService,
6588
],
6689
templateUrl: './release-candidate-details.component.html',
6790
})
@@ -73,6 +96,7 @@ export class ReleaseCandidateDetailsComponent implements OnInit {
7396
private queryClient = inject(QueryClient);
7497
private router = inject(Router);
7598
private route = inject(ActivatedRoute);
99+
private dialogService = inject(DialogService);
76100

77101
name = input.required<string>();
78102
releaseCandidateQuery = injectQuery(() => ({
@@ -195,7 +219,40 @@ export class ReleaseCandidateDetailsComponent implements OnInit {
195219
}
196220

197221
evaluateReleaseCandidate = (isWorking: boolean) => {
198-
this.evaluateReleaseCandidateMutation.mutate({ body: { name: this.name(), isWorking } });
222+
const release = this.releaseCandidateQuery.data();
223+
if (!release) {
224+
return;
225+
}
226+
227+
// Find the current user’s previous evaluation (if any)
228+
const me = this.keycloakService.getPreferredUsername()?.toLowerCase();
229+
const userEvaluation = release.evaluations.find(e => e.user.login.toLowerCase() === me);
230+
231+
// Reuse the comment only when the state is the same
232+
const comment = userEvaluation && userEvaluation.isWorking === isWorking ? (userEvaluation.comment ?? '') : '';
233+
234+
const dialogData: ReleaseEvaluationDialogData = {
235+
releaseName: this.name(),
236+
isWorking: isWorking,
237+
comment: comment,
238+
};
239+
240+
const ref = this.dialogService.open(ReleaseEvaluationDialogComponent, {
241+
width: '500px',
242+
data: dialogData,
243+
});
244+
245+
ref.onClose.subscribe((result: ReleaseEvaluationDialogResult) => {
246+
if (result) {
247+
this.evaluateReleaseCandidateMutation.mutate({
248+
body: {
249+
name: this.name(),
250+
isWorking: result.isWorking,
251+
comment: result.comment,
252+
},
253+
});
254+
}
255+
});
199256
};
200257

201258
deleteReleaseCandidate = (rc: ReleaseInfoDetailsDto) => {
@@ -297,4 +354,14 @@ export class ReleaseCandidateDetailsComponent implements OnInit {
297354
cancelEditingName() {
298355
this.isEditingName.set(false);
299356
}
357+
358+
getEvaluationTooltip(evaluation: ReleaseEvaluationDto): string {
359+
const status = evaluation.isWorking ? 'Marked as working' : 'Marked as broken';
360+
361+
if (!evaluation.comment || evaluation.comment.trim() === '') {
362+
return status;
363+
}
364+
365+
return `${status}\n\nComment: ${evaluation.comment}`;
366+
}
300367
}

0 commit comments

Comments
 (0)