Skip to content

Commit e770b6a

Browse files
feat(Unit Library): Filter units by location (#2217)
Co-authored-by: Jonathan Lim-Breitbart <breity10@gmail.com>
1 parent c0ad950 commit e770b6a

8 files changed

Lines changed: 241 additions & 48 deletions

File tree

src/app/domain/projectFilterValues.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Subject } from 'rxjs';
22
import { LibraryProject } from '../modules/library/libraryProject';
3+
import { Location } from '../modules/library/Location';
34

45
export class ProjectFilterValues {
56
disciplineValue: string[] = [];
67
featureValue: string[] = [];
78
gradeLevelValue: number[] = [];
9+
locationValue: string[] = [];
810
publicUnitType?: ('wiseTested' | 'communityBuilt')[] = [];
911
publicUnitTypeValue?: ('wiseTested' | 'communityBuilt')[] = [];
1012
searchValue: string = '';
@@ -21,7 +23,8 @@ export class ProjectFilterValues {
2123
this.matchesDiscipline(project) &&
2224
this.matchesUnitType(project) &&
2325
this.matchesFeature(project) &&
24-
this.matchesGradeLevel(project)
26+
this.matchesGradeLevel(project) &&
27+
this.matchesLocation(project)
2528
);
2629
}
2730

@@ -54,7 +57,8 @@ export class ProjectFilterValues {
5457
this.disciplineValue.length +
5558
this.unitTypeValue.length +
5659
this.gradeLevelValue.length +
57-
this.featureValue.length >
60+
this.featureValue.length +
61+
this.locationValue.length >
5862
0
5963
);
6064
}
@@ -67,6 +71,7 @@ export class ProjectFilterValues {
6771
this.searchValue = '';
6872
this.standardValue = [];
6973
this.unitTypeValue = [];
74+
this.locationValue = [];
7075
}
7176

7277
private matchesUnitType(project: LibraryProject): boolean {
@@ -95,6 +100,17 @@ export class ProjectFilterValues {
95100
);
96101
}
97102

