From b2f481c67eca8576e43c3b182f5284913010c717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eyl=C3=BCl=20Naz=20Can?= Date: Sat, 29 Mar 2025 17:42:49 +0100 Subject: [PATCH 01/10] display subtitle in sidebar for exercise and lecture --- .../sidebar-accordion.component.html | 35 ++++++---- .../sidebar-accordion.component.ts | 64 +++++++++++++++++++ 2 files changed, 86 insertions(+), 13 deletions(-) diff --git a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html index 895cc5ea858a..6630ef9113eb 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html +++ b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html @@ -24,19 +24,28 @@ @if ((groupedData[groupKey].entityData | searchFilter: ['title', 'type'] : searchValue)?.length) {
-
- @for (sidebarItem of groupedData[groupKey].entityData | searchFilter: ['title', 'type'] : searchValue; let last = $last; track sidebarItem) { -
- -
+
+ @for (weekGroup of getGroupedByWeek(groupKey); track weekGroup.weekRange) { + @if (weekGroup.weekRange && weekGroup.weekRange !== 'No Date' && groupKey !== 'noDate') { +
+ {{ weekGroup.weekRange }} +
+ } +
+ @for (sidebarItem of weekGroup.items | searchFilter: ['title', 'type'] : searchValue; let last = $last; track sidebarItem) { +
+ +
+
+ }
}
diff --git a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts index e5305c6e9fee..cd004b2580ab 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts +++ b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts @@ -8,6 +8,17 @@ import { SidebarCardDirective } from '../sidebar-card.directive'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { SearchFilterPipe } from 'app/shared/pipes/search-filter.pipe'; import { AccordionGroups, ChannelTypeIcons, CollapseState, SidebarCardElement, SidebarItemShowAlways, SidebarTypes } from 'app/shared/types/sidebar'; +import dayjs from 'dayjs/esm'; +import isoWeek from 'dayjs/plugin/isoWeek'; + +dayjs.extend(isoWeek); + +interface WeekGroup { + weekRange: string; + items: SidebarCardElement[]; +} + +const MIN_ITEMS_TO_GROUP_BY_WEEK = 5; @Component({ selector: 'jhi-sidebar-accordion', @@ -93,4 +104,57 @@ export class SidebarAccordionComponent implements OnChanges, OnInit { this.collapseState[groupCategoryKey] = !this.collapseState[groupCategoryKey]; localStorage.setItem('sidebar.accordion.collapseState.' + this.storageId + '.byCourse.' + this.courseId, JSON.stringify(this.collapseState)); } + + private getWeekKey(date: dayjs.Dayjs): string { + const weekStart = date.startOf('isoWeek'); + const weekEnd = date.endOf('isoWeek'); + return `${weekStart.format('DD MMM')} - ${weekEnd.format('DD MMM YYYY')}`; + } + + getGroupedByWeek(groupKey: string): WeekGroup[] { + const items = this.groupedData[groupKey].entityData; + + // Apply search filter + const filteredItems = this.searchValue + ? items.filter((item) => item.title?.toLowerCase().includes(this.searchValue.toLowerCase()) || item.type?.toLowerCase().includes(this.searchValue.toLowerCase())) + : items; + + // For exams, always return as a single group without week ranges + if (groupKey === 'real' || groupKey === 'test' || groupKey === 'attempt') { + return [{ weekRange: '', items: filteredItems }]; + } + + if (filteredItems.length <= MIN_ITEMS_TO_GROUP_BY_WEEK || this.searchValue) { + return [{ weekRange: '', items: filteredItems }]; + } + + const weekGroups = new Map(); + + for (const item of filteredItems) { + const date = item.exercise?.dueDate || item.startDateWithTime; + if (!date) { + const noDateKey = 'No Date'; + const noDateGroup = weekGroups.get(noDateKey) || []; + noDateGroup.push(item); + weekGroups.set(noDateKey, noDateGroup); + continue; + } + + const weekKey = this.getWeekKey(date); + const group = weekGroups.get(weekKey) || []; + group.push(item); + weekGroups.set(weekKey, group); + } + + return Array.from(weekGroups.entries()) + .map(([weekRange, items]) => ({ weekRange, items })) + .sort((a, b) => { + if (a.weekRange === 'No Date') return 1; + if (b.weekRange === 'No Date') return -1; + + const aDate = dayjs(a.weekRange.split(' - ')[0], 'DD MMM'); + const bDate = dayjs(b.weekRange.split(' - ')[0], 'DD MMM'); + return aDate.isBefore(bDate) ? -1 : 1; + }); + } } From e9f59c2fe18df6a96362bd18c80c153dbc2d36a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eyl=C3=BCl=20Naz=20Can?= Date: Sat, 29 Mar 2025 20:03:41 +0100 Subject: [PATCH 02/10] add test cases --- .../sidebar-accordion.component.spec.ts | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/src/test/javascript/spec/component/shared/sidebar/sidebar-accordion.component.spec.ts b/src/test/javascript/spec/component/shared/sidebar/sidebar-accordion.component.spec.ts index 14ae1f25f8bb..53033fbad9df 100644 --- a/src/test/javascript/spec/component/shared/sidebar/sidebar-accordion.component.spec.ts +++ b/src/test/javascript/spec/component/shared/sidebar/sidebar-accordion.component.spec.ts @@ -12,6 +12,10 @@ import { ActivatedRoute, RouterModule } from '@angular/router'; import { DebugElement } from '@angular/core'; import { By } from '@angular/platform-browser'; import { MockActivatedRoute } from '../../../helpers/mocks/activated-route/mock-activated-route'; +import dayjs from 'dayjs/esm'; +import isoWeek from 'dayjs/plugin/isoWeek'; + +dayjs.extend(isoWeek); describe('SidebarAccordionComponent', () => { let component: SidebarAccordionComponent; @@ -153,4 +157,104 @@ describe('SidebarAccordionComponent', () => { expect(component.totalUnreadMessagesPerGroup['future']).toBe(1); expect(component.totalUnreadMessagesPerGroup['noDate']).toBe(0); }); + + ['real', 'test', 'attempt'].forEach((examKey) => { + it(`should return a single group with an empty weekRange for exam-type group key '${examKey}'`, () => { + component.groupedData = { + [examKey]: { + entityData: [ + { title: `${examKey} Exam 1`, id: 1, exercise: { dueDate: dayjs('2025-01-01') } }, + { title: `${examKey} Exam 2`, id: 2, exercise: { dueDate: dayjs('2025-01-02') } }, + ], + }, + } as any; + component.searchValue = ''; + + const result = component.getGroupedByWeek(examKey); + expect(result.length).toBe(1); + expect(result[0].weekRange).toBe(''); + expect(result[0].items).toHaveLength(2); + }); + }); + + // --- Merged test for small group and filtered group cases --- + [ + { groupKey: 'smallGroup', searchValue: '', expectedCount: 2 }, + { groupKey: 'filteredGroup', searchValue: 'Alpha', expectedCount: 1 }, + ].forEach(({ groupKey, searchValue, expectedCount }) => { + it(`should return a single group for '${groupKey}' when searchValue is '${searchValue}'`, () => { + let entityData; + if (groupKey === 'smallGroup') { + entityData = [ + { title: 'Small 1', id: 1, exercise: { dueDate: dayjs('2025-01-03') } }, + { title: 'Small 2', id: 2, exercise: { dueDate: dayjs('2025-01-04') } }, + ]; + } else if (groupKey === 'filteredGroup') { + entityData = [ + { title: 'Alpha', id: 1, exercise: { dueDate: dayjs('2025-02-01') } }, + { title: 'Beta', id: 2, exercise: { dueDate: dayjs('2025-02-02') } }, + ]; + } + component.groupedData = { [groupKey]: { entityData } } as any; + component.searchValue = searchValue; + const result = component.getGroupedByWeek(groupKey); + expect(result.length).toBe(1); + expect(result[0].weekRange).toBe(''); + expect(result[0].items).toHaveLength(expectedCount); + if (searchValue === 'Alpha') { + expect(result[0].items[0].title).toBe('Alpha'); + } + }); + }); + + it('should group items by week if there are more than 5 items and no search filter', () => { + component.groupedData = { + bigGroup: { + entityData: [ + { title: 'Item 1', id: 1, exercise: { dueDate: dayjs('2025-01-05') } }, + { title: 'Item 2', id: 2, exercise: { dueDate: dayjs('2025-01-06') } }, + { title: 'Item 3', id: 3, exercise: { dueDate: dayjs('2025-01-07') } }, + { title: 'Item 4', id: 4, exercise: { dueDate: dayjs('2025-01-08') } }, + { title: 'Item 5', id: 5, exercise: { dueDate: dayjs('2025-01-09') } }, + { title: 'Item 6', id: 6, exercise: { dueDate: dayjs('2025-01-15') } }, + ], + }, + } as any; + component.searchValue = ''; + const result = component.getGroupedByWeek('bigGroup'); + expect(result.length).toBeGreaterThan(1); + }); + + it('should place an item in a correct week if it only has startDateWithTime', () => { + component.groupedData = { + startDateGroup: { + entityData: [{ title: 'StartDate Only', id: 1, startDateWithTime: dayjs('2025-03-10') }], + }, + } as any; + component.searchValue = ''; + const result = component.getGroupedByWeek('startDateGroup'); + expect(result).toHaveLength(1); + expect(result[0].items).toHaveLength(1); + expect(result[0].items[0].title).toBe('StartDate Only'); + }); + + it('should place items without date in the "No Date" group when there are more than 5 items', () => { + component.groupedData = { + past: { + entityData: [ + { title: 'No Date 1', id: 1 }, + { title: 'No Date 2', id: 2 }, + { title: 'No Date 3', id: 3 }, + { title: 'No Date 4', id: 4 }, + { title: 'No Date 5', id: 5 }, + { title: 'No Date 6', id: 6 }, + ], + }, + } as any; + component.searchValue = ''; + const result = component.getGroupedByWeek('past'); + expect(result).toHaveLength(1); + expect(result[0].weekRange).toEqual('No Date'); + expect(result[0].items).toHaveLength(6); + }); }); From 829cfdb08d4e0b3acd64df4cad196b759502ea98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eyl=C3=BCl=20Naz=20Can?= Date: Sun, 30 Mar 2025 16:10:41 +0200 Subject: [PATCH 03/10] add start date for lecture --- .../app/core/course/overview/course-overview.service.ts | 1 + .../sidebar/sidebar-accordion/sidebar-accordion.component.ts | 2 +- src/main/webapp/app/shared/types/sidebar.ts | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/webapp/app/core/course/overview/course-overview.service.ts b/src/main/webapp/app/core/course/overview/course-overview.service.ts index f0ca69902af1..23b41d134f9a 100644 --- a/src/main/webapp/app/core/course/overview/course-overview.service.ts +++ b/src/main/webapp/app/core/course/overview/course-overview.service.ts @@ -308,6 +308,7 @@ export class CourseOverviewService { id: lecture.id ?? '', subtitleLeft: lecture.startDate?.format('MMM DD, YYYY') ?? this.translate.instant('artemisApp.courseOverview.sidebar.noDate'), size: 'M', + startDate: lecture.startDate, }; return lectureCardItem; } diff --git a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts index cd004b2580ab..14d1de999fe8 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts +++ b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts @@ -131,7 +131,7 @@ export class SidebarAccordionComponent implements OnChanges, OnInit { const weekGroups = new Map(); for (const item of filteredItems) { - const date = item.exercise?.dueDate || item.startDateWithTime; + const date = item.exercise?.dueDate || item.startDateWithTime || item.startDate; if (!date) { const noDateKey = 'No Date'; const noDateGroup = weekGroups.get(noDateKey) || []; diff --git a/src/main/webapp/app/shared/types/sidebar.ts b/src/main/webapp/app/shared/types/sidebar.ts index 16f782646159..93a27ba0b1a9 100644 --- a/src/main/webapp/app/shared/types/sidebar.ts +++ b/src/main/webapp/app/shared/types/sidebar.ts @@ -156,6 +156,10 @@ export interface SidebarCardElement { * Set for Conversation. Will be removed after refactoring */ conversation?: ConversationDTO; + /** + * Set for Lectures, shows the start date + */ + startDate?: dayjs.Dayjs; isCurrent?: boolean; } From e95cace9344fe06baeec4285e6ee0e2011682530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eyl=C3=BCl=20Naz=20Can?= Date: Sun, 30 Mar 2025 21:46:06 +0200 Subject: [PATCH 04/10] fix date order --- .../sidebar-accordion.component.ts | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts index 14d1de999fe8..8fca644c351b 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts +++ b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts @@ -108,7 +108,8 @@ export class SidebarAccordionComponent implements OnChanges, OnInit { private getWeekKey(date: dayjs.Dayjs): string { const weekStart = date.startOf('isoWeek'); const weekEnd = date.endOf('isoWeek'); - return `${weekStart.format('DD MMM')} - ${weekEnd.format('DD MMM YYYY')}`; + const year = weekStart.year(); + return `${year} - ${weekStart.format('DD MMM YYYY')} - ${weekEnd.format('DD MMM YYYY')}`; } getGroupedByWeek(groupKey: string): WeekGroup[] { @@ -146,15 +147,41 @@ export class SidebarAccordionComponent implements OnChanges, OnInit { weekGroups.set(weekKey, group); } + // Sort items within each group by date (newest first) + for (const [, items] of weekGroups) { + items.sort((a, b) => { + const dateA = a.exercise?.dueDate || a.startDateWithTime || a.startDate; + const dateB = b.exercise?.dueDate || b.startDateWithTime || b.startDate; + if (!dateA && !dateB) return 0; + if (!dateA) return 1; + if (!dateB) return -1; + return dateB.valueOf() - dateA.valueOf(); + }); + } + return Array.from(weekGroups.entries()) - .map(([weekRange, items]) => ({ weekRange, items })) + .map(([weekRange, items]) => { + const displayRange = weekRange === 'No Date' ? weekRange : weekRange.split(' - ').slice(1).join(' - '); + return { weekRange: displayRange, items }; + }) .sort((a, b) => { if (a.weekRange === 'No Date') return 1; if (b.weekRange === 'No Date') return -1; + const aFullKey = Array.from(weekGroups.keys()).find((key) => key.includes(a.weekRange)); + const bFullKey = Array.from(weekGroups.keys()).find((key) => key.includes(b.weekRange)); + + if (!aFullKey || !bFullKey) return 0; + + const [aYear] = aFullKey.split(' - '); + const [bYear] = bFullKey.split(' - '); + if (aYear !== bYear) { + return Number(bYear) - Number(aYear); + } + const aDate = dayjs(a.weekRange.split(' - ')[0], 'DD MMM'); const bDate = dayjs(b.weekRange.split(' - ')[0], 'DD MMM'); - return aDate.isBefore(bDate) ? -1 : 1; + return bDate.valueOf() - aDate.valueOf(); }); } } From 8c140ec91ac2a7adcad717542048cdff64b2f24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eyl=C3=BCl=20Naz=20Can?= Date: Mon, 21 Apr 2025 22:19:11 +0200 Subject: [PATCH 05/10] implement reviews and fix language issue --- .../sidebar-accordion.component.html | 15 +- .../sidebar-accordion.component.spec.ts | 107 +------------ .../sidebar-accordion.component.ts | 93 +---------- .../app/shared/util/week-grouping.spec.ts | 147 ++++++++++++++++++ .../app/shared/util/week-grouping.util.ts | 122 +++++++++++++++ .../webapp/i18n/de/student-dashboard.json | 3 +- .../webapp/i18n/en/student-dashboard.json | 3 +- 7 files changed, 295 insertions(+), 195 deletions(-) create mode 100644 src/main/webapp/app/shared/util/week-grouping.spec.ts create mode 100644 src/main/webapp/app/shared/util/week-grouping.util.ts diff --git a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html index bdc145037997..02f7fa9e9ebe 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html +++ b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.html @@ -25,10 +25,19 @@ @if ((groupedData[groupKey].entityData | searchFilter: ['title', 'type'] : searchValue)?.length) {
- @for (weekGroup of getGroupedByWeek(groupKey); track weekGroup.weekRange) { - @if (weekGroup.weekRange && weekGroup.weekRange !== 'No Date' && groupKey !== 'noDate') { + @for (weekGroup of getGroupedByWeek(groupKey); track weekGroup.start?.valueOf()) { + @if (weekGroup.showDateHeader) {
- {{ weekGroup.weekRange }} + {{ + weekGroup.isNoDate + ? ('artemisApp.courseOverview.sidebar.noDate' | artemisTranslate) + : ('artemisApp.courseOverview.sidebar.weekRange' + | artemisTranslate + : { + start: (weekGroup.start | artemisDate: 'long-date'), + end: (weekGroup.end | artemisDate: 'long-date'), + }) + }}
}
diff --git a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.spec.ts b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.spec.ts index d8449bf24559..f518b6516499 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.spec.ts +++ b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.spec.ts @@ -6,16 +6,13 @@ import { SidebarCardDirective } from 'app/shared/sidebar/directive/sidebar-card. import { SearchFilterPipe } from 'app/shared/pipes/search-filter.pipe'; import { SearchFilterComponent } from 'app/shared/search-filter/search-filter.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { MockComponent, MockModule, MockPipe } from 'ng-mocks'; import { NgbCollapseModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { ActivatedRoute, RouterModule } from '@angular/router'; import { DebugElement } from '@angular/core'; import { By } from '@angular/platform-browser'; import { MockActivatedRoute } from 'test/helpers/mocks/activated-route/mock-activated-route'; -import dayjs from 'dayjs/esm'; -import isoWeek from 'dayjs/plugin/isoWeek'; - -dayjs.extend(isoWeek); describe('SidebarAccordionComponent', () => { let component: SidebarAccordionComponent; @@ -33,6 +30,7 @@ describe('SidebarAccordionComponent', () => { SearchFilterComponent, MockPipe(ArtemisTranslatePipe), MockComponent(SearchFilterComponent), + MockPipe(ArtemisDatePipe), ], providers: [{ provide: ActivatedRoute, useValue: new MockActivatedRoute() }], }).compileComponents(); @@ -158,102 +156,9 @@ describe('SidebarAccordionComponent', () => { expect(component.totalUnreadMessagesPerGroup['noDate']).toBe(0); }); - ['real', 'test', 'attempt'].forEach((examKey) => { - it(`should return a single group with an empty weekRange for exam-type group key '${examKey}'`, () => { - component.groupedData = { - [examKey]: { - entityData: [ - { title: `${examKey} Exam 1`, id: 1, exercise: { dueDate: dayjs('2025-01-01') } }, - { title: `${examKey} Exam 2`, id: 2, exercise: { dueDate: dayjs('2025-01-02') } }, - ], - }, - } as any; - component.searchValue = ''; - - const result = component.getGroupedByWeek(examKey); - expect(result).toHaveLength(1); - expect(result[0].weekRange).toBe(''); - expect(result[0].items).toHaveLength(2); - }); - }); - - [ - { groupKey: 'smallGroup', searchValue: '', expectedCount: 2 }, - { groupKey: 'filteredGroup', searchValue: 'Alpha', expectedCount: 1 }, - ].forEach(({ groupKey, searchValue, expectedCount }) => { - it(`should return a single group for '${groupKey}' when searchValue is '${searchValue}'`, () => { - let entityData; - if (groupKey === 'smallGroup') { - entityData = [ - { title: 'Small 1', id: 1, exercise: { dueDate: dayjs('2025-01-03') } }, - { title: 'Small 2', id: 2, exercise: { dueDate: dayjs('2025-01-04') } }, - ]; - } else if (groupKey === 'filteredGroup') { - entityData = [ - { title: 'Alpha', id: 1, exercise: { dueDate: dayjs('2025-02-01') } }, - { title: 'Beta', id: 2, exercise: { dueDate: dayjs('2025-02-02') } }, - ]; - } - component.groupedData = { [groupKey]: { entityData } } as any; - component.searchValue = searchValue; - const result = component.getGroupedByWeek(groupKey); - expect(result).toHaveLength(1); - expect(result[0].weekRange).toBe(''); - expect(result[0].items).toHaveLength(expectedCount); - if (searchValue === 'Alpha') { - expect(result[0].items[0].title).toBe('Alpha'); - } - }); - }); - - it('should group items by week if there are more than 5 items and no search filter', () => { - component.groupedData = { - bigGroup: { - entityData: [ - { title: 'Item 1', id: 1, exercise: { dueDate: dayjs('2025-01-05') } }, - { title: 'Item 2', id: 2, exercise: { dueDate: dayjs('2025-01-06') } }, - { title: 'Item 3', id: 3, exercise: { dueDate: dayjs('2025-01-07') } }, - { title: 'Item 4', id: 4, exercise: { dueDate: dayjs('2025-01-08') } }, - { title: 'Item 5', id: 5, exercise: { dueDate: dayjs('2025-01-09') } }, - { title: 'Item 6', id: 6, exercise: { dueDate: dayjs('2025-01-15') } }, - ], - }, - } as any; - component.searchValue = ''; - const result = component.getGroupedByWeek('bigGroup'); - expect(result.length).toBeGreaterThan(1); - }); - - it('should place an item in a correct week if it only has startDateWithTime', () => { - component.groupedData = { - startDateGroup: { - entityData: [{ title: 'StartDate Only', id: 1, startDateWithTime: dayjs('2025-03-10') }], - }, - } as any; - component.searchValue = ''; - const result = component.getGroupedByWeek('startDateGroup'); - expect(result).toHaveLength(1); - expect(result[0].items).toHaveLength(1); - expect(result[0].items[0].title).toBe('StartDate Only'); - }); - - it('should place items without date in the "No Date" group when there are more than 5 items', () => { - component.groupedData = { - past: { - entityData: [ - { title: 'No Date 1', id: 1 }, - { title: 'No Date 2', id: 2 }, - { title: 'No Date 3', id: 3 }, - { title: 'No Date 4', id: 4 }, - { title: 'No Date 5', id: 5 }, - { title: 'No Date 6', id: 6 }, - ], - }, - } as any; - component.searchValue = ''; - const result = component.getGroupedByWeek('past'); - expect(result).toHaveLength(1); - expect(result[0].weekRange).toBe('No Date'); - expect(result[0].items).toHaveLength(6); + it('should use the week grouping utility for grouping items', () => { + const result = component.getGroupedByWeek('current'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBeTruthy(); }); }); diff --git a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts index a5dd408fe1e9..ded1246dc3ae 100644 --- a/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts +++ b/src/main/webapp/app/shared/sidebar/sidebar-accordion/sidebar-accordion.component.ts @@ -6,25 +6,16 @@ import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'; import { NgClass, TitleCasePipe } from '@angular/common'; import { SidebarCardDirective } from '../directive/sidebar-card.directive'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { SearchFilterPipe } from 'app/shared/pipes/search-filter.pipe'; import { AccordionGroups, ChannelTypeIcons, CollapseState, SidebarCardElement, SidebarItemShowAlways, SidebarTypes } from 'app/shared/types/sidebar'; -import dayjs from 'dayjs/esm'; -import isoWeek from 'dayjs/plugin/isoWeek'; - -dayjs.extend(isoWeek); - -interface WeekGroup { - weekRange: string; - items: SidebarCardElement[]; -} - -const MIN_ITEMS_TO_GROUP_BY_WEEK = 5; +import { WeekGroup, WeekGroupingUtil } from 'app/shared/util/week-grouping.util'; @Component({ selector: 'jhi-sidebar-accordion', templateUrl: './sidebar-accordion.component.html', styleUrls: ['./sidebar-accordion.component.scss'], - imports: [FaIconComponent, NgbCollapse, NgClass, SidebarCardDirective, TitleCasePipe, ArtemisTranslatePipe, SearchFilterPipe], + imports: [FaIconComponent, NgbCollapse, NgClass, SidebarCardDirective, TitleCasePipe, ArtemisTranslatePipe, ArtemisDatePipe, SearchFilterPipe], }) export class SidebarAccordionComponent implements OnChanges, OnInit { protected readonly Object = Object; @@ -105,83 +96,7 @@ export class SidebarAccordionComponent implements OnChanges, OnInit { localStorage.setItem('sidebar.accordion.collapseState.' + this.storageId + '.byCourse.' + this.courseId, JSON.stringify(this.collapseState)); } - private getWeekKey(date: dayjs.Dayjs): string { - const weekStart = date.startOf('isoWeek'); - const weekEnd = date.endOf('isoWeek'); - const year = weekStart.year(); - return `${year} - ${weekStart.format('DD MMM YYYY')} - ${weekEnd.format('DD MMM YYYY')}`; - } - getGroupedByWeek(groupKey: string): WeekGroup[] { - const items = this.groupedData[groupKey].entityData; - - // Apply search filter - const filteredItems = this.searchValue - ? items.filter((item) => item.title?.toLowerCase().includes(this.searchValue.toLowerCase()) || item.type?.toLowerCase().includes(this.searchValue.toLowerCase())) - : items; - - // For exams, always return as a single group without week ranges - if (groupKey === 'real' || groupKey === 'test' || groupKey === 'attempt') { - return [{ weekRange: '', items: filteredItems }]; - } - - if (filteredItems.length <= MIN_ITEMS_TO_GROUP_BY_WEEK || this.searchValue) { - return [{ weekRange: '', items: filteredItems }]; - } - - const weekGroups = new Map(); - - for (const item of filteredItems) { - const date = item.exercise?.dueDate || item.startDateWithTime || item.startDate; - if (!date) { - const noDateKey = 'No Date'; - const noDateGroup = weekGroups.get(noDateKey) || []; - noDateGroup.push(item); - weekGroups.set(noDateKey, noDateGroup); - continue; - } - - const weekKey = this.getWeekKey(date); - const group = weekGroups.get(weekKey) || []; - group.push(item); - weekGroups.set(weekKey, group); - } - - // Sort items within each group by date (newest first) - for (const [, items] of weekGroups) { - items.sort((a, b) => { - const dateA = a.exercise?.dueDate || a.startDateWithTime || a.startDate; - const dateB = b.exercise?.dueDate || b.startDateWithTime || b.startDate; - if (!dateA && !dateB) return 0; - if (!dateA) return 1; - if (!dateB) return -1; - return dateB.valueOf() - dateA.valueOf(); - }); - } - - return Array.from(weekGroups.entries()) - .map(([weekRange, items]) => { - const displayRange = weekRange === 'No Date' ? weekRange : weekRange.split(' - ').slice(1).join(' - '); - return { weekRange: displayRange, items }; - }) - .sort((a, b) => { - if (a.weekRange === 'No Date') return 1; - if (b.weekRange === 'No Date') return -1; - - const aFullKey = Array.from(weekGroups.keys()).find((key) => key.includes(a.weekRange)); - const bFullKey = Array.from(weekGroups.keys()).find((key) => key.includes(b.weekRange)); - - if (!aFullKey || !bFullKey) return 0; - - const [aYear] = aFullKey.split(' - '); - const [bYear] = bFullKey.split(' - '); - if (aYear !== bYear) { - return Number(bYear) - Number(aYear); - } - - const aDate = dayjs(a.weekRange.split(' - ')[0], 'DD MMM'); - const bDate = dayjs(b.weekRange.split(' - ')[0], 'DD MMM'); - return bDate.valueOf() - aDate.valueOf(); - }); + return WeekGroupingUtil.getGroupedByWeek(this.groupedData[groupKey].entityData, groupKey, this.searchValue); } } diff --git a/src/main/webapp/app/shared/util/week-grouping.spec.ts b/src/main/webapp/app/shared/util/week-grouping.spec.ts new file mode 100644 index 000000000000..564befaa8bb5 --- /dev/null +++ b/src/main/webapp/app/shared/util/week-grouping.spec.ts @@ -0,0 +1,147 @@ +import { MIN_ITEMS_TO_GROUP_BY_WEEK, WeekGroupingUtil } from './week-grouping.util'; +import { SidebarCardElement } from '../types/sidebar'; +import dayjs from 'dayjs/esm'; + +describe('WeekGroupingUtil', () => { + const createItem = ( + title: string, + options: { + dueDate?: string; + startDateWithTime?: string; + startDate?: string; + type?: string; + } = {}, + ): SidebarCardElement => ({ + title, + type: options.type, + id: title, + size: 'M', + exercise: options.dueDate ? { dueDate: options.dueDate } : undefined, + startDateWithTime: options.startDateWithTime ? dayjs(options.startDateWithTime) : undefined, + startDate: options.startDate ? dayjs(options.startDate) : undefined, + }); + + describe('getGroupedByWeek', () => { + it('should return a single group without header for special sections', () => { + const items = [createItem('Exam 1', { dueDate: '2024-01-01' }), createItem('Exam 2', { dueDate: '2024-01-02' })]; + + ['real', 'test', 'attempt'].forEach((groupKey) => { + const result = WeekGroupingUtil.getGroupedByWeek(items, groupKey); + expect(result).toHaveLength(1); + expect(result[0].isNoDate).toBeTrue(); + expect(result[0].showDateHeader).toBeFalse(); + expect(result[0].items).toHaveLength(2); + }); + }); + + it('should return a single group without header when searching', () => { + const items = [createItem('Item 1', { dueDate: '2024-01-01' }), createItem('Item 2', { dueDate: '2024-01-02' }), createItem('Other', { dueDate: '2024-01-03' })]; + + const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise', 'item'); + expect(result).toHaveLength(1); + expect(result[0].isNoDate).toBeTrue(); + expect(result[0].showDateHeader).toBeFalse(); + expect(result[0].items).toHaveLength(2); + }); + + it('should search in both title and type', () => { + const items = [ + createItem('Normal Item', { dueDate: '2024-01-01', type: 'quiz' }), + createItem('Quiz Title', { dueDate: '2024-01-02' }), + createItem('Other', { dueDate: '2024-01-03' }), + ]; + + const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise', 'quiz'); + expect(result).toHaveLength(1); + expect(result[0].items).toHaveLength(2); + expect(result[0].items.map((i) => i.title)).toContain('Normal Item'); + expect(result[0].items.map((i) => i.title)).toContain('Quiz Title'); + }); + + it('should return a single group without header when items <= MIN_ITEMS_TO_GROUP_BY_WEEK', () => { + const items = Array.from({ length: MIN_ITEMS_TO_GROUP_BY_WEEK }, (_, i) => createItem(`Item ${i + 1}`, { dueDate: `2024-01-0${i + 1}` })); + + const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); + expect(result).toHaveLength(1); + expect(result[0].isNoDate).toBeTrue(); + expect(result[0].showDateHeader).toBeFalse(); + expect(result[0].items).toHaveLength(MIN_ITEMS_TO_GROUP_BY_WEEK); + }); + + it('should group items by week and show headers when items > MIN_ITEMS_TO_GROUP_BY_WEEK', () => { + const items = [ + ...Array.from({ length: 4 }, (_, i) => createItem(`Week1 Item ${i + 1}`, { dueDate: '2024-01-01' })), + ...Array.from({ length: 3 }, (_, i) => createItem(`Week2 Item ${i + 1}`, { dueDate: '2024-01-08' })), + ]; + + const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); + expect(result.length).toBeGreaterThan(1); + result.forEach((group) => { + expect(group.showDateHeader).toBeTrue(); + }); + }); + + it('should prioritize dates correctly', () => { + const items = [ + createItem('Due Date', { + dueDate: '2024-01-01', + startDateWithTime: '2024-01-02', + startDate: '2024-01-03', + }), + createItem('Start Time', { + startDateWithTime: '2024-01-02', + startDate: '2024-01-03', + }), + createItem('Start Date', { + startDate: '2024-01-03', + }), + createItem('No Date', {}), + ]; + + const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); + expect(result[0].items[0].title).toBe('Due Date'); + expect(result[0].items[1].title).toBe('Start Time'); + expect(result[0].items[2].title).toBe('Start Date'); + }); + + it('should sort items within groups by date in descending order', () => { + const items = [ + createItem('Item 2', { dueDate: '2024-01-02' }), + createItem('Item 1', { dueDate: '2024-01-01' }), + createItem('Item 3', { dueDate: '2024-01-03' }), + createItem('Item 5', { dueDate: '2024-01-05' }), + createItem('Item 4', { dueDate: '2024-01-04' }), + createItem('Item 6', { dueDate: '2024-01-06' }), + ]; + + const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); + const firstGroup = result[0]; + expect(firstGroup.items.map((item) => item.title)).toEqual(['Item 6', 'Item 5', 'Item 4', 'Item 3', 'Item 2', 'Item 1']); + }); + + it('should handle week boundaries correctly', () => { + const items = [ + createItem('Sunday 1', { dueDate: '2024-01-07' }), // Sunday of week 1 + createItem('Sunday 2', { dueDate: '2024-01-07' }), // Sunday of week 1 + createItem('Monday 1', { dueDate: '2024-01-08' }), // Monday of week 2 + createItem('Monday 2', { dueDate: '2024-01-08' }), // Monday of week 2 + createItem('Saturday 1', { dueDate: '2024-01-13' }), // Saturday of week 2 + createItem('Saturday 2', { dueDate: '2024-01-13' }), // Saturday of week 2 + ]; + + const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); + + // Items from different ISO weeks should be in different groups + expect(result[0].start!.isSame(dayjs('2024-01-08'), 'week')).toBeTrue(); + expect(result[1].start!.isSame(dayjs('2024-01-01'), 'week')).toBeTrue(); + + // Verify items are in correct groups + expect(result[0].items.map((i) => i.title)).toContain('Monday 1'); + expect(result[0].items.map((i) => i.title)).toContain('Monday 2'); + expect(result[0].items.map((i) => i.title)).toContain('Saturday 1'); + expect(result[0].items.map((i) => i.title)).toContain('Saturday 2'); + expect(result[1].items.map((i) => i.title)).toContain('Sunday 1'); + expect(result[1].items.map((i) => i.title)).toContain('Sunday 2'); + }); + }); +}); diff --git a/src/main/webapp/app/shared/util/week-grouping.util.ts b/src/main/webapp/app/shared/util/week-grouping.util.ts new file mode 100644 index 000000000000..79b59c57b011 --- /dev/null +++ b/src/main/webapp/app/shared/util/week-grouping.util.ts @@ -0,0 +1,122 @@ +import dayjs from 'dayjs/esm'; +import { SidebarCardElement } from 'app/shared/types/sidebar'; + +export interface WeekGroup { + isNoDate: boolean; + start?: dayjs.Dayjs; + end?: dayjs.Dayjs; + items: SidebarCardElement[]; + showDateHeader: boolean; +} + +export const NO_DATE_KEY = 'artemisApp.courseOverview.sidebar.noDate'; +export const MIN_ITEMS_TO_GROUP_BY_WEEK = 5; + +export class WeekGroupingUtil { + /** + * Extracts the most relevant date from a sidebar element. + * + * @param item - The sidebar element to extract the date from + * @returns The extracted date, or undefined if no date is found + */ + static getDateFromItem(item: SidebarCardElement): dayjs.Dayjs | undefined { + const date = item.exercise?.dueDate ?? item.startDateWithTime ?? item.startDate; + return date ? dayjs(date) : undefined; + } + + /** + * Creates a unique key for a week based on its start and end dates. + * + * @param date - The date to get the week key for + * @returns A unique string identifier for the week + */ + static getWeekKey(date: dayjs.Dayjs): string { + const startOfWeek = date.startOf('isoWeek'); + const endOfWeek = date.endOf('isoWeek'); + return `${startOfWeek.year()} - ${startOfWeek.format('DD MMM YYYY')} - ${endOfWeek.format('DD MMM YYYY')}`; + } + + /** + * Compares two dates for sorting purposes. + * Undefined dates are sorted to the end. + * + * @param a - First date to compare + * @param b - Second date to compare + * @returns Negative if a < b, positive if a > b, 0 if equal + */ + static compareDates(a?: dayjs.Dayjs, b?: dayjs.Dayjs): number { + if (!a && !b) return 0; + if (!a) return 1; + if (!b) return -1; + return b.valueOf() - a.valueOf(); + } + + /** + * Groups sidebar items into ISO‑weeks (or returns them as‑is for special groups). + * + * @param items - The items to group + * @param groupKey - Name of the high‑level group ("lecture", "exercise", …) + * @param searchValue - Optional search string to filter items + * @returns Array of WeekGroup objects containing the grouped items + */ + static getGroupedByWeek(items: SidebarCardElement[], groupKey: string, searchValue = ''): WeekGroup[] { + // Filter items based on search value if provided + const filtered = searchValue + ? items.filter((i) => { + const title = i.title?.toLowerCase() ?? ''; + const type = i.type?.toLowerCase() ?? ''; + return title.includes(searchValue.toLowerCase()) || type.includes(searchValue.toLowerCase()); + }) + : items; + + // Return single group without headers for special cases + if (groupKey === 'real' || groupKey === 'test' || groupKey === 'attempt' || !!searchValue || filtered.length <= MIN_ITEMS_TO_GROUP_BY_WEEK) { + return [{ isNoDate: true, items: filtered, showDateHeader: false }]; + } + + // Group items by week + const weekMap = new Map(); + for (const item of filtered) { + const date = this.getDateFromItem(item); + const key = date ? this.getWeekKey(date) : NO_DATE_KEY; + const bucket = weekMap.get(key) ?? []; + bucket.push(item); + weekMap.set(key, bucket); + } + + // Sort items within each week by date + for (const list of weekMap.values()) { + list.sort((a, b) => this.compareDates(this.getDateFromItem(a), this.getDateFromItem(b))); + } + + // Convert week map to WeekGroup array + const groups: WeekGroup[] = Array.from(weekMap.entries()).map(([key, list]) => { + if (key === NO_DATE_KEY) { + return { + isNoDate: true, + items: list, + showDateHeader: true, + }; + } + + const [, startStr, endStr] = key.split(' - '); + return { + isNoDate: false, + start: dayjs(startStr, 'DD MMM YYYY'), + end: dayjs(endStr, 'DD MMM YYYY'), + items: list, + showDateHeader: true, + }; + }); + + // Sort groups: dated groups first (by year and date), then no-date groups + return groups.sort((a, b) => { + if (a.isNoDate) return 1; + if (b.isNoDate) return -1; + if (a.start!.year() !== b.start!.year()) { + return b.start!.year() - a.start!.year(); + } + return b.start!.valueOf() - a.start!.valueOf(); + }); + } +} diff --git a/src/main/webapp/i18n/de/student-dashboard.json b/src/main/webapp/i18n/de/student-dashboard.json index 8a9b4c1d9638..1406eee8c4a1 100644 --- a/src/main/webapp/i18n/de/student-dashboard.json +++ b/src/main/webapp/i18n/de/student-dashboard.json @@ -90,7 +90,8 @@ "directMessages": "Direktnachrichten", "filterConversationPlaceholder": "Konversationen filtern", "setChannelAsRead": "Alle Kanäle als gelesen markieren", - "recents": "Kürzliches" + "recents": "Kürzliches", + "weekRange": "{{ start }} - {{ end }}" }, "menu": { "exercises": "Aufgaben", diff --git a/src/main/webapp/i18n/en/student-dashboard.json b/src/main/webapp/i18n/en/student-dashboard.json index 3b9e92bb7c9e..14bc8abc617b 100644 --- a/src/main/webapp/i18n/en/student-dashboard.json +++ b/src/main/webapp/i18n/en/student-dashboard.json @@ -90,7 +90,8 @@ "directMessages": "Direct Messages", "filterConversationPlaceholder": "Filter conversations", "setChannelAsRead": "Mark all channels as read", - "recents": "Recents" + "recents": "Recents", + "weekRange": "{{ start }} - {{ end }}" }, "menu": { "exercises": "Exercises", From b0841a82804e9bf7fc2118fbafbb41a90971d4f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eyl=C3=BCl=20Naz=20Can?= Date: Mon, 21 Apr 2025 22:55:17 +0200 Subject: [PATCH 06/10] fix test --- .../app/shared/util/week-grouping.spec.ts | 147 --------- .../shared/util/week-grouping.util.spec.ts | 289 ++++++++++++++++++ .../app/shared/util/week-grouping.util.ts | 4 +- 3 files changed, 291 insertions(+), 149 deletions(-) delete mode 100644 src/main/webapp/app/shared/util/week-grouping.spec.ts create mode 100644 src/main/webapp/app/shared/util/week-grouping.util.spec.ts diff --git a/src/main/webapp/app/shared/util/week-grouping.spec.ts b/src/main/webapp/app/shared/util/week-grouping.spec.ts deleted file mode 100644 index 564befaa8bb5..000000000000 --- a/src/main/webapp/app/shared/util/week-grouping.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { MIN_ITEMS_TO_GROUP_BY_WEEK, WeekGroupingUtil } from './week-grouping.util'; -import { SidebarCardElement } from '../types/sidebar'; -import dayjs from 'dayjs/esm'; - -describe('WeekGroupingUtil', () => { - const createItem = ( - title: string, - options: { - dueDate?: string; - startDateWithTime?: string; - startDate?: string; - type?: string; - } = {}, - ): SidebarCardElement => ({ - title, - type: options.type, - id: title, - size: 'M', - exercise: options.dueDate ? { dueDate: options.dueDate } : undefined, - startDateWithTime: options.startDateWithTime ? dayjs(options.startDateWithTime) : undefined, - startDate: options.startDate ? dayjs(options.startDate) : undefined, - }); - - describe('getGroupedByWeek', () => { - it('should return a single group without header for special sections', () => { - const items = [createItem('Exam 1', { dueDate: '2024-01-01' }), createItem('Exam 2', { dueDate: '2024-01-02' })]; - - ['real', 'test', 'attempt'].forEach((groupKey) => { - const result = WeekGroupingUtil.getGroupedByWeek(items, groupKey); - expect(result).toHaveLength(1); - expect(result[0].isNoDate).toBeTrue(); - expect(result[0].showDateHeader).toBeFalse(); - expect(result[0].items).toHaveLength(2); - }); - }); - - it('should return a single group without header when searching', () => { - const items = [createItem('Item 1', { dueDate: '2024-01-01' }), createItem('Item 2', { dueDate: '2024-01-02' }), createItem('Other', { dueDate: '2024-01-03' })]; - - const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise', 'item'); - expect(result).toHaveLength(1); - expect(result[0].isNoDate).toBeTrue(); - expect(result[0].showDateHeader).toBeFalse(); - expect(result[0].items).toHaveLength(2); - }); - - it('should search in both title and type', () => { - const items = [ - createItem('Normal Item', { dueDate: '2024-01-01', type: 'quiz' }), - createItem('Quiz Title', { dueDate: '2024-01-02' }), - createItem('Other', { dueDate: '2024-01-03' }), - ]; - - const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise', 'quiz'); - expect(result).toHaveLength(1); - expect(result[0].items).toHaveLength(2); - expect(result[0].items.map((i) => i.title)).toContain('Normal Item'); - expect(result[0].items.map((i) => i.title)).toContain('Quiz Title'); - }); - - it('should return a single group without header when items <= MIN_ITEMS_TO_GROUP_BY_WEEK', () => { - const items = Array.from({ length: MIN_ITEMS_TO_GROUP_BY_WEEK }, (_, i) => createItem(`Item ${i + 1}`, { dueDate: `2024-01-0${i + 1}` })); - - const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); - expect(result).toHaveLength(1); - expect(result[0].isNoDate).toBeTrue(); - expect(result[0].showDateHeader).toBeFalse(); - expect(result[0].items).toHaveLength(MIN_ITEMS_TO_GROUP_BY_WEEK); - }); - - it('should group items by week and show headers when items > MIN_ITEMS_TO_GROUP_BY_WEEK', () => { - const items = [ - ...Array.from({ length: 4 }, (_, i) => createItem(`Week1 Item ${i + 1}`, { dueDate: '2024-01-01' })), - ...Array.from({ length: 3 }, (_, i) => createItem(`Week2 Item ${i + 1}`, { dueDate: '2024-01-08' })), - ]; - - const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); - expect(result.length).toBeGreaterThan(1); - result.forEach((group) => { - expect(group.showDateHeader).toBeTrue(); - }); - }); - - it('should prioritize dates correctly', () => { - const items = [ - createItem('Due Date', { - dueDate: '2024-01-01', - startDateWithTime: '2024-01-02', - startDate: '2024-01-03', - }), - createItem('Start Time', { - startDateWithTime: '2024-01-02', - startDate: '2024-01-03', - }), - createItem('Start Date', { - startDate: '2024-01-03', - }), - createItem('No Date', {}), - ]; - - const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); - expect(result[0].items[0].title).toBe('Due Date'); - expect(result[0].items[1].title).toBe('Start Time'); - expect(result[0].items[2].title).toBe('Start Date'); - }); - - it('should sort items within groups by date in descending order', () => { - const items = [ - createItem('Item 2', { dueDate: '2024-01-02' }), - createItem('Item 1', { dueDate: '2024-01-01' }), - createItem('Item 3', { dueDate: '2024-01-03' }), - createItem('Item 5', { dueDate: '2024-01-05' }), - createItem('Item 4', { dueDate: '2024-01-04' }), - createItem('Item 6', { dueDate: '2024-01-06' }), - ]; - - const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); - const firstGroup = result[0]; - expect(firstGroup.items.map((item) => item.title)).toEqual(['Item 6', 'Item 5', 'Item 4', 'Item 3', 'Item 2', 'Item 1']); - }); - - it('should handle week boundaries correctly', () => { - const items = [ - createItem('Sunday 1', { dueDate: '2024-01-07' }), // Sunday of week 1 - createItem('Sunday 2', { dueDate: '2024-01-07' }), // Sunday of week 1 - createItem('Monday 1', { dueDate: '2024-01-08' }), // Monday of week 2 - createItem('Monday 2', { dueDate: '2024-01-08' }), // Monday of week 2 - createItem('Saturday 1', { dueDate: '2024-01-13' }), // Saturday of week 2 - createItem('Saturday 2', { dueDate: '2024-01-13' }), // Saturday of week 2 - ]; - - const result = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); - - // Items from different ISO weeks should be in different groups - expect(result[0].start!.isSame(dayjs('2024-01-08'), 'week')).toBeTrue(); - expect(result[1].start!.isSame(dayjs('2024-01-01'), 'week')).toBeTrue(); - - // Verify items are in correct groups - expect(result[0].items.map((i) => i.title)).toContain('Monday 1'); - expect(result[0].items.map((i) => i.title)).toContain('Monday 2'); - expect(result[0].items.map((i) => i.title)).toContain('Saturday 1'); - expect(result[0].items.map((i) => i.title)).toContain('Saturday 2'); - expect(result[1].items.map((i) => i.title)).toContain('Sunday 1'); - expect(result[1].items.map((i) => i.title)).toContain('Sunday 2'); - }); - }); -}); diff --git a/src/main/webapp/app/shared/util/week-grouping.util.spec.ts b/src/main/webapp/app/shared/util/week-grouping.util.spec.ts new file mode 100644 index 000000000000..50476f037360 --- /dev/null +++ b/src/main/webapp/app/shared/util/week-grouping.util.spec.ts @@ -0,0 +1,289 @@ +import { MIN_ITEMS_TO_GROUP_BY_WEEK, WeekGroupingUtil } from './week-grouping.util'; +import { SidebarCardElement } from '../types/sidebar'; +import dayjs from 'dayjs/esm'; + +describe('WeekGroupingUtil • getGroupedByWeek', () => { + it('returns a single group without header for the special exam sections', () => { + const items: SidebarCardElement[] = [ + { + title: 'Exam 1', + id: 'e1', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-01'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + { + title: 'Exam 2', + id: 'e2', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-02'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + ]; + + ['real', 'test', 'attempt'].forEach((key) => { + const groups = WeekGroupingUtil.getGroupedByWeek(items, key); + expect(groups).toHaveLength(1); + expect(groups[0].isNoDate).toBeTruthy(); + expect(groups[0].showDateHeader).toBeFalsy(); + expect(groups[0].items).toHaveLength(2); + }); + }); + + it('returns a single group while searching (no headers)', () => { + const items: SidebarCardElement[] = [ + { + title: 'Item 1', + id: 'i1', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-01'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + { + title: 'Item 2', + id: 'i2', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-02'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + { + title: 'Other', + id: 'i3', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-03'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + ]; + + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'exercise', 'item'); + expect(groups).toHaveLength(1); + expect(groups[0].showDateHeader).toBeFalsy(); + expect(groups[0].items).toHaveLength(2); + expect(groups[0].items.map((i) => i.title)).toEqual(['Item 1', 'Item 2']); + }); + + it('searches in both title and type', () => { + const items: SidebarCardElement[] = [ + { + title: 'Normal Item', + id: 'n1', + size: 'M', + type: 'quiz', + exercise: { + dueDate: dayjs('2024-01-01'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + { + title: 'Quiz Title', + id: 'n2', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-02'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + { + title: 'Other', + id: 'n3', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-03'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + ]; + + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'exercise', 'quiz'); + expect(groups).toHaveLength(1); + expect(groups[0].items.map((i) => i.title)).toEqual(expect.arrayContaining(['Normal Item', 'Quiz Title'])); + }); + + it('keeps a single group when item count ≤ MIN_ITEMS_TO_GROUP_BY_WEEK', () => { + const items: SidebarCardElement[] = Array.from({ length: MIN_ITEMS_TO_GROUP_BY_WEEK }, (_, i) => ({ + title: `Item ${i + 1}`, + id: `s${i}`, + size: 'M', + exercise: { + dueDate: dayjs(`2024-01-0${i + 1}`), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + })); + + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); + expect(groups).toHaveLength(1); + expect(groups[0].showDateHeader).toBeFalsy(); + expect(groups[0].items).toHaveLength(MIN_ITEMS_TO_GROUP_BY_WEEK); + }); + + it('honours the date-priority order (dueDate > startDateWithTime > startDate)', () => { + const items: SidebarCardElement[] = [ + { + title: 'Due Date', + id: 'p1', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-01'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + startDateWithTime: dayjs('2024-01-02'), + startDate: dayjs('2024-01-03'), + }, + { + title: 'Start Time', + id: 'p2', + size: 'M', + startDateWithTime: dayjs('2024-01-02'), + startDate: dayjs('2024-01-03'), + }, + { + title: 'Start Date', + id: 'p3', + size: 'M', + startDate: dayjs('2024-01-03'), + }, + ]; + + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); + expect(groups[0].items.map((i) => i.title)).toEqual(['Due Date', 'Start Time', 'Start Date']); + }); + + it('sorts items inside each group by date (descending)', () => { + const items: SidebarCardElement[] = ['06', '05', '04', '03', '02', '01'].map((d) => ({ + title: `Item ${d}`, + id: `d${d}`, + size: 'M', + exercise: { + dueDate: dayjs(`2024-01-${d}`), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + })); + + const [firstGroup] = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); + expect(firstGroup.items.map((i) => i.title)).toEqual(['Item 06', 'Item 05', 'Item 04', 'Item 03', 'Item 02', 'Item 01']); + }); + + it('splits week boundaries correctly', () => { + const items: SidebarCardElement[] = [ + { + title: 'Sun 1', + id: 's1', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-07'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + { + title: 'Sun 2', + id: 's2', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-07'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + { + title: 'Mon 1', + id: 'm1', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-08'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + { + title: 'Mon 2', + id: 'm2', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-08'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + { + title: 'Sat 1', + id: 't1', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-13'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + { + title: 'Sat 2', + id: 't2', + size: 'M', + exercise: { + dueDate: dayjs('2024-01-13'), + numberOfAssessmentsOfCorrectionRounds: [], + studentAssignedTeamIdComputed: false, + secondCorrectionEnabled: false, + }, + }, + ]; + + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); + + // first group is ISO week beginning 2024-01-08 + expect(groups[0].start!.isSame(dayjs('2024-01-08'), 'week')).toBeTruthy(); + expect(groups[1].start!.isSame(dayjs('2024-01-01'), 'week')).toBeTruthy(); + + expect(groups[0].items.map((i) => i.title)).toEqual(expect.arrayContaining(['Mon 1', 'Mon 2', 'Sat 1', 'Sat 2'])); + expect(groups[1].items.map((i) => i.title)).toEqual(expect.arrayContaining(['Sun 1', 'Sun 2'])); + }); + + it('handles lectures with startDate', () => { + const items: SidebarCardElement[] = [ + { title: 'Lecture 1', id: 'l1', size: 'M', startDate: dayjs('2024-01-01') }, + { title: 'Lecture 2', id: 'l2', size: 'M', startDate: dayjs('2024-01-08') }, + { title: 'Lecture 3', id: 'l3', size: 'M', startDate: dayjs('2024-01-15') }, + ]; + + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'lecture'); + expect(groups).toHaveLength(1); + groups.forEach((g) => expect(g.showDateHeader).toBeFalsy()); + }); +}); diff --git a/src/main/webapp/app/shared/util/week-grouping.util.ts b/src/main/webapp/app/shared/util/week-grouping.util.ts index 79b59c57b011..70c76e047b86 100644 --- a/src/main/webapp/app/shared/util/week-grouping.util.ts +++ b/src/main/webapp/app/shared/util/week-grouping.util.ts @@ -31,8 +31,8 @@ export class WeekGroupingUtil { * @returns A unique string identifier for the week */ static getWeekKey(date: dayjs.Dayjs): string { - const startOfWeek = date.startOf('isoWeek'); - const endOfWeek = date.endOf('isoWeek'); + const startOfWeek = date.startOf('week'); + const endOfWeek = date.endOf('week'); return `${startOfWeek.year()} - ${startOfWeek.format('DD MMM YYYY')} - ${endOfWeek.format('DD MMM YYYY')}`; } From 186e0c73bf011555179eae5c6e37b099c03d94de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eyl=C3=BCl=20Naz=20Can?= Date: Mon, 21 Apr 2025 23:16:30 +0200 Subject: [PATCH 07/10] fix test --- .../shared/util/week-grouping.util.spec.ts | 181 ++++++------------ 1 file changed, 56 insertions(+), 125 deletions(-) diff --git a/src/main/webapp/app/shared/util/week-grouping.util.spec.ts b/src/main/webapp/app/shared/util/week-grouping.util.spec.ts index 50476f037360..cb0ae3aa987f 100644 --- a/src/main/webapp/app/shared/util/week-grouping.util.spec.ts +++ b/src/main/webapp/app/shared/util/week-grouping.util.spec.ts @@ -2,7 +2,7 @@ import { MIN_ITEMS_TO_GROUP_BY_WEEK, WeekGroupingUtil } from './week-grouping.ut import { SidebarCardElement } from '../types/sidebar'; import dayjs from 'dayjs/esm'; -describe('WeekGroupingUtil • getGroupedByWeek', () => { +describe('WeekGroupingUtil', () => { it('returns a single group without header for the special exam sections', () => { const items: SidebarCardElement[] = [ { @@ -40,89 +40,53 @@ describe('WeekGroupingUtil • getGroupedByWeek', () => { it('returns a single group while searching (no headers)', () => { const items: SidebarCardElement[] = [ - { - title: 'Item 1', - id: 'i1', - size: 'M', - exercise: { - dueDate: dayjs('2024-01-01'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, - }, - { - title: 'Item 2', - id: 'i2', - size: 'M', - exercise: { - dueDate: dayjs('2024-01-02'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, - }, - { - title: 'Other', - id: 'i3', - size: 'M', - exercise: { - dueDate: dayjs('2024-01-03'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, - }, + { title: 'Item 1', id: 'i1', size: 'M', startDate: dayjs('2024-01-01') }, + { title: 'Item 2', id: 'i2', size: 'M', startDate: dayjs('2024-01-02') }, + { title: 'Other', id: 'i3', size: 'M', startDate: dayjs('2024-01-03') }, ]; - const groups = WeekGroupingUtil.getGroupedByWeek(items, 'exercise', 'item'); + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'lecture', 'item'); expect(groups).toHaveLength(1); expect(groups[0].showDateHeader).toBeFalsy(); expect(groups[0].items).toHaveLength(2); expect(groups[0].items.map((i) => i.title)).toEqual(['Item 1', 'Item 2']); }); - it('searches in both title and type', () => { + it('searches in both title and type for lectures', () => { const items: SidebarCardElement[] = [ - { - title: 'Normal Item', - id: 'n1', - size: 'M', - type: 'quiz', - exercise: { - dueDate: dayjs('2024-01-01'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, - }, - { - title: 'Quiz Title', - id: 'n2', - size: 'M', - exercise: { - dueDate: dayjs('2024-01-02'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, - }, - { - title: 'Other', - id: 'n3', - size: 'M', - exercise: { - dueDate: dayjs('2024-01-03'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, - }, + { title: 'Lecture 1', id: 'l1', size: 'M', type: 'lecture', startDate: dayjs('2024-01-01') }, + { title: 'Lecture 2', id: 'l2', size: 'M', startDate: dayjs('2024-01-02') }, + { title: 'Lecture 3', id: 'l3', size: 'M', startDate: dayjs('2024-01-03') }, + { title: 'Lecture 4', id: 'l4', size: 'M', startDate: dayjs('2024-01-04') }, + { title: 'Lecture 5', id: 'l5', size: 'M', startDate: dayjs('2024-01-05') }, + { title: 'Lecture 6', id: 'l6', size: 'M', startDate: dayjs('2024-01-07') }, ]; - const groups = WeekGroupingUtil.getGroupedByWeek(items, 'exercise', 'quiz'); + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'lecture', 'lecture'); expect(groups).toHaveLength(1); - expect(groups[0].items.map((i) => i.title)).toEqual(expect.arrayContaining(['Normal Item', 'Quiz Title'])); + expect(groups[0].items.map((i) => i.title)).toEqual(expect.arrayContaining(['Lecture 1', 'Lecture 2', 'Lecture 3', 'Lecture 4', 'Lecture 5', 'Lecture 6'])); + }); + + it('displays correct week range title', () => { + const items: SidebarCardElement[] = [ + { title: 'L1', id: 'm1', size: 'M', startDate: dayjs('2024-01-01') }, + { title: 'L2', id: 'w1', size: 'M', startDate: dayjs('2024-01-03') }, + { title: 'L3', id: 'w1', size: 'M', startDate: dayjs('2024-01-04') }, + { title: 'L4', id: 'm2', size: 'M', startDate: dayjs('2024-01-08') }, + { title: 'L5', id: 'w2', size: 'M', startDate: dayjs('2024-01-10') }, + { title: 'L6', id: 'w2', size: 'M', startDate: dayjs('2024-01-10') }, + ]; + + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'lecture'); + expect(groups).toHaveLength(2); + + // First group should be Week 2 (Jan 8-14) + expect(groups[0].start!.format('DD MMM YYYY')).toBe('07 Jan 2024'); + expect(groups[0].end!.format('DD MMM YYYY')).toBe('13 Jan 2024'); + + // Second group should be Week 1 (Jan 1-7) + expect(groups[1].start!.format('DD MMM YYYY')).toBe('31 Dec 2023'); + expect(groups[1].end!.format('DD MMM YYYY')).toBe('06 Jan 2024'); }); it('keeps a single group when item count ≤ MIN_ITEMS_TO_GROUP_BY_WEEK', () => { @@ -198,81 +162,48 @@ describe('WeekGroupingUtil • getGroupedByWeek', () => { it('splits week boundaries correctly', () => { const items: SidebarCardElement[] = [ { - title: 'Sun 1', + title: 'Week 1 - Sun', id: 's1', size: 'M', - exercise: { - dueDate: dayjs('2024-01-07'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, + startDate: dayjs('2024-01-07'), }, { - title: 'Sun 2', - id: 's2', + title: 'Week 1 - Mon', + id: 'm1', size: 'M', - exercise: { - dueDate: dayjs('2024-01-07'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, + startDate: dayjs('2024-01-08'), }, { - title: 'Mon 1', - id: 'm1', + title: 'Week 1 - Sat', + id: 't1', size: 'M', - exercise: { - dueDate: dayjs('2024-01-08'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, + startDate: dayjs('2024-01-13'), }, { - title: 'Mon 2', - id: 'm2', + title: 'Week 2 - Sun', + id: 's2', size: 'M', - exercise: { - dueDate: dayjs('2024-01-08'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, + startDate: dayjs('2024-01-14'), }, { - title: 'Sat 1', - id: 't1', + title: 'Week 2 - Mon', + id: 'm2', size: 'M', - exercise: { - dueDate: dayjs('2024-01-13'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, + startDate: dayjs('2024-01-15'), }, { - title: 'Sat 2', + title: 'Week 2 - Sat', id: 't2', size: 'M', - exercise: { - dueDate: dayjs('2024-01-13'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, + startDate: dayjs('2024-01-20'), }, ]; - const groups = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); - - // first group is ISO week beginning 2024-01-08 - expect(groups[0].start!.isSame(dayjs('2024-01-08'), 'week')).toBeTruthy(); - expect(groups[1].start!.isSame(dayjs('2024-01-01'), 'week')).toBeTruthy(); + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'lecture'); - expect(groups[0].items.map((i) => i.title)).toEqual(expect.arrayContaining(['Mon 1', 'Mon 2', 'Sat 1', 'Sat 2'])); - expect(groups[1].items.map((i) => i.title)).toEqual(expect.arrayContaining(['Sun 1', 'Sun 2'])); + expect(groups).toHaveLength(2); + expect(groups[0].items.map((i) => i.title)).toEqual(expect.arrayContaining(['Week 2 - Sun', 'Week 2 - Mon', 'Week 2 - Sat'])); + expect(groups[1].items.map((i) => i.title)).toEqual(expect.arrayContaining(['Week 1 - Sun', 'Week 1 - Mon', 'Week 1 - Sat'])); }); it('handles lectures with startDate', () => { From 5928bf84f163089190d8f9beba121aee8bd5ceb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eyl=C3=BCl=20Naz=20Can?= Date: Tue, 22 Apr 2025 01:31:46 +0200 Subject: [PATCH 08/10] fix wrong groupkey usage in util test --- .../shared/util/week-grouping.util.spec.ts | 249 +++++++----------- 1 file changed, 100 insertions(+), 149 deletions(-) diff --git a/src/main/webapp/app/shared/util/week-grouping.util.spec.ts b/src/main/webapp/app/shared/util/week-grouping.util.spec.ts index cb0ae3aa987f..f6b5ea7972f3 100644 --- a/src/main/webapp/app/shared/util/week-grouping.util.spec.ts +++ b/src/main/webapp/app/shared/util/week-grouping.util.spec.ts @@ -1,41 +1,19 @@ -import { MIN_ITEMS_TO_GROUP_BY_WEEK, WeekGroupingUtil } from './week-grouping.util'; +import { WeekGroupingUtil } from './week-grouping.util'; import { SidebarCardElement } from '../types/sidebar'; import dayjs from 'dayjs/esm'; describe('WeekGroupingUtil', () => { - it('returns a single group without header for the special exam sections', () => { + it('returns a single group for noDate', () => { const items: SidebarCardElement[] = [ - { - title: 'Exam 1', - id: 'e1', - size: 'M', - exercise: { - dueDate: dayjs('2024-01-01'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, - }, - { - title: 'Exam 2', - id: 'e2', - size: 'M', - exercise: { - dueDate: dayjs('2024-01-02'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, - }, + { title: 'Item 1', id: 'i1', size: 'M' }, + { title: 'Item 2', id: 'i2', size: 'M' }, ]; - ['real', 'test', 'attempt'].forEach((key) => { - const groups = WeekGroupingUtil.getGroupedByWeek(items, key); - expect(groups).toHaveLength(1); - expect(groups[0].isNoDate).toBeTruthy(); - expect(groups[0].showDateHeader).toBeFalsy(); - expect(groups[0].items).toHaveLength(2); - }); + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'noDate'); + expect(groups).toHaveLength(1); + expect(groups[0].isNoDate).toBeTruthy(); + expect(groups[0].showDateHeader).toBeFalsy(); + expect(groups[0].items).toHaveLength(2); }); it('returns a single group while searching (no headers)', () => { @@ -45,26 +23,26 @@ describe('WeekGroupingUtil', () => { { title: 'Other', id: 'i3', size: 'M', startDate: dayjs('2024-01-03') }, ]; - const groups = WeekGroupingUtil.getGroupedByWeek(items, 'lecture', 'item'); + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'current', 'item'); expect(groups).toHaveLength(1); expect(groups[0].showDateHeader).toBeFalsy(); expect(groups[0].items).toHaveLength(2); expect(groups[0].items.map((i) => i.title)).toEqual(['Item 1', 'Item 2']); }); - it('searches in both title and type for lectures', () => { + it('searches in both title and type', () => { const items: SidebarCardElement[] = [ - { title: 'Lecture 1', id: 'l1', size: 'M', type: 'lecture', startDate: dayjs('2024-01-01') }, - { title: 'Lecture 2', id: 'l2', size: 'M', startDate: dayjs('2024-01-02') }, - { title: 'Lecture 3', id: 'l3', size: 'M', startDate: dayjs('2024-01-03') }, - { title: 'Lecture 4', id: 'l4', size: 'M', startDate: dayjs('2024-01-04') }, - { title: 'Lecture 5', id: 'l5', size: 'M', startDate: dayjs('2024-01-05') }, - { title: 'Lecture 6', id: 'l6', size: 'M', startDate: dayjs('2024-01-07') }, + { title: 'Exercise 1', id: 'e1', size: 'M', type: 'exercise', startDate: dayjs('2024-01-01') }, + { title: 'Exercise 2', id: 'e2', size: 'M', startDate: dayjs('2024-01-02') }, + { title: 'Exercise 3', id: 'e3', size: 'M', startDate: dayjs('2024-01-03') }, + { title: 'Exercise 4', id: 'e4', size: 'M', startDate: dayjs('2024-01-04') }, + { title: 'Exercise 5', id: 'e5', size: 'M', startDate: dayjs('2024-01-05') }, + { title: 'Exercise 6', id: 'e6', size: 'M', startDate: dayjs('2024-01-07') }, ]; - const groups = WeekGroupingUtil.getGroupedByWeek(items, 'lecture', 'lecture'); + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'current', 'exercise'); expect(groups).toHaveLength(1); - expect(groups[0].items.map((i) => i.title)).toEqual(expect.arrayContaining(['Lecture 1', 'Lecture 2', 'Lecture 3', 'Lecture 4', 'Lecture 5', 'Lecture 6'])); + expect(groups[0].items.map((i) => i.title)).toEqual(expect.arrayContaining(['Exercise 1', 'Exercise 2', 'Exercise 3', 'Exercise 4', 'Exercise 5', 'Exercise 6'])); }); it('displays correct week range title', () => { @@ -77,7 +55,7 @@ describe('WeekGroupingUtil', () => { { title: 'L6', id: 'w2', size: 'M', startDate: dayjs('2024-01-10') }, ]; - const groups = WeekGroupingUtil.getGroupedByWeek(items, 'lecture'); + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'current'); expect(groups).toHaveLength(2); // First group should be Week 2 (Jan 8-14) @@ -89,132 +67,105 @@ describe('WeekGroupingUtil', () => { expect(groups[1].end!.format('DD MMM YYYY')).toBe('06 Jan 2024'); }); - it('keeps a single group when item count ≤ MIN_ITEMS_TO_GROUP_BY_WEEK', () => { - const items: SidebarCardElement[] = Array.from({ length: MIN_ITEMS_TO_GROUP_BY_WEEK }, (_, i) => ({ - title: `Item ${i + 1}`, - id: `s${i}`, + it('sorts items inside each group by date (descending)', () => { + const items: SidebarCardElement[] = ['06', '05', '04', '03', '02', '01'].map((d) => ({ + title: `Item ${d}`, + id: `d${d}`, size: 'M', - exercise: { - dueDate: dayjs(`2024-01-0${i + 1}`), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, + startDate: dayjs(`2024-01-${d}`), })); - const groups = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); - expect(groups).toHaveLength(1); - expect(groups[0].showDateHeader).toBeFalsy(); - expect(groups[0].items).toHaveLength(MIN_ITEMS_TO_GROUP_BY_WEEK); + const [firstGroup] = WeekGroupingUtil.getGroupedByWeek(items, 'current'); + expect(firstGroup.items.map((i) => i.title)).toEqual(['Item 06', 'Item 05', 'Item 04', 'Item 03', 'Item 02', 'Item 01']); }); - it('honours the date-priority order (dueDate > startDateWithTime > startDate)', () => { - const items: SidebarCardElement[] = [ - { - title: 'Due Date', - id: 'p1', - size: 'M', - exercise: { - dueDate: dayjs('2024-01-01'), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, - startDateWithTime: dayjs('2024-01-02'), - startDate: dayjs('2024-01-03'), - }, - { - title: 'Start Time', - id: 'p2', - size: 'M', - startDateWithTime: dayjs('2024-01-02'), - startDate: dayjs('2024-01-03'), - }, - { - title: 'Start Date', - id: 'p3', - size: 'M', - startDate: dayjs('2024-01-03'), - }, + it('handles different group keys correctly', () => { + const currentItems: SidebarCardElement[] = [ + { title: 'Current 1', id: 'c1', size: 'M', startDate: dayjs() }, + { title: 'Current 2', id: 'c2', size: 'M', startDate: dayjs().add(1, 'day') }, + { title: 'Current 3', id: 'c3', size: 'M', startDate: dayjs().add(1, 'day') }, + { title: 'Current 4', id: 'c4', size: 'M', startDate: dayjs().add(1, 'day') }, + { title: 'Current 5', id: 'c5', size: 'M', startDate: dayjs().add(1, 'day') }, + { title: 'Current 6', id: 'c6', size: 'M', startDate: dayjs().add(1, 'day') }, ]; - const groups = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); - expect(groups[0].items.map((i) => i.title)).toEqual(['Due Date', 'Start Time', 'Start Date']); - }); + const futureItems: SidebarCardElement[] = [ + { title: 'Future 1', id: 'f1', size: 'M', startDate: dayjs().add(1, 'month') }, + { title: 'Future 2', id: 'f2', size: 'M', startDate: dayjs().add(2, 'month') }, + ]; - it('sorts items inside each group by date (descending)', () => { - const items: SidebarCardElement[] = ['06', '05', '04', '03', '02', '01'].map((d) => ({ - title: `Item ${d}`, - id: `d${d}`, - size: 'M', - exercise: { - dueDate: dayjs(`2024-01-${d}`), - numberOfAssessmentsOfCorrectionRounds: [], - studentAssignedTeamIdComputed: false, - secondCorrectionEnabled: false, - }, - })); + const noDateItems: SidebarCardElement[] = [ + { title: 'No Date 1', id: 'n1', size: 'M' }, + { title: 'No Date 2', id: 'n2', size: 'M' }, + ]; - const [firstGroup] = WeekGroupingUtil.getGroupedByWeek(items, 'exercise'); - expect(firstGroup.items.map((i) => i.title)).toEqual(['Item 06', 'Item 05', 'Item 04', 'Item 03', 'Item 02', 'Item 01']); + const currentGroups = WeekGroupingUtil.getGroupedByWeek(currentItems, 'current'); + expect(currentGroups).toHaveLength(1); + expect(currentGroups[0].showDateHeader).toBeTruthy(); + + const futureGroups = WeekGroupingUtil.getGroupedByWeek(futureItems, 'future'); + expect(futureGroups).toHaveLength(1); + expect(futureGroups[0].showDateHeader).toBeFalsy(); + + const noDateGroups = WeekGroupingUtil.getGroupedByWeek(noDateItems, 'noDate'); + expect(noDateGroups).toHaveLength(1); + expect(noDateGroups[0].isNoDate).toBeTruthy(); + expect(noDateGroups[0].showDateHeader).toBeFalsy(); }); - it('splits week boundaries correctly', () => { + it('handles mixed dates within each group key', () => { const items: SidebarCardElement[] = [ - { - title: 'Week 1 - Sun', - id: 's1', - size: 'M', - startDate: dayjs('2024-01-07'), - }, - { - title: 'Week 1 - Mon', - id: 'm1', - size: 'M', - startDate: dayjs('2024-01-08'), - }, - { - title: 'Week 1 - Sat', - id: 't1', - size: 'M', - startDate: dayjs('2024-01-13'), - }, - { - title: 'Week 2 - Sun', - id: 's2', - size: 'M', - startDate: dayjs('2024-01-14'), - }, - { - title: 'Week 2 - Mon', - id: 'm2', - size: 'M', - startDate: dayjs('2024-01-15'), - }, - { - title: 'Week 2 - Sat', - id: 't2', - size: 'M', - startDate: dayjs('2024-01-20'), - }, + { title: 'Week 1', id: 'c1', size: 'M', startDate: dayjs() }, + { title: 'Week 2', id: 'c2', size: 'M', startDate: dayjs() }, + { title: 'Week 3', id: 'c3', size: 'M', startDate: dayjs() }, + { title: 'Week 4', id: 'c4', size: 'M', startDate: dayjs() }, + { title: 'Week 5', id: 'c5', size: 'M', startDate: dayjs() }, + { title: 'Week 6', id: 'c6', size: 'M', startDate: dayjs() }, + { title: 'Week 7', id: 'c7', size: 'M' }, + { title: 'Week 8', id: 'c8', size: 'M' }, + { title: 'Week 9', id: 'c9', size: 'M' }, ]; - const groups = WeekGroupingUtil.getGroupedByWeek(items, 'lecture'); + const currentGroups = WeekGroupingUtil.getGroupedByWeek(items, 'future'); + expect(currentGroups).toHaveLength(2); - expect(groups).toHaveLength(2); - expect(groups[0].items.map((i) => i.title)).toEqual(expect.arrayContaining(['Week 2 - Sun', 'Week 2 - Mon', 'Week 2 - Sat'])); - expect(groups[1].items.map((i) => i.title)).toEqual(expect.arrayContaining(['Week 1 - Sun', 'Week 1 - Mon', 'Week 1 - Sat'])); + expect(currentGroups[0].showDateHeader).toBeTruthy(); + expect(currentGroups[0].isNoDate).toBeFalsy(); + expect(currentGroups[0].items[0].title).toBe('Week 1'); + + expect(currentGroups[1].showDateHeader).toBeTruthy(); + expect(currentGroups[1].isNoDate).toBeTruthy(); + expect(currentGroups[1].items[0].title).toBe('Week 7'); }); - it('handles lectures with startDate', () => { + it('sorts groups correctly within the same year', () => { const items: SidebarCardElement[] = [ - { title: 'Lecture 1', id: 'l1', size: 'M', startDate: dayjs('2024-01-01') }, - { title: 'Lecture 2', id: 'l2', size: 'M', startDate: dayjs('2024-01-08') }, - { title: 'Lecture 3', id: 'l3', size: 'M', startDate: dayjs('2024-01-15') }, + // March items + { title: 'March 1', id: 'm1', size: 'M', startDate: dayjs('2025-03-01') }, + { title: 'March 15', id: 'm2', size: 'M', startDate: dayjs('2025-03-15') }, + // January items + { title: 'January 1', id: 'j1', size: 'M', startDate: dayjs('2025-01-01') }, + { title: 'January 15', id: 'j2', size: 'M', startDate: dayjs('2025-01-15') }, + // February items + { title: 'February 1', id: 'f1', size: 'M', startDate: dayjs('2025-02-01') }, + { title: 'February 15', id: 'f2', size: 'M', startDate: dayjs('2025-02-15') }, + // No date items + { title: 'No Date 1', id: 'n1', size: 'M' }, + { title: 'No Date 2', id: 'n2', size: 'M' }, ]; - const groups = WeekGroupingUtil.getGroupedByWeek(items, 'lecture'); - expect(groups).toHaveLength(1); - groups.forEach((g) => expect(g.showDateHeader).toBeFalsy()); + const groups = WeekGroupingUtil.getGroupedByWeek(items, 'current'); + + // Should have 7 groups (2 weeks per month for 3 months + 1 no-date group) + expect(groups).toHaveLength(7); + + // Groups should be ordered by date (newest first) with no-date group at the end + expect(groups[0].start!.format('YYYY-MM-DD')).toBe('2025-03-09'); // March week 2 + expect(groups[1].start!.format('YYYY-MM-DD')).toBe('2025-02-23'); // March week 1 + expect(groups[2].start!.format('YYYY-MM-DD')).toBe('2025-02-09'); // February week 2 + expect(groups[3].start!.format('YYYY-MM-DD')).toBe('2025-01-26'); // February week 1 + expect(groups[4].start!.format('YYYY-MM-DD')).toBe('2025-01-12'); // January week 2 + expect(groups[5].start!.format('YYYY-MM-DD')).toBe('2024-12-29'); // January week 1 + expect(groups[6].isNoDate).toBeTruthy(); }); }); From 136d474d72b3a83c13a46ee679a981933474546b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eyl=C3=BCl=20Naz=20Can?= Date: Tue, 29 Apr 2025 14:04:30 +0200 Subject: [PATCH 09/10] update test case --- .../shared/util/week-grouping.util.spec.ts | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/webapp/app/shared/util/week-grouping.util.spec.ts b/src/main/webapp/app/shared/util/week-grouping.util.spec.ts index f6b5ea7972f3..b0182bf75c2c 100644 --- a/src/main/webapp/app/shared/util/week-grouping.util.spec.ts +++ b/src/main/webapp/app/shared/util/week-grouping.util.spec.ts @@ -114,28 +114,34 @@ describe('WeekGroupingUtil', () => { }); it('handles mixed dates within each group key', () => { + const baseDate = dayjs('2025-01-01'); + const items: SidebarCardElement[] = [ - { title: 'Week 1', id: 'c1', size: 'M', startDate: dayjs() }, - { title: 'Week 2', id: 'c2', size: 'M', startDate: dayjs() }, - { title: 'Week 3', id: 'c3', size: 'M', startDate: dayjs() }, - { title: 'Week 4', id: 'c4', size: 'M', startDate: dayjs() }, - { title: 'Week 5', id: 'c5', size: 'M', startDate: dayjs() }, - { title: 'Week 6', id: 'c6', size: 'M', startDate: dayjs() }, + { title: 'Week 1', id: 'c1', size: 'M', startDate: baseDate.add(0, 'day') }, + { title: 'Week 2', id: 'c2', size: 'M', startDate: baseDate.add(1, 'day') }, + { title: 'Week 3', id: 'c3', size: 'M', startDate: baseDate.add(2, 'day') }, + { title: 'Week 4', id: 'c4', size: 'M', startDate: baseDate.add(3, 'day') }, + { title: 'Week 5', id: 'c5', size: 'M', startDate: baseDate.add(4, 'day') }, + { title: 'Week 6', id: 'c6', size: 'M', startDate: baseDate.add(5, 'day') }, { title: 'Week 7', id: 'c7', size: 'M' }, { title: 'Week 8', id: 'c8', size: 'M' }, { title: 'Week 9', id: 'c9', size: 'M' }, ]; const currentGroups = WeekGroupingUtil.getGroupedByWeek(items, 'future'); - expect(currentGroups).toHaveLength(2); + expect(currentGroups).toHaveLength(3); expect(currentGroups[0].showDateHeader).toBeTruthy(); expect(currentGroups[0].isNoDate).toBeFalsy(); - expect(currentGroups[0].items[0].title).toBe('Week 1'); + expect(currentGroups[0].items[0].title).toBe('Week 6'); expect(currentGroups[1].showDateHeader).toBeTruthy(); - expect(currentGroups[1].isNoDate).toBeTruthy(); - expect(currentGroups[1].items[0].title).toBe('Week 7'); + expect(currentGroups[1].isNoDate).toBeFalsy(); + expect(currentGroups[1].items[0].title).toBe('Week 4'); + + expect(currentGroups[2].showDateHeader).toBeTruthy(); + expect(currentGroups[2].isNoDate).toBeTruthy(); + expect(currentGroups[2].items[0].title).toBe('Week 7'); }); it('sorts groups correctly within the same year', () => { From 7fdf2b64f760c0bf29ce5e8957fdc4d54da87c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eyl=C3=BCl=20Naz=20Can?= Date: Wed, 30 Apr 2025 09:43:19 +0200 Subject: [PATCH 10/10] add missing braces --- .../app/shared/util/week-grouping.util.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/shared/util/week-grouping.util.ts b/src/main/webapp/app/shared/util/week-grouping.util.ts index 70c76e047b86..a434b2611779 100644 --- a/src/main/webapp/app/shared/util/week-grouping.util.ts +++ b/src/main/webapp/app/shared/util/week-grouping.util.ts @@ -45,9 +45,15 @@ export class WeekGroupingUtil { * @returns Negative if a < b, positive if a > b, 0 if equal */ static compareDates(a?: dayjs.Dayjs, b?: dayjs.Dayjs): number { - if (!a && !b) return 0; - if (!a) return 1; - if (!b) return -1; + if (!a && !b) { + return 0; + } + if (!a) { + return 1; + } + if (!b) { + return -1; + } return b.valueOf() - a.valueOf(); } @@ -111,8 +117,12 @@ export class WeekGroupingUtil { // Sort groups: dated groups first (by year and date), then no-date groups return groups.sort((a, b) => { - if (a.isNoDate) return 1; - if (b.isNoDate) return -1; + if (a.isNoDate) { + return 1; + } + if (b.isNoDate) { + return -1; + } if (a.start!.year() !== b.start!.year()) { return b.start!.year() - a.start!.year(); }