diff --git a/src/app/issues/issues.css b/src/app/issues/issues.css index 6a9f3bf..e628db9 100644 --- a/src/app/issues/issues.css +++ b/src/app/issues/issues.css @@ -32,5 +32,28 @@ font-weight: 700; } +.sortable-col { + cursor: pointer; + user-select: none; + white-space: nowrap; +} + +.sortable-col:hover { + background-color: #e9ecef; +} + +.sortable-col.sorted { + color: #2563eb; +} + +.sort-icon { + font-size: 0.75rem; + margin-left: 0.25rem; +} + +.sort-icon--idle { + color: #adb5bd; +} + diff --git a/src/app/issues/issues.html b/src/app/issues/issues.html index 11adf5c..887e456 100644 --- a/src/app/issues/issues.html +++ b/src/app/issues/issues.html @@ -165,7 +165,17 @@ # Titre Type - Priorite + + Priorité + @if (sortPriority === 'desc') { } + @else if (sortPriority === 'asc') { } + @else { } + Statut Milestone Assignee diff --git a/src/app/issues/issues.spec.ts b/src/app/issues/issues.spec.ts index 2de6c9f..79358be 100644 --- a/src/app/issues/issues.spec.ts +++ b/src/app/issues/issues.spec.ts @@ -356,6 +356,59 @@ describe('Issues', () => { }); }); + describe('sortPriority — toggleSortPriority', () => { + it('starts with no sort active', () => { + expect((component as any).sortPriority).toBeNull(); + }); + + it('first toggle sets sort to desc (highest first)', () => { + (component as any).toggleSortPriority(); + expect((component as any).sortPriority).toBe('desc'); + }); + + it('second toggle sets sort to asc (lowest first)', () => { + (component as any).sortPriority = 'desc'; + (component as any).toggleSortPriority(); + expect((component as any).sortPriority).toBe('asc'); + }); + + it('third toggle resets sort to null', () => { + (component as any).sortPriority = 'asc'; + (component as any).toggleSortPriority(); + expect((component as any).sortPriority).toBeNull(); + }); + }); + + describe('filteredIssues — priority sort', () => { + beforeEach(() => { + store.upsert(makeIssue({ id: 10, name: 'Très haute', priority: 'TRES_HAUTE' })); + store.upsert(makeIssue({ id: 11, name: 'Basse', priority: 'BASSE' })); + store.upsert(makeIssue({ id: 12, name: 'Haute', priority: 'HAUTE' })); + }); + + it('does not reorder issues when sortPriority is null', () => { + (component as any).sortPriority = null; + const ids = (component as any).filteredIssues.map((i: IssueEntity) => i.id); + expect(ids).toEqual(store.issues().map((i) => i.id)); + }); + + it('sorts highest priority first when sortPriority is desc', () => { + (component as any).sortPriority = 'desc'; + const priorities = (component as any).filteredIssues.map((i: IssueEntity) => i.priority); + const order = ['TRES_HAUTE', 'HAUTE', 'MOYENNE', 'BASSE', 'TRES_FAIBLE']; + const indices = priorities.map((p: string) => order.indexOf(p)); + expect(indices).toEqual([...indices].sort((a, b) => a - b)); + }); + + it('sorts lowest priority first when sortPriority is asc', () => { + (component as any).sortPriority = 'asc'; + const priorities = (component as any).filteredIssues.map((i: IssueEntity) => i.priority); + const order = ['TRES_HAUTE', 'HAUTE', 'MOYENNE', 'BASSE', 'TRES_FAIBLE']; + const indices = priorities.map((p: string) => order.indexOf(p)); + expect(indices).toEqual([...indices].sort((a, b) => b - a)); + }); + }); + describe('createIssue', () => { it('navigates to /projects/:pid/issues/new', () => { const spy = vi.spyOn(router, 'navigate').mockResolvedValue(true); diff --git a/src/app/issues/issues.ts b/src/app/issues/issues.ts index 5e59570..6f55ecf 100644 --- a/src/app/issues/issues.ts +++ b/src/app/issues/issues.ts @@ -57,6 +57,12 @@ export class Issues { 'TRES_HAUTE', 'HAUTE', 'MOYENNE', 'BASSE', 'TRES_FAIBLE', ]; + private readonly PRIORITY_ORDER: Record = { + TRES_HAUTE: 0, HAUTE: 1, MOYENNE: 2, BASSE: 3, TRES_FAIBLE: 4, + }; + + protected sortPriority: 'desc' | 'asc' | null = null; + protected readonly statusOptions = this.statusesStore.statuses; protected getMilestoneForIssue(issueId: number): MilestoneEntity | undefined { @@ -66,7 +72,7 @@ export class Issues { protected get filteredIssues(): IssueEntity[] { const q = this.searchQuery.trim().toLowerCase(); const milestoneActive = this.selectedMilestoneIds.size > 0 || this.showNoMilestone; - return this.issues().filter((i) => { + const filtered = this.issues().filter((i) => { if (this.selectedTypes.size > 0 && !this.selectedTypes.has(i.type)) return false; if (this.selectedStatuses.size > 0 && !this.selectedStatuses.has(i.status)) return false; if (this.selectedPriorities.size > 0 && !this.selectedPriorities.has(i.priority)) return false; @@ -79,6 +85,17 @@ export class Issues { if (q && !i.name.toLowerCase().includes(q)) return false; return true; }); + if (this.sortPriority === null) return filtered; + const dir = this.sortPriority === 'desc' ? 1 : -1; + return [...filtered].sort( + (a, b) => (this.PRIORITY_ORDER[a.priority] - this.PRIORITY_ORDER[b.priority]) * dir, + ); + } + + protected toggleSortPriority(): void { + if (this.sortPriority === null) this.sortPriority = 'desc'; + else if (this.sortPriority === 'desc') this.sortPriority = 'asc'; + else this.sortPriority = null; } protected toggleDropdown(name: 'type' | 'status' | 'priority' | 'milestone', event: Event): void {