diff --git a/src/app/issues/issue-comments/issue-comments.css b/src/app/issues/issue-comments/issue-comments.css
index fdef573..e6e7298 100644
--- a/src/app/issues/issue-comments/issue-comments.css
+++ b/src/app/issues/issue-comments/issue-comments.css
@@ -90,3 +90,152 @@
.comment-new {
padding-top: 0.5rem;
}
+
+/* --- Issues liées --- */
+
+.linked-issues {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+}
+
+.linked-issue-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ background: #f8fafc;
+ border: 1px solid #e2e8f0;
+ border-radius: 6px;
+ padding: 0.2rem 0.5rem;
+ max-width: fit-content;
+}
+
+.linked-issue-type-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ border-radius: 3px;
+ font-size: 0.65rem;
+ font-weight: 700;
+ color: #fff;
+ flex-shrink: 0;
+}
+
+.linked-issue-name {
+ font-size: 0.8rem;
+ color: #1d4ed8;
+ text-decoration: none;
+ font-weight: 500;
+}
+
+.linked-issue-name:hover {
+ text-decoration: underline;
+}
+
+.linked-issue-status {
+ font-size: 0.68rem;
+ font-weight: 600;
+ padding: 0.1em 0.4em;
+ border-radius: 4px;
+}
+
+.linked-issue-unlink {
+ border: none;
+ background: none;
+ color: #9ca3af;
+ cursor: pointer;
+ font-size: 1rem;
+ line-height: 1;
+ padding: 0;
+ margin-left: 0.1rem;
+}
+
+.linked-issue-unlink:hover {
+ color: #b91c1c;
+}
+
+/* --- Boutons d'action liée --- */
+
+.comment-link-actions {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.comment-link-btn {
+ border: none;
+ background: none;
+ font-size: 0.75rem;
+ color: #6b7280;
+ cursor: pointer;
+ padding: 0;
+ text-decoration: none;
+}
+
+.comment-link-btn:hover {
+ color: #1d4ed8;
+ text-decoration: underline;
+}
+
+/* --- Formulaire création tâche --- */
+
+.comment-task-form {
+ background: #f8fafc;
+ border: 1px solid #e2e8f0;
+ border-radius: 6px;
+ padding: 0.5rem;
+}
+
+/* --- Dropdown lier issue existante --- */
+
+.comment-link-form {
+ background: #f8fafc;
+ border: 1px solid #e2e8f0;
+ border-radius: 6px;
+ padding: 0.5rem;
+}
+
+.link-issue-list {
+ margin-top: 0.4rem;
+ max-height: 180px;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 0.15rem;
+}
+
+.link-issue-option {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ background: #fff;
+ border: 1px solid #e2e8f0;
+ border-radius: 4px;
+ padding: 0.3rem 0.5rem;
+ cursor: pointer;
+ text-align: left;
+ width: 100%;
+}
+
+.link-issue-option:hover {
+ background: #eff6ff;
+ border-color: #bfdbfe;
+}
+
+.link-issue-option-name {
+ font-size: 0.8rem;
+ color: #111827;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.link-issue-empty {
+ font-size: 0.78rem;
+ color: #9ca3af;
+ padding: 0.4rem 0;
+ text-align: center;
+}
diff --git a/src/app/issues/issue-comments/issue-comments.html b/src/app/issues/issue-comments/issue-comments.html
index 70e8705..ac23325 100644
--- a/src/app/issues/issue-comments/issue-comments.html
+++ b/src/app/issues/issue-comments/issue-comments.html
@@ -31,6 +31,97 @@
} @else {
+
+
+ @if (getLinkedIssues(comment).length > 0) {
+
+ @for (linked of getLinkedIssues(comment); track linked.id) {
+
+
{{ typeIcon(linked.type).letter }}
+
{{ linked.name }}
+
{{ statusLabel(linked.status).label }}
+
+
+ }
+
+ }
+
+
+ @if (creatingTaskForCommentId === comment.id) {
+
+ } @else if (linkingIssueForCommentId === comment.id) {
+
+ } @else {
+
+ }
}
}
diff --git a/src/app/issues/issue-comments/issue-comments.spec.ts b/src/app/issues/issue-comments/issue-comments.spec.ts
index 3b7d8f3..5aeb88b 100644
--- a/src/app/issues/issue-comments/issue-comments.spec.ts
+++ b/src/app/issues/issue-comments/issue-comments.spec.ts
@@ -1,5 +1,6 @@
import { signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
import { afterEach, vi } from 'vitest';
import { IssueComments } from './issue-comments';
import { IssueEntity, IssuesStore } from '../issues.store';
@@ -43,11 +44,18 @@ class FakeIssuesStore {
upsert(issue: any): Promise {
const { comments: c, estimatedTime: et, dependsOnIds: deps, ...rest } = issue;
+ const id = rest.id || this.getNextId();
const normalized: IssueEntity = {
...makeIssue(),
...rest,
+ id,
dependsOnIds: Array.isArray(deps) ? deps.filter((v: unknown) => typeof v === 'number') : [],
- comments: Array.isArray(c) ? c : [],
+ comments: Array.isArray(c)
+ ? c.map((comment: any) => ({
+ ...comment,
+ linkedIssueIds: Array.isArray(comment.linkedIssueIds) ? comment.linkedIssueIds : [],
+ }))
+ : [],
estimatedTime: et ?? null,
};
this._data.update((issues) => {
@@ -78,7 +86,7 @@ describe('IssueComments', () => {
beforeEach(async () => {
store = new FakeIssuesStore();
await TestBed.configureTestingModule({
- imports: [IssueComments],
+ imports: [IssueComments, RouterTestingModule],
providers: [{ provide: IssuesStore, useValue: store }],
}).compileComponents();
@@ -166,6 +174,12 @@ describe('IssueComments', () => {
(component as any).addComment();
expect(store.getById(1)?.comments[0].updatedAt).toBeNull();
});
+
+ it('initialises linkedIssueIds as empty array', () => {
+ (component as any).newCommentText = 'New comment';
+ (component as any).addComment();
+ expect(store.getById(1)?.comments[0].linkedIssueIds).toEqual([]);
+ });
});
describe('startEditComment', () => {
@@ -179,6 +193,19 @@ describe('IssueComments', () => {
expect((component as any).editingCommentId).toBe(comment.id);
expect((component as any).editingCommentText).toBe('Editable');
});
+
+ it('cancels any active task creation or link mode', () => {
+ (component as any).newCommentText = 'X';
+ (component as any).addComment();
+ const comment = store.getById(1)!.comments[0];
+ (component as any).creatingTaskForCommentId = comment.id;
+ (component as any).linkingIssueForCommentId = comment.id;
+
+ (component as any).startEditComment(comment);
+
+ expect((component as any).creatingTaskForCommentId).toBeNull();
+ expect((component as any).linkingIssueForCommentId).toBeNull();
+ });
});
describe('cancelEditComment', () => {
@@ -307,4 +334,252 @@ describe('IssueComments', () => {
expect(store.getById(1)?.comments[0].text).toBe('Keep me');
});
});
+
+ describe('createTaskForComment', () => {
+ beforeEach(async () => {
+ (component as any).newCommentText = 'CR réunion';
+ await (component as any).addComment();
+ });
+
+ it('creates a Task issue in the store', async () => {
+ const commentId = store.getById(1)!.comments[0].id;
+ (component as any).newTaskName = 'Action item';
+ await (component as any).createTaskForComment(commentId);
+
+ const tasks = store.issues().filter((i) => i.type === 'Task');
+ expect(tasks.length).toBe(1);
+ expect(tasks[0].name).toBe('Action item');
+ });
+
+ it('links the created task to the comment', async () => {
+ const commentId = store.getById(1)!.comments[0].id;
+ (component as any).newTaskName = 'Action item';
+ await (component as any).createTaskForComment(commentId);
+
+ const tasks = store.issues().filter((i) => i.type === 'Task');
+ const comment = store.getById(1)!.comments[0];
+ expect(comment.linkedIssueIds).toContain(tasks[0].id);
+ });
+
+ it('resets task form state after creation', async () => {
+ const commentId = store.getById(1)!.comments[0].id;
+ (component as any).newTaskName = 'Task';
+ await (component as any).createTaskForComment(commentId);
+
+ expect((component as any).creatingTaskForCommentId).toBeNull();
+ expect((component as any).newTaskName).toBe('');
+ });
+
+ it('only adds the task link to the target comment when multiple exist', async () => {
+ (component as any).newCommentText = 'Second comment';
+ await (component as any).addComment();
+ const comments = store.getById(1)!.comments;
+ const firstId = comments[0].id;
+ const secondId = comments[1].id;
+
+ (component as any).newTaskName = 'Task for first';
+ await (component as any).createTaskForComment(firstId);
+
+ expect(store.getById(1)!.comments.find((c: any) => c.id === firstId)!.linkedIssueIds.length).toBe(1);
+ expect(store.getById(1)!.comments.find((c: any) => c.id === secondId)!.linkedIssueIds.length).toBe(0);
+ });
+
+ it('does nothing when newTaskName is empty', async () => {
+ const commentId = store.getById(1)!.comments[0].id;
+ (component as any).newTaskName = ' ';
+ await (component as any).createTaskForComment(commentId);
+
+ expect(store.issues().filter((i) => i.type === 'Task').length).toBe(0);
+ });
+ });
+
+ describe('linkIssueToComment', () => {
+ beforeEach(async () => {
+ await store.upsert(makeIssue({ id: 2, name: 'Other issue' }));
+ (component as any).newCommentText = 'Un commentaire';
+ await (component as any).addComment();
+ });
+
+ it('adds the issue id to the comment linkedIssueIds', async () => {
+ const commentId = store.getById(1)!.comments[0].id;
+ await (component as any).linkIssueToComment(commentId, 2);
+
+ expect(store.getById(1)!.comments[0].linkedIssueIds).toContain(2);
+ });
+
+ it('does not add the same issue twice', async () => {
+ const commentId = store.getById(1)!.comments[0].id;
+ await (component as any).linkIssueToComment(commentId, 2);
+ await (component as any).linkIssueToComment(commentId, 2);
+
+ expect(store.getById(1)!.comments[0].linkedIssueIds.filter((id: number) => id === 2).length).toBe(1);
+ });
+
+ it('resets link form state after linking', async () => {
+ const commentId = store.getById(1)!.comments[0].id;
+ (component as any).issueSearchText = 'Other';
+ await (component as any).linkIssueToComment(commentId, 2);
+
+ expect((component as any).linkingIssueForCommentId).toBeNull();
+ expect((component as any).issueSearchText).toBe('');
+ });
+
+ it('only modifies the target comment when multiple exist', async () => {
+ (component as any).newCommentText = 'Second';
+ await (component as any).addComment();
+ const comments = store.getById(1)!.comments;
+ const firstId = comments[0].id;
+ const secondId = comments[1].id;
+
+ await (component as any).linkIssueToComment(firstId, 2);
+
+ expect(store.getById(1)!.comments.find((c: any) => c.id === firstId)!.linkedIssueIds).toContain(2);
+ expect(store.getById(1)!.comments.find((c: any) => c.id === secondId)!.linkedIssueIds).toEqual([]);
+ });
+ });
+
+ describe('unlinkIssueFromComment', () => {
+ beforeEach(async () => {
+ await store.upsert(makeIssue({ id: 2, name: 'Linked issue' }));
+ (component as any).newCommentText = 'Commentaire avec lien';
+ await (component as any).addComment();
+ const commentId = store.getById(1)!.comments[0].id;
+ await (component as any).linkIssueToComment(commentId, 2);
+ });
+
+ it('removes the issue id from linkedIssueIds', async () => {
+ const commentId = store.getById(1)!.comments[0].id;
+ await (component as any).unlinkIssueFromComment(commentId, 2);
+
+ expect(store.getById(1)!.comments[0].linkedIssueIds).not.toContain(2);
+ });
+
+ it('does not modify other comments when unlinking', async () => {
+ (component as any).newCommentText = 'Second comment';
+ await (component as any).addComment();
+ const comments = store.getById(1)!.comments;
+ const firstId = comments[0].id;
+
+ await (component as any).unlinkIssueFromComment(firstId, 2);
+
+ expect(store.getById(1)!.comments.find((c: any) => c.id !== firstId)!.linkedIssueIds).toEqual([]);
+ });
+ });
+
+ describe('getLinkedIssues', () => {
+ it('returns empty array when no linked issues', async () => {
+ (component as any).newCommentText = 'Test';
+ await (component as any).addComment();
+ const comment = store.getById(1)!.comments[0];
+ expect((component as any).getLinkedIssues(comment)).toEqual([]);
+ });
+
+ it('returns resolved IssueEntity objects for linked ids', async () => {
+ await store.upsert(makeIssue({ id: 2, name: 'Linked' }));
+ (component as any).newCommentText = 'Test';
+ await (component as any).addComment();
+ const commentId = store.getById(1)!.comments[0].id;
+ await (component as any).linkIssueToComment(commentId, 2);
+
+ const comment = store.getById(1)!.comments[0];
+ const linked = (component as any).getLinkedIssues(comment);
+ expect(linked.length).toBe(1);
+ expect(linked[0].name).toBe('Linked');
+ });
+ });
+
+ describe('filteredIssuesForLink', () => {
+ beforeEach(async () => {
+ await store.upsert(makeIssue({ id: 2, name: 'Alpha issue' }));
+ await store.upsert(makeIssue({ id: 3, name: 'Beta issue' }));
+ (component as any).newCommentText = 'Commentaire';
+ await (component as any).addComment();
+ });
+
+ it('excludes the current issue from results', () => {
+ const commentId = store.getById(1)!.comments[0].id;
+ const results = (component as any).filteredIssuesForLink(commentId);
+ expect(results.find((i: IssueEntity) => i.id === 1)).toBeUndefined();
+ });
+
+ it('filters by search text', () => {
+ (component as any).issueSearchText = 'alpha';
+ const commentId = store.getById(1)!.comments[0].id;
+ const results = (component as any).filteredIssuesForLink(commentId);
+ expect(results.length).toBe(1);
+ expect(results[0].name).toBe('Alpha issue');
+ });
+
+ it('excludes already linked issues', async () => {
+ const commentId = store.getById(1)!.comments[0].id;
+ await (component as any).linkIssueToComment(commentId, 2);
+
+ const results = (component as any).filteredIssuesForLink(commentId);
+ expect(results.find((i: IssueEntity) => i.id === 2)).toBeUndefined();
+ });
+ });
+
+ describe('startCreateTask', () => {
+ it('sets creatingTaskForCommentId and clears other modes', () => {
+ (component as any).linkingIssueForCommentId = 99;
+ (component as any).issueSearchText = 'foo';
+ (component as any).startCreateTask(5);
+
+ expect((component as any).creatingTaskForCommentId).toBe(5);
+ expect((component as any).newTaskName).toBe('');
+ expect((component as any).linkingIssueForCommentId).toBeNull();
+ expect((component as any).issueSearchText).toBe('');
+ });
+ });
+
+ describe('cancelCreateTask', () => {
+ it('resets creatingTaskForCommentId and newTaskName', () => {
+ (component as any).creatingTaskForCommentId = 3;
+ (component as any).newTaskName = 'Unfinished task';
+ (component as any).cancelCreateTask();
+
+ expect((component as any).creatingTaskForCommentId).toBeNull();
+ expect((component as any).newTaskName).toBe('');
+ });
+ });
+
+ describe('startLinkIssue', () => {
+ it('sets linkingIssueForCommentId and clears other modes', () => {
+ (component as any).creatingTaskForCommentId = 7;
+ (component as any).newTaskName = 'in progress';
+ (component as any).startLinkIssue(4);
+
+ expect((component as any).linkingIssueForCommentId).toBe(4);
+ expect((component as any).issueSearchText).toBe('');
+ expect((component as any).creatingTaskForCommentId).toBeNull();
+ expect((component as any).newTaskName).toBe('');
+ });
+ });
+
+ describe('cancelLinkIssue', () => {
+ it('resets linkingIssueForCommentId and issueSearchText', () => {
+ (component as any).linkingIssueForCommentId = 2;
+ (component as any).issueSearchText = 'something';
+ (component as any).cancelLinkIssue();
+
+ expect((component as any).linkingIssueForCommentId).toBeNull();
+ expect((component as any).issueSearchText).toBe('');
+ });
+ });
+
+ describe('typeIcon', () => {
+ it('returns the correct letter and bg for Task', () => {
+ const icon = (component as any).typeIcon('Task');
+ expect(icon.letter).toBe('T');
+ expect(icon.bg).toBe('#2563eb');
+ });
+ });
+
+ describe('statusLabel', () => {
+ it('returns label and colors for done status', () => {
+ const s = (component as any).statusLabel('done');
+ expect(s.label).toBe('Terminé');
+ expect(s.color).toBe('#166534');
+ });
+ });
});
diff --git a/src/app/issues/issue-comments/issue-comments.ts b/src/app/issues/issue-comments/issue-comments.ts
index 1408481..a4687b6 100644
--- a/src/app/issues/issue-comments/issue-comments.ts
+++ b/src/app/issues/issue-comments/issue-comments.ts
@@ -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 {
@@ -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 = {
+ 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 = {
+ 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 {
+ 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 {
+ 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 {
+ 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 });
+ }
}
diff --git a/src/app/issues/issues.store.ts b/src/app/issues/issues.store.ts
index 464a6b8..4a72186 100644
--- a/src/app/issues/issues.store.ts
+++ b/src/app/issues/issues.store.ts
@@ -11,6 +11,7 @@ export type IssueComment = {
text: string;
createdAt: string;
updatedAt: string | null;
+ linkedIssueIds: number[];
};
export type IssueEntity = {
@@ -67,9 +68,17 @@ export class IssuesStore {
this.data.update((issues) => [...issues, created]);
return created;
} else {
- const updated = this.normalizeIssue(
- await firstValueFrom(this.api.update(normalized.id, normalized)),
- );
+ const apiResult = await firstValueFrom(this.api.update(normalized.id, normalized));
+ // L'API ne retourne pas linkedIssueIds dans les commentaires : on le restaure
+ // depuis les données envoyées pour ne pas perdre les liens.
+ if (Array.isArray(apiResult.comments) && Array.isArray(normalized.comments)) {
+ apiResult.comments = apiResult.comments.map((c: IssueComment) => {
+ if (Array.isArray(c.linkedIssueIds)) return c;
+ const sent = normalized.comments.find((nc) => nc.id === c.id);
+ return { ...c, linkedIssueIds: sent?.linkedIssueIds ?? [] };
+ });
+ }
+ const updated = this.normalizeIssue(apiResult);
this.data.update((issues) => {
const idx = issues.findIndex((i) => i.id === normalized.id);
if (idx === -1) return issues;
@@ -103,7 +112,12 @@ export class IssuesStore {
type: issue.type ?? 'Story',
estimatedTime: issue.estimatedTime ?? null,
dependsOnIds: normalizedDependencies,
- comments: Array.isArray(issue.comments) ? issue.comments : [],
+ comments: Array.isArray(issue.comments)
+ ? issue.comments.map((c) => ({
+ ...c,
+ linkedIssueIds: Array.isArray(c.linkedIssueIds) ? c.linkedIssueIds : [],
+ }))
+ : [],
} as IssueEntity;
}
}