Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,61 +1,51 @@
<ng-template #bucketTemplate let-bucket="bucket" let-first="first">
<div class="bucket" [ngClass]="{ 'selected-bg-bg': first, 'notice-bg-bg': !first }">
<ng-template #choiceTemplate let-choice="choice">
<div class="choice notice-bg-bg" [class.secondary-text]="choice.choiceDataPoints.length === 0">
<h3 class="mat-body-2">
<span [innerHTML]="bucket.value"></span>
@if (first) {
(<span i18n>Source Bucket</span>)
}
<mat-icon class="mat-18 align-sub" aria-label="Item" i18n-aria-label>crop_16_9</mat-icon
>&nbsp;<span [innerHTML]="choice.choiceValue"></span>
</h3>
<ul>
@for (choice of bucket.choices; track $index) {
@if ($index < 3 || getBucketShowMore(bucket.value)) {
@if (choice.choiceDataPoints.length > 0) {
<ul>
@for (bucketDataPoint of choice.choiceDataPoints; track $index) {
<li>
<div class="choice">
<div [innerHTML]="choice.getId()"></div>
@if (!isChoiceReuseMatch || !first) {
<div class="shrink-0">
(<mat-icon class="mat-18">person</mat-icon>{{ choice.getCount() }})
</div>
}
<div class="bucket">
<mat-icon class="mat-18 shrink-0 mt-0.25" aria-label="Bucket" i18n-aria-label
>inventory_2</mat-icon
>
<div class="flex-1" [innerHTML]="bucketDataPoint.getBucketValue()"></div>
<div class="shrink-0">
<mat-icon class="mat-18 align-middle">person</mat-icon
>{{ bucketDataPoint.getCount() }}
</div>
</div>
</li>
}
}
@if (bucket.choices.length > 3) {
<a href="#" class="text-sm px-1" (click)="toggleBucketShowMore(bucket.value, $event)">
@if (getBucketShowMore(bucket.value)) {
<span i18n>Show less</span>
} @else {
<span i18n>Show more</span>
}
</a>
}
</ul>
</ul>
} @else {
<div class="bucket">
<mat-icon class="mat-18" aria-label="Not moved" i18n-aria-label>do_not_disturb</mat-icon>
<div i18n>Not moved by any students</div>
</div>
}
</div>
</ng-template>

<div [class.expanded]="expanded">
<h2 class="mat-subtitle-1" i18n>Choice Frequency</h2>
<div class="max-h-160 overflow-y-auto" [class.max-h-none]="expanded">
@if (bucketData.length > 0) {
<p i18n>
Number of teams that moved each item (choice) into the different buckets (categories).
<h2 class="mat-subtitle-1" i18n>Bucket Frequency</h2>
<div class="max-h-160 overflow-y-auto @container" [class.max-h-none]="expanded">
@if (choiceData.length > 0) {
<p class="!mt-0" i18n>
Number of times each item <mat-icon class="mat-18 align-sub">crop_16_9</mat-icon> was moved
into the different buckets <mat-icon class="mat-18 align-sub">inventory_2</mat-icon>.
</p>
<div class="columns-3xs gap-2">
<div class="break-inside-avoid">
<ng-container
[ngTemplateOutlet]="bucketTemplate"
[ngTemplateOutletContext]="{ bucket: bucketData[0], first: true }"
/>
</div>
@for (bucket of bucketData; track $index) {
@if ($index > 0) {
<div class="break-inside-avoid">
<ng-container
[ngTemplateOutlet]="bucketTemplate"
[ngTemplateOutletContext]="{ bucket: bucket, first: false }"
/>
</div>
}
<div class="columns-1 @xl:columns-2 @4xl:columns-3 gap-2 mt-2">
@for (choice of choiceData; track $index) {
<div class="break-inside-avoid">
<ng-container
[ngTemplateOutlet]="choiceTemplate"
[ngTemplateOutletContext]="{ choice: choice }"
/>
</div>
}
</div>
} @else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
@use "tailwindcss";
@reference "tailwindcss";

h3,
.mat-subtitle-1 {
margin-bottom: 8px;
margin-top: 0;
}

.bucket {
@apply p-2 mb-2 rounded-md;
}

.choice {
@apply flex gap-1 px-2 py-1 mt-1 rounded-md bg-white border border-neutral-200 text-sm;
@apply p-2 mb-2 rounded-md;
}

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

ul {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,45 +51,40 @@ describe('MatchSummaryDisplayComponent', () => {
expect(component).toBeTruthy();
});

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

it('should only show Show more button if more than 3 choices in bucket', () => {
expect(fixtureQueryAll(fixture, 'a').length).toEqual(1);
it('should order choices by total count descending then alphabetically', () => {
const cards = fixtureQueryAll(fixture, '.choice');
const labels = Array.from(cards).map((el) => el.querySelector('h3')?.textContent?.trim());
expect(labels[0]).toContain('Choice B');
expect(labels[1]).toContain('Choice D');
expect(labels[2]).toContain('Choice C');
expect(labels[3]).toContain('Choice E');
expect(labels[4]).toContain('Choice A');
});

it('should display choices within bucket sorted by count', () => {
const choices = fixtureQueryAll(fixture, '.choice');
expect(choices[0].textContent.includes('Choice B'));
expect(choices[1].textContent.includes('Choice A'));
expect(choices[2].textContent.includes('Choice C'));
expect(choices[3].textContent.includes('Choice D'));
it('should show bucket rows sorted by count within each choice', () => {
const cards = fixtureQueryAll(fixture, '.choice');
const choiceDCard = cards[1];
const bucketRows = choiceDCard.querySelectorAll('.bucket');
expect(bucketRows.length).toEqual(2);
expect(bucketRows[0].textContent).toContain('Bucket 2');
expect(bucketRows[0].textContent).toContain('2');
});

it('should show the correct count on each choice per bucket', () => {
const choices = fixtureQueryAll(fixture, '.choice');
expect(choices[0].textContent.includes('3'));
expect(choices[1].textContent.includes('2'));
expect(choices[2].textContent.includes('2'));
expect(choices[3].textContent.includes('1'));
it('should show the correct count for Choice B in Bucket 1', () => {
const cards = fixtureQueryAll(fixture, '.choice');
const choiceBCard = cards[0];
expect(choiceBCard.textContent).toContain('3');
});

it('should change Show more to Show less when clicked', () => {
let button = fixture.nativeElement.querySelector('a');
expect(button.innerText).toEqual('Show more');
button.click();
fixture.detectChanges();
button = fixture.nativeElement.querySelector('a');
expect(button.innerText).toEqual('Show less');
button.click();
fixture.detectChanges();
button = fixture.nativeElement.querySelector('a');
expect(button.innerText).toEqual('Show more');
it('should show "Not moved by any students" for choices left in the source bucket', () => {
const cards = fixtureQueryAll(fixture, '.choice');
const choiceACard = cards[4];
expect(choiceACard.textContent).toContain('Not moved by any students');
expect(choiceACard.querySelectorAll('.bucket').length).toEqual(1);
});
});

Expand All @@ -114,17 +109,18 @@ function getComponentStates(): any {
id: '0',
type: 'bucket',
value: 'Choices',
items: []
},
{
id: 'b1',
value: 'Bucket 1',
items: [
{
isIncorrectPosition: null,
id: 'a',
value: 'Choice A'
},
}
]
},
{
id: 'b1',
value: 'Bucket 1',
items: [
{
isIncorrectPosition: null,
id: 'b',
Expand Down Expand Up @@ -169,17 +165,18 @@ function getComponentStates(): any {
id: '0',
type: 'bucket',
value: 'Choices',
items: []
},
{
id: 'b1',
value: 'Bucket 1',
items: [
{
isIncorrectPosition: null,
id: 'a',
value: 'Choice A'
},
}
]
},
{
id: 'b1',
value: 'Bucket 1',
items: [
{
isIncorrectPosition: null,
id: 'b',
Expand Down Expand Up @@ -240,7 +237,13 @@ function getComponentStates(): any {
{
id: 'b2',
value: 'Bucket 2',
items: []
items: [
{
isIncorrectPosition: null,
id: 'b',
value: 'Choice D'
}
]
}
]
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { CommonModule } from '@angular/common';
import { Component, Input, OnInit } from '@angular/core';
import { MatchContent } from '../../../components/match/MatchContent';
import { MatchSummaryData } from '../summary-data/MatchSummaryData';
import { ChoiceData, MatchSummaryData } from '../summary-data/MatchSummaryData';
import { MatchSummaryDataPoint } from '../summary-data/MatchSummaryDataPoint';
import { MatIconModule } from '@angular/material/icon';
import { TeacherSummaryDisplayComponent } from '../teacher-summary-display.component';
Expand All @@ -16,71 +15,45 @@ import { TeacherSummaryDisplayComponent } from '../teacher-summary-display.compo
templateUrl: './match-summary-display.component.html'
})
export class MatchSummaryDisplayComponent extends TeacherSummaryDisplayComponent implements OnInit {
protected bucketData: { value: string; choices: MatchSummaryDataPoint[] }[] = [];
private bucketsShowMore: Map<string, boolean> = new Map<string, boolean>();
private bucketValues: Set<string> = new Set<string>();
protected choiceData: ChoiceData[] = [];
@Input() expanded: boolean;
protected isChoiceReuseMatch: boolean;
private matchSummaryData: MatchSummaryData;

ngOnInit(): void {
this.setIsChoiceReuseMatch();
this.generateSummary();
}

private setIsChoiceReuseMatch(): void {
this.isChoiceReuseMatch = (
this.projectService.getComponent(this.nodeId, this.componentId) as MatchContent
).choiceReuseEnabled;
}

private generateSummary(): void {
this.getLatestWork().subscribe((componentStates) => {
this.bucketData = [];
this.bucketValues.clear();
this.choiceData = [];
this.matchSummaryData = new MatchSummaryData(
this.projectService.injectAssetPaths(componentStates)
);
this.setBucketValues();
this.setBucketData();
this.setBucketShowMore();
this.setChoiceData();
});
}

protected setBucketValues(): void {
this.matchSummaryData
.getBucketsData()
.forEach((bucket) => this.bucketValues.add(bucket.bucketValue));
}

protected setBucketData(): void {
this.bucketValues.forEach((value) =>
this.bucketData.push({ value: value, choices: this.getBucketDataByValue(value) })
);
}

private getBucketDataByValue(bucketValue: string): MatchSummaryDataPoint[] {
return this.matchSummaryData
.getBucketsData()
.find((bucket) => bucket.bucketValue === bucketValue)
.bucketDataPoints.sort(this.sortChoices);
}

private sortChoices(choiceA: MatchSummaryDataPoint, choiceB: MatchSummaryDataPoint): number {
return choiceB.getCount() - choiceA.getCount();
protected setChoiceData(): void {
this.matchSummaryData.getChoicesData().forEach((choice) => {
this.choiceData.push({
choiceValue: choice.choiceValue,
choiceDataPoints: choice.choiceDataPoints.sort(this.sortBuckets)
});
});
this.choiceData.sort(this.sortChoices);
}

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

protected getBucketShowMore(bucketValue: string): boolean {
return this.bucketsShowMore.get(bucketValue);
}
private sortChoices = (a: ChoiceData, b: ChoiceData): number => {
const countDiff = this.getTotalCount(b) - this.getTotalCount(a);
return countDiff !== 0 ? countDiff : a.choiceValue.localeCompare(b.choiceValue);
};

protected toggleBucketShowMore(bucketValue: string, event: Event): void {
event.preventDefault();
this.bucketsShowMore.set(bucketValue, !this.bucketsShowMore.get(bucketValue));
private sortBuckets(a: MatchSummaryDataPoint, b: MatchSummaryDataPoint): number {
return b.getCount() - a.getCount();
}

protected renderDisplay(): void {
Expand Down
Loading