From 52f1cbf790ccc5c8ee887f7738a8391d2fec3630 Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Fri, 29 Aug 2025 09:35:59 -0700 Subject: [PATCH 1/4] Add Location filter to unit library. Each location can have 3 levels --- src/app/domain/projectFilterValues.ts | 16 +++- src/app/modules/library/Location.ts | 40 ++++++++ .../library-filters.component.html | 17 ++++ .../library-filters.component.ts | 25 ++++- .../location-select-menu.component.html | 25 +++++ .../location-select-menu.component.ts | 35 +++++++ src/messages.xlf | 92 ++++++++++++------- 7 files changed, 213 insertions(+), 37 deletions(-) create mode 100644 src/app/modules/library/Location.ts create mode 100644 src/app/modules/shared/location-select-menu/location-select-menu.component.html create mode 100644 src/app/modules/shared/location-select-menu/location-select-menu.component.ts diff --git a/src/app/domain/projectFilterValues.ts b/src/app/domain/projectFilterValues.ts index 49434eb6659..9a3a3e7017e 100644 --- a/src/app/domain/projectFilterValues.ts +++ b/src/app/domain/projectFilterValues.ts @@ -1,10 +1,12 @@ import { Subject } from 'rxjs'; import { LibraryProject } from '../modules/library/libraryProject'; +import { Location } from '../modules/library/Location'; export class ProjectFilterValues { disciplineValue: string[] = []; featureValue: string[] = []; gradeLevelValue: number[] = []; + locationValue: string[] = []; publicUnitType?: ('wiseTested' | 'communityBuilt')[] = []; publicUnitTypeValue?: ('wiseTested' | 'communityBuilt')[] = []; searchValue: string = ''; @@ -21,7 +23,8 @@ export class ProjectFilterValues { this.matchesDiscipline(project) && this.matchesUnitType(project) && this.matchesFeature(project) && - this.matchesGradeLevel(project) + this.matchesGradeLevel(project) && + this.matchesLocation(project) ); } @@ -95,6 +98,17 @@ export class ProjectFilterValues { ); } + private matchesLocation(project: LibraryProject): boolean { + return ( + this.locationValue.length === 0 || + project.metadata.locations + ?.map((location) => Object.assign(new Location(), location)) + .map((location) => location.getLocationOptions()) + .flat() + .some((locationOption) => this.locationValue.includes(locationOption.id)) + ); + } + private matchesFeature(project: LibraryProject): boolean { return ( this.featureValue.length === 0 || diff --git a/src/app/modules/library/Location.ts b/src/app/modules/library/Location.ts new file mode 100644 index 00000000000..b3d80bc76ba --- /dev/null +++ b/src/app/modules/library/Location.ts @@ -0,0 +1,40 @@ +export type LocationType = 'level1' | 'level2' | 'level3'; + +export const locationTypeToLabel: { [key in LocationType]: string } = { + level3: $localize`Locale`, + level2: $localize`State`, + level1: $localize`Country` +}; + +export class LocationOption { + id: string; + name: string; + type: LocationType; + constructor(type: LocationType, name: string) { + this.type = type; + this.name = name; + this.id = `${locationTypeToLabel[type]}:${name}`; + } +} + +// Represents a geographical location associated with a project +export class Location { + id: string = ''; + level1: string = ''; // country + level2: string = ''; // state + level3: string = ''; // city, county, or other locale + + getLocationOptions(): LocationOption[] { + const options = []; + if (this.level1) { + options.push(new LocationOption('level1', this.level1)); + } + if (this.level2) { + options.push(new LocationOption('level2', `${this.level2}, ${this.level1}`)); + } + if (this.level3) { + options.push(new LocationOption('level3', `${this.level3}, ${this.level2}, ${this.level1}`)); + } + return options; + } +} diff --git a/src/app/modules/library/library-filters/library-filters.component.html b/src/app/modules/library/library-filters/library-filters.component.html index 52a9022624b..d5b43c73c39 100644 --- a/src/app/modules/library/library-filters/library-filters.component.html +++ b/src/app/modules/library/library-filters/library-filters.component.html @@ -127,5 +127,22 @@

Filters

/> } + @if (locationOptions.length > 0) { +
+ +
+ } diff --git a/src/app/modules/library/library-filters/library-filters.component.ts b/src/app/modules/library/library-filters/library-filters.component.ts index 808b7e5cb9b..8dac1800901 100644 --- a/src/app/modules/library/library-filters/library-filters.component.ts +++ b/src/app/modules/library/library-filters/library-filters.component.ts @@ -16,6 +16,8 @@ import { Feature } from '../Feature'; import { Grade, GradeLevel } from '../GradeLevel'; import { MatDialog } from '@angular/material/dialog'; import { DialogWithCloseComponent } from '../../../../assets/wise5/directives/dialog-with-close/dialog-with-close.component'; +import { Location } from '../Location'; +import { LocationSelectMenuComponent } from '../../shared/location-select-menu/location-select-menu.component'; @Component({ imports: [ @@ -23,6 +25,7 @@ import { DialogWithCloseComponent } from '../../../../assets/wise5/directives/di MatBadgeModule, MatButtonModule, MatIconModule, + LocationSelectMenuComponent, SearchBarComponent, SelectMenuComponent, StandardsSelectMenuComponent @@ -44,6 +47,7 @@ export class LibraryFiltersComponent { private sharedProjects: LibraryProject[] = []; protected showFilters: boolean = false; protected standardOptions: Standard[] = []; + protected locationOptions: Location[] = []; protected unitTypeOptions: { id: string; name: string }[] = [ { id: 'WISE Platform', name: $localize`WISE Platform` }, { id: 'Other Platform', name: $localize`Other Platform` } @@ -97,6 +101,7 @@ export class LibraryFiltersComponent { ); this.populateGradeLevels(project); this.populateStandards(project); + this.populateLocations(project); } private populateGradeLevels(project: LibraryProject): void { @@ -123,12 +128,23 @@ export class LibraryFiltersComponent { }); } + private populateLocations(project: LibraryProject): void { + project.metadata.locations?.forEach((location: Location) => + this.locationOptions.push(Object.assign(new Location(), location)) + ); + } + private removeDuplicatesAndSortAlphabetically(): void { this.standardOptions = this.utilService.removeObjectArrayDuplicatesByProperty( this.standardOptions, 'id' ); this.utilService.sortObjectArrayByProperty(this.standardOptions, 'id'); + this.locationOptions = this.utilService.removeObjectArrayDuplicatesByProperty( + this.locationOptions, + 'id' + ); + this.utilService.sortObjectArrayByProperty(this.locationOptions, 'id'); this.disciplineOptions = this.utilService.removeObjectArrayDuplicatesByProperty( this.disciplineOptions, 'id' @@ -168,6 +184,9 @@ export class LibraryFiltersComponent { case 'unitType': this.filterValues.unitTypeValue = value; break; + case 'location': + this.filterValues.locationValue = value; + break; } this.emitFilterValues(); } @@ -182,9 +201,9 @@ export class LibraryFiltersComponent { } protected showTypeInfo(): void { - const message = $localize`"Type" indicates the platform on which a unit runs. "WISE Platform" units are created - using the WISE authoring tool. Students use WISE accounts to complete lessons and teachers can review and grade - work on the WISE platform. "Other" units are created using different platforms. Resources for these units + const message = $localize`"Type" indicates the platform on which a unit runs. "WISE Platform" units are created + using the WISE authoring tool. Students use WISE accounts to complete lessons and teachers can review and grade + work on the WISE platform. "Other" units are created using different platforms. Resources for these units are linked in the unit details.`; this.dialog.open(DialogWithCloseComponent, { data: { diff --git a/src/app/modules/shared/location-select-menu/location-select-menu.component.html b/src/app/modules/shared/location-select-menu/location-select-menu.component.html new file mode 100644 index 00000000000..99bf52db579 --- /dev/null +++ b/src/app/modules/shared/location-select-menu/location-select-menu.component.html @@ -0,0 +1,25 @@ + + {{ placeholderText }} + + @if (multiple) { + + {{ selectField.value ? selectField.value[0] : '' }} + @if (selectField.value?.length > 1) { + (+{{ selectField.value.length - 1 }} more) + } + + } + @for (label of labels; track label) { + + @for (option of locationOptions[label]; track option.id) { + {{ option[viewValueProp] }} + } + + } + + diff --git a/src/app/modules/shared/location-select-menu/location-select-menu.component.ts b/src/app/modules/shared/location-select-menu/location-select-menu.component.ts new file mode 100644 index 00000000000..28230ae521a --- /dev/null +++ b/src/app/modules/shared/location-select-menu/location-select-menu.component.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; +import { SelectMenuComponent } from '../select-menu/select-menu.component'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatSelectModule } from '@angular/material/select'; +import { + Location, + LocationOption, + LocationType, + locationTypeToLabel +} from '../../library/Location'; + +@Component({ + imports: [FormsModule, MatSelectModule, ReactiveFormsModule], + selector: 'location-select-menu', + templateUrl: './location-select-menu.component.html' +}) +export class LocationSelectMenuComponent extends SelectMenuComponent { + protected labels: LocationType[]; + protected locationOptions = { level3: [], level2: [], level1: [] }; + protected locationTypeToLabel = locationTypeToLabel; + + ngOnInit(): void { + super.ngOnInit(); + this.options + .flatMap((option: Location) => option.getLocationOptions()) + .forEach((option: LocationOption) => { + if (!this.locationOptions[option.type].some((opt) => opt.id === option.id)) { + this.locationOptions[option.type].push(option); + } + }); + this.labels = Object.keys(this.locationOptions).filter( + (key: LocationType) => this.locationOptions[key].length > 0 + ) as LocationType[]; + } +} diff --git a/src/messages.xlf b/src/messages.xlf index ec439f0e92c..4757149f526 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -5753,6 +5753,43 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.27,30 + + Locale + + src/app/modules/library/Location.ts + 4 + + + + State + + src/app/modules/library/Location.ts + 5 + + + src/app/register/register-teacher-form/register-teacher-form.component.html + 69,70 + + + src/app/teacher/account/edit-profile/edit-profile.component.html + 63,64 + + + + Country + + src/app/modules/library/Location.ts + 6 + + + src/app/register/register-teacher-form/register-teacher-form.component.html + 78,79 + + + src/app/teacher/account/edit-profile/edit-profile.component.html + 72,73 + + Copy Unit @@ -5868,25 +5905,32 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.104,106 + + Locations + + src/app/modules/library/library-filters/library-filters.component.html + 138,140 + + WISE Platform src/app/modules/library/library-filters/library-filters.component.ts - 48 + 52 Other Platform src/app/modules/library/library-filters/library-filters.component.ts - 49 + 53 NGSS src/app/modules/library/library-filters/library-filters.component.ts - 114 + 119 src/app/modules/library/library-project-details/library-project-details.component.ts @@ -5897,7 +5941,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Common Core src/app/modules/library/library-filters/library-filters.component.ts - 115 + 120 src/app/modules/library/library-project-details/library-project-details.component.ts @@ -5908,28 +5952,28 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Learning For Justice src/app/modules/library/library-filters/library-filters.component.ts - 116 + 121 src/app/modules/library/library-project-details/library-project-details.component.ts 52 - - "Type" indicates the platform on which a unit runs. "WISE Platform" units are created - using the WISE authoring tool. Students use WISE accounts to complete lessons and teachers can review and grade - work on the WISE platform. "Other" units are created using different platforms. Resources for these units + + "Type" indicates the platform on which a unit runs. "WISE Platform" units are created + using the WISE authoring tool. Students use WISE accounts to complete lessons and teachers can review and grade + work on the WISE platform. "Other" units are created using different platforms. Resources for these units are linked in the unit details. src/app/modules/library/library-filters/library-filters.component.ts - 185,188 + 204,207 Unit Type src/app/modules/library/library-filters/library-filters.component.ts - 192 + 211 src/assets/wise5/authoringTool/edit-unit-type/edit-unit-type.component.html @@ -6685,6 +6729,10 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. more + + src/app/modules/shared/location-select-menu/location-select-menu.component.html + 13,17 + src/app/modules/shared/select-menu/select-menu.component.html 14,18 @@ -7761,17 +7809,6 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.57,62 - - State - - src/app/register/register-teacher-form/register-teacher-form.component.html - 69,70 - - - src/app/teacher/account/edit-profile/edit-profile.component.html - 63,64 - - State required @@ -7783,17 +7820,6 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.66,71 - - Country - - src/app/register/register-teacher-form/register-teacher-form.component.html - 78,79 - - - src/app/teacher/account/edit-profile/edit-profile.component.html - 72,73 - - Country required From d0263010f21f6fd40851910312f28939210635ab Mon Sep 17 00:00:00 2001 From: Hiroki Terashima Date: Fri, 29 Aug 2025 09:55:14 -0700 Subject: [PATCH 2/4] Show locations in unit details --- .../library-project-details.component.html | 10 +++++++ src/messages.xlf | 27 ++++++++++++------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/app/modules/library/library-project-details/library-project-details.component.html b/src/app/modules/library/library-project-details/library-project-details.component.html index dbee909a7c0..044984a80ba 100644 --- a/src/app/modules/library/library-project-details/library-project-details.component.html +++ b/src/app/modules/library/library-project-details/library-project-details.component.html @@ -148,6 +148,16 @@ }

} + @if (project.metadata.locations?.length > 0) { +

+ Locations: + @for (location of project.metadata.locations; track location.id; let last = $last) { + {{ location.level3 ? location.level3 + ', ' : '' + }}{{ location.level2 ? location.level2 + ', ' : '' }}{{ location.level1 }} + {{ last ? '' : ' • ' }} + } +

+ } @if (project.tags) { } diff --git a/src/messages.xlf b/src/messages.xlf index 4757149f526..167134f8dff 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -283,7 +283,7 @@
src/app/modules/library/library-project-details/library-project-details.component.html - 208,212 + 218,222 src/app/modules/library/public-unit-type-selector/community-library-details.html @@ -6065,53 +6065,60 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.121,123
+ + Locations: + + src/app/modules/library/library-project-details/library-project-details.component.html + 153,154 + + This unit is a copy of (used under CC BY-SA). src/app/modules/library/library-project-details/library-project-details.component.html - 164,166 + 174,176 This unit is a copy of by (used under CC BY-SA). src/app/modules/library/library-project-details/library-project-details.component.html - 170,173 + 180,183 This unit is licensed under CC BY-SA. src/app/modules/library/library-project-details/library-project-details.component.html - 181,183 + 191,193 This unit is licensed under CC BY-SA by . src/app/modules/library/library-project-details/library-project-details.component.html - 186,188 + 196,198 View License src/app/modules/library/library-project-details/library-project-details.component.html - 193,197 + 203,207 More src/app/modules/library/library-project-details/library-project-details.component.html - 201,207 + 211,217 Use with Class src/app/modules/library/library-project-details/library-project-details.component.html - 217,222 + 227,232 src/app/teacher/create-run-dialog/create-run-dialog.component.html @@ -6122,7 +6129,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Preview src/app/modules/library/library-project-details/library-project-details.component.html - 225,227 + 235,237 src/app/teacher/run-menu/run-menu.component.html @@ -6157,7 +6164,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Unit Resources src/app/modules/library/library-project-details/library-project-details.component.html - 227,232 + 237,242 From c4f452b634c43783d5c609c7e925cfd60f2f3604 Mon Sep 17 00:00:00 2001 From: Jonathan Lim-Breitbart Date: Tue, 2 Sep 2025 13:01:57 -0700 Subject: [PATCH 3/4] Filter locations by name field instead of id --- src/app/domain/projectFilterValues.ts | 2 +- src/app/modules/library/Location.ts | 2 -- .../library/library-filters/library-filters.component.html | 2 +- .../location-select-menu/location-select-menu.component.ts | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app/domain/projectFilterValues.ts b/src/app/domain/projectFilterValues.ts index 9a3a3e7017e..1f0b459a3c8 100644 --- a/src/app/domain/projectFilterValues.ts +++ b/src/app/domain/projectFilterValues.ts @@ -105,7 +105,7 @@ export class ProjectFilterValues { ?.map((location) => Object.assign(new Location(), location)) .map((location) => location.getLocationOptions()) .flat() - .some((locationOption) => this.locationValue.includes(locationOption.id)) + .some((locationOption) => this.locationValue.includes(locationOption.name)) ); } diff --git a/src/app/modules/library/Location.ts b/src/app/modules/library/Location.ts index b3d80bc76ba..fd6c9e10980 100644 --- a/src/app/modules/library/Location.ts +++ b/src/app/modules/library/Location.ts @@ -7,13 +7,11 @@ export const locationTypeToLabel: { [key in LocationType]: string } = { }; export class LocationOption { - id: string; name: string; type: LocationType; constructor(type: LocationType, name: string) { this.type = type; this.name = name; - this.id = `${locationTypeToLabel[type]}:${name}`; } } diff --git a/src/app/modules/library/library-filters/library-filters.component.html b/src/app/modules/library/library-filters/library-filters.component.html index d5b43c73c39..a7f5cbb888d 100644 --- a/src/app/modules/library/library-filters/library-filters.component.html +++ b/src/app/modules/library/library-filters/library-filters.component.html @@ -138,7 +138,7 @@

Filters

placeholderText="Locations" [value]="filterValues.locationValue" (update)="filterUpdated($event, 'location')" - [valueProp]="'id'" + [valueProp]="'name'" [viewValueProp]="'name'" [multiple]="true" /> diff --git a/src/app/modules/shared/location-select-menu/location-select-menu.component.ts b/src/app/modules/shared/location-select-menu/location-select-menu.component.ts index 28230ae521a..5fc1ade8149 100644 --- a/src/app/modules/shared/location-select-menu/location-select-menu.component.ts +++ b/src/app/modules/shared/location-select-menu/location-select-menu.component.ts @@ -24,7 +24,7 @@ export class LocationSelectMenuComponent extends SelectMenuComponent { this.options .flatMap((option: Location) => option.getLocationOptions()) .forEach((option: LocationOption) => { - if (!this.locationOptions[option.type].some((opt) => opt.id === option.id)) { + if (!this.locationOptions[option.type].some((opt) => opt.name === option.name)) { this.locationOptions[option.type].push(option); } }); From 587819e884ddb8eea886869a8dfaf0df4f6d11e8 Mon Sep 17 00:00:00 2001 From: Jonathan Lim-Breitbart Date: Tue, 2 Sep 2025 13:03:01 -0700 Subject: [PATCH 4/4] Add location to hasFilters check and clear filters action --- src/app/domain/projectFilterValues.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/domain/projectFilterValues.ts b/src/app/domain/projectFilterValues.ts index 1f0b459a3c8..154e253a212 100644 --- a/src/app/domain/projectFilterValues.ts +++ b/src/app/domain/projectFilterValues.ts @@ -57,7 +57,8 @@ export class ProjectFilterValues { this.disciplineValue.length + this.unitTypeValue.length + this.gradeLevelValue.length + - this.featureValue.length > + this.featureValue.length + + this.locationValue.length > 0 ); } @@ -70,6 +71,7 @@ export class ProjectFilterValues { this.searchValue = ''; this.standardValue = []; this.unitTypeValue = []; + this.locationValue = []; } private matchesUnitType(project: LibraryProject): boolean {