Skip to content

Commit f2ff655

Browse files
authored
feat: Adding comments to releases (#763)
1 parent bdfba9d commit f2ff655

File tree

13 files changed

+227
-12
lines changed

13 files changed

+227
-12
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/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/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
}

server/application-server/openapi.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2182,6 +2182,10 @@ components:
21822182
type: string
21832183
isWorking:
21842184
type: boolean
2185+
comment:
2186+
type: string
2187+
maxLength: 500
2188+
minLength: 0
21852189
BranchInfoDto:
21862190
type: object
21872191
properties:
@@ -2252,6 +2256,8 @@ components:
22522256
$ref: "#/components/schemas/UserInfoDto"
22532257
isWorking:
22542258
type: boolean
2259+
comment:
2260+
type: string
22552261
required:
22562262
- user
22572263
ReleaseDto:
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
package de.tum.cit.aet.helios.releaseinfo;
22

3-
public record ReleaseEvaluationDto(String name, boolean isWorking) {}
3+
import jakarta.validation.constraints.Size;
4+
5+
public record ReleaseEvaluationDto(
6+
String name,
7+
boolean isWorking,
8+
@Size(max = 500, message = "Comment cannot exceed 500 characters") String comment) {}

server/application-server/src/main/java/de/tum/cit/aet/helios/releaseinfo/ReleaseInfoController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ public ResponseEntity<ReleaseInfoListDto> createReleaseCandidate(
4949

5050
@PostMapping("/evaluate")
5151
public ResponseEntity<Void> evaluate(@RequestBody ReleaseEvaluationDto evaluationDto) {
52-
releaseInfoService.evaluateReleaseCandidate(evaluationDto.name(), evaluationDto.isWorking());
52+
releaseInfoService.evaluateReleaseCandidate(
53+
evaluationDto.name(), evaluationDto.isWorking(), evaluationDto.comment());
5354
return ResponseEntity.ok().build();
5455
}
5556

server/application-server/src/main/java/de/tum/cit/aet/helios/releaseinfo/ReleaseInfoDetailsDto.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ public record ReleaseInfoDetailsDto(
2222
@NonNull OffsetDateTime createdAt,
2323
String body) {
2424

25-
public record ReleaseCandidateEvaluationDto(@NonNull UserInfoDto user, boolean isWorking) {
25+
public record ReleaseCandidateEvaluationDto(
26+
@NonNull UserInfoDto user, boolean isWorking, String comment) {
2627
public static ReleaseCandidateEvaluationDto fromEvaluation(
2728
@NonNull ReleaseCandidateEvaluation evaluation) {
2829
return new ReleaseCandidateEvaluationDto(
29-
UserInfoDto.fromUser(evaluation.getEvaluatedBy()), evaluation.isWorking());
30+
UserInfoDto.fromUser(evaluation.getEvaluatedBy()),
31+
evaluation.isWorking(),
32+
evaluation.getComment());
3033
}
3134
}
3235

server/application-server/src/main/java/de/tum/cit/aet/helios/releaseinfo/ReleaseInfoService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ public ReleaseInfoListDto createReleaseCandidate(ReleaseCandidateCreateDto relea
239239
releaseCandidateRepository.save(newReleaseCandidate));
240240
}
241241

242-
public void evaluateReleaseCandidate(String name, boolean isWorking) {
242+
public void evaluateReleaseCandidate(String name, boolean isWorking, String comment) {
243243
final Long repositoryId = RepositoryContext.getRepositoryId();
244244

245245
final ReleaseCandidate releaseCandidate =
@@ -265,6 +265,7 @@ public void evaluateReleaseCandidate(String name, boolean isWorking) {
265265
});
266266

267267
evaluation.setWorking(isWorking);
268+
evaluation.setComment(comment);
268269
releaseCandidateEvaluationRepository.save(evaluation);
269270
}
270271

server/application-server/src/main/java/de/tum/cit/aet/helios/releaseinfo/releasecandidate/ReleaseCandidateEvaluation.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import jakarta.persistence.ManyToOne;
1212
import jakarta.persistence.Table;
1313
import jakarta.persistence.UniqueConstraint;
14+
import jakarta.validation.constraints.Size;
1415
import lombok.Getter;
1516
import lombok.Setter;
1617
import lombok.ToString;
@@ -38,4 +39,8 @@ public class ReleaseCandidateEvaluation {
3839
@JoinColumn(name = "evaluated_by_id", nullable = false)
3940
@ToString.Exclude
4041
private User evaluatedBy;
42+
43+
@Column(length = 500)
44+
@Size(max = 500, message = "Comment cannot exceed 500 characters")
45+
private String comment;
4146
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- Add comment column to release_candidate_evaluation table
2+
ALTER TABLE release_candidate_evaluation
3+
ADD COLUMN comment VARCHAR(500);

0 commit comments

Comments
 (0)