Skip to content

Commit 150219d

Browse files
authored
feat(TeacherTools): Restore Match summary organize by bucket (#2307)
1 parent 0346081 commit 150219d

6 files changed

Lines changed: 252 additions & 74 deletions

File tree

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
1+
<ng-template #bucketTemplate let-bucket="bucket">
2+
<div class="bucket-card notice-bg-bg">
3+
<h3 class="mat-body-2">
4+
<mat-icon class="mat-18 align-sub" aria-label="Bucket" i18n-aria-label>inventory_2</mat-icon
5+
>&nbsp;<span [innerHTML]="bucket.value"></span>
6+
</h3>
7+
<ul>
8+
@for (choice of bucket.choices; track $index) {
9+
<li>
10+
<div class="choice-row">
11+
<mat-icon class="mat-18 shrink-0 mt-0.25" aria-label="Item" i18n-aria-label
12+
>crop_16_9</mat-icon
13+
>
14+
<span class="flex-1" [innerHTML]="choice.getChoiceValue()"></span>
15+
@if (!isChoiceReuseMatch) {
16+
<span class="shrink-0">
17+
<mat-icon class="mat-18 align-middle">person</mat-icon>{{ choice.getCount() }}
18+
</span>
19+
}
20+
</div>
21+
</li>
22+
}
23+
</ul>
24+
</div>
25+
</ng-template>
26+
127
<ng-template #choiceTemplate let-choice="choice">
2-
<div class="choice notice-bg-bg" [class.secondary-text]="choice.choiceDataPoints.length === 0">
28+
<div
29+
class="choice-card notice-bg-bg"
30+
[class.secondary-text]="choice.choiceDataPoints.length === 0"
31+
>
332
<h3 class="mat-body-2">
433
<mat-icon class="mat-18 align-sub" aria-label="Item" i18n-aria-label>crop_16_9</mat-icon
534
>&nbsp;<span [innerHTML]="choice.choiceValue"></span>
@@ -8,50 +37,82 @@ <h3 class="mat-body-2">
837
<ul>
938
@for (bucketDataPoint of choice.choiceDataPoints; track $index) {
1039
<li>
11-
<div class="bucket">
40+
<div class="bucket-row">
1241
<mat-icon class="mat-18 shrink-0 mt-0.25" aria-label="Bucket" i18n-aria-label
1342
>inventory_2</mat-icon
1443
>
15-
<div class="flex-1" [innerHTML]="bucketDataPoint.getBucketValue()"></div>
16-
<div class="shrink-0">
44+
<span class="flex-1" [innerHTML]="bucketDataPoint.getBucketValue()"></span>
45+
<span class="shrink-0">
1746
<mat-icon class="mat-18 align-middle">person</mat-icon
1847
>{{ bucketDataPoint.getCount() }}
19-
</div>
48+
</span>
2049
</div>
2150
</li>
2251
}
2352
</ul>
2453
} @else {
25-
<div class="bucket">
26-
<mat-icon class="mat-18" aria-label="Not moved" i18n-aria-label>do_not_disturb</mat-icon>
27-
<div i18n>Not moved by any students</div>
54+
<div class="bucket-row !justify-start">
55+
<mat-icon class="mat-18 mt-0.25">do_not_disturb</mat-icon>
56+
<span i18n>Not moved by any students</span>
2857
</div>
2958
}
3059
</div>
3160
</ng-template>
3261

