Skip to content

General: Add subgrouping to sidebar for lectures and exercises #10608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b2f481c
display subtitle in sidebar for exercise and lecture
eylulnc Mar 29, 2025
e9f59c2
add test cases
eylulnc Mar 29, 2025
829cfdb
add start date for lecture
eylulnc Mar 30, 2025
e95cace
fix date order
eylulnc Mar 30, 2025
a82a22f
Merge branch 'develop' into feature/general/sidebar-exercise-lecture-…
eylulnc Mar 31, 2025
915df0c
Merge branch 'develop' of https://github.com/ls1intum/Artemis into fe…
eylulnc Apr 4, 2025
9d07717
Merge branch 'develop' of https://github.com/ls1intum/Artemis into fe…
eylulnc Apr 7, 2025
6e21676
Merge branch 'develop' into feature/general/sidebar-exercise-lecture-…
eylulnc Apr 8, 2025
2feed8d
Merge branch 'develop' of https://github.com/ls1intum/Artemis into fe…
eylulnc Apr 17, 2025
2f4a500
Merge branch 'develop' into feature/general/sidebar-exercise-lecture-…
eylulnc Apr 18, 2025
1a92efb
Merge branch 'develop' into feature/general/sidebar-exercise-lecture-…
eylulnc Apr 19, 2025
bde97a7
Merge branch 'develop' into feature/general/sidebar-exercise-lecture-…
eylulnc Apr 21, 2025
8c140ec
implement reviews and fix language issue
eylulnc Apr 21, 2025
b0841a8
fix test
eylulnc Apr 21, 2025
186e0c7
fix test
eylulnc Apr 21, 2025
5928bf8
fix wrong groupkey usage in util test
eylulnc Apr 21, 2025
be96d1f
Merge branch 'develop' into feature/general/sidebar-exercise-lecture-…
eylulnc Apr 23, 2025
518a9b6
Merge branch 'develop' of https://github.com/ls1intum/Artemis into fe…
eylulnc Apr 27, 2025
1381e9f
Merge branch 'develop' into feature/general/sidebar-exercise-lecture-…
eylulnc Apr 29, 2025
136d474
update test case
eylulnc Apr 29, 2025
7fdf2b6
add missing braces
eylulnc Apr 30, 2025
c1c059c
Merge branch 'develop' into feature/general/sidebar-exercise-lecture-…
eylulnc Apr 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,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,
};
}
mapTutorialGroupToSidebarCardElement(tutorialGroup: TutorialGroup): SidebarCardElement {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,37 @@
</div>
@if ((groupedData[groupKey].entityData | searchFilter: ['title', 'type'] : searchValue)?.length) {
<hr class="my-0" />
<div id="test-accordion-item-content" [ngbCollapse]="collapseState[groupKey]" class="p-2 bg-body">
@for (sidebarItem of groupedData[groupKey].entityData | searchFilter: ['title', 'type'] : searchValue; let last = $last; track sidebarItem.id) {
<div [ngClass]="{ 'mb-2': !last }">
<!-- loading sidebarCard with help of a directive depending on its size input-->
<div
jhiSidebarCard
[size]="sidebarItem.size"
[itemSelected]="itemSelected"
[sidebarType]="sidebarType"
[sidebarItem]="sidebarItem"
[groupKey]="groupKey"
(onUpdateSidebar)="onUpdateSidebar.emit()"
></div>
<div id="test-accordion-item-content" [ngbCollapse]="collapseState[groupKey]" class="p-2 bg-module">
@for (weekGroup of getGroupedByWeek(groupKey); track weekGroup.start?.valueOf()) {
@if (weekGroup.showDateHeader) {
<div class="text-muted small p-2">
{{
weekGroup.isNoDate
? ('artemisApp.courseOverview.sidebar.noDate' | artemisTranslate)
: ('artemisApp.courseOverview.sidebar.weekRange'
| artemisTranslate
: {
start: (weekGroup.start | artemisDate: 'long-date'),
end: (weekGroup.end | artemisDate: 'long-date'),
})
}}
</div>
}
<div class="bg-body p-2">
@for (sidebarItem of weekGroup.items | searchFilter: ['title', 'type'] : searchValue; let last = $last; track sidebarItem.id) {
<div [ngClass]="{ 'mb-2': !last }">
<!-- loading sidebarCard with help of a directive depending on its size input-->
<div
jhiSidebarCard
[size]="sidebarItem.size"
[itemSelected]="itemSelected"
[sidebarType]="sidebarType"
[sidebarItem]="sidebarItem"
[groupKey]="groupKey"
(onUpdateSidebar)="onUpdateSidebar.emit()"
></div>
</div>
}
</div>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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';
Expand All @@ -29,6 +30,7 @@ describe('SidebarAccordionComponent', () => {
SearchFilterComponent,
MockPipe(ArtemisTranslatePipe),
MockComponent(SearchFilterComponent),
MockPipe(ArtemisDatePipe),
],
providers: [{ provide: ActivatedRoute, useValue: new MockActivatedRoute() }],
}).compileComponents();
Expand Down Expand Up @@ -153,4 +155,10 @@ describe('SidebarAccordionComponent', () => {
expect(component.totalUnreadMessagesPerGroup['future']).toBe(1);
expect(component.totalUnreadMessagesPerGroup['noDate']).toBe(0);
});

it('should use the week grouping utility for grouping items', () => {
const result = component.getGroupedByWeek('current');
expect(result).toBeDefined();
expect(Array.isArray(result)).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +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 { 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;
Expand Down Expand Up @@ -93,4 +95,8 @@ 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));
}

getGroupedByWeek(groupKey: string): WeekGroup[] {
return WeekGroupingUtil.getGroupedByWeek(this.groupedData[groupKey].entityData, groupKey, this.searchValue);
}
}
4 changes: 4 additions & 0 deletions src/main/webapp/app/shared/types/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
171 changes: 171 additions & 0 deletions src/main/webapp/app/shared/util/week-grouping.util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { WeekGroupingUtil } from './week-grouping.util';
import { SidebarCardElement } from '../types/sidebar';
import dayjs from 'dayjs/esm';

describe('WeekGroupingUtil', () => {
it('returns a single group for noDate', () => {
const items: SidebarCardElement[] = [
{ title: 'Item 1', id: 'i1', size: 'M' },
{ title: 'Item 2', id: 'i2', size: 'M' },
];

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)', () => {
const items: SidebarCardElement[] = [
{ 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, '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', () => {
const items: SidebarCardElement[] = [
{ 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, 'current', 'exercise');
expect(groups).toHaveLength(1);
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', () => {
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, 'current');
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('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',
startDate: dayjs(`2024-01-${d}`),
}));

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('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 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') },
];

const noDateItems: SidebarCardElement[] = [
{ title: 'No Date 1', id: 'n1', size: 'M' },
{ title: 'No Date 2', id: 'n2', size: 'M' },
];

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('handles mixed dates within each group key', () => {
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 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[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('sorts groups correctly within the same year', () => {
const items: SidebarCardElement[] = [
// 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, '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();
});
});
Loading
Loading