Ajout commentaire + icone menu

This commit is contained in:
2026-05-23 10:23:46 +02:00
parent 5410ad779e
commit 1b165aaae8
6 changed files with 246 additions and 1 deletions
@@ -0,0 +1,92 @@
.section-header {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: #6b7280;
background-color: #f9fafb;
}
.comment-item {
padding-bottom: 1rem;
border-bottom: 1px solid #f3f4f6;
}
.comment-item:last-of-type {
border-bottom: none;
padding-bottom: 0;
}
.comment-meta {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.comment-date {
font-size: 0.78rem;
font-weight: 600;
color: #374151;
}
.comment-edited {
font-size: 0.75rem;
color: #9ca3af;
font-style: italic;
}
.comment-actions {
margin-left: auto;
display: flex;
gap: 0.5rem;
}
.comment-action-btn {
border: none;
background: none;
font-size: 0.75rem;
color: #6b7280;
cursor: pointer;
padding: 0;
}
.comment-action-btn:hover {
color: #111827;
text-decoration: underline;
}
.comment-action-delete:hover {
color: #b91c1c;
}
.comment-text {
font-size: 0.875rem;
line-height: 1.6;
color: #374151;
}
.comment-text :is(h1, h2, h3, h4) {
margin-top: 0.5rem;
margin-bottom: 0.25rem;
font-weight: 700;
}
.comment-text p {
margin-bottom: 0.4rem;
}
.comment-text p:last-child {
margin-bottom: 0;
}
.comment-text code {
background: #f3f4f6;
border-radius: 0.25rem;
padding: 0.1em 0.35em;
font-size: 0.85em;
}
.comment-new {
padding-top: 0.5rem;
}
@@ -0,0 +1,51 @@
<!-- suppress HtmlUnknownAttribute -->
<div class="card shadow-sm mb-3">
<div class="card-header section-header">Commentaires</div>
<div class="card-body d-flex flex-column gap-3">
@for (comment of comments(); track comment.id) {
<div class="comment-item">
<div class="comment-meta">
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
@if (comment.updatedAt) {
<span class="comment-edited">(modifié le {{ formatDate(comment.updatedAt) }})</span>
}
<div class="comment-actions">
<button type="button" class="comment-action-btn" (click)="startEditComment(comment)">Modifier</button>
<button type="button" class="comment-action-btn comment-action-delete" (click)="deleteComment(comment.id)">Supprimer</button>
</div>
</div>
@if (editingCommentId === comment.id) {
<textarea
aria-label="Modifier le commentaire"
class="form-control form-control-sm mt-2"
rows="3"
[(ngModel)]="editingCommentText"
(keydown.escape)="cancelEditComment()"
></textarea>
<div class="d-flex gap-2 mt-2">
<button type="button" class="btn btn-sm btn-primary" (click)="saveEditComment()" [disabled]="!editingCommentText.trim()">Enregistrer</button>
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="cancelEditComment()">Annuler</button>
</div>
} @else {
<div class="comment-text markdown-body mt-1" [innerHTML]="parseMarkdown(comment.text)"></div>
}
</div>
}
<div class="comment-new">
<textarea
aria-label="Nouveau commentaire"
class="form-control form-control-sm"
rows="3"
placeholder="Ajouter un commentaire... (Markdown supporté)"
[(ngModel)]="newCommentText"
></textarea>
<div class="d-flex justify-content-end mt-2">
<button type="button" class="btn btn-sm btn-primary" (click)="addComment()" [disabled]="!newCommentText.trim()">Ajouter</button>
</div>
</div>
</div>
</div>
@@ -0,0 +1,81 @@
import { Component, computed, inject, input } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { marked } from 'marked';
import { IssueComment, IssuesStore } from '../issues.store';
@Component({
selector: 'app-issue-comments',
imports: [FormsModule],
templateUrl: './issue-comments.html',
styleUrl: './issue-comments.css',
})
export class IssueComments {
private readonly issuesStore = inject(IssuesStore);
private readonly sanitizer = inject(DomSanitizer);
readonly issueId = input.required<number>();
protected readonly comments = computed(
() => this.issuesStore.issues().find((i) => i.id === this.issueId())?.comments ?? [],
);
protected newCommentText = '';
protected editingCommentId: number | null = null;
protected editingCommentText = '';
protected parseMarkdown(text: string): SafeHtml {
const html = marked.parse(text) as string;
return this.sanitizer.bypassSecurityTrustHtml(html);
}
protected formatDate(iso: string): string {
return new Date(iso).toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
protected addComment(): void {
const text = this.newCommentText.trim();
if (!text) return;
const issue = this.issuesStore.getById(this.issueId());
if (!issue) return;
const nextId = Math.max(0, ...issue.comments.map((c) => c.id)) + 1;
const comment: IssueComment = { id: nextId, text, createdAt: new Date().toISOString(), updatedAt: null };
this.issuesStore.upsert({ ...issue, comments: [...issue.comments, comment] });
this.newCommentText = '';
}
protected startEditComment(comment: IssueComment): void {
this.editingCommentId = comment.id;
this.editingCommentText = comment.text;
}
protected saveEditComment(): void {
const text = this.editingCommentText.trim();
if (!text || this.editingCommentId === null) return;
const issue = this.issuesStore.getById(this.issueId());
if (!issue) return;
const updatedComments = issue.comments.map((c) =>
c.id === this.editingCommentId ? { ...c, text, updatedAt: new Date().toISOString() } : c,
);
this.issuesStore.upsert({ ...issue, comments: updatedComments });
this.editingCommentId = null;
this.editingCommentText = '';
}
protected cancelEditComment(): void {
this.editingCommentId = null;
this.editingCommentText = '';
}
protected deleteComment(id: number): void {
const issue = this.issuesStore.getById(this.issueId());
if (!issue) return;
this.issuesStore.upsert({ ...issue, comments: issue.comments.filter((c) => c.id !== id) });
}
}
@@ -247,6 +247,11 @@
</div>
}
<!-- Commentaires -->
@if (!isNewIssueRoute) {
<app-issue-comments [issueId]="issue.id" />
}
@if (isNewIssueRoute) {
<div class="d-flex justify-content-end gap-2 mt-4">
<button type="button" class="btn btn-outline-secondary" (click)="cancelCreation()">Annuler</button>
+5 -1
View File
@@ -5,10 +5,11 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { marked } from 'marked';
import { IssueEntity, IssuesStore } from '../issues.store';
import { IssueComments } from '../issue-comments/issue-comments';
@Component({
selector: 'app-issue-detail',
imports: [FormsModule],
imports: [FormsModule, IssueComments],
templateUrl: './issue-detail.html',
styleUrl: './issue-detail.css',
})
@@ -158,6 +159,7 @@ export class IssueDetail {
description: '',
estimatedTime: null,
dependsOnIds: [],
comments: [],
priority: 'Moyenne',
status: 'draft',
progress: 0,
@@ -277,6 +279,7 @@ export class IssueDetail {
description: '',
estimatedTime: null,
dependsOnIds: [],
comments: [],
priority: 'Moyenne',
status: 'draft',
progress: 0,
@@ -296,6 +299,7 @@ export class IssueDetail {
description: '',
estimatedTime: null,
dependsOnIds: [],
comments: [],
priority: 'Moyenne',
status: 'draft',
progress: 0,
+12
View File
@@ -6,6 +6,13 @@ export type IssueStatus = 'draft' | 'todo' | 'done' | 'in-progress';
export type IssuePriority = 'Basse' | 'Moyenne' | 'Haute';
export type IssueType = 'Epic' | 'Bug' | 'Study' | 'Story' | 'Task' | 'Technical Story';
export type IssueComment = {
id: number;
text: string;
createdAt: string;
updatedAt: string | null;
};
export type IssueEntity = {
id: number;
type: IssueType;
@@ -16,6 +23,7 @@ export type IssueEntity = {
description: string;
estimatedTime: number | null;
dependsOnIds: number[];
comments: IssueComment[];
priority: IssuePriority;
status: IssueStatus;
progress: number;
@@ -32,6 +40,7 @@ const DEFAULT_ISSUES: IssueEntity[] = [
description: 'Corriger le comportement du menu sur petits ecrans.',
estimatedTime: 8,
dependsOnIds: [],
comments: [],
priority: 'Haute',
status: 'in-progress',
progress: 35,
@@ -46,6 +55,7 @@ const DEFAULT_ISSUES: IssueEntity[] = [
description: 'Fiabiliser les regles de validation du formulaire projet.',
estimatedTime: 16,
dependsOnIds: [],
comments: [],
priority: 'Moyenne',
status: 'todo',
progress: 20,
@@ -60,6 +70,7 @@ const DEFAULT_ISSUES: IssueEntity[] = [
description: 'Mettre a jour le wording d accueil selon la charte produit.',
estimatedTime: 4,
dependsOnIds: [],
comments: [],
priority: 'Basse',
status: 'done',
progress: 100,
@@ -135,6 +146,7 @@ export class IssuesStore {
type: issue.type ?? 'Story',
estimatedTime: issue.estimatedTime ?? null,
dependsOnIds: normalizedDependencies,
comments: Array.isArray(issue.comments) ? issue.comments : [],
} as IssueEntity;
}