3362
<div [class.expanded]="expanded">
34-
<h2 class="mat-subtitle-1" i18n>Bucket Frequency</h2>
35-
<div class="max-h-160 overflow-y-auto @container" [class.max-h-none]="expanded">
36-
@if (choiceData.length > 0) {
63+
<h2 class="mat-subtitle-1" i18n>Choice Frequency</h2>
64+
@if (choiceData.length > 0) {
65+
<div class="flex gap-2 flex-wrap justify-between items-center">
3766
<p class="!mt-0" i18n>
3867
Number of times each item <mat-icon class="mat-18 align-sub">crop_16_9</mat-icon> was moved
3968
into the different buckets <mat-icon class="mat-18 align-sub">inventory_2</mat-icon>.
4069
</p>
70+
<div class="flex gap-2 items-center">
71+
<span i18n>Organize by:</span>
72+
<mat-button-toggle-group
73+
[hideSingleSelectionIndicator]="true"
74+
[value]="viewMode"
75+
(change)="viewMode = $event.value"
76+
aria-label="Organize by"
77+
i18n-aria-label
78+
>
79+
<mat-button-toggle value="bucket" aria-label="Organize by bucket" i18n-aria-label>
80+
<mat-icon class="mat-18">inventory_2</mat-icon>&nbsp;
81+
<span i18n>Bucket</span>
82+
</mat-button-toggle>
83+
<mat-button-toggle value="choice" aria-label="Organize by choice" i18n-aria-label>
84+
<mat-icon class="mat-18">crop_16_9</mat-icon>&nbsp;
85+
<span i18n>Item</span>
86+
</mat-button-toggle>
87+
</mat-button-toggle-group>
88+
</div>
89+
</div>
90+
<div class="max-h-160 overflow-y-auto @container" [class.max-h-none]="expanded">
4191
<div class="columns-1 @xl:columns-2 @4xl:columns-3 gap-2 mt-2">
42-
@for (choice of choiceData; track $index) {
43-
<div class="break-inside-avoid">
44-
<ng-container
45-
[ngTemplateOutlet]="choiceTemplate"
46-
[ngTemplateOutletContext]="{ choice: choice }"
47-
/>
48-
</div>
92+
@if (viewMode === 'bucket') {
93+
@for (bucket of bucketData; track $index) {
94+
<div class="break-inside-avoid">
95+
<ng-container
96+
[ngTemplateOutlet]="bucketTemplate"
97+
[ngTemplateOutletContext]="{ bucket: bucket, first: false }"
98+
/>
99+
</div>
100+
}
101+
} @else {
102+
@for (choice of choiceData; track $index) {
103+
<div class="break-inside-avoid">
104+
<ng-container
105+
[ngTemplateOutlet]="choiceTemplate"
106+
[ngTemplateOutletContext]="{ choice: choice }"
107+
/>
108+
</div>
109+
}
49110
}
50111
</div>
51-
} @else {
52-
<div class="notice" i18n>
53-
Your students' choices will show up here when they complete the activity.
54-
</div>
55-
}
56-
</div>
112+
</div>
113+
} @else {
114+
<div class="notice" i18n>
115+
Your students' choices will show up here when they complete the activity.
116+
</div>
117+
}
57118
</div>

src/assets/wise5/directives/teacher-summary-display/match-summary-display/match-summary-display.component.scss

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
@reference "tailwindcss";
22

3+
:host {
4+
--mat-button-toggle-divider-color: transparent;
5+
--mat-button-toggle-height: 32px;
6+
.mat-button-toggle-group-appearance-standard, .mat-button-toggle-appearance-standard {
7+
@apply rounded-md;
8+
}
9+
}
10+
311
h3,
412
.mat-subtitle-1 {
513
margin-bottom: 8px;
614
margin-top: 0;
715
}
816

9-
.choice {
17+
.choice-card, .bucket-card {
1018
@apply p-2 mb-2 rounded-md;
1119
}
1220

13-
.bucket {
21+
.choice-row, .bucket-row {
1422
@apply flex gap-1 px-2 py-1 mt-1 rounded-md bg-white border border-neutral-200 text-sm items-start justify-between;
1523
}
1624

src/assets/wise5/directives/teacher-summary-display/match-summary-display/match-summary-display.component.spec.ts

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -51,40 +51,66 @@ describe('MatchSummaryDisplayComponent', () => {
5151
expect(component).toBeTruthy();
5252
});
5353

54-
it('should display one card per unique choice', () => {
55-
expect(fixtureQueryAll(fixture, '.choice').length).toEqual(5);
56-
});
54+
describe('Bucket view', () => {
55+
it('should display one card per unique non-source bucket', () => {
56+
expect(fixtureQueryAll(fixture, '.bucket-card').length).toBe(2);
57+
});
5758

58-
it('should order choices by total count descending then alphabetically', () => {
59-
const cards = fixtureQueryAll(fixture, '.choice');
60-
const labels = Array.from(cards).map((el) => el.querySelector('h3')?.textContent?.trim());
61-
expect(labels[0]).toContain('Choice B');
62-
expect(labels[1]).toContain('Choice D');
63-
expect(labels[2]).toContain('Choice C');
64-
expect(labels[3]).toContain('Choice E');
65-
expect(labels[4]).toContain('Choice A');
59+
it('should show choices sorted by count within each bucket', () => {
60+
const cards = fixtureQueryAll(fixture, '.bucket-card');
61+
const bucket1Card = cards[0];
62+
const choiceRows = bucket1Card.querySelectorAll('.choice-row');
63+
expect(choiceRows.length).toBe(3);
64+
expect(choiceRows[0].textContent).toContain('Choice B');
65+
expect(choiceRows[0].textContent).toContain('3');
66+
expect(choiceRows[1].textContent).toContain('Choice C');
67+
expect(choiceRows[1].textContent).toContain('2');
68+
expect(choiceRows[2].textContent).toContain('Choice D');
69+
expect(choiceRows[2].textContent).toContain('1');
70+
});
6671
});
6772

68-
it('should show bucket rows sorted by count within each choice', () => {
69-
const cards = fixtureQueryAll(fixture, '.choice');
70-
const choiceDCard = cards[1];
71-
const bucketRows = choiceDCard.querySelectorAll('.bucket');
72-
expect(bucketRows.length).toEqual(2);
73-
expect(bucketRows[0].textContent).toContain('Bucket 2');
74-
expect(bucketRows[0].textContent).toContain('2');
75-
});
73+
describe('Choice view', () => {
74+
beforeEach(() => {
75+
component.viewMode = 'choice';
76+
fixture.detectChanges();
77+
});
7678

77-
it('should show the correct count for Choice B in Bucket 1', () => {
78-
const cards = fixtureQueryAll(fixture, '.choice');
79-
const choiceBCard = cards[0];
80-
expect(choiceBCard.textContent).toContain('3');
81-
});
79+
it('should display one card per unique choice', () => {
80+
expect(fixtureQueryAll(fixture, '.choice-card').length).toEqual(5);
81+
});
8282

83-
it('should show "Not moved by any students" for choices left in the source bucket', () => {
84-
const cards = fixtureQueryAll(fixture, '.choice');
85-
const choiceACard = cards[4];
86-
expect(choiceACard.textContent).toContain('Not moved by any students');
87-
expect(choiceACard.querySelectorAll('.bucket').length).toEqual(1);
83+
it('should order choices by total count descending then alphabetically', () => {
84+
const cards = fixtureQueryAll(fixture, '.choice-card');
85+
const labels = Array.from(cards).map((el) => el.querySelector('h3')?.textContent?.trim());
86+
expect(labels[0]).toContain('Choice B');
87+
expect(labels[1]).toContain('Choice D');
88+
expect(labels[2]).toContain('Choice C');
89+
expect(labels[3]).toContain('Choice E');
90+
expect(labels[4]).toContain('Choice A');
91+
});
92+
93+
it('should show bucket rows sorted by count within each choice', () => {
94+
const cards = fixtureQueryAll(fixture, '.choice-card');
95+
const choiceDCard = cards[1];
96+
const bucketRows = choiceDCard.querySelectorAll('.bucket-row');
97+
expect(bucketRows.length).toEqual(2);
98+
expect(bucketRows[0].textContent).toContain('Bucket 2');
99+
expect(bucketRows[0].textContent).toContain('2');
100+
});
101+
102+
it('should show the correct count for Choice B in Bucket 1', () => {
103+
const cards = fixtureQueryAll(fixture, '.choice-card');
104+
const choiceBCard = cards[0];
105+
expect(choiceBCard.textContent).toContain('3');
106+
});
107+
108+
it('should show "Not moved by any students" for choices left in the source bucket', () => {
109+
const cards = fixtureQueryAll(fixture, '.choice-card');
110+
const choiceACard = cards[4];
111+
expect(choiceACard.textContent).toContain('Not moved by any students');
112+
expect(choiceACard.querySelectorAll('.bucket-row').length).toEqual(1);
113+
});
88114
});
89115
});
90116

src/assets/wise5/directives/teacher-summary-display/match-summary-display/match-summary-display.component.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import { CommonModule } from '@angular/common';
22
import { Component, Input, OnInit } from '@angular/core';
3-
import { ChoiceData, MatchSummaryData } from '../summary-data/MatchSummaryData';
3+
import { MatchContent } from '../../../components/match/MatchContent';
4+
import { BucketData, ChoiceData, MatchSummaryData } from '../summary-data/MatchSummaryData';
45
import { MatchSummaryDataPoint } from '../summary-data/MatchSummaryDataPoint';
6+
import { MatButtonToggleModule } from '@angular/material/button-toggle';
57
import { MatIconModule } from '@angular/material/icon';
68
import { TeacherSummaryDisplayComponent } from '../teacher-summary-display.component';
79

10+
export type SummaryViewMode = 'choice' | 'bucket';
11+
812
@Component({
9-
imports: [CommonModule, MatIconModule],
13+
imports: [CommonModule, MatButtonToggleModule, MatIconModule],
1014
selector: 'match-summary-display',
1115
styleUrls: [
1216
'./match-summary-display.component.scss',
@@ -15,29 +19,41 @@ import { TeacherSummaryDisplayComponent } from '../teacher-summary-display.compo
1519
templateUrl: './match-summary-display.component.html'
1620
})
1721
export class MatchSummaryDisplayComponent extends TeacherSummaryDisplayComponent implements OnInit {
22+
protected bucketData: { value: string; choices: MatchSummaryDataPoint[] }[] = [];
1823
protected choiceData: ChoiceData[] = [];
1924
@Input() expanded: boolean;
25+
protected isChoiceReuseMatch: boolean;
2026
private matchSummaryData: MatchSummaryData;
27+
viewMode: SummaryViewMode = 'bucket';
2128

2229
ngOnInit(): void {
30+
this.setIsChoiceReuseMatch();
2331
this.generateSummary();
2432
}
2533

34+
private setIsChoiceReuseMatch(): void {
35+
this.isChoiceReuseMatch = (
36+
this.projectService.getComponent(this.nodeId, this.componentId) as MatchContent
37+
).choiceReuseEnabled;
38+
}
39+
2640
private generateSummary(): void {
2741
this.getLatestWork().subscribe((componentStates) => {
42+
this.bucketData = [];
2843
this.choiceData = [];
2944
this.matchSummaryData = new MatchSummaryData(
3045
this.projectService.injectAssetPaths(componentStates)
3146
);
3247
this.setChoiceData();
48+
this.setBucketData();
3349
});
3450
}
3551

3652
protected setChoiceData(): void {
3753
this.matchSummaryData.getChoicesData().forEach((choice) => {
3854
this.choiceData.push({
3955
choiceValue: choice.choiceValue,
40-
choiceDataPoints: choice.choiceDataPoints.sort(this.sortBuckets)
56+
choiceDataPoints: choice.choiceDataPoints.sort(this.sortByCount)
4157
});
4258
});
4359
this.choiceData.sort(this.sortChoices);
@@ -52,7 +68,16 @@ export class MatchSummaryDisplayComponent extends TeacherSummaryDisplayComponent
5268
return countDiff !== 0 ? countDiff : a.choiceValue.localeCompare(b.choiceValue);
5369
};
5470

55-
private sortBuckets(a: MatchSummaryDataPoint, b: MatchSummaryDataPoint): number {
71+
protected setBucketData(): void {
72+
this.matchSummaryData.getBucketsData().forEach((bucket: BucketData) => {
73+
this.bucketData.push({
74+
value: bucket.bucketValue,
75+
choices: bucket.bucketDataPoints.sort(this.sortByCount)
76+
});
77+
});
78+
}
79+
80+
private sortByCount(a: MatchSummaryDataPoint, b: MatchSummaryDataPoint): number {
5681
return b.getCount() - a.getCount();
5782
}
5883

src/assets/wise5/directives/teacher-summary-display/summary-data/MatchSummaryData.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MatchSummaryDataPoint } from './MatchSummaryDataPoint';
33
import { SummaryData } from '../../summary-display/summary-data/SummaryData';
44

55
export type ChoiceData = { choiceValue: string; choiceDataPoints: MatchSummaryDataPoint[] };
6+
export type BucketData = { bucketValue: string; bucketDataPoints: MatchSummaryDataPoint[] };
67

78
/**
89
* Summary data for all choices, each with a breakdown per bucket
@@ -19,14 +20,34 @@ export class MatchSummaryData extends SummaryData {
1920
return this.choicesData;
2021
}
2122

23+
getBucketsData(): BucketData[] {
24+
const bucketsMap = new Map<string, BucketData>();
25+
this.choicesData.forEach((choice) => {
26+
choice.choiceDataPoints.forEach((point) => {
27+
const bucketValue = point.getBucketValue();
28+
if (!bucketsMap.has(bucketValue)) {
29+
bucketsMap.set(bucketValue, { bucketValue, bucketDataPoints: [] });
30+
}
31+
bucketsMap.get(bucketValue).bucketDataPoints.push(point);
32+
});
33+
});
34+
return Array.from(bucketsMap.values()).sort(
35+
(a, b) => this.getTotalCount(b) - this.getTotalCount(a)
36+
);
37+
}
38+
39+
private getTotalCount(bucket: BucketData): number {
40+
return bucket.bucketDataPoints.reduce((sum, point) => sum + point.getCount(), 0);
41+
}
42+
2243
private extractChoiceData(componentStates: ComponentState[]): void {
2344
componentStates.forEach((componentState) => {
2445
componentState.studentData.buckets.forEach((bucketStudentData, index) => {
2546
if (index === 0) {
26-
bucketStudentData.items.forEach((item) => this.registerChoice(item.value));
47+
bucketStudentData.items.forEach((choice) => this.registerChoice(choice.value));
2748
} else {
28-
bucketStudentData.items.forEach((item) => {
29-
this.extractBucketDataPerChoice(item.value, bucketStudentData.value);
49+
bucketStudentData.items.forEach((choice) => {
50+
this.extractBucketDataPerChoice(choice.value, bucketStudentData.value);
3051
});
3152
}
3253
});

0 commit comments

Comments
 (0)