Skip to content

feat: Adding comments to releases #763

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

Merged
merged 12 commits into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<div class="flex items-center gap-3 mb-4 pb-3">
<i-tabler name="clipboard-check" class="!size-6 text-primary" />
<h2 class="text-xl font-semibold">Evaluate Release Candidate</h2>
</div>
<div class="p-6">
<!-- Header with Icon -->

<div class="flex flex-col gap-4">
<p class="text-muted-color">
You are marking release candidate "{{ data.releaseName }}" as
<span [class]="data.isWorking ? 'text-green-600 font-semibold' : 'text-red-600 font-semibold'">
<i [class]="data.isWorking ? 'pi pi-check mr-1' : 'pi pi-times mr-1'"></i>
{{ data.isWorking ? 'Working' : 'Broken' }}
</span>
</p>

<form [formGroup]="evaluationForm" class="space-y-4">
<div>
<label for="comment" class="block text-sm font-medium mb-2"> Comment <span class="text-muted-color">(optional, max 500 characters)</span> </label>
<textarea
id="comment"
pTextarea
formControlName="comment"
placeholder="Add your comments about this release candidate..."
rows="4"
class="w-full"
maxlength="500"
></textarea>
<div class="text-right text-sm text-muted-color mt-1">{{ evaluationForm.get('comment')?.value?.length || 0 }}/500</div>
</div>
</form>

<div class="flex justify-between w-full mt-4">
<p-button label="Cancel" severity="secondary" [text]="true" (onClick)="cancel()" />
<p-button label="Submit Evaluation" severity="primary" (onClick)="submit()" />
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Component, inject } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { ButtonModule } from 'primeng/button';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { TextareaModule } from 'primeng/textarea';
import { provideTablerIcons, TablerIconComponent } from 'angular-tabler-icons';
import { IconClipboardCheck, IconX } from 'angular-tabler-icons/icons';

export interface ReleaseEvaluationDialogData {
releaseName: string;
isWorking: boolean;
comment?: string;
}

export interface ReleaseEvaluationDialogResult {
isWorking: boolean;
comment: string;
}

@Component({
selector: 'app-release-evaluation-dialog',
imports: [ReactiveFormsModule, TextareaModule, ButtonModule, TablerIconComponent],
providers: [
provideTablerIcons({
IconClipboardCheck,
IconX,
}),
],
templateUrl: './release-evaluation-dialog.component.html',
})
export class ReleaseEvaluationDialogComponent {
private ref = inject(DynamicDialogRef);
private config = inject(DynamicDialogConfig);

data: ReleaseEvaluationDialogData = this.config.data;

evaluationForm = new FormGroup({
comment: new FormControl(this.data.comment ?? '', [Validators.maxLength(500)]),
});

submit() {
if (this.evaluationForm.valid) {
const comment = this.evaluationForm.get('comment')?.value || '';

const result: ReleaseEvaluationDialogResult = {
isWorking: this.data.isWorking,
comment: comment,
};

this.ref.close(result);
}
}

cancel() {
this.ref.close();
}
}
8 changes: 8 additions & 0 deletions client/src/app/core/modules/openapi/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,11 @@ export const ReleaseEvaluationDtoSchema = {
isWorking: {
type: 'boolean',
},
comment: {
type: 'string',
maxLength: 500,
minLength: 0,
},
},
} as const;

