Calcule du temps estimé des Milestone et Epic
This commit is contained in:
@@ -186,7 +186,11 @@
|
|||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div [class]="isEpicIssue ? 'col-12' : 'col-6'">
|
<div [class]="isEpicIssue ? 'col-12' : 'col-6'">
|
||||||
<label class="field-label">Temps estimé (h)</label>
|
<label class="field-label">Temps estimé (h)</label>
|
||||||
|
@if (isEpicIssue) {
|
||||||
|
<div class="form-control form-control-sm bg-body-secondary text-secondary">{{ epicEstimatedTime !== null ? epicEstimatedTime : '—' }}</div>
|
||||||
|
} @else {
|
||||||
<input aria-label="Temps estimé" class="form-control form-control-sm" type="number" min="0" step="0.5" [(ngModel)]="estimatedTimeValue" (blur)="saveIssue()" />
|
<input aria-label="Temps estimé" class="form-control form-control-sm" type="number" min="0" step="0.5" [(ngModel)]="estimatedTimeValue" (blur)="saveIssue()" />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (!isEpicIssue) {
|
@if (!isEpicIssue) {
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
|
|||||||
@@ -580,6 +580,39 @@ describe('IssueDetail — existing issue', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('epicEstimatedTime', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(component as any).issue.type = 'Epic';
|
||||||
|
(component as any).issue.name = 'Test Epic';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when there are no child issues', () => {
|
||||||
|
expect((component as any).epicEstimatedTime).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when all children have null estimatedTime', () => {
|
||||||
|
store.upsert(makeIssue({ id: 200, epic: 'Test Epic', estimatedTime: null }));
|
||||||
|
expect((component as any).epicEstimatedTime).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the sum of children estimatedTime', () => {
|
||||||
|
store.upsert(makeIssue({ id: 200, epic: 'Test Epic', estimatedTime: 8 }));
|
||||||
|
store.upsert(makeIssue({ id: 201, epic: 'Test Epic', estimatedTime: 4 }));
|
||||||
|
expect((component as any).epicEstimatedTime).toBe(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores children with null estimatedTime in the sum', () => {
|
||||||
|
store.upsert(makeIssue({ id: 200, epic: 'Test Epic', estimatedTime: 8 }));
|
||||||
|
store.upsert(makeIssue({ id: 201, epic: 'Test Epic', estimatedTime: null }));
|
||||||
|
expect((component as any).epicEstimatedTime).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes children linked via dependsOnIds', () => {
|
||||||
|
store.upsert(makeIssue({ id: 200, dependsOnIds: [1], estimatedTime: 6 }));
|
||||||
|
expect((component as any).epicEstimatedTime).toBe(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('create-in-epic flow', () => {
|
describe('create-in-epic flow', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(component as any).issue.type = 'Epic';
|
(component as any).issue.type = 'Epic';
|
||||||
|
|||||||
@@ -150,6 +150,13 @@ export class IssueDetail {
|
|||||||
this.issue.type = value;
|
this.issue.type = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get epicEstimatedTime(): number | null {
|
||||||
|
const times = this.composedIssues
|
||||||
|
.filter((i): i is IssueEntity & { estimatedTime: number } => i.estimatedTime !== null)
|
||||||
|
.map((i) => i.estimatedTime);
|
||||||
|
return times.length === 0 ? null : times.reduce((a, b) => a + b, 0);
|
||||||
|
}
|
||||||
|
|
||||||
protected get epicIssues(): IssueEntity[] {
|
protected get epicIssues(): IssueEntity[] {
|
||||||
return this.issues().filter((issue) => issue.type === 'Epic');
|
return this.issues().filter((issue) => issue.type === 'Epic');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,10 @@
|
|||||||
<span class="text-secondary small" style="min-width: 2.5rem; text-align: right;">{{ progress }}%</span>
|
<span class="text-secondary small" style="min-width: 2.5rem; text-align: right;">{{ progress }}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="field-label">Temps estimé total (h)</label>
|
||||||
|
<div class="form-control form-control-sm bg-body-secondary text-secondary">{{ totalEstimatedTime !== null ? totalEstimatedTime : '—' }}</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -580,6 +580,69 @@ describe('MilestoneDetail', () => {
|
|||||||
expect((component as any).milestone.description).toContain('![image]');
|
expect((component as any).milestone.description).toContain('![image]');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('totalEstimatedTime', () => {
|
||||||
|
it('returns null when no linked issues', () => {
|
||||||
|
issuesStore.seed([]);
|
||||||
|
(component as any).milestone.issueIds = [];
|
||||||
|
expect((component as any).totalEstimatedTime).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when all linked issues have null estimatedTime', () => {
|
||||||
|
issuesStore.seed([
|
||||||
|
makeIssue({ id: 1, estimatedTime: null }),
|
||||||
|
makeIssue({ id: 2, estimatedTime: null }),
|
||||||
|
]);
|
||||||
|
(component as any).milestone.issueIds = [1, 2];
|
||||||
|
expect((component as any).totalEstimatedTime).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the sum of estimatedTime for non-Epic issues', () => {
|
||||||
|
issuesStore.seed([
|
||||||
|
makeIssue({ id: 1, estimatedTime: 8 }),
|
||||||
|
makeIssue({ id: 2, estimatedTime: 4 }),
|
||||||
|
]);
|
||||||
|
(component as any).milestone.issueIds = [1, 2];
|
||||||
|
expect((component as any).totalEstimatedTime).toBe(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores null estimatedTime in the sum', () => {
|
||||||
|
issuesStore.seed([
|
||||||
|
makeIssue({ id: 1, estimatedTime: 8 }),
|
||||||
|
makeIssue({ id: 2, estimatedTime: null }),
|
||||||
|
]);
|
||||||
|
(component as any).milestone.issueIds = [1, 2];
|
||||||
|
expect((component as any).totalEstimatedTime).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the Epic own estimatedTime, not its children', () => {
|
||||||
|
issuesStore.seed([
|
||||||
|
makeIssue({ id: 1, type: 'Epic', name: 'My Epic', estimatedTime: 10 }),
|
||||||
|
makeIssue({ id: 2, epic: 'My Epic', estimatedTime: 5 }),
|
||||||
|
makeIssue({ id: 3, epic: 'My Epic', estimatedTime: 3 }),
|
||||||
|
]);
|
||||||
|
(component as any).milestone.issueIds = [1];
|
||||||
|
expect((component as any).totalEstimatedTime).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for an Epic with null estimatedTime', () => {
|
||||||
|
issuesStore.seed([
|
||||||
|
makeIssue({ id: 1, type: 'Epic', name: 'My Epic', estimatedTime: null }),
|
||||||
|
makeIssue({ id: 2, epic: 'My Epic', estimatedTime: 5 }),
|
||||||
|
]);
|
||||||
|
(component as any).milestone.issueIds = [1];
|
||||||
|
expect((component as any).totalEstimatedTime).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mixes Epics and plain issues correctly', () => {
|
||||||
|
issuesStore.seed([
|
||||||
|
makeIssue({ id: 1, type: 'Epic', name: 'My Epic', estimatedTime: 8 }),
|
||||||
|
makeIssue({ id: 3, type: 'Story', estimatedTime: 6 }),
|
||||||
|
]);
|
||||||
|
(component as any).milestone.issueIds = [1, 3];
|
||||||
|
expect((component as any).totalEstimatedTime).toBe(14);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('MilestoneDetail — new route', () => {
|
describe('MilestoneDetail — new route', () => {
|
||||||
|
|||||||
@@ -110,6 +110,13 @@ export class MilestoneDetail {
|
|||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get totalEstimatedTime(): number | null {
|
||||||
|
const times = this.linkedIssues
|
||||||
|
.filter((i): i is IssueEntity & { estimatedTime: number } => i.estimatedTime !== null)
|
||||||
|
.map((i) => i.estimatedTime);
|
||||||
|
return times.length === 0 ? null : times.reduce((a, b) => a + b, 0);
|
||||||
|
}
|
||||||
|
|
||||||
protected get progress(): number {
|
protected get progress(): number {
|
||||||
if (this.linkedIssues.length === 0) return 0;
|
if (this.linkedIssues.length === 0) return 0;
|
||||||
return Math.round(
|
return Math.round(
|
||||||
|
|||||||
Reference in New Issue
Block a user