103+
private matchesLocation(project: LibraryProject): boolean {
104+
return (
105+
this.locationValue.length === 0 ||
106+
project.metadata.locations
107+
?.map((location) => Object.assign(new Location(), location))
108+
.map((location) => location.getLocationOptions())
109+
.flat()
110+
.some((locationOption) => this.locationValue.includes(locationOption.name))
111+
);
112+
}
113+
98114
private matchesFeature(project: LibraryProject): boolean {
99115
return (
100116
this.featureValue.length === 0 ||
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export type LocationType = 'level1' | 'level2' | 'level3';
2+
3+
export const locationTypeToLabel: { [key in LocationType]: string } = {
4+
level3: $localize`Locale`,
5+
level2: $localize`State`,
6+
level1: $localize`Country`
7+
};
8+
9+
export class LocationOption {
10+
name: string;
11+
type: LocationType;
12+
constructor(type: LocationType, name: string) {
13+
this.type = type;
14+
this.name = name;
15+
}
16+
}
17+
18+
// Represents a geographical location associated with a project
19+
export class Location {
20+
id: string = '';
21+
level1: string = ''; // country
22+
level2: string = ''; // state
23+
level3: string = ''; // city, county, or other locale
24+
25+
getLocationOptions(): LocationOption[] {
26+
const options = [];
27+
if (this.level1) {
28+
options.push(new LocationOption('level1', this.level1));
29+
}
30+
if (this.level2) {
31+
options.push(new LocationOption('level2', `${this.level2}, ${this.level1}`));
32+
}
33+
if (this.level3) {
34+
options.push(new LocationOption('level3', `${this.level3}, ${this.level2}, ${this.level1}`));
35+
}
36+
return options;
37+
}
38+
}

src/app/modules/library/library-filters/library-filters.component.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,5 +127,22 @@ <h3 class="mat-subtitle-2" i18n>Filters</h3>
127127
/>
128128
</div>
129129
}
130+
@if (locationOptions.length > 0) {
131+
<div
132+
class="library-filter"
133+
[ngClass]="{ 'md:w-full': isSplitScreen, 'md:w-1/4': !isSplitScreen }"
134+
>
135+
<location-select-menu
136+
[options]="locationOptions"
137+
i18n-placeholderText
138+
placeholderText="Locations"
139+
[value]="filterValues.locationValue"
140+
(update)="filterUpdated($event, 'location')"
141+
[valueProp]="'name'"
142+
[viewValueProp]="'name'"
143+
[multiple]="true"
144+
/>
145+
</div>
146+
}
130147
</div>
131148
</div>

src/app/modules/library/library-filters/library-filters.component.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ import { Feature } from '../Feature';
1616
import { Grade, GradeLevel } from '../GradeLevel';
1717
import { MatDialog } from '@angular/material/dialog';
1818
import { DialogWithCloseComponent } from '../../../../assets/wise5/directives/dialog-with-close/dialog-with-close.component';
19+
import { Location } from '../Location';
20+
import { LocationSelectMenuComponent } from '../../shared/location-select-menu/location-select-menu.component';
1921

2022
@Component({
2123
imports: [
2224
CommonModule,
2325
MatBadgeModule,
2426
MatButtonModule,
2527
MatIconModule,
28+
LocationSelectMenuComponent,
2629
SearchBarComponent,
2730
SelectMenuComponent,
2831
StandardsSelectMenuComponent
@@ -44,6 +47,7 @@ export class LibraryFiltersComponent {
4447
private sharedProjects: LibraryProject[] = [];
4548
protected showFilters: boolean = false;
4649
protected standardOptions: Standard[] = [];
50+
protected locationOptions: Location[] = [];
4751
protected unitTypeOptions: { id: string; name: string }[] = [
4852
{ id: 'WISE Platform', name: $localize`WISE Platform` },
4953
{ id: 'Other Platform', name: $localize`Other Platform` }
@@ -97,6 +101,7 @@ export class LibraryFiltersComponent {
97101
);
98102
this.populateGradeLevels(project);
99103
this.populateStandards(project);
104+
this.populateLocations(project);
100105
}
101106

102107
private populateGradeLevels(project: LibraryProject): void {
@@ -123,12 +128,23 @@ export class LibraryFiltersComponent {
123128
});
124129
}
125130

131+
private populateLocations(project: LibraryProject): void {
132+
project.metadata.locations?.forEach((location: Location) =>
133+
this.locationOptions.push(Object.assign(new Location(), location))
134+
);
135+
}
136+
126137
private removeDuplicatesAndSortAlphabetically(): void {
127138
this.standardOptions = this.utilService.removeObjectArrayDuplicatesByProperty(
128139
this.standardOptions,
129140
'id'
130141
);
131142
this.utilService.sortObjectArrayByProperty(this.standardOptions, 'id');
143+
this.locationOptions = this.utilService.removeObjectArrayDuplicatesByProperty(
144+
this.locationOptions,
145+
'id'
146+
);
147+
this.utilService.sortObjectArrayByProperty(this.locationOptions, 'id');
132148
this.disciplineOptions = this.utilService.removeObjectArrayDuplicatesByProperty(
133149
this.disciplineOptions,
134150
'id'
@@ -168,6 +184,9 @@ export class LibraryFiltersComponent {
168184
case 'unitType':
169185
this.filterValues.unitTypeValue = value;
170186
break;
187+
case 'location':
188+
this.filterValues.locationValue = value;
189+
break;
171190
}
172191
this.emitFilterValues();
173192
}
@@ -182,9 +201,9 @@ export class LibraryFiltersComponent {
182201
}
183202

184203
protected showTypeInfo(): void {
185-
const message = $localize`"Type" indicates the platform on which a unit runs. "WISE Platform" units are created
186-
using the WISE authoring tool. Students use WISE accounts to complete lessons and teachers can review and grade
187-
work on the WISE platform. "Other" units are created using different platforms. Resources for these units
204+
const message = $localize`"Type" indicates the platform on which a unit runs. "WISE Platform" units are created
205+
using the WISE authoring tool. Students use WISE accounts to complete lessons and teachers can review and grade
206+
work on the WISE platform. "Other" units are created using different platforms. Resources for these units
188207
are linked in the unit details.`;
189208
this.dialog.open(DialogWithCloseComponent, {
190209
data: {

src/app/modules/library/library-project-details/library-project-details.component.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,16 @@
148148
}
149149
</p>
150150
}
151+
@if (project.metadata.locations?.length > 0) {
152+
<p>
153+
<strong i18n>Locations:</strong>
154+
@for (location of project.metadata.locations; track location.id; let last = $last) {
155+
{{ location.level3 ? location.level3 + ', ' : ''
156+
}}{{ location.level2 ? location.level2 + ', ' : '' }}{{ location.level1 }}
157+
{{ last ? '' : ' • ' }}
158+
}
159+
</p>
160+
}
151161
@if (project.tags) {
152162
<unit-tags [tags]="project.tags" />
153163
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<mat-form-field class="select-menu" appearance="fill" [subscriptSizing]="'dynamic'">
2+
<mat-label>{{ placeholderText }}</mat-label>
3+
<mat-select
4+
[formControl]="selectField"
5+
[placeholder]="placeholderText"
6+
[(value)]="value"
7+
[multiple]="multiple"
8+
>
9+
@if (multiple) {
10+
<mat-select-trigger>
11+
{{ selectField.value ? selectField.value[0] : '' }}
12+
@if (selectField.value?.length > 1) {
13+
<span>(+{{ selectField.value.length - 1 }} <ng-container i18n>more</ng-container>)</span>
14+
}
15+
</mat-select-trigger>
16+
}
17+
@for (label of labels; track label) {
18+
<mat-optgroup [label]="locationTypeToLabel[label]">
19+
@for (option of locationOptions[label]; track option.id) {
20+
<mat-option [value]="option[valueProp]">{{ option[viewValueProp] }}</mat-option>
21+
}
22+
</mat-optgroup>
23+
}
24+
</mat-select>
25+
</mat-form-field>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Component } from '@angular/core';
2+
import { SelectMenuComponent } from '../select-menu/select-menu.component';
3+
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4+
import { MatSelectModule } from '@angular/material/select';
5+
import {
6+
Location,
7+
LocationOption,
8+
LocationType,
9+
locationTypeToLabel
10+
} from '../../library/Location';
11+
12+
@Component({
13+
imports: [FormsModule, MatSelectModule, ReactiveFormsModule],
14+
selector: 'location-select-menu',
15+
templateUrl: './location-select-menu.component.html'
16+
})
17+
export class LocationSelectMenuComponent extends SelectMenuComponent {
18+
protected labels: LocationType[];
19+
protected locationOptions = { level3: [], level2: [], level1: [] };
20+
protected locationTypeToLabel = locationTypeToLabel;
21+
22+
ngOnInit(): void {
23+
super.ngOnInit();
24+
this.options
25+
.flatMap((option: Location) => option.getLocationOptions())
26+
.forEach((option: LocationOption) => {
27+
if (!this.locationOptions[option.type].some((opt) => opt.name === option.name)) {
28+
this.locationOptions[option.type].push(option);
29+
}
30+
});
31+
this.labels = Object.keys(this.locationOptions).filter(
32+
(key: LocationType) => this.locationOptions[key].length > 0
33+
) as LocationType[];
34+
}
35+
}

0 commit comments

Comments
 (0)