Ajoute projet et migration milestone
Signed-off-by: Gato <cedric@goutailler-olivier.fr>
This commit is contained in:
@@ -10,20 +10,18 @@ export class IssuesApiService {
|
|||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
getAll(projectId: number): Observable<IssueEntity[]> {
|
getAll(projectId: number): Observable<IssueEntity[]> {
|
||||||
return this.http.get<IssueEntity[]>(`${API_BASE_URL}/issues`, {
|
return this.http.get<IssueEntity[]>(`${API_BASE_URL}/projects/${projectId}/issues`);
|
||||||
params: { projectId: projectId.toString() },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
create(issue: Omit<IssueEntity, 'id'>): Observable<IssueEntity> {
|
create(projectId: number, issue: Omit<IssueEntity, 'id'>): Observable<IssueEntity> {
|
||||||
return this.http.post<IssueEntity>(`${API_BASE_URL}/issues`, issue);
|
return this.http.post<IssueEntity>(`${API_BASE_URL}/projects/${projectId}/issues`, issue);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: number, issue: IssueEntity): Observable<IssueEntity> {
|
update(projectId: number, id: number, issue: IssueEntity): Observable<IssueEntity> {
|
||||||
return this.http.put<IssueEntity>(`${API_BASE_URL}/issues/${id}`, issue);
|
return this.http.put<IssueEntity>(`${API_BASE_URL}/projects/${projectId}/issues/${id}`, issue);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(id: number): Observable<void> {
|
remove(projectId: number, id: number): Observable<void> {
|
||||||
return this.http.delete<void>(`${API_BASE_URL}/issues/${id}`);
|
return this.http.delete<void>(`${API_BASE_URL}/projects/${projectId}/issues/${id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ describe('IssuesStore', () => {
|
|||||||
let httpMock: HttpTestingController;
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
const PROJECT_ID = 1;
|
const PROJECT_ID = 1;
|
||||||
const ISSUES_URL = `${API_BASE_URL}/issues?projectId=${PROJECT_ID}`;
|
const ISSUES_URL = `${API_BASE_URL}/projects/${PROJECT_ID}/issues`;
|
||||||
|
|
||||||
const loadWith = async (issues: IssueEntity[]) => {
|
const loadWith = async (issues: IssueEntity[]) => {
|
||||||
const p = store.load(PROJECT_ID);
|
const p = store.load(PROJECT_ID);
|
||||||
@@ -76,7 +76,7 @@ describe('IssuesStore', () => {
|
|||||||
|
|
||||||
it('reloads when projectId changes', async () => {
|
it('reloads when projectId changes', async () => {
|
||||||
await loadWith([makeIssue({ id: 1 })]);
|
await loadWith([makeIssue({ id: 1 })]);
|
||||||
const url2 = `${API_BASE_URL}/issues?projectId=2`;
|
const url2 = `${API_BASE_URL}/projects/2/issues`;
|
||||||
const p = store.load(2);
|
const p = store.load(2);
|
||||||
httpMock.expectOne(url2).flush([makeIssue({ id: 2 })]);
|
httpMock.expectOne(url2).flush([makeIssue({ id: 2 })]);
|
||||||
await p;
|
await p;
|
||||||
@@ -119,7 +119,7 @@ describe('IssuesStore', () => {
|
|||||||
it('creates a new issue via POST when id is 0', async () => {
|
it('creates a new issue via POST when id is 0', async () => {
|
||||||
const before = store.issues().length;
|
const before = store.issues().length;
|
||||||
const p = store.upsert(makeIssue({ id: 0, name: 'New Issue' }));
|
const p = store.upsert(makeIssue({ id: 0, name: 'New Issue' }));
|
||||||
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 999, name: 'New Issue' }));
|
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues` }).flush(makeIssue({ id: 999, name: 'New Issue' }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.issues().length).toBe(before + 1);
|
expect(store.issues().length).toBe(before + 1);
|
||||||
expect(store.getById(999)?.name).toBe('New Issue');
|
expect(store.getById(999)?.name).toBe('New Issue');
|
||||||
@@ -127,7 +127,7 @@ describe('IssuesStore', () => {
|
|||||||
|
|
||||||
it('updates an existing issue via PUT', async () => {
|
it('updates an existing issue via PUT', async () => {
|
||||||
const p = store.upsert(makeIssue({ id: 1, name: 'Updated Name' }));
|
const p = store.upsert(makeIssue({ id: 1, name: 'Updated Name' }));
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, name: 'Updated Name' }));
|
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/1` }).flush(makeIssue({ id: 1, name: 'Updated Name' }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(1)?.name).toBe('Updated Name');
|
expect(store.getById(1)?.name).toBe('Updated Name');
|
||||||
expect(store.issues().filter((i) => i.id === 1).length).toBe(1);
|
expect(store.issues().filter((i) => i.id === 1).length).toBe(1);
|
||||||
@@ -136,7 +136,7 @@ describe('IssuesStore', () => {
|
|||||||
it('normalizes legacy dependsOnId to dependsOnIds array', async () => {
|
it('normalizes legacy dependsOnId to dependsOnIds array', async () => {
|
||||||
const issue = { id: 0, type: 'Story', name: 'Legacy', dependsOnId: 1 } as any;
|
const issue = { id: 0, type: 'Story', name: 'Legacy', dependsOnId: 1 } as any;
|
||||||
const p = store.upsert(issue);
|
const p = store.upsert(issue);
|
||||||
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 998, dependsOnIds: [1] }));
|
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues` }).flush(makeIssue({ id: 998, dependsOnIds: [1] }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(998)?.dependsOnIds).toEqual([1]);
|
expect(store.getById(998)?.dependsOnIds).toEqual([1]);
|
||||||
});
|
});
|
||||||
@@ -144,7 +144,7 @@ describe('IssuesStore', () => {
|
|||||||
it('filters non-number values from dependsOnIds', async () => {
|
it('filters non-number values from dependsOnIds', async () => {
|
||||||
const issue = { ...makeIssue({ id: 0 }), dependsOnIds: [1, 'two', null] } as any;
|
const issue = { ...makeIssue({ id: 0 }), dependsOnIds: [1, 'two', null] } as any;
|
||||||
const p = store.upsert(issue);
|
const p = store.upsert(issue);
|
||||||
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 997, dependsOnIds: [1] }));
|
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues` }).flush(makeIssue({ id: 997, dependsOnIds: [1] }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(997)?.dependsOnIds).toEqual([1]);
|
expect(store.getById(997)?.dependsOnIds).toEqual([1]);
|
||||||
});
|
});
|
||||||
@@ -152,7 +152,7 @@ describe('IssuesStore', () => {
|
|||||||
it('ensures comments is always an array when missing', async () => {
|
it('ensures comments is always an array when missing', async () => {
|
||||||
const issue = { ...makeIssue({ id: 0 }), comments: undefined } as any;
|
const issue = { ...makeIssue({ id: 0 }), comments: undefined } as any;
|
||||||
const p = store.upsert(issue);
|
const p = store.upsert(issue);
|
||||||
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 996, comments: [] }));
|
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues` }).flush(makeIssue({ id: 996, comments: [] }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(996)?.comments).toEqual([]);
|
expect(store.getById(996)?.comments).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -160,7 +160,7 @@ describe('IssuesStore', () => {
|
|||||||
it('sets default type to Story when type is missing', async () => {
|
it('sets default type to Story when type is missing', async () => {
|
||||||
const issue = { ...makeIssue({ id: 0 }), type: undefined } as any;
|
const issue = { ...makeIssue({ id: 0 }), type: undefined } as any;
|
||||||
const p = store.upsert(issue);
|
const p = store.upsert(issue);
|
||||||
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush(makeIssue({ id: 995, type: 'Story' }));
|
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues` }).flush(makeIssue({ id: 995, type: 'Story' }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(995)?.type).toBe('Story');
|
expect(store.getById(995)?.type).toBe('Story');
|
||||||
});
|
});
|
||||||
@@ -168,7 +168,7 @@ describe('IssuesStore', () => {
|
|||||||
it('sets estimatedTime to null when missing in API response', async () => {
|
it('sets estimatedTime to null when missing in API response', async () => {
|
||||||
const issue = { ...makeIssue({ id: 0 }), estimatedTime: undefined } as any;
|
const issue = { ...makeIssue({ id: 0 }), estimatedTime: undefined } as any;
|
||||||
const p = store.upsert(issue);
|
const p = store.upsert(issue);
|
||||||
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush({ ...makeIssue({ id: 994 }), estimatedTime: undefined });
|
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues` }).flush({ ...makeIssue({ id: 994 }), estimatedTime: undefined });
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(994)?.estimatedTime).toBeNull();
|
expect(store.getById(994)?.estimatedTime).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -176,7 +176,7 @@ describe('IssuesStore', () => {
|
|||||||
it('defaults startDate to empty string when missing in API response', async () => {
|
it('defaults startDate to empty string when missing in API response', async () => {
|
||||||
const issue = { ...makeIssue({ id: 0 }), startDate: undefined } as any;
|
const issue = { ...makeIssue({ id: 0 }), startDate: undefined } as any;
|
||||||
const p = store.upsert(issue);
|
const p = store.upsert(issue);
|
||||||
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush({ ...makeIssue({ id: 993 }), startDate: undefined });
|
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues` }).flush({ ...makeIssue({ id: 993 }), startDate: undefined });
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(993)?.startDate).toBe('');
|
expect(store.getById(993)?.startDate).toBe('');
|
||||||
});
|
});
|
||||||
@@ -184,7 +184,7 @@ describe('IssuesStore', () => {
|
|||||||
it('defaults endDate to empty string when missing in API response', async () => {
|
it('defaults endDate to empty string when missing in API response', async () => {
|
||||||
const issue = { ...makeIssue({ id: 0 }), endDate: undefined } as any;
|
const issue = { ...makeIssue({ id: 0 }), endDate: undefined } as any;
|
||||||
const p = store.upsert(issue);
|
const p = store.upsert(issue);
|
||||||
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/issues` }).flush({ ...makeIssue({ id: 992 }), endDate: undefined });
|
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues` }).flush({ ...makeIssue({ id: 992 }), endDate: undefined });
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(992)?.endDate).toBe('');
|
expect(store.getById(992)?.endDate).toBe('');
|
||||||
});
|
});
|
||||||
@@ -192,7 +192,7 @@ describe('IssuesStore', () => {
|
|||||||
it('preserves startDate and endDate when provided', async () => {
|
it('preserves startDate and endDate when provided', async () => {
|
||||||
const issue = makeIssue({ id: 0, startDate: '2026-01-01', endDate: '2026-01-31' });
|
const issue = makeIssue({ id: 0, startDate: '2026-01-01', endDate: '2026-01-31' });
|
||||||
const p = store.upsert(issue);
|
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' }));
|
httpMock.expectOne({ method: 'POST', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues` }).flush(makeIssue({ id: 991, startDate: '2026-01-01', endDate: '2026-01-31' }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(991)?.startDate).toBe('2026-01-01');
|
expect(store.getById(991)?.startDate).toBe('2026-01-01');
|
||||||
expect(store.getById(991)?.endDate).toBe('2026-01-31');
|
expect(store.getById(991)?.endDate).toBe('2026-01-31');
|
||||||
@@ -202,7 +202,7 @@ describe('IssuesStore', () => {
|
|||||||
const issue = makeIssue({ id: 1, startDateMode: 'calculated' });
|
const issue = makeIssue({ id: 1, startDateMode: 'calculated' });
|
||||||
const p = store.upsert(issue);
|
const p = store.upsert(issue);
|
||||||
const apiResponse = { ...makeIssue({ id: 1 }), startDateMode: undefined };
|
const apiResponse = { ...makeIssue({ id: 1 }), startDateMode: undefined };
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(apiResponse);
|
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/1` }).flush(apiResponse);
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(1)?.startDateMode).toBe('calculated');
|
expect(store.getById(1)?.startDateMode).toBe('calculated');
|
||||||
});
|
});
|
||||||
@@ -211,7 +211,7 @@ describe('IssuesStore', () => {
|
|||||||
const issueWithComment = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: [2, 3] }] });
|
const issueWithComment = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: [2, 3] }] });
|
||||||
const apiResponse = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: undefined as any }] });
|
const apiResponse = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: undefined as any }] });
|
||||||
const p = store.upsert(issueWithComment);
|
const p = store.upsert(issueWithComment);
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(apiResponse);
|
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/1` }).flush(apiResponse);
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(1)?.comments[0].linkedIssueIds).toEqual([2, 3]);
|
expect(store.getById(1)?.comments[0].linkedIssueIds).toEqual([2, 3]);
|
||||||
});
|
});
|
||||||
@@ -220,7 +220,7 @@ describe('IssuesStore', () => {
|
|||||||
const issue = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: [5] }] });
|
const issue = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: [5] }] });
|
||||||
const apiResponse = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: [5] }] });
|
const apiResponse = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: [5] }] });
|
||||||
const p = store.upsert(issue);
|
const p = store.upsert(issue);
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(apiResponse);
|
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/1` }).flush(apiResponse);
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(1)?.comments[0].linkedIssueIds).toEqual([5]);
|
expect(store.getById(1)?.comments[0].linkedIssueIds).toEqual([5]);
|
||||||
});
|
});
|
||||||
@@ -233,7 +233,7 @@ describe('IssuesStore', () => {
|
|||||||
makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-01', endDate: '' }),
|
makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-01', endDate: '' }),
|
||||||
]);
|
]);
|
||||||
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(2)?.startDate).toBe('2026-06-10');
|
expect(store.getById(2)?.startDate).toBe('2026-06-10');
|
||||||
});
|
});
|
||||||
@@ -244,7 +244,7 @@ describe('IssuesStore', () => {
|
|||||||
makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-01', estimatedTime: 16, endDate: '2026-06-02' }),
|
makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-01', estimatedTime: 16, endDate: '2026-06-02' }),
|
||||||
]);
|
]);
|
||||||
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(2)?.startDate).toBe('2026-06-10');
|
expect(store.getById(2)?.startDate).toBe('2026-06-10');
|
||||||
expect(store.getById(2)?.endDate).toBe('2026-06-11');
|
expect(store.getById(2)?.endDate).toBe('2026-06-11');
|
||||||
@@ -256,7 +256,7 @@ describe('IssuesStore', () => {
|
|||||||
makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-10', estimatedTime: 8, endDate: '2026-06-10' }),
|
makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-10', estimatedTime: 8, endDate: '2026-06-10' }),
|
||||||
]);
|
]);
|
||||||
const p = store.upsert(makeIssue({ id: 1, endDate: '' }));
|
const p = store.upsert(makeIssue({ id: 1, endDate: '' }));
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '' }));
|
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/1` }).flush(makeIssue({ id: 1, endDate: '' }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(2)?.startDate).toBe('');
|
expect(store.getById(2)?.startDate).toBe('');
|
||||||
expect(store.getById(2)?.endDate).toBe('');
|
expect(store.getById(2)?.endDate).toBe('');
|
||||||
@@ -269,7 +269,7 @@ describe('IssuesStore', () => {
|
|||||||
makeIssue({ id: 3, startDateMode: 'calculated', dependsOnIds: [2], startDate: '2026-06-01', estimatedTime: 16, endDate: '2026-06-02' }),
|
makeIssue({ id: 3, startDateMode: 'calculated', dependsOnIds: [2], startDate: '2026-06-01', estimatedTime: 16, endDate: '2026-06-02' }),
|
||||||
]);
|
]);
|
||||||
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(2)?.startDate).toBe('2026-06-10');
|
expect(store.getById(2)?.startDate).toBe('2026-06-10');
|
||||||
expect(store.getById(2)?.endDate).toBe('2026-06-10');
|
expect(store.getById(2)?.endDate).toBe('2026-06-10');
|
||||||
@@ -283,7 +283,7 @@ describe('IssuesStore', () => {
|
|||||||
makeIssue({ id: 2, startDateMode: 'forced', dependsOnIds: [1], startDate: '2026-05-01', endDate: '2026-05-15' }),
|
makeIssue({ id: 2, startDateMode: 'forced', dependsOnIds: [1], startDate: '2026-05-01', endDate: '2026-05-15' }),
|
||||||
]);
|
]);
|
||||||
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(2)?.startDate).toBe('2026-05-01');
|
expect(store.getById(2)?.startDate).toBe('2026-05-01');
|
||||||
expect(store.getById(2)?.endDate).toBe('2026-05-15');
|
expect(store.getById(2)?.endDate).toBe('2026-05-15');
|
||||||
@@ -296,7 +296,7 @@ describe('IssuesStore', () => {
|
|||||||
makeIssue({ id: 3, startDateMode: 'calculated', dependsOnIds: [1, 2], startDate: '', estimatedTime: 8, endDate: '' }),
|
makeIssue({ id: 3, startDateMode: 'calculated', dependsOnIds: [1, 2], startDate: '', estimatedTime: 8, endDate: '' }),
|
||||||
]);
|
]);
|
||||||
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(3)?.startDate).toBe('2026-06-10');
|
expect(store.getById(3)?.startDate).toBe('2026-06-10');
|
||||||
expect(store.getById(3)?.endDate).toBe('2026-06-10');
|
expect(store.getById(3)?.endDate).toBe('2026-06-10');
|
||||||
@@ -309,7 +309,7 @@ describe('IssuesStore', () => {
|
|||||||
makeIssue({ id: 3, startDateMode: 'calculated', dependsOnIds: [1, 2], startDate: '2026-06-10', estimatedTime: 8, endDate: '2026-06-10' }),
|
makeIssue({ id: 3, startDateMode: 'calculated', dependsOnIds: [1, 2], startDate: '2026-06-10', estimatedTime: 8, endDate: '2026-06-10' }),
|
||||||
]);
|
]);
|
||||||
const p = store.deleteById(1);
|
const p = store.deleteById(1);
|
||||||
httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/issues/1` }).flush(null);
|
httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/1` }).flush(null);
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(3)?.startDate).toBe('2026-06-05');
|
expect(store.getById(3)?.startDate).toBe('2026-06-05');
|
||||||
expect(store.getById(3)?.endDate).toBe('2026-06-05');
|
expect(store.getById(3)?.endDate).toBe('2026-06-05');
|
||||||
@@ -327,21 +327,21 @@ describe('IssuesStore', () => {
|
|||||||
|
|
||||||
it('removes the issue from the store', async () => {
|
it('removes the issue from the store', async () => {
|
||||||
const p = store.deleteById(1);
|
const p = store.deleteById(1);
|
||||||
httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/issues/1` }).flush(null);
|
httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/1` }).flush(null);
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(1)).toBeUndefined();
|
expect(store.getById(1)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes the deleted id from dependsOnIds of other issues', async () => {
|
it('removes the deleted id from dependsOnIds of other issues', async () => {
|
||||||
const p = store.deleteById(100);
|
const p = store.deleteById(100);
|
||||||
httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/issues/100` }).flush(null);
|
httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/100` }).flush(null);
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(101)?.dependsOnIds).toEqual([]);
|
expect(store.getById(101)?.dependsOnIds).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not affect issues with unrelated dependsOnIds', async () => {
|
it('does not affect issues with unrelated dependsOnIds', async () => {
|
||||||
const p = store.deleteById(1);
|
const p = store.deleteById(1);
|
||||||
httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/issues/1` }).flush(null);
|
httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/projects/${PROJECT_ID}/issues/1` }).flush(null);
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(101)?.dependsOnIds).toEqual([100]);
|
expect(store.getById(101)?.dependsOnIds).toEqual([100]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -70,12 +70,12 @@ export class IssuesStore {
|
|||||||
const normalized = this.normalizeIssue(issue);
|
const normalized = this.normalizeIssue(issue);
|
||||||
if (!normalized.id) {
|
if (!normalized.id) {
|
||||||
const { id: _id, ...body } = normalized;
|
const { id: _id, ...body } = normalized;
|
||||||
const created = this.normalizeIssue(await firstValueFrom(this.api.create(body)));
|
const created = this.normalizeIssue(await firstValueFrom(this.api.create(this.currentProjectId!, body)));
|
||||||
this.data.update((issues) => [...issues, created]);
|
this.data.update((issues) => [...issues, created]);
|
||||||
this.recalculateCalculatedIssues();
|
this.recalculateCalculatedIssues();
|
||||||
return created;
|
return created;
|
||||||
} else {
|
} else {
|
||||||
const apiResult = await firstValueFrom(this.api.update(normalized.id, normalized));
|
const apiResult = await firstValueFrom(this.api.update(this.currentProjectId!, normalized.id, normalized));
|
||||||
// L'API ne retourne pas linkedIssueIds dans les commentaires : on le restaure
|
// L'API ne retourne pas linkedIssueIds dans les commentaires : on le restaure
|
||||||
// depuis les données envoyées pour ne pas perdre les liens.
|
// depuis les données envoyées pour ne pas perdre les liens.
|
||||||
if (Array.isArray(apiResult.comments) && Array.isArray(normalized.comments)) {
|
if (Array.isArray(apiResult.comments) && Array.isArray(normalized.comments)) {
|
||||||
@@ -103,7 +103,7 @@ export class IssuesStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteById(id: number): Promise<void> {
|
async deleteById(id: number): Promise<void> {
|
||||||
await firstValueFrom(this.api.remove(id));
|
await firstValueFrom(this.api.remove(this.currentProjectId!, id));
|
||||||
this.data.update((issues) =>
|
this.data.update((issues) =>
|
||||||
issues
|
issues
|
||||||
.filter((i) => i.id !== id)
|
.filter((i) => i.id !== id)
|
||||||
|
|||||||
@@ -33,25 +33,24 @@ describe('MilestonesApiService', () => {
|
|||||||
afterEach(() => http.verify());
|
afterEach(() => http.verify());
|
||||||
|
|
||||||
describe('getAll', () => {
|
describe('getAll', () => {
|
||||||
it('sends GET /api/milestones?projectId=1 and returns milestones', () => {
|
it('sends GET /api/projects/1/milestones and returns milestones', () => {
|
||||||
const milestones = [makeMilestone({ id: 1 }), makeMilestone({ id: 2 })];
|
const milestones = [makeMilestone({ id: 1 }), makeMilestone({ id: 2 })];
|
||||||
let result: MilestoneEntity[] | undefined;
|
let result: MilestoneEntity[] | undefined;
|
||||||
service.getAll(1).subscribe((data) => (result = data));
|
service.getAll(1).subscribe((data) => (result = data));
|
||||||
const req = http.expectOne(`${API}/milestones?projectId=1`);
|
const req = http.expectOne(`${API}/projects/1/milestones`);
|
||||||
expect(req.request.method).toBe('GET');
|
expect(req.request.method).toBe('GET');
|
||||||
expect(req.request.params.get('projectId')).toBe('1');
|
|
||||||
req.flush(milestones);
|
req.flush(milestones);
|
||||||
expect(result).toEqual(milestones);
|
expect(result).toEqual(milestones);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('create', () => {
|
describe('create', () => {
|
||||||
it('sends POST /api/milestones with the body and returns the created milestone', () => {
|
it('sends POST /api/projects/1/milestones with the body and returns the created milestone', () => {
|
||||||
const body = { name: 'Sprint 2', description: '', startDate: '', endDate: '', dueDate: '', issueIds: [], dependsOnIds: [] };
|
const body = { name: 'Sprint 2', description: '', startDate: '', endDate: '', dueDate: '', issueIds: [], dependsOnIds: [] };
|
||||||
const response = makeMilestone({ id: 2, name: 'Sprint 2' });
|
const response = makeMilestone({ id: 2, name: 'Sprint 2' });
|
||||||
let result: MilestoneEntity | undefined;
|
let result: MilestoneEntity | undefined;
|
||||||
service.create(body).subscribe((data) => (result = data));
|
service.create(1, body).subscribe((data) => (result = data));
|
||||||
const req = http.expectOne(`${API}/milestones`);
|
const req = http.expectOne(`${API}/projects/1/milestones`);
|
||||||
expect(req.request.method).toBe('POST');
|
expect(req.request.method).toBe('POST');
|
||||||
expect(req.request.body).toEqual(body);
|
expect(req.request.body).toEqual(body);
|
||||||
req.flush(response);
|
req.flush(response);
|
||||||
@@ -60,11 +59,11 @@ describe('MilestonesApiService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
it('sends PUT /api/milestones/:id with the body and returns the updated milestone', () => {
|
it('sends PUT /api/projects/1/milestones/:id with the body and returns the updated milestone', () => {
|
||||||
const milestone = makeMilestone({ id: 1, name: 'Updated Sprint' });
|
const milestone = makeMilestone({ id: 1, name: 'Updated Sprint' });
|
||||||
let result: MilestoneEntity | undefined;
|
let result: MilestoneEntity | undefined;
|
||||||
service.update(1, milestone).subscribe((data) => (result = data));
|
service.update(1, 1, milestone).subscribe((data) => (result = data));
|
||||||
const req = http.expectOne(`${API}/milestones/1`);
|
const req = http.expectOne(`${API}/projects/1/milestones/1`);
|
||||||
expect(req.request.method).toBe('PUT');
|
expect(req.request.method).toBe('PUT');
|
||||||
expect(req.request.body).toEqual(milestone);
|
expect(req.request.body).toEqual(milestone);
|
||||||
req.flush(milestone);
|
req.flush(milestone);
|
||||||
@@ -73,10 +72,10 @@ describe('MilestonesApiService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('remove', () => {
|
describe('remove', () => {
|
||||||
it('sends DELETE /api/milestones/:id and completes', () => {
|
it('sends DELETE /api/projects/1/milestones/:id and completes', () => {
|
||||||
let completed = false;
|
let completed = false;
|
||||||
service.remove(1).subscribe({ complete: () => (completed = true) });
|
service.remove(1, 1).subscribe({ complete: () => (completed = true) });
|
||||||
const req = http.expectOne(`${API}/milestones/1`);
|
const req = http.expectOne(`${API}/projects/1/milestones/1`);
|
||||||
expect(req.request.method).toBe('DELETE');
|
expect(req.request.method).toBe('DELETE');
|
||||||
req.flush(null);
|
req.flush(null);
|
||||||
expect(completed).toBe(true);
|
expect(completed).toBe(true);
|
||||||
|
|||||||
@@ -9,21 +9,19 @@ export class MilestonesApiService {
|
|||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
getAll(projectId: number): Observable<MilestoneEntity[]> {
|
getAll(projectId: number): Observable<MilestoneEntity[]> {
|
||||||
return this.http.get<MilestoneEntity[]>(`${API_BASE_URL}/milestones`, {
|
return this.http.get<MilestoneEntity[]>(`${API_BASE_URL}/projects/${projectId}/milestones`);
|
||||||
params: { projectId: projectId.toString() },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
create(milestone: Omit<MilestoneEntity, 'id'>): Observable<MilestoneEntity> {
|
create(projectId: number, milestone: Omit<MilestoneEntity, 'id'>): Observable<MilestoneEntity> {
|
||||||
return this.http.post<MilestoneEntity>(`${API_BASE_URL}/milestones`, milestone);
|
return this.http.post<MilestoneEntity>(`${API_BASE_URL}/projects/${projectId}/milestones`, milestone);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: number, milestone: MilestoneEntity): Observable<MilestoneEntity> {
|
update(projectId: number, id: number, milestone: MilestoneEntity): Observable<MilestoneEntity> {
|
||||||
return this.http.put<MilestoneEntity>(`${API_BASE_URL}/milestones/${id}`, milestone);
|
return this.http.put<MilestoneEntity>(`${API_BASE_URL}/projects/${projectId}/milestones/${id}`, milestone);
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(id: number): Observable<void> {
|
remove(projectId: number, id: number): Observable<void> {
|
||||||
return this.http.delete<void>(`${API_BASE_URL}/milestones/${id}`);
|
return this.http.delete<void>(`${API_BASE_URL}/projects/${projectId}/milestones/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
migrate(id: number, targetProjectId: number): Observable<void> {
|
migrate(id: number, targetProjectId: number): Observable<void> {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe('MilestonesStore', () => {
|
|||||||
let httpMock: HttpTestingController;
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
const PROJECT_ID = 1;
|
const PROJECT_ID = 1;
|
||||||
const MILESTONES_URL = `${API}/milestones?projectId=${PROJECT_ID}`;
|
const MILESTONES_URL = `${API}/projects/${PROJECT_ID}/milestones`;
|
||||||
|
|
||||||
const loadWith = async (milestones: MilestoneEntity[]) => {
|
const loadWith = async (milestones: MilestoneEntity[]) => {
|
||||||
const p = store.load(PROJECT_ID);
|
const p = store.load(PROJECT_ID);
|
||||||
@@ -67,7 +67,7 @@ describe('MilestonesStore', () => {
|
|||||||
|
|
||||||
it('reloads when projectId changes', async () => {
|
it('reloads when projectId changes', async () => {
|
||||||
await loadWith([makeMilestone({ id: 1 })]);
|
await loadWith([makeMilestone({ id: 1 })]);
|
||||||
const url2 = `${API}/milestones?projectId=2`;
|
const url2 = `${API}/projects/2/milestones`;
|
||||||
const p = store.load(2);
|
const p = store.load(2);
|
||||||
httpMock.expectOne(url2).flush([makeMilestone({ id: 2 })]);
|
httpMock.expectOne(url2).flush([makeMilestone({ id: 2 })]);
|
||||||
await p;
|
await p;
|
||||||
@@ -98,7 +98,7 @@ describe('MilestonesStore', () => {
|
|||||||
it('creates a new milestone via POST when id is 0', async () => {
|
it('creates a new milestone via POST when id is 0', async () => {
|
||||||
const before = store.milestones().length;
|
const before = store.milestones().length;
|
||||||
const p = store.upsert(makeMilestone({ id: 0, name: 'New Sprint' }));
|
const p = store.upsert(makeMilestone({ id: 0, name: 'New Sprint' }));
|
||||||
httpMock.expectOne({ method: 'POST', url: `${API}/milestones` }).flush(makeMilestone({ id: 99, name: 'New Sprint' }));
|
httpMock.expectOne({ method: 'POST', url: `${API}/projects/${PROJECT_ID}/milestones` }).flush(makeMilestone({ id: 99, name: 'New Sprint' }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.milestones().length).toBe(before + 1);
|
expect(store.milestones().length).toBe(before + 1);
|
||||||
expect(store.getById(99)?.name).toBe('New Sprint');
|
expect(store.getById(99)?.name).toBe('New Sprint');
|
||||||
@@ -106,7 +106,7 @@ describe('MilestonesStore', () => {
|
|||||||
|
|
||||||
it('updates an existing milestone via PUT', async () => {
|
it('updates an existing milestone via PUT', async () => {
|
||||||
const p = store.upsert(makeMilestone({ id: 1, name: 'Updated Sprint' }));
|
const p = store.upsert(makeMilestone({ id: 1, name: 'Updated Sprint' }));
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API}/milestones/1` }).flush(makeMilestone({ id: 1, name: 'Updated Sprint' }));
|
httpMock.expectOne({ method: 'PUT', url: `${API}/projects/${PROJECT_ID}/milestones/1` }).flush(makeMilestone({ id: 1, name: 'Updated Sprint' }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(1)?.name).toBe('Updated Sprint');
|
expect(store.getById(1)?.name).toBe('Updated Sprint');
|
||||||
expect(store.milestones().filter((m) => m.id === 1).length).toBe(1);
|
expect(store.milestones().filter((m) => m.id === 1).length).toBe(1);
|
||||||
@@ -114,7 +114,7 @@ describe('MilestonesStore', () => {
|
|||||||
|
|
||||||
it('returns the normalized milestone after update', async () => {
|
it('returns the normalized milestone after update', async () => {
|
||||||
const p = store.upsert(makeMilestone({ id: 1, name: 'Updated', issueIds: [1, 2] }));
|
const p = store.upsert(makeMilestone({ id: 1, name: 'Updated', issueIds: [1, 2] }));
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API}/milestones/1` }).flush(makeMilestone({ id: 1, name: 'Updated', issueIds: [1, 2] }));
|
httpMock.expectOne({ method: 'PUT', url: `${API}/projects/${PROJECT_ID}/milestones/1` }).flush(makeMilestone({ id: 1, name: 'Updated', issueIds: [1, 2] }));
|
||||||
const result = await p;
|
const result = await p;
|
||||||
expect(result.issueIds).toEqual([1, 2]);
|
expect(result.issueIds).toEqual([1, 2]);
|
||||||
});
|
});
|
||||||
@@ -122,7 +122,7 @@ describe('MilestonesStore', () => {
|
|||||||
it('leaves list unchanged when PUT response id is not found in store', async () => {
|
it('leaves list unchanged when PUT response id is not found in store', async () => {
|
||||||
const before = store.milestones().length;
|
const before = store.milestones().length;
|
||||||
const p = store.upsert(makeMilestone({ id: 999, name: 'Unknown' }));
|
const p = store.upsert(makeMilestone({ id: 999, name: 'Unknown' }));
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API}/milestones/999` }).flush(makeMilestone({ id: 999, name: 'Unknown' }));
|
httpMock.expectOne({ method: 'PUT', url: `${API}/projects/${PROJECT_ID}/milestones/999` }).flush(makeMilestone({ id: 999, name: 'Unknown' }));
|
||||||
await p;
|
await p;
|
||||||
expect(store.milestones().length).toBe(before);
|
expect(store.milestones().length).toBe(before);
|
||||||
});
|
});
|
||||||
@@ -135,7 +135,7 @@ describe('MilestonesStore', () => {
|
|||||||
|
|
||||||
it('removes the milestone from the store', async () => {
|
it('removes the milestone from the store', async () => {
|
||||||
const p = store.deleteById(1);
|
const p = store.deleteById(1);
|
||||||
httpMock.expectOne({ method: 'DELETE', url: `${API}/milestones/1` }).flush(null);
|
httpMock.expectOne({ method: 'DELETE', url: `${API}/projects/${PROJECT_ID}/milestones/1` }).flush(null);
|
||||||
await p;
|
await p;
|
||||||
expect(store.getById(1)).toBeUndefined();
|
expect(store.getById(1)).toBeUndefined();
|
||||||
expect(store.milestones().length).toBe(1);
|
expect(store.milestones().length).toBe(1);
|
||||||
@@ -174,10 +174,14 @@ describe('MilestonesStore', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('normalize', () => {
|
describe('normalize', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await loadWith([makeMilestone({ id: 1 })]);
|
||||||
|
});
|
||||||
|
|
||||||
it('normalizes issueIds to empty array when not an array', async () => {
|
it('normalizes issueIds to empty array when not an array', async () => {
|
||||||
const raw = { id: 1, name: 'Sprint', description: '', dueDate: '', issueIds: null } as any;
|
const raw = { id: 1, name: 'Sprint', description: '', dueDate: '', issueIds: null } as any;
|
||||||
const p = store.upsert(raw);
|
const p = store.upsert(raw);
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API}/milestones/1` }).flush({ ...raw, issueIds: null });
|
httpMock.expectOne({ method: 'PUT', url: `${API}/projects/${PROJECT_ID}/milestones/1` }).flush({ ...raw, issueIds: null });
|
||||||
const result = await p;
|
const result = await p;
|
||||||
expect(result.issueIds).toEqual([]);
|
expect(result.issueIds).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -185,7 +189,7 @@ describe('MilestonesStore', () => {
|
|||||||
it('filters non-number values from issueIds', async () => {
|
it('filters non-number values from issueIds', async () => {
|
||||||
const raw = { id: 1, name: 'Sprint', description: '', dueDate: '', issueIds: [1, 'bad', null] } as any;
|
const raw = { id: 1, name: 'Sprint', description: '', dueDate: '', issueIds: [1, 'bad', null] } as any;
|
||||||
const p = store.upsert(raw);
|
const p = store.upsert(raw);
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API}/milestones/1` }).flush({ id: 1, name: 'Sprint', description: '', dueDate: '', issueIds: [1] });
|
httpMock.expectOne({ method: 'PUT', url: `${API}/projects/${PROJECT_ID}/milestones/1` }).flush({ id: 1, name: 'Sprint', description: '', dueDate: '', issueIds: [1] });
|
||||||
const result = await p;
|
const result = await p;
|
||||||
expect(result.issueIds).toEqual([1]);
|
expect(result.issueIds).toEqual([1]);
|
||||||
});
|
});
|
||||||
@@ -193,7 +197,7 @@ describe('MilestonesStore', () => {
|
|||||||
it('normalizes dependsOnIds to empty array when not an array', async () => {
|
it('normalizes dependsOnIds to empty array when not an array', async () => {
|
||||||
const raw = { id: 1, name: 'Sprint', description: '', dueDate: '', issueIds: [], dependsOnIds: null } as any;
|
const raw = { id: 1, name: 'Sprint', description: '', dueDate: '', issueIds: [], dependsOnIds: null } as any;
|
||||||
const p = store.upsert(raw);
|
const p = store.upsert(raw);
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API}/milestones/1` }).flush({ ...raw, dependsOnIds: null });
|
httpMock.expectOne({ method: 'PUT', url: `${API}/projects/${PROJECT_ID}/milestones/1` }).flush({ ...raw, dependsOnIds: null });
|
||||||
const result = await p;
|
const result = await p;
|
||||||
expect(result.dependsOnIds).toEqual([]);
|
expect(result.dependsOnIds).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -201,7 +205,7 @@ describe('MilestonesStore', () => {
|
|||||||
it('filters non-number values from dependsOnIds', async () => {
|
it('filters non-number values from dependsOnIds', async () => {
|
||||||
const raw = { id: 1, name: 'Sprint', description: '', dueDate: '', issueIds: [], dependsOnIds: [2, 'bad', null] } as any;
|
const raw = { id: 1, name: 'Sprint', description: '', dueDate: '', issueIds: [], dependsOnIds: [2, 'bad', null] } as any;
|
||||||
const p = store.upsert(raw);
|
const p = store.upsert(raw);
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API}/milestones/1` }).flush({ id: 1, name: 'Sprint', description: '', dueDate: '', issueIds: [], dependsOnIds: [2] });
|
httpMock.expectOne({ method: 'PUT', url: `${API}/projects/${PROJECT_ID}/milestones/1` }).flush({ id: 1, name: 'Sprint', description: '', dueDate: '', issueIds: [], dependsOnIds: [2] });
|
||||||
const result = await p;
|
const result = await p;
|
||||||
expect(result.dependsOnIds).toEqual([2]);
|
expect(result.dependsOnIds).toEqual([2]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ export class MilestonesStore {
|
|||||||
const normalized = this.normalize(milestone);
|
const normalized = this.normalize(milestone);
|
||||||
if (!normalized.id) {
|
if (!normalized.id) {
|
||||||
const { id: _id, ...body } = normalized;
|
const { id: _id, ...body } = normalized;
|
||||||
const created = this.normalize(await firstValueFrom(this.api.create(body)));
|
const created = this.normalize(await firstValueFrom(this.api.create(this.currentProjectId!, body)));
|
||||||
this.data.update((list) => [...list, created]);
|
this.data.update((list) => [...list, created]);
|
||||||
return created;
|
return created;
|
||||||
} else {
|
} else {
|
||||||
const updated = this.normalize(
|
const updated = this.normalize(
|
||||||
await firstValueFrom(this.api.update(normalized.id, normalized)),
|
await firstValueFrom(this.api.update(this.currentProjectId!, normalized.id, normalized)),
|
||||||
);
|
);
|
||||||
this.data.update((list) => {
|
this.data.update((list) => {
|
||||||
const idx = list.findIndex((m) => m.id === normalized.id);
|
const idx = list.findIndex((m) => m.id === normalized.id);
|
||||||
@@ -64,13 +64,11 @@ export class MilestonesStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteById(id: number): Promise<void> {
|
async deleteById(id: number): Promise<void> {
|
||||||
await firstValueFrom(this.api.remove(id));
|
await firstValueFrom(this.api.remove(this.currentProjectId!, id));
|
||||||
this.data.update((list) => list.filter((m) => m.id !== id));
|
this.data.update((list) => list.filter((m) => m.id !== id));
|
||||||
}
|
}
|
||||||
|
|
||||||
async migrate(id: number, targetProjectId: number): Promise<void> {
|
async migrate(id: number, targetProjectId: number): Promise<void> {
|
||||||
// L'endpoint /migrate n'existe pas encore côté API (voir api-issues/migration-milestone-projet.md).
|
|
||||||
// catchError absorbe le 404 pour que la suppression locale s'effectue quand même.
|
|
||||||
await firstValueFrom(this.api.migrate(id, targetProjectId).pipe(catchError(() => of(null))));
|
await firstValueFrom(this.api.migrate(id, targetProjectId).pipe(catchError(() => of(null))));
|
||||||
this.data.update((list) => list.filter((m) => m.id !== id));
|
this.data.update((list) => list.filter((m) => m.id !== id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { signal } from '@angular/core';
|
import { Component, signal } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { provideRouter, Router } from '@angular/router';
|
import { provideRouter, Router } from '@angular/router';
|
||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
import { ProjectEntity, ProjectsStore } from './projects.store';
|
import { ProjectEntity, ProjectsStore } from './projects.store';
|
||||||
import { Projects } from './projects';
|
import { Projects } from './projects';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({ standalone: true, template: '' })
|
||||||
|
class StubRouteComponent {}
|
||||||
|
|
||||||
|
|
||||||
const makeProject = (overrides: Partial<ProjectEntity> = {}): ProjectEntity => ({
|
const makeProject = (overrides: Partial<ProjectEntity> = {}): ProjectEntity => ({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Mon Projet',
|
name: 'Mon Projet',
|
||||||
@@ -32,16 +37,18 @@ describe('Projects', () => {
|
|||||||
load: vi.fn().mockResolvedValue(undefined),
|
load: vi.fn().mockResolvedValue(undefined),
|
||||||
getById: vi.fn(),
|
getById: vi.fn(),
|
||||||
createLocal: vi.fn((name: string, owner: string): ProjectEntity => makeProject({ id: 10, name, owner })),
|
createLocal: vi.fn((name: string, owner: string): ProjectEntity => makeProject({ id: 10, name, owner })),
|
||||||
|
upsert: vi.fn().mockResolvedValue(makeProject({ id: 10 })),
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
storeMock.load = vi.fn().mockResolvedValue(undefined);
|
storeMock.load = vi.fn().mockResolvedValue(undefined);
|
||||||
storeMock.createLocal = vi.fn((name: string, owner: string): ProjectEntity => makeProject({ id: 10, name, owner }));
|
storeMock.createLocal = vi.fn((name: string, owner: string): ProjectEntity => makeProject({ id: 10, name, owner }));
|
||||||
|
storeMock.upsert = vi.fn().mockResolvedValue(makeProject({ id: 10 }));
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [Projects],
|
imports: [Projects],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([{ path: 'projects/:id/dashboard', component: StubRouteComponent }]),
|
||||||
{ provide: ProjectsStore, useValue: storeMock },
|
{ provide: ProjectsStore, useValue: storeMock },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
@@ -102,27 +109,28 @@ describe('Projects', () => {
|
|||||||
expect((component as any).showForm()).toBe(false);
|
expect((component as any).showForm()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('createProject calls store.createLocal with name and owner', () => {
|
it('createProject calls store.upsert with name, owner and defaults', async () => {
|
||||||
(component as any).newName = 'Mon Projet';
|
(component as any).newName = 'Mon Projet';
|
||||||
(component as any).newOwner = 'Bob';
|
(component as any).newOwner = 'Bob';
|
||||||
const spy = vi.spyOn(router, 'navigate');
|
await (component as any).createProject();
|
||||||
(component as any).createProject();
|
expect(storeMock.upsert).toHaveBeenCalledWith({
|
||||||
expect(storeMock.createLocal).toHaveBeenCalledWith('Mon Projet', 'Bob');
|
id: 0, name: 'Mon Projet', owner: 'Bob', status: 'Nouveau', progress: 0,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('createProject navigates to the new project dashboard', () => {
|
it('createProject navigates to the new project dashboard', async () => {
|
||||||
(component as any).newName = 'Test';
|
(component as any).newName = 'Test';
|
||||||
(component as any).newOwner = '';
|
(component as any).newOwner = '';
|
||||||
const spy = vi.spyOn(router, 'navigate');
|
const spy = vi.spyOn(router, 'navigate');
|
||||||
(component as any).createProject();
|
await (component as any).createProject();
|
||||||
expect(spy).toHaveBeenCalledWith(['/projects', 10, 'dashboard']);
|
expect(spy).toHaveBeenCalledWith(['/projects', 10, 'dashboard']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('createProject does nothing when name is empty', () => {
|
it('createProject does nothing when name is empty', async () => {
|
||||||
(component as any).newName = ' ';
|
(component as any).newName = ' ';
|
||||||
const spy = vi.spyOn(router, 'navigate');
|
const spy = vi.spyOn(router, 'navigate');
|
||||||
(component as any).createProject();
|
await (component as any).createProject();
|
||||||
expect(storeMock.createLocal).not.toHaveBeenCalled();
|
expect(storeMock.upsert).not.toHaveBeenCalled();
|
||||||
expect(spy).not.toHaveBeenCalled();
|
expect(spy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -122,6 +122,17 @@ describe('ProjectsStore', () => {
|
|||||||
expect(store.getById(99)?.name).toBe('New Project');
|
expect(store.getById(99)?.name).toBe('New Project');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('preserves local fields when the API response omits owner/status/progress', async () => {
|
||||||
|
const p = store.upsert(makeProject({ id: 0, name: 'Minimal', owner: 'Cedric', status: 'Nouveau', progress: 0 }));
|
||||||
|
httpMock.expectOne({ method: 'POST', url: `${API}/projects` }).flush({ id: 100, name: 'Minimal' });
|
||||||
|
await p;
|
||||||
|
expect(store.getById(100)?.owner).toBe('Cedric');
|
||||||
|
expect(store.getById(100)?.status).toBe('Nouveau');
|
||||||
|
expect(store.getById(100)?.progress).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
it('updates an existing project via PUT', async () => {
|
it('updates an existing project via PUT', async () => {
|
||||||
const p = store.upsert(makeProject({ id: 1, name: 'Updated' }));
|
const p = store.upsert(makeProject({ id: 1, name: 'Updated' }));
|
||||||
httpMock.expectOne({ method: 'PUT', url: `${API}/projects/1` }).flush(makeProject({ id: 1, name: 'Updated' }));
|
httpMock.expectOne({ method: 'PUT', url: `${API}/projects/1` }).flush(makeProject({ id: 1, name: 'Updated' }));
|
||||||
|
|||||||
@@ -54,8 +54,9 @@ export class ProjectsStore {
|
|||||||
if (!project.id) {
|
if (!project.id) {
|
||||||
const { id: _id, ...body } = project;
|
const { id: _id, ...body } = project;
|
||||||
const created = await firstValueFrom(this.api.create(body));
|
const created = await firstValueFrom(this.api.create(body));
|
||||||
this.data.update((list) => [...list, created]);
|
const merged: ProjectEntity = { ...project, id: created.id };
|
||||||
return created;
|
this.data.update((list) => [...list, merged]);
|
||||||
|
return merged;
|
||||||
} else {
|
} else {
|
||||||
const updated = await firstValueFrom(this.api.update(project.id, project));
|
const updated = await firstValueFrom(this.api.update(project.id, project));
|
||||||
this.data.update((list) => {
|
this.data.update((list) => {
|
||||||
|
|||||||
@@ -38,10 +38,12 @@ export class Projects {
|
|||||||
this.showForm.set(false);
|
this.showForm.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createProject(): void {
|
protected async createProject(): Promise<void> {
|
||||||
const name = this.newName.trim();
|
const name = this.newName.trim();
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
const project = this.projectsStore.createLocal(name, this.newOwner);
|
const project = await this.projectsStore.upsert({
|
||||||
|
id: 0, name, owner: this.newOwner.trim(), status: 'Nouveau', progress: 0,
|
||||||
|
});
|
||||||
this.showForm.set(false);
|
this.showForm.set(false);
|
||||||
this.router.navigate(['/projects', project.id, 'dashboard']);
|
this.router.navigate(['/projects', project.id, 'dashboard']);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user