Milestone et Epic pour les tache commentaire

Signed-off-by: Gato <cedric@goutailler-olivier.fr>
This commit is contained in:
2026-05-30 14:07:16 +02:00
parent 7f266cd4cc
commit e2abbbb68c
2 changed files with 95 additions and 2 deletions
@@ -4,6 +4,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { afterEach, vi } from 'vitest'; import { afterEach, vi } from 'vitest';
import { IssueComments } from './issue-comments'; import { IssueComments } from './issue-comments';
import { IssueEntity, IssuesStore } from '../issues.store'; import { IssueEntity, IssuesStore } from '../issues.store';
import { MilestoneEntity, MilestonesStore } from '../../milestones/milestones.store';
const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
id: 1, id: 1,
@@ -81,16 +82,60 @@ class FakeIssuesStore {
} }
} }
const makeMilestone = (overrides: Partial<MilestoneEntity> = {}): MilestoneEntity => ({
id: 1,
name: 'Sprint 1',
description: '',
startDate: '',
endDate: '',
dueDate: '',
issueIds: [],
dependsOnIds: [],
...overrides,
});
class FakeMilestonesStore {
private _data = signal<MilestoneEntity[]>([]);
readonly milestones = this._data.asReadonly();
readonly loading = signal(false);
readonly loaded = signal(true);
getById(id: number): MilestoneEntity | undefined {
return this._data().find((m) => m.id === id);
}
load(): Promise<void> {
return Promise.resolve();
}
upsert(milestone: MilestoneEntity): Promise<MilestoneEntity> {
this._data.update((list) => {
const idx = list.findIndex((m) => m.id === milestone.id);
if (idx === -1) return [...list, milestone];
const copy = [...list];
copy[idx] = milestone;
return copy;
});
return Promise.resolve(milestone);
}
}
describe('IssueComments', () => { describe('IssueComments', () => {
let component: IssueComments; let component: IssueComments;
let fixture: ComponentFixture<IssueComments>; let fixture: ComponentFixture<IssueComments>;
let store: FakeIssuesStore; let store: FakeIssuesStore;
let milestonesStore: FakeMilestonesStore;
beforeEach(async () => { beforeEach(async () => {
store = new FakeIssuesStore(); store = new FakeIssuesStore();
milestonesStore = new FakeMilestonesStore();
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [IssueComments, RouterTestingModule], imports: [IssueComments, RouterTestingModule],
providers: [{ provide: IssuesStore, useValue: store }], providers: [
{ provide: IssuesStore, useValue: store },
{ provide: MilestonesStore, useValue: milestonesStore },
],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(IssueComments); fixture = TestBed.createComponent(IssueComments);
@@ -394,6 +439,46 @@ describe('IssueComments', () => {
expect(store.issues().filter((i) => i.type === 'Task').length).toBe(0); expect(store.issues().filter((i) => i.type === 'Task').length).toBe(0);
}); });
it('inherits the epic from the parent issue', async () => {
await store.upsert(makeIssue({ id: 1, epic: 'Mon Epic', comments: store.getById(1)!.comments }));
const commentId = store.getById(1)!.comments[0].id;
(component as any).newTaskName = 'Tâche avec epic';
await (component as any).createTaskForComment(commentId);
const task = store.issues().find((i) => i.type === 'Task');
expect(task?.epic).toBe('Mon Epic');
});
it('does not set epic when parent issue has none', async () => {
const commentId = store.getById(1)!.comments[0].id;
(component as any).newTaskName = 'Tâche sans epic';
await (component as any).createTaskForComment(commentId);
const task = store.issues().find((i) => i.type === 'Task');
expect(task?.epic).toBe('');
});
it('adds the new task to the same milestone as the parent issue', async () => {
await milestonesStore.upsert(makeMilestone({ id: 10, name: 'Release 1', issueIds: [1] }));
const commentId = store.getById(1)!.comments[0].id;
(component as any).newTaskName = 'Tâche dans milestone';
await (component as any).createTaskForComment(commentId);
const task = store.issues().find((i) => i.type === 'Task')!;
expect(milestonesStore.getById(10)!.issueIds).toContain(task.id);
});
it('does not modify any milestone when parent issue is not in one', async () => {
await milestonesStore.upsert(makeMilestone({ id: 10, name: 'Release 1', issueIds: [99] }));
const commentId = store.getById(1)!.comments[0].id;
(component as any).newTaskName = 'Tâche hors milestone';
await (component as any).createTaskForComment(commentId);
expect(milestonesStore.getById(10)!.issueIds).not.toContain(
store.issues().find((i) => i.type === 'Task')!.id,
);
});
}); });
describe('linkIssueToComment', () => { describe('linkIssueToComment', () => {
@@ -6,6 +6,7 @@ import { marked } from 'marked';
import { handleImagePaste, insertAtSelection } from '../paste-image.util'; import { handleImagePaste, insertAtSelection } from '../paste-image.util';
import { IssueComment, IssueEntity, IssuesStore } from '../issues.store'; import { IssueComment, IssueEntity, IssuesStore } from '../issues.store';
import { StatusEntity, StatusesStore } from '../../settings/statuses/statuses.store'; import { StatusEntity, StatusesStore } from '../../settings/statuses/statuses.store';
import { MilestonesStore } from '../../milestones/milestones.store';
@Component({ @Component({
selector: 'app-issue-comments', selector: 'app-issue-comments',
@@ -15,6 +16,7 @@ import { StatusEntity, StatusesStore } from '../../settings/statuses/statuses.st
}) })
export class IssueComments { export class IssueComments {
private readonly issuesStore = inject(IssuesStore); private readonly issuesStore = inject(IssuesStore);
private readonly milestonesStore = inject(MilestonesStore);
private readonly sanitizer = inject(DomSanitizer); private readonly sanitizer = inject(DomSanitizer);
private readonly statusesStore = inject(StatusesStore); private readonly statusesStore = inject(StatusesStore);
@@ -169,7 +171,7 @@ export class IssueComments {
type: 'Task', type: 'Task',
name, name,
assignee: '', assignee: '',
epic: '', epic: issue.epic,
startDate: '', startDate: '',
startDateMode: 'forced', startDateMode: 'forced',
endDate: '', endDate: '',
@@ -189,6 +191,12 @@ export class IssueComments {
return { ...c, linkedIssueIds: [...c.linkedIssueIds, created.id] }; return { ...c, linkedIssueIds: [...c.linkedIssueIds, created.id] };
}); });
await this.issuesStore.upsert({ ...issue, comments: updatedComments }); await this.issuesStore.upsert({ ...issue, comments: updatedComments });
const milestone = this.milestonesStore.milestones().find((m) => m.issueIds.includes(issue.id));
if (milestone) {
await this.milestonesStore.upsert({ ...milestone, issueIds: [...milestone.issueIds, created.id] });
}
this.creatingTaskForCommentId = null; this.creatingTaskForCommentId = null;
this.newTaskName = ''; this.newTaskName = '';
} }