Skip to content

Commit 707b27a

Browse files
authored
feat(TeacherTools): Redesign Match summary to organize by choice (#2299)
1 parent 76889fc commit 707b27a

7 files changed

Lines changed: 182 additions & 216 deletions

File tree

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

Lines changed: 38 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,51 @@
1-
<ng-template #bucketTemplate let-bucket="bucket" let-first="first">
2-
<div class="bucket" [ngClass]="{ 'selected-bg-bg': first, 'notice-bg-bg': !first }">
1+
<ng-template #choiceTemplate let-choice="choice">
2+
<div class="choice notice-bg-bg" [class.secondary-text]="choice.choiceDataPoints.length === 0">
33
<h3 class="mat-body-2">
4-
<span [innerHTML]="bucket.value"></span>
5-
@if (first) {
6-
(<span i18n>Source Bucket</span>)
7-
}
4+
<mat-icon class="mat-18 align-sub" aria-label="Item" i18n-aria-label>crop_16_9</mat-icon
5+
>&nbsp;<span [innerHTML]="choice.choiceValue"></span>
86
</h3>
9-
<ul>
10-
@for (choice of bucket.choices; track $index) {
11-
@if ($index < 3 || getBucketShowMore(bucket.value)) {
7+
@if (choice.choiceDataPoints.length > 0) {
8+
<ul>
9+
@for (bucketDataPoint of choice.choiceDataPoints; track $index) {
1210
<li>
13-
<div class="choice">
14-
<div [innerHTML]="choice.getId()"></div>
15-
@if (!isChoiceReuseMatch || !first) {
16-
<div class="shrink-0">
17-
(<mat-icon class="mat-18">person</mat-icon>{{ choice.getCount() }})
18-
</div>
19-
}
11+
<div class="bucket">
12+
<mat-icon class="mat-18 shrink-0 mt-0.25" aria-label="Bucket" i18n-aria-label
13+
>inventory_2</mat-icon
14+
>
15+
<div class="flex-1" [innerHTML]="bucketDataPoint.getBucketValue()"></div>
16+
<div class="shrink-0">
17+
<mat-icon class="mat-18 align-middle">person</mat-icon
18+
>{{ bucketDataPoint.getCount() }}
19+
</div>
2020
</div>
2121
</li>
2222
}
23-
}
24-
@if (bucket.choices.length > 3) {
25-
<a href="#" class="text-sm px-1" (click)="toggleBucketShowMore(bucket.value, $event)">
26-
@if (getBucketShowMore(bucket.value)) {
27-
<span i18n>Show less</span>
28-
} @else {
29-
<span i18n>Show more</span>
30-
}
31-
</a>
32-
}
33-
</ul>
23+
</ul>
24+
} @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>
28+
</div>
29+
}
3430
</div>
3531
</ng-template>
32+
3633
<div [class.expanded]="expanded">
37-
<h2 class="mat-subtitle-1" i18n>Choice Frequency</h2>
38-
<div class="max-h-160 overflow-y-auto" [class.max-h-none]="expanded">
39-
@if (bucketData.length > 0) {
40-
<p i18n>
41-
Number of teams that moved each item (choice) into the different buckets (categories).
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) {
37+
<p class="!mt-0" i18n>
38+
Number of times each item <mat-icon class="mat-18 align-sub">crop_16_9</mat-icon> was moved
39+
into the different buckets <mat-icon class="mat-18 align-sub">inventory_2</mat-icon>.
4240
</p>
43-
<div class="columns-3xs gap-2">
44-
<div class="break-inside-avoid">
45-
<ng-container
46-
[ngTemplateOutlet]="bucketTemplate"
47-
[ngTemplateOutletContext]="{ bucket: bucketData[0], first: true }"
48-
/>
49-
</div>
50-
@for (bucket of bucketData; track $index) {
51-
@if ($index > 0) {
52-
<div class="break-inside-avoid">
53-
<ng-container
54-
[ngTemplateOutlet]="bucketTemplate"
55-
[ngTemplateOutletContext]="{ bucket: bucket, first: false }"
56-
/>
57-
</div>
58-
}
41+
<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>
5949
}
6050
</div>
6151
} @else {

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
1-
@use "tailwindcss";
1+
@reference "tailwindcss";
22

33
h3,
44
.mat-subtitle-1 {
55
margin-bottom: 8px;
66
margin-top: 0;
77
}
88

9-
.bucket {
10-
@apply p-2 mb-2 rounded-md;
11-
}
12-
139
.choice {
14-
@apply flex gap-1 px-2 py-1 mt-1 rounded-md bg-white border border-neutral-200 text-sm;
10+
@apply p-2 mb-2 rounded-md;
1511
}
1612

17-
.mat-icon {
18-
vertical-align: middle;
13+
.bucket {
14+
@apply flex gap-1 px-2 py-1 mt-1 rounded-md bg-white border border-neutral-200 text-sm items-start justify-between;
1915
}
2016

2117
ul {

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

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

54-
it('should show the correct number of buckets and choices', () => {
55-
expect(fixtureQueryAll(fixture, '.bucket').length).toEqual(3);
54+
it('should display one card per unique choice', () => {
5655
expect(fixtureQueryAll(fixture, '.choice').length).toEqual(5);
57-
fixture.nativeElement.querySelector('a').click();
58-
fixture.detectChanges();
59-
expect(fixtureQueryAll(fixture, '.choice').length).toEqual(6);
6056
});
6157

62-
it('should only show Show more button if more than 3 choices in bucket', () => {
63-
expect(fixtureQueryAll(fixture, 'a').length).toEqual(1);
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');
6466
});
6567

66-
it('should display choices within bucket sorted by count', () => {
67-
const choices = fixtureQueryAll(fixture, '.choice');
68-
expect(choices[0].textContent.includes('Choice B'));
69-
expect(choices[1].textContent.includes('Choice A'));
70-
expect(choices[2].textContent.includes('Choice C'));
71-
expect(choices[3].textContent.includes('Choice D'));
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');
7275
});
7376

74-
it('should show the correct count on each choice per bucket', () => {
75-
const choices = fixtureQueryAll(fixture, '.choice');
76-
expect(choices[0].textContent.includes('3'));
77-
expect(choices[1].textContent.includes('2'));
78-
expect(choices[2].textContent.includes('2'));
79-
expect(choices[3].textContent.includes('1'));
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');
8081
});
8182

82-
it('should change Show more to Show less when clicked', () => {
83-
let button = fixture.nativeElement.querySelector('a');
84-
expect(button.innerText).toEqual('Show more');
85-
button.click();
86-
fixture.detectChanges();
87-
button = fixture.nativeElement.querySelector('a');
88-
expect(button.innerText).toEqual('Show less');
89-
button.click();
90-
fixture.detectChanges();
91-
button = fixture.nativeElement.querySelector('a');
92-
expect(button.innerText).toEqual('Show more');
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);
9388
});
9489
});
9590

@@ -114,17 +109,18 @@ function getComponentStates(): any {
114109
id: '0',
115110
type: 'bucket',
116111
value: 'Choices',
117-
items: []
118-
},
119-
{
120-
id: 'b1',
121-
value: 'Bucket 1',
122112
items: [
123113
{
124114
isIncorrectPosition: null,
125115
id: 'a',
126116
value: 'Choice A'
127-
},
117+
}
118+
]
119+
},
120+
{
121+
id: 'b1',
122+
value: 'Bucket 1',
123+
items: [
128124
{
129125
isIncorrectPosition: null,
130126
id: 'b',
@@ -169,17 +165,18 @@ function getComponentStates(): any {
169165
id: '0',
170166
type: 'bucket',
171167
value: 'Choices',
172-
items: []
173-
},
174-
{
175-
id: 'b1',
176-
value: 'Bucket 1',
177168
items: [
178169
{
179170
isIncorrectPosition: null,
180171
id: 'a',
181172
value: 'Choice A'
182-
},
173+
}
174+
]
175+
},
176+
{
177+
id: 'b1',
178+
value: 'Bucket 1',
179+
items: [
183180
{
184181
isIncorrectPosition: null,
185182
id: 'b',
@@ -240,7 +237,13 @@ function getComponentStates(): any {
240237
{
241238
id: 'b2',
242239
value: 'Bucket 2',
243-
items: []
240+
items: [
241+
{
242+
isIncorrectPosition: null,
243+
id: 'b',
244+
value: 'Choice D'
245+
}
246+
]
244247
}
245248
]
246249
},

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

Lines changed: 20 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { CommonModule } from '@angular/common';
22
import { Component, Input, OnInit } from '@angular/core';
3-
import { MatchContent } from '../../../components/match/MatchContent';
4-
import { MatchSummaryData } from '../summary-data/MatchSummaryData';
3+
import { ChoiceData, MatchSummaryData } from '../summary-data/MatchSummaryData';
54
import { MatchSummaryDataPoint } from '../summary-data/MatchSummaryDataPoint';
65
import { MatIconModule } from '@angular/material/icon';
76
import { TeacherSummaryDisplayComponent } from '../teacher-summary-display.component';
@@ -16,71 +15,45 @@ import { TeacherSummaryDisplayComponent } from '../teacher-summary-display.compo
1615
templateUrl: './match-summary-display.component.html'
1716
})
1817
export class MatchSummaryDisplayComponent extends TeacherSummaryDisplayComponent implements OnInit {
19-
protected bucketData: { value: string; choices: MatchSummaryDataPoint[] }[] = [];
20-
private bucketsShowMore: Map<string, boolean> = new Map<string, boolean>();
21-
private bucketValues: Set<string> = new Set<string>();
18+
protected choiceData: ChoiceData[] = [];
2219
@Input() expanded: boolean;
23-
protected isChoiceReuseMatch: boolean;
2420
private matchSummaryData: MatchSummaryData;
2521

2622
ngOnInit(): void {
27-
this.setIsChoiceReuseMatch();
2823
this.generateSummary();
2924
}
3025

31-
private setIsChoiceReuseMatch(): void {
32-
this.isChoiceReuseMatch = (
33-
this.projectService.getComponent(this.nodeId, this.componentId) as MatchContent
34-
).choiceReuseEnabled;
35-
}
36-
3726
private generateSummary(): void {
3827
this.getLatestWork().subscribe((componentStates) => {
39-
this.bucketData = [];
40-
this.bucketValues.clear();
28+
this.choiceData = [];
4129
this.matchSummaryData = new MatchSummaryData(
4230
this.projectService.injectAssetPaths(componentStates)
4331
);
44-
this.setBucketValues();
45-
this.setBucketData();
46-
this.setBucketShowMore();
32+
this.setChoiceData();
4733
});
4834
}
4935

50-
protected setBucketValues(): void {
51-
this.matchSummaryData
52-
.getBucketsData()
53-
.forEach((bucket) => this.bucketValues.add(bucket.bucketValue));
54-
}
55-
56-
protected setBucketData(): void {
57-
this.bucketValues.forEach((value) =>
58-
this.bucketData.push({ value: value, choices: this.getBucketDataByValue(value) })
59-
);
60-
}
61-
62-
private getBucketDataByValue(bucketValue: string): MatchSummaryDataPoint[] {
63-
return this.matchSummaryData
64-
.getBucketsData()
65-
.find((bucket) => bucket.bucketValue === bucketValue)
66-
.bucketDataPoints.sort(this.sortChoices);
67-
}
68-
69-
private sortChoices(choiceA: MatchSummaryDataPoint, choiceB: MatchSummaryDataPoint): number {
70-
return choiceB.getCount() - choiceA.getCount();
36+
protected setChoiceData(): void {
37+
this.matchSummaryData.getChoicesData().forEach((choice) => {
38+
this.choiceData.push({
39+
choiceValue: choice.choiceValue,
40+
choiceDataPoints: choice.choiceDataPoints.sort(this.sortBuckets)
41+
});
42+
});
43+
this.choiceData.sort(this.sortChoices);
7144
}
7245

73-
private setBucketShowMore(): void {
74-
this.bucketValues.forEach((value) => this.bucketsShowMore.set(value, false));
46+
private getTotalCount(choice: ChoiceData): number {
47+
return choice.choiceDataPoints.reduce((sum, dp) => sum + dp.getCount(), 0);
7548
}
7649

77-
protected getBucketShowMore(bucketValue: string): boolean {
78-
return this.bucketsShowMore.get(bucketValue);
79-
}
50+
private sortChoices = (a: ChoiceData, b: ChoiceData): number => {
51+
const countDiff = this.getTotalCount(b) - this.getTotalCount(a);
52+
return countDiff !== 0 ? countDiff : a.choiceValue.localeCompare(b.choiceValue);
53+
};
8054

81-
protected toggleBucketShowMore(bucketValue: string, event: Event): void {
82-
event.preventDefault();
83-
this.bucketsShowMore.set(bucketValue, !this.bucketsShowMore.get(bucketValue));
55+
private sortBuckets(a: MatchSummaryDataPoint, b: MatchSummaryDataPoint): number {
56+
return b.getCount() - a.getCount();
8457
}
8558

8659
protected renderDisplay(): void {

0 commit comments

Comments
 (0)