Ajoute date debut et date de fin
Signed-off-by: Gato <cedric@goutailler-olivier.fr>
This commit is contained in:
@@ -12,6 +12,8 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
|||||||
assignee: '',
|
assignee: '',
|
||||||
epic: '',
|
epic: '',
|
||||||
name: 'Test Issue',
|
name: 'Test Issue',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
description: '',
|
description: '',
|
||||||
estimatedTime: null,
|
estimatedTime: null,
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
|||||||
assignee: '',
|
assignee: '',
|
||||||
epic: '',
|
epic: '',
|
||||||
name: 'Test Issue',
|
name: 'Test Issue',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
description: '',
|
description: '',
|
||||||
estimatedTime: null,
|
estimatedTime: null,
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ export class IssueComments {
|
|||||||
name,
|
name,
|
||||||
assignee: '',
|
assignee: '',
|
||||||
epic: '',
|
epic: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
description: '',
|
description: '',
|
||||||
estimatedTime: null,
|
estimatedTime: null,
|
||||||
|
|||||||
@@ -153,6 +153,33 @@
|
|||||||
<label class="field-label">Assignee</label>
|
<label class="field-label">Assignee</label>
|
||||||
<input aria-label="Assignee" class="form-control form-control-sm" type="text" [(ngModel)]="issue.assignee" (blur)="saveIssue()" />
|
<input aria-label="Assignee" class="form-control form-control-sm" type="text" [(ngModel)]="issue.assignee" (blur)="saveIssue()" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="field-label">Date de début</label>
|
||||||
|
<input
|
||||||
|
aria-label="Date de début"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
[class.is-invalid]="!!dateValidationError"
|
||||||
|
type="date"
|
||||||
|
[(ngModel)]="issue.startDate"
|
||||||
|
(blur)="saveIssue()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="field-label">Date de fin</label>
|
||||||
|
<input
|
||||||
|
aria-label="Date de fin"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
[class.is-invalid]="!!dateValidationError"
|
||||||
|
type="date"
|
||||||
|
[(ngModel)]="issue.endDate"
|
||||||
|
(blur)="saveIssue()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@if (dateValidationError) {
|
||||||
|
<div class="col-12 text-danger small">{{ dateValidationError }}</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="field-label">Date d'échéance</label>
|
<label class="field-label">Date d'échéance</label>
|
||||||
<input aria-label="Date d'échéance" class="form-control form-control-sm" type="date" [(ngModel)]="issue.dueDate" (blur)="saveIssue()" />
|
<input aria-label="Date d'échéance" class="form-control form-control-sm" type="date" [(ngModel)]="issue.dueDate" (blur)="saveIssue()" />
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
|||||||
assignee: '',
|
assignee: '',
|
||||||
epic: '',
|
epic: '',
|
||||||
name: 'Test Issue',
|
name: 'Test Issue',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
description: '',
|
description: '',
|
||||||
estimatedTime: null,
|
estimatedTime: null,
|
||||||
@@ -213,6 +215,86 @@ describe('IssueDetail — existing issue', () => {
|
|||||||
(component as any).saveIssue();
|
(component as any).saveIssue();
|
||||||
expect(store.issues().length).toBe(countBefore);
|
expect(store.issues().length).toBe(countBefore);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not persist when dateValidationError is set', async () => {
|
||||||
|
(component as any).issue.name = 'Has Dates';
|
||||||
|
(component as any).issue.startDate = '2026-02-01';
|
||||||
|
(component as any).issue.endDate = '2026-01-01';
|
||||||
|
await (component as any).saveIssue();
|
||||||
|
expect(store.getById(1)?.name).not.toBe('Has Dates');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dateValidationError', () => {
|
||||||
|
it('returns null when both dates are empty', () => {
|
||||||
|
(component as any).issue.startDate = '';
|
||||||
|
(component as any).issue.endDate = '';
|
||||||
|
expect((component as any).dateValidationError).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when only startDate is set', () => {
|
||||||
|
(component as any).issue.startDate = '2026-01-01';
|
||||||
|
(component as any).issue.endDate = '';
|
||||||
|
expect((component as any).dateValidationError).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when only endDate is set', () => {
|
||||||
|
(component as any).issue.startDate = '';
|
||||||
|
(component as any).issue.endDate = '2026-01-31';
|
||||||
|
expect((component as any).dateValidationError).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when startDate equals endDate', () => {
|
||||||
|
(component as any).issue.startDate = '2026-01-15';
|
||||||
|
(component as any).issue.endDate = '2026-01-15';
|
||||||
|
expect((component as any).dateValidationError).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when startDate is before endDate', () => {
|
||||||
|
(component as any).issue.startDate = '2026-01-01';
|
||||||
|
(component as any).issue.endDate = '2026-01-31';
|
||||||
|
expect((component as any).dateValidationError).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when startDate is after endDate', () => {
|
||||||
|
(component as any).issue.startDate = '2026-02-01';
|
||||||
|
(component as any).issue.endDate = '2026-01-01';
|
||||||
|
expect((component as any).dateValidationError).toContain('supérieure à la date de fin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when dependency has no endDate and startDate is set', async () => {
|
||||||
|
store.upsert(makeIssue({ id: 10, startDate: '2026-01-01', endDate: '' }));
|
||||||
|
(component as any).issue.dependsOnIds = [10];
|
||||||
|
(component as any).issue.startDate = '2025-12-01';
|
||||||
|
expect((component as any).dateValidationError).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when startDate is before dependency endDate (Finish-to-Start)', async () => {
|
||||||
|
store.upsert(makeIssue({ id: 10, endDate: '2026-02-01' }));
|
||||||
|
(component as any).issue.dependsOnIds = [10];
|
||||||
|
(component as any).issue.startDate = '2026-01-15';
|
||||||
|
expect((component as any).dateValidationError).toContain('#10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when startDate equals dependency endDate', async () => {
|
||||||
|
store.upsert(makeIssue({ id: 10, endDate: '2026-02-01' }));
|
||||||
|
(component as any).issue.dependsOnIds = [10];
|
||||||
|
(component as any).issue.startDate = '2026-02-01';
|
||||||
|
expect((component as any).dateValidationError).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when startDate is after dependency endDate', async () => {
|
||||||
|
store.upsert(makeIssue({ id: 10, endDate: '2026-02-01' }));
|
||||||
|
(component as any).issue.dependsOnIds = [10];
|
||||||
|
(component as any).issue.startDate = '2026-02-15';
|
||||||
|
expect((component as any).dateValidationError).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when there are no dependencies', () => {
|
||||||
|
(component as any).issue.dependsOnIds = [];
|
||||||
|
(component as any).issue.startDate = '2026-01-01';
|
||||||
|
expect((component as any).dateValidationError).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteIssue', () => {
|
describe('deleteIssue', () => {
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ export class IssueDetail {
|
|||||||
assignee: '',
|
assignee: '',
|
||||||
epic: this.issue.name,
|
epic: this.issue.name,
|
||||||
name,
|
name,
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
description: '',
|
description: '',
|
||||||
estimatedTime: null,
|
estimatedTime: null,
|
||||||
@@ -235,6 +237,22 @@ export class IssueDetail {
|
|||||||
return !!this.issue.epic;
|
return !!this.issue.epic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get dateValidationError(): string | null {
|
||||||
|
const { startDate, endDate } = this.issue;
|
||||||
|
if (startDate && endDate && startDate > endDate) {
|
||||||
|
return 'La date de début ne peut pas être supérieure à la date de fin.';
|
||||||
|
}
|
||||||
|
if (startDate && this.issue.dependsOnIds.length > 0) {
|
||||||
|
for (const depId of this.issue.dependsOnIds) {
|
||||||
|
const dep = this.issuesStore.getById(depId);
|
||||||
|
if (dep?.endDate && startDate < dep.endDate) {
|
||||||
|
return `La date de début ne peut pas être antérieure à la date de fin de la dépendance #${depId}.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected startEditDescription(): void {
|
protected startEditDescription(): void {
|
||||||
this._descriptionBeforeEdit = this.issue.description;
|
this._descriptionBeforeEdit = this.issue.description;
|
||||||
this.editingDescription = true;
|
this.editingDescription = true;
|
||||||
@@ -358,6 +376,7 @@ export class IssueDetail {
|
|||||||
protected async saveIssue(explicit = false): Promise<void> {
|
protected async saveIssue(explicit = false): Promise<void> {
|
||||||
if (this.isNewIssueRoute && !explicit) return;
|
if (this.isNewIssueRoute && !explicit) return;
|
||||||
if (!this.issue.name.trim()) return;
|
if (!this.issue.name.trim()) return;
|
||||||
|
if (this.dateValidationError) return;
|
||||||
const saved = await this.issuesStore.upsert(this.issue);
|
const saved = await this.issuesStore.upsert(this.issue);
|
||||||
this.issue = { ...saved };
|
this.issue = { ...saved };
|
||||||
if (this.isNewIssueRoute) {
|
if (this.isNewIssueRoute) {
|
||||||
@@ -413,6 +432,8 @@ export class IssueDetail {
|
|||||||
assignee: '',
|
assignee: '',
|
||||||
epic: '',
|
epic: '',
|
||||||
name: '',
|
name: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
description: '',
|
description: '',
|
||||||
estimatedTime: null,
|
estimatedTime: null,
|
||||||
@@ -434,6 +455,8 @@ export class IssueDetail {
|
|||||||
assignee: '',
|
assignee: '',
|
||||||
epic: '',
|
epic: '',
|
||||||
name: '',
|
name: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
description: '',
|
description: '',
|
||||||
estimatedTime: null,
|
estimatedTime: null,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
|||||||
assignee: '',
|
assignee: '',
|
||||||
epic: '',
|
epic: '',
|
||||||
name: 'Test Issue',
|
name: 'Test Issue',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
description: '',
|
description: '',
|
||||||
estimatedTime: null,
|
estimatedTime: null,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
|||||||
assignee: '',
|
assignee: '',
|
||||||
epic: '',
|
epic: '',
|
||||||
name: 'Test Issue',
|
name: 'Test Issue',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
description: '',
|
description: '',
|
||||||
estimatedTime: null,
|
estimatedTime: null,
|
||||||
@@ -156,6 +158,31 @@ describe('IssuesStore', () => {
|
|||||||
await p;
|
await p;
|
||||||
expect(store.getById(994)?.estimatedTime).toBeNull();
|
expect(store.getById(994)?.estimatedTime).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('defaults startDate to empty string when missing in API response', async () => {
|
||||||
|
const issue = { ...makeIssue({ id: 0 }), startDate: undefined } as any;
|
||||||
|
const p = store.upsert(issue);
|
||||||
|
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush({ ...makeIssue({ id: 993 }), startDate: undefined });
|
||||||
|
await p;
|
||||||
|
expect(store.getById(993)?.startDate).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults endDate to empty string when missing in API response', async () => {
|
||||||
|
const issue = { ...makeIssue({ id: 0 }), endDate: undefined } as any;
|
||||||
|
const p = store.upsert(issue);
|
||||||
|
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush({ ...makeIssue({ id: 992 }), endDate: undefined });
|
||||||
|
await p;
|
||||||
|
expect(store.getById(992)?.endDate).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves startDate and endDate when provided', async () => {
|
||||||
|
const issue = makeIssue({ id: 0, startDate: '2026-01-01', endDate: '2026-01-31' });
|
||||||
|
const p = store.upsert(issue);
|
||||||
|
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 991, startDate: '2026-01-01', endDate: '2026-01-31' }));
|
||||||
|
await p;
|
||||||
|
expect(store.getById(991)?.startDate).toBe('2026-01-01');
|
||||||
|
expect(store.getById(991)?.endDate).toBe('2026-01-31');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteById', () => {
|
describe('deleteById', () => {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export type IssueEntity = {
|
|||||||
assignee: string;
|
assignee: string;
|
||||||
epic: string;
|
epic: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
dueDate: string;
|
dueDate: string;
|
||||||
description: string;
|
description: string;
|
||||||
estimatedTime: number | null;
|
estimatedTime: number | null;
|
||||||
@@ -110,6 +112,8 @@ export class IssuesStore {
|
|||||||
return {
|
return {
|
||||||
...issue,
|
...issue,
|
||||||
type: issue.type ?? 'Story',
|
type: issue.type ?? 'Story',
|
||||||
|
startDate: issue.startDate ?? '',
|
||||||
|
endDate: issue.endDate ?? '',
|
||||||
estimatedTime: issue.estimatedTime ?? null,
|
estimatedTime: issue.estimatedTime ?? null,
|
||||||
dependsOnIds: normalizedDependencies,
|
dependsOnIds: normalizedDependencies,
|
||||||
comments: Array.isArray(issue.comments)
|
comments: Array.isArray(issue.comments)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ const makeIssue = (overrides: Partial<IssueEntity> = {}): IssueEntity => ({
|
|||||||
assignee: '',
|
assignee: '',
|
||||||
epic: '',
|
epic: '',
|
||||||
name: 'Test Issue',
|
name: 'Test Issue',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
description: '',
|
description: '',
|
||||||
estimatedTime: null,
|
estimatedTime: null,
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ export class MilestoneDetail {
|
|||||||
assignee: '',
|
assignee: '',
|
||||||
epic: '',
|
epic: '',
|
||||||
name,
|
name,
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
dueDate: '',
|
dueDate: '',
|
||||||
description: '',
|
description: '',
|
||||||
estimatedTime: null,
|
estimatedTime: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user