Ajout issue dans les commentaires

Signed-off-by: Gato <cedric@goutailler-olivier.fr>
This commit is contained in:
2026-05-28 18:50:36 +02:00
parent 081b48841a
commit cd93533b7c
5 changed files with 674 additions and 9 deletions
+139 -3
View File
@@ -1,13 +1,14 @@
import { Component, computed, inject, input } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { marked } from 'marked';
import { handleImagePaste, insertAtSelection } from '../paste-image.util';
import { IssueComment, IssuesStore } from '../issues.store';
import { IssueComment, IssueEntity, IssuesStore } from '../issues.store';
@Component({
selector: 'app-issue-comments',
imports: [FormsModule],
imports: [FormsModule, RouterLink],
templateUrl: './issue-comments.html',
styleUrl: './issue-comments.css',
})
@@ -21,10 +22,20 @@ export class IssueComments {
() => this.issuesStore.issues().find((i) => i.id === this.issueId())?.comments ?? [],
);
protected readonly allOtherIssues = computed(() =>
this.issuesStore.issues().filter((i) => i.id !== this.issueId()),
);
protected newCommentText = '';
protected editingCommentId: number | null = null;
protected editingCommentText = '';
protected creatingTaskForCommentId: number | null = null;
protected newTaskName = '';
protected linkingIssueForCommentId: number | null = null;
protected issueSearchText = '';
protected parseMarkdown(text: string): SafeHtml {
const html = marked.parse(text) as string;
return this.sanitizer.bypassSecurityTrustHtml(html);
@@ -46,7 +57,13 @@ export class IssueComments {
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 };
const comment: IssueComment = {
id: nextId,
text,
createdAt: new Date().toISOString(),
updatedAt: null,
linkedIssueIds: [],
};
await this.issuesStore.upsert({ ...issue, comments: [...issue.comments, comment] });
this.newCommentText = '';
}
@@ -54,6 +71,8 @@ export class IssueComments {
protected startEditComment(comment: IssueComment): void {
this.editingCommentId = comment.id;
this.editingCommentText = comment.text;
this.creatingTaskForCommentId = null;
this.linkingIssueForCommentId = null;
}
protected async saveEditComment(): Promise<void> {
@@ -92,4 +111,121 @@ export class IssueComments {
if (!issue) return;
await this.issuesStore.upsert({ ...issue, comments: issue.comments.filter((c) => c.id !== id) });
}
protected getLinkedIssues(comment: IssueComment): IssueEntity[] {
if (!comment.linkedIssueIds.length) return [];
return comment.linkedIssueIds
.map((id) => this.issuesStore.getById(id))
.filter((i): i is IssueEntity => i !== undefined);
}
protected filteredIssuesForLink(commentId: number): IssueEntity[] {
const comment = this.comments().find((c) => c.id === commentId);
const linkedIds = new Set(comment?.linkedIssueIds ?? []);
const search = this.issueSearchText.toLowerCase();
return this.allOtherIssues().filter(
(i) => !linkedIds.has(i.id) && (!search || i.name.toLowerCase().includes(search)),
);
}
protected typeIcon(type: IssueEntity['type']): { letter: string; bg: string } {
const map: Record<IssueEntity['type'], { letter: string; bg: string }> = {
Epic: { letter: 'E', bg: '#7c3aed' },
Bug: { letter: 'B', bg: '#dc2626' },
Story: { letter: 'S', bg: '#16a34a' },
Task: { letter: 'T', bg: '#2563eb' },
Study: { letter: 'St', bg: '#6b7280' },
'Technical Story': { letter: 'TS', bg: '#d97706' },
};
return map[type] ?? { letter: '?', bg: '#6b7280' };
}
protected statusLabel(status: IssueEntity['status']): { label: string; bg: string; color: string } {
const map: Record<IssueEntity['status'], { label: string; bg: string; color: string }> = {
draft: { label: 'Brouillon', bg: '#e2e8f0', color: '#475569' },
todo: { label: 'À faire', bg: '#dbeafe', color: '#1d4ed8' },
'in-progress': { label: 'En cours', bg: '#ffedd5', color: '#9a3412' },
done: { label: 'Terminé', bg: '#dcfce7', color: '#166534' },
};
return map[status] ?? { label: status, bg: '#e2e8f0', color: '#475569' };
}
protected startCreateTask(commentId: number): void {
this.creatingTaskForCommentId = commentId;
this.newTaskName = '';
this.linkingIssueForCommentId = null;
this.issueSearchText = '';
}
protected cancelCreateTask(): void {
this.creatingTaskForCommentId = null;
this.newTaskName = '';
}
protected async createTaskForComment(commentId: number): Promise<void> {
const name = this.newTaskName.trim();
if (!name) return;
const issue = this.issuesStore.getById(this.issueId());
if (!issue) return;
const newTask: IssueEntity = {
id: 0,
type: 'Task',
name,
assignee: '',
epic: '',
dueDate: '',
description: '',
estimatedTime: null,
dependsOnIds: [],
comments: [],
priority: 'MOYENNE',
status: 'draft',
progress: 0,
};
const created = await this.issuesStore.upsert(newTask);
const updatedComments = issue.comments.map((c) => {
if (c.id !== commentId) return c;
return { ...c, linkedIssueIds: [...c.linkedIssueIds, created.id] };
});
await this.issuesStore.upsert({ ...issue, comments: updatedComments });
this.creatingTaskForCommentId = null;
this.newTaskName = '';
}
protected startLinkIssue(commentId: number): void {
this.linkingIssueForCommentId = commentId;
this.issueSearchText = '';
this.creatingTaskForCommentId = null;
this.newTaskName = '';
}
protected cancelLinkIssue(): void {
this.linkingIssueForCommentId = null;
this.issueSearchText = '';
}
protected async linkIssueToComment(commentId: number, issueId: number): Promise<void> {
const issue = this.issuesStore.getById(this.issueId());
if (!issue) return;
const updatedComments = issue.comments.map((c) => {
if (c.id !== commentId) return c;
if (c.linkedIssueIds.includes(issueId)) return c;
return { ...c, linkedIssueIds: [...c.linkedIssueIds, issueId] };
});
await this.issuesStore.upsert({ ...issue, comments: updatedComments });
this.linkingIssueForCommentId = null;
this.issueSearchText = '';
}
protected async unlinkIssueFromComment(commentId: number, issueId: number): Promise<void> {
const issue = this.issuesStore.getById(this.issueId());
if (!issue) return;
const updatedComments = issue.comments.map((c) => {
if (c.id !== commentId) return c;
return { ...c, linkedIssueIds: c.linkedIssueIds.filter((id) => id !== issueId) };
});
await this.issuesStore.upsert({ ...issue, comments: updatedComments });
}
}