Expand Down Expand Up @@ -631,6 +636,9 @@ export const ReleaseCandidateEvaluationDtoSchema = {
isWorking: {
type: 'boolean',
},
comment: {
type: 'string',
},
},
required: ['user'],
} as const;
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/core/modules/openapi/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export type ReleaseNameDto = {
export type ReleaseEvaluationDto = {
name?: string;
isWorking?: boolean;
comment?: string;
};

export type BranchInfoDto = {
Expand Down Expand Up @@ -205,6 +206,7 @@ export type ReleaseCandidateDeploymentDto = {
export type ReleaseCandidateEvaluationDto = {
user: UserInfoDto;
isWorking?: boolean;
comment?: string;
};

export type ReleaseDto = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,24 @@ <h2 class="text-2xl flex items-center gap-2 flex-wrap">
}

<p-buttongroup>
<p-button label="Working" severity="success" [disabled]="hasUserEvaluatedTo(true)" (onClick)="evaluateReleaseCandidate(true)">
<p-button
label="Working"
severity="success"
(onClick)="evaluateReleaseCandidate(true)"
[ngClass]="{
'opacity-50': hasUserEvaluatedTo(true),
}"
>
<i-tabler name="check" class="!size-5" />
</p-button>
<p-button label="Broken" severity="danger" [disabled]="hasUserEvaluatedTo(false)" (onClick)="evaluateReleaseCandidate(false)">
<p-button
label="Broken"
severity="danger"
(onClick)="evaluateReleaseCandidate(false)"
[ngClass]="{
'opacity-50': hasUserEvaluatedTo(false),
}"
>
<i-tabler name="x" class="!size-5" />
</p-button>
</p-buttongroup>
Expand All @@ -86,7 +100,7 @@ <h2 class="text-2xl flex items-center gap-2 flex-wrap">
<div class="text-muted-color text-sm uppercase font-semibold tracking-wider mb-1">Reviews</div>
<div>
@for (evaluation of releaseCandidate.evaluations; track evaluation.user.id) {
<div class="relative inline-block pt-2 pr-1">
<div class="relative inline-block pt-2 pr-1" [pTooltip]="getEvaluationTooltip(evaluation)" tooltipPosition="top">
<p-avatar [image]="evaluation.user.avatarUrl" class="size-7" shape="circle" />
<div class="absolute top-0 right-0">
@if (evaluation.isWorking) {
Expand All @@ -95,6 +109,11 @@ <h2 class="text-2xl flex items-center gap-2 flex-wrap">
<i-tabler name="x" class="!size-3 text-white bg-red-500 rounded-full" />
}
</div>
@if (evaluation.comment && evaluation.comment.trim() !== '') {
<div class="absolute bottom-0 -left-1">
<i-tabler name="message-circle" class="!size-3 text-white bg-blue-500 rounded-full p-0.5" />
</div>
}
</div>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,32 @@ import { ConfirmationService, MessageService } from 'primeng/api';
import { KeycloakService } from '@app/core/services/keycloak/keycloak.service';
import { PermissionService } from '@app/core/services/permission.service';
import { ActivatedRoute, Router } from '@angular/router';
import { ReleaseInfoDetailsDto } from '@app/core/modules/openapi';
import { ReleaseEvaluationDto, ReleaseInfoDetailsDto } from '@app/core/modules/openapi';
import { MarkdownPipe } from '@app/core/modules/markdown/markdown.pipe';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { TextareaModule } from 'primeng/textarea';
import { SlicePipe } from '@angular/common';
import { InputTextModule } from 'primeng/inputtext';
import { NgClass, SlicePipe } from '@angular/common';
import { provideTablerIcons, TablerIconComponent } from 'angular-tabler-icons';
import { IconBrandGithub, IconCheck, IconUpload, IconExternalLink, IconGitCommit, IconPencil, IconPlus, IconTrash, IconUser, IconX } from 'angular-tabler-icons/icons';
import {
IconBrandGithub,
IconCheck,
IconUpload,
IconExternalLink,
IconGitCommit,
IconMessageCircle,
IconPencil,
IconPlus,
IconTrash,
IconUser,
IconX,
} from 'angular-tabler-icons/icons';
import { DialogService } from 'primeng/dynamicdialog';
import {
ReleaseEvaluationDialogComponent,
ReleaseEvaluationDialogData,
ReleaseEvaluationDialogResult,
} from '@app/components/dialogs/release-evaluation-dialog/release-evaluation-dialog.component';
import { PublishDraftReleaseConfirmationComponent } from '@app/components/dialogs/publish-draft-release-confirmation/publish-draft-release-confirmation.component';

@Component({
Expand All @@ -48,6 +67,8 @@ import { PublishDraftReleaseConfirmationComponent } from '@app/components/dialog
ReactiveFormsModule,
TextareaModule,
PublishDraftReleaseConfirmationComponent,
InputTextModule,
NgClass,
],
providers: [
provideTablerIcons({
Expand All @@ -61,7 +82,9 @@ import { PublishDraftReleaseConfirmationComponent } from '@app/components/dialog
IconPlus,
IconPencil,
IconBrandGithub,
IconMessageCircle,
}),
DialogService,
],
templateUrl: './release-candidate-details.component.html',
})
Expand All @@ -73,6 +96,7 @@ export class ReleaseCandidateDetailsComponent implements OnInit {
private queryClient = inject(QueryClient);
private router = inject(Router);
private route = inject(ActivatedRoute);
private dialogService = inject(DialogService);

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

evaluateReleaseCandidate = (isWorking: boolean) => {
this.evaluateReleaseCandidateMutation.mutate({ body: { name: this.name(), isWorking } });
const release = this.releaseCandidateQuery.data();
if (!release) {
return;
}

// Find the current user’s previous evaluation (if any)
const me = this.keycloakService.getPreferredUsername()?.toLowerCase();
const userEvaluation = release.evaluations.find(e => e.user.login.toLowerCase() === me);

// Reuse the comment only when the state is the same
const comment = userEvaluation && userEvaluation.isWorking === isWorking ? (userEvaluation.comment ?? '') : '';

const dialogData: ReleaseEvaluationDialogData = {
releaseName: this.name(),
isWorking: isWorking,
comment: comment,
};

const ref = this.dialogService.open(ReleaseEvaluationDialogComponent, {
width: '500px',
data: dialogData,
});

ref.onClose.subscribe((result: ReleaseEvaluationDialogResult) => {
if (result) {
this.evaluateReleaseCandidateMutation.mutate({
body: {
name: this.name(),
isWorking: result.isWorking,
comment: result.comment,
},
});
}
});
};

deleteReleaseCandidate = (rc: ReleaseInfoDetailsDto) => {
Expand Down Expand Up @@ -297,4 +354,14 @@ export class ReleaseCandidateDetailsComponent implements OnInit {
cancelEditingName() {
this.isEditingName.set(false);
}

getEvaluationTooltip(evaluation: ReleaseEvaluationDto): string {
const status = evaluation.isWorking ? 'Marked as working' : 'Marked as broken';

if (!evaluation.comment || evaluation.comment.trim() === '') {
return status;
}

return `${status}\n\nComment: ${evaluation.comment}`;
}
}
6 changes: 6 additions & 0 deletions server/application-server/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2182,6 +2182,10 @@ components:
type: string
isWorking:
type: boolean
comment:
type: string
maxLength: 500
minLength: 0
BranchInfoDto:
type: object
properties:
Expand Down Expand Up @@ -2252,6 +2256,8 @@ components:
$ref: "#/components/schemas/UserInfoDto"
isWorking:
type: boolean
comment:
type: string
required:
- user
ReleaseDto:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
package de.tum.cit.aet.helios.releaseinfo;

public record ReleaseEvaluationDto(String name, boolean isWorking) {}
import jakarta.validation.constraints.Size;

public record ReleaseEvaluationDto(
String name,
boolean isWorking,
@Size(max = 500, message = "Comment cannot exceed 500 characters") String comment) {}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public ResponseEntity<ReleaseInfoListDto> createReleaseCandidate(

@PostMapping("/evaluate")
public ResponseEntity<Void> evaluate(@RequestBody ReleaseEvaluationDto evaluationDto) {
releaseInfoService.evaluateReleaseCandidate(evaluationDto.name(), evaluationDto.isWorking());
releaseInfoService.evaluateReleaseCandidate(
evaluationDto.name(), evaluationDto.isWorking(), evaluationDto.comment());
return ResponseEntity.ok().build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ public record ReleaseInfoDetailsDto(
@NonNull OffsetDateTime createdAt,
String body) {

public record ReleaseCandidateEvaluationDto(@NonNull UserInfoDto user, boolean isWorking) {
public record ReleaseCandidateEvaluationDto(
@NonNull UserInfoDto user, boolean isWorking, String comment) {
public static ReleaseCandidateEvaluationDto fromEvaluation(
@NonNull ReleaseCandidateEvaluation evaluation) {
return new ReleaseCandidateEvaluationDto(
UserInfoDto.fromUser(evaluation.getEvaluatedBy()), evaluation.isWorking());
UserInfoDto.fromUser(evaluation.getEvaluatedBy()),
evaluation.isWorking(),
evaluation.getComment());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ public ReleaseInfoListDto createReleaseCandidate(ReleaseCandidateCreateDto relea
releaseCandidateRepository.save(newReleaseCandidate));
}

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

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

evaluation.setWorking(isWorking);
evaluation.setComment(comment);
releaseCandidateEvaluationRepository.save(evaluation);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
Expand Down Expand Up @@ -38,4 +39,8 @@ public class ReleaseCandidateEvaluation {
@JoinColumn(name = "evaluated_by_id", nullable = false)
@ToString.Exclude
private User evaluatedBy;

@Column(length = 500)
@Size(max = 500, message = "Comment cannot exceed 500 characters")
private String comment;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Add comment column to release_candidate_evaluation table
ALTER TABLE release_candidate_evaluation
ADD COLUMN comment VARCHAR(500);
Loading