Skip to content

Commit ceb8bbc

Browse files
authored
Merge pull request #1340 from b0ink/fix/portfolios-filters
fix: persist portfolio filters in url
2 parents 59f389b + 322eef2 commit ceb8bbc

4 files changed

Lines changed: 133 additions & 9 deletions

File tree

src/app/units/states/portfolios/directives/portfolios-list/portfolios-list.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ <h2>Mark portfolios</h2>
1010
matInput
1111
placeholder="Eg. Student ID"
1212
[disabled]="loading"
13-
(keyup)="applyFilter($event)"
13+
[value]="filterText"
14+
(input)="applyFilter($event)"
1415
/>
1516
</mat-form-field>
1617
<mat-button-toggle-group

src/app/units/states/portfolios/directives/portfolios-list/portfolios-list.component.ts

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@ import {AlertService} from 'src/app/common/services/alert.service';
2525
import {GradeService} from 'src/app/common/services/grade.service';
2626
import {D2lTransferModal} from '../../d2l-transfer-modal/d2l-transfer.component';
2727

28+
export interface PortfolioListFilters {
29+
portfolioFilter: 'all' | 'submitted_only';
30+
tutorialFilter: 'all' | 'mine';
31+
gradeFilter: number | null;
32+
filterText: string;
33+
}
34+
35+
export const DEFAULT_PORTFOLIO_LIST_FILTERS: PortfolioListFilters = {
36+
portfolioFilter: 'submitted_only',
37+
tutorialFilter: 'all',
38+
gradeFilter: null,
39+
filterText: '',
40+
};
41+
2842
@Component({
2943
selector: 'f-portfolios-list',
3044
templateUrl: './portfolios-list.component.html',
@@ -35,9 +49,12 @@ import {D2lTransferModal} from '../../d2l-transfer-modal/d2l-transfer.component'
3549
export class PortfoliosListComponent implements OnChanges, AfterViewInit {
3650
@Input() unit: Unit;
3751
@Input() loading = true;
52+
@Input() filters: PortfolioListFilters = DEFAULT_PORTFOLIO_LIST_FILTERS;
3853

3954
@Output()
4055
public studentSelected: EventEmitter<Project> = new EventEmitter();
56+
@Output()
57+
public filtersChange: EventEmitter<PortfolioListFilters> = new EventEmitter();
4158

4259
displayedColumns: string[] = [];
4360

@@ -49,6 +66,7 @@ export class PortfoliosListComponent implements OnChanges, AfterViewInit {
4966
public portfolioFilter: 'all' | 'submitted_only' = 'submitted_only';
5067
public tutorialFilter: 'all' | 'mine' = 'all';
5168
public gradeFilter: number | null = null;
69+
public filterText = '';
5270

5371
constructor(
5472
private taskService: TaskService,
@@ -64,12 +82,27 @@ export class PortfoliosListComponent implements OnChanges, AfterViewInit {
6482
ngAfterViewInit() {
6583
this.dataSource.paginator = this.paginator;
6684
this.dataSource.sort = this.sort;
85+
this.applyTextFilter();
6786
}
6887

6988
ngOnChanges(changes: SimpleChanges): void {
89+
if (changes.filters) {
90+
this.portfolioFilter = this.filters.portfolioFilter;
91+
this.tutorialFilter = this.filters.tutorialFilter;
92+
this.gradeFilter = this.filters.gradeFilter;
93+
this.filterText = this.filters.filterText;
94+
}
95+
7096
if (!this.loading && this.unit && (changes.loading || changes.unit)) {
7197
this.updateDataSource();
7298
}
99+
100+
if (changes.filters) {
101+
this.applyTextFilter();
102+
if (!this.loading && this.unit) {
103+
this.updateDataSource();
104+
}
105+
}
73106
}
74107

75108
openProject(event: Event, project: Project) {
@@ -144,31 +177,34 @@ export class PortfoliosListComponent implements OnChanges, AfterViewInit {
144177

145178
this.dataSource.data = students;
146179
this.dataSource.paginator?.firstPage();
180+
this.applyTextFilter();
147181
}
148182

149183
onPortfolioFilterChange(event: MatButtonToggleChange) {
150184
this.portfolioFilter = event.value;
151185
this.updateDataSource();
186+
this.emitFilters();
152187
}
153188

154189
onTutorialFilterChange(event: MatButtonToggleChange) {
155190
this.tutorialFilter = event.value;
156191
this.updateDataSource();
192+
this.emitFilters();
157193
}
158194

159195
onGradeFilterChange(event: MatButtonToggleChange) {
160196
this.gradeFilter = event.value;
161197
this.updateDataSource();
198+
this.emitFilters();
162199
}
163200

164201
applyFilter(event: Event) {
165-
const filterValue = (event.target as HTMLInputElement).value;
166-
this.dataSource.filter = filterValue.trim().toLowerCase();
167-
168-
if (this.dataSource.paginator) {
169-
this.dataSource.paginator.firstPage();
170-
}
202+
this.filterText = (event.target as HTMLInputElement).value;
203+
this.applyTextFilter();
204+
this.emitFilters();
205+
}
171206

207+
private applyTextFilter() {
172208
this.dataSource.filterPredicate = (project, filter) => {
173209
const text = [
174210
project.student.studentId,
@@ -183,6 +219,21 @@ export class PortfoliosListComponent implements OnChanges, AfterViewInit {
183219

184220
return text.includes(filter);
185221
};
222+
223+
this.dataSource.filter = this.filterText.trim().toLowerCase();
224+
225+
if (this.dataSource.paginator) {
226+
this.dataSource.paginator.firstPage();
227+
}
228+
}
229+
230+
private emitFilters() {
231+
this.filtersChange.emit({
232+
portfolioFilter: this.portfolioFilter,
233+
tutorialFilter: this.tutorialFilter,
234+
gradeFilter: this.gradeFilter,
235+
filterText: this.filterText,
236+
});
186237
}
187238

188239
selectStudent(project: Project) {

src/app/units/states/portfolios/portfolios.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
@switch (currentTab.routeSegment) {
1616
@case ('select') {
1717
<f-portfolios-list
18+
[filters]="portfolioListFilters"
1819
[loading]="loadingStudents"
1920
[unit]="unit"
21+
(filtersChange)="portfolioListFiltersChange($event)"
2022
(studentSelected)="studentSelected($event)"
2123
></f-portfolios-list>
2224
}

src/app/units/states/portfolios/portfolios.component.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import {ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit} from '@angular/core';
22
import {MatTabChangeEvent} from '@angular/material/tabs';
3-
import {ActivatedRoute, Router} from '@angular/router';
3+
import {ActivatedRoute, ParamMap, Router} from '@angular/router';
44
import {BehaviorSubject, Observable, Subscription, first, of} from 'rxjs';
55
import {Project} from 'src/app/api/models/project';
66
import {Unit} from 'src/app/api/models/unit';
77
import {ProjectService} from 'src/app/api/services/project.service';
88
import {UserService} from 'src/app/api/services/user.service';
99
import {AlertService} from 'src/app/common/services/alert.service';
10+
import {
11+
DEFAULT_PORTFOLIO_LIST_FILTERS,
12+
PortfolioListFilters,
13+
} from './directives/portfolios-list/portfolios-list.component';
1014

1115
type PortfolioTabKey = 'select' | 'progress' | 'student-notes' | 'portfolio' | 'assessment';
1216

@@ -39,6 +43,7 @@ export class PortfoliosComponent implements OnInit, OnDestroy {
3943
public selectedProject$: BehaviorSubject<Project | null> = new BehaviorSubject(null);
4044
public loadingStudents = true;
4145
public currentTab: PortfolioTab = this.tabs[0];
46+
public portfolioListFilters: PortfolioListFilters = {...DEFAULT_PORTFOLIO_LIST_FILTERS};
4247

4348
private subscriptions: Subscription[] = [];
4449
private selectedProjectId: number | null = null;
@@ -70,6 +75,9 @@ export class PortfoliosComponent implements OnInit, OnDestroy {
7075
this.route.paramMap.subscribe((params) => {
7176
this.updateCurrentTabFromState(params.get('tab'), params.get('projectId'));
7277
}),
78+
this.route.queryParamMap.subscribe((params) => {
79+
this.updatePortfolioListFiltersFromQueryParams(params);
80+
}),
7381
);
7482
},
7583
error: (error) => {
@@ -101,12 +109,27 @@ export class PortfoliosComponent implements OnInit, OnDestroy {
101109
this.navigateToProject(project.id, 'progress');
102110
}
103111

112+
public portfolioListFiltersChange(filters: PortfolioListFilters): void {
113+
this.portfolioListFilters = {...filters};
114+
115+
if (this.currentTab.routeSegment === 'select') {
116+
this.router.navigate([], {
117+
relativeTo: this.route,
118+
queryParams: this.portfolioListFilterQueryParams(),
119+
replaceUrl: true,
120+
});
121+
}
122+
}
123+
104124
public onTabChange(event: MatTabChangeEvent): void {
105125
const nextTab = this.tabs[event.index] ?? this.tabs[0];
106126
this.currentTab = nextTab;
107127

108128
if (nextTab.routeSegment === 'select' || !this.selectedProject) {
109-
this.router.navigate(['/units', this.unit.id, 'students', 'portfolios'], {replaceUrl: true});
129+
this.router.navigate(['/units', this.unit.id, 'students', 'portfolios'], {
130+
queryParams: this.portfolioListFilterQueryParams(),
131+
replaceUrl: true,
132+
});
110133
return;
111134
}
112135

@@ -125,6 +148,52 @@ export class PortfoliosComponent implements OnInit, OnDestroy {
125148
});
126149
}
127150

151+
private updatePortfolioListFiltersFromQueryParams(params: ParamMap): void {
152+
const filters: PortfolioListFilters = {
153+
portfolioFilter: params.get('portfolio') === 'all' ? 'all' : 'submitted_only',
154+
tutorialFilter: params.get('tutorial') === 'mine' ? 'mine' : 'all',
155+
gradeFilter: this.gradeFilterFromQueryParam(params.get('grade')),
156+
filterText: params.get('search') ?? '',
157+
};
158+
159+
if (!this.portfolioListFiltersEqual(filters, this.portfolioListFilters)) {
160+
this.portfolioListFilters = filters;
161+
}
162+
}
163+
164+
private gradeFilterFromQueryParam(grade: string | null): number | null {
165+
if (grade === null) {
166+
return null;
167+
}
168+
169+
const gradeValue = Number(grade);
170+
return Number.isFinite(gradeValue) ? gradeValue : null;
171+
}
172+
173+
private portfolioListFilterQueryParams(): Record<string, string | number | null> {
174+
return {
175+
portfolio:
176+
this.portfolioListFilters.portfolioFilter === DEFAULT_PORTFOLIO_LIST_FILTERS.portfolioFilter
177+
? null
178+
: this.portfolioListFilters.portfolioFilter,
179+
tutorial:
180+
this.portfolioListFilters.tutorialFilter === DEFAULT_PORTFOLIO_LIST_FILTERS.tutorialFilter
181+
? null
182+
: this.portfolioListFilters.tutorialFilter,
183+
grade: this.portfolioListFilters.gradeFilter,
184+
search: this.portfolioListFilters.filterText.trim() || null,
185+
};
186+
}
187+
188+
private portfolioListFiltersEqual(a: PortfolioListFilters, b: PortfolioListFilters): boolean {
189+
return (
190+
a.portfolioFilter === b.portfolioFilter &&
191+
a.tutorialFilter === b.tutorialFilter &&
192+
a.gradeFilter === b.gradeFilter &&
193+
a.filterText === b.filterText
194+
);
195+
}
196+
128197
private updateCurrentTabFromState(
129198
tabParam?: string | null,
130199
projectIdParam?: string | null,
@@ -184,6 +253,7 @@ export class PortfoliosComponent implements OnInit, OnDestroy {
184253

185254
private navigateToProject(projectId: number, tab: PortfolioTabKey): void {
186255
this.router.navigate(['/units', this.unit.id, 'students', 'portfolios', projectId, tab], {
256+
queryParams: this.portfolioListFilterQueryParams(),
187257
replaceUrl: true,
188258
});
189259
}

0 commit comments

Comments
 (0)