Skip to content

Commit 192c55c

Browse files
feat(Teacher Tools): Important activities (#2292)
Co-authored-by: Jonathan Lim-Breitbart <breity10@gmail.com>
1 parent a6ccd16 commit 192c55c

File tree

16 files changed

+386
-24
lines changed

16 files changed

+386
-24
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { ToggleComponentTagComponent } from './toggle-component-tag.component';
3+
import { TeacherProjectService } from '../../../services/teacherProjectService';
4+
import { ComponentContent } from '../../../common/ComponentContent';
5+
6+
describe('ToggleComponentTagComponent', () => {
7+
let component: ToggleComponentTagComponent;
8+
let fixture: ComponentFixture<ToggleComponentTagComponent>;
9+
let mockProjectService: jasmine.SpyObj<TeacherProjectService>;
10+
11+
beforeEach(async () => {
12+
mockProjectService = jasmine.createSpyObj('TeacherProjectService', ['saveProject']);
13+
14+
await TestBed.configureTestingModule({
15+
imports: [ToggleComponentTagComponent],
16+
providers: [{ provide: TeacherProjectService, useValue: mockProjectService }]
17+
}).compileComponents();
18+
19+
fixture = TestBed.createComponent(ToggleComponentTagComponent);
20+
component = fixture.componentInstance;
21+
component.component = { id: 'test', type: 'OpenResponse' } as ComponentContent;
22+
});
23+
24+
it('should create', () => {
25+
fixture.detectChanges();
26+
expect(component).toBeTruthy();
27+
});
28+
29+
describe('ngOnInit', () => {
30+
it('should set hasTag to false and update tooltip if tag is not present', () => {
31+
component.component.tags = ['other-tag'];
32+
fixture.detectChanges();
33+
34+
expect(component['hasTag']).toBeFalse();
35+
expect(component['tooltip']).toEqual('Bookmark for teachers');
36+
});
37+
38+
it('should set hasTag to true and update tooltip if tag is present', () => {
39+
component.component.tags = ['!important'];
40+
fixture.detectChanges();
41+
42+
expect(component['hasTag']).toBeTrue();
43+
expect(component['tooltip']).toEqual('Remove bookmark for teachers');
44+
});
45+
46+
it('should handle undefined tags array', () => {
47+
component.component.tags = undefined;
48+
fixture.detectChanges();
49+
50+
expect(component['hasTag']).toBeFalsy();
51+
expect(component['tooltip']).toEqual('Bookmark for teachers');
52+
});
53+
});
54+
55+
describe('toggleTag', () => {
56+
it('should remove tag, set hasTag to false, update tooltip, and save project if tag is present', () => {
57+
component.component.tags = ['!important', 'other-tag'];
58+
fixture.detectChanges();
59+
60+
component['toggleTag']();
61+
62+
expect(component.component.tags).toEqual(['other-tag']);
63+
expect(component['hasTag']).toBeFalse();
64+
expect(component['tooltip']).toEqual('Bookmark for teachers');
65+
expect(mockProjectService.saveProject).toHaveBeenCalled();
66+
});
67+
68+
it('should add tag, set hasTag to true, update tooltip, and save project if tag is not present', () => {
69+
component.component.tags = ['other-tag'];
70+
fixture.detectChanges();
71+
72+
component['toggleTag']();
73+
74+
expect(component.component.tags).toEqual(['other-tag', '!important']);
75+
expect(component['hasTag']).toBeTrue();
76+
expect(component['tooltip']).toEqual('Remove bookmark for teachers');
77+
expect(mockProjectService.saveProject).toHaveBeenCalled();
78+
});
79+
80+
it('should create tags array, add tag, set hasTag to true, update tooltip, and save project if tags array is null', () => {
81+
component.component.tags = undefined;
82+
fixture.detectChanges();
83+
84+
component['toggleTag']();
85+
86+
expect(component.component.tags).toEqual(['!important']);
87+
expect(component['hasTag']).toBeTrue();
88+
expect(component['tooltip']).toEqual('Remove bookmark for teachers');
89+
expect(mockProjectService.saveProject).toHaveBeenCalled();
90+
});
91+
});
92+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Component, inject, Input } from '@angular/core';
2+
import { MatIconModule } from '@angular/material/icon';
3+
import { MatTooltipModule } from '@angular/material/tooltip';
4+
import { ComponentContent } from '../../../common/ComponentContent';
5+
import { TeacherProjectService } from '../../../services/teacherProjectService';
6+
import { MatButtonModule } from '@angular/material/button';
7+
8+
@Component({
9+
imports: [MatButtonModule, MatIconModule, MatTooltipModule],
10+
selector: 'toggle-component-tag',
11+
template: `<button
12+
mat-icon-button
13+
(click)="toggleTag()"
14+
[matTooltip]="tooltip"
15+
matTooltipPosition="above"
16+
>
17+
@if (hasTag) {
18+
<mat-icon color="primary">bookmark</mat-icon>
19+
} @else {
20+
<mat-icon>bookmark_border</mat-icon>
21+
}
22+
</button>`
23+
})
24+
export class ToggleComponentTagComponent {
25+
private projectService = inject(TeacherProjectService);
26+
27+
@Input() component: ComponentContent;
28+
protected hasTag: boolean;
29+
private tag: string = '!important';
30+
protected tooltip: string;
31+
32+
ngOnInit(): void {
33+
this.hasTag = this.component.tags?.includes(this.tag);
34+
this.updateTooltip();
35+
}
36+
37+
private updateTooltip(): void {
38+
if (this.hasTag) {
39+
this.tooltip = $localize`Remove bookmark for teachers`;
40+
} else {
41+
this.tooltip = $localize`Bookmark for teachers`;
42+
}
43+
}
44+
45+
protected toggleTag(): void {
46+
if (this.component.tags?.includes(this.tag)) {
47+
this.component.tags = this.component.tags.filter((t) => t !== this.tag);
48+
this.hasTag = false;
49+
} else {
50+
if (this.component.tags == null) {
51+
this.component.tags = [];
52+
}
53+
this.component.tags.push(this.tag);
54+
this.hasTag = true;
55+
}
56+
this.updateTooltip();
57+
this.projectService.saveProject();
58+
}
59+
}

src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
}
4949
<span class="heavy">{{ i + 1 }}. {{ getComponentTypeLabel(component.type) }}</span>
5050
</div>
51+
<toggle-component-tag [component]="component" />
5152
<span class="flex grow"></span>
5253
<div class="text flex justify-end items-center">
5354
@if (component.id === editingComponentId) {

src/assets/wise5/authoringTool/node/node-authoring/node-authoring.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { CommonModule } from '@angular/common';
2626
import { ComponentAuthoringComponent } from '../../components/component-authoring.component';
2727
import { RouterModule } from '@angular/router';
2828
import { EditComponentAdvancedButtonComponent } from '../../components/edit-component-advanced-button/edit-component-advanced-button.component';
29+
import { ToggleComponentTagComponent } from '../../components/toggle-component-tag/toggle-component-tag.component';
2930

3031
@Component({
3132
imports: [
@@ -45,7 +46,8 @@ import { EditComponentAdvancedButtonComponent } from '../../components/edit-comp
4546
MatInputModule,
4647
MatTooltipModule,
4748
RouterModule,
48-
TeacherNodeIconComponent
49+
TeacherNodeIconComponent,
50+
ToggleComponentTagComponent
4951
],
5052
styleUrl: './node-authoring.component.scss',
5153
templateUrl: './node-authoring.component.html'

src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/filter-components/filter-components.component.html

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
<mat-form-field appearance="fill" class="component-select form-field-no-hint">
22
@if (components.length == 1) {
3-
<mat-select disabled placeholder="1 item ({{ getComponentTypeLabel(components[0].type) }})" />
3+
<mat-select
4+
disabled
5+
placeholder="1 item ({{ getComponentTypeLabel(components[0].type) }})"
6+
i18n-placeholder
7+
/>
48
} @else if (components.length > 1) {
59
<mat-select
610
[(ngModel)]="selectedComponents"
@@ -12,7 +16,19 @@
1216
<mat-select-trigger>{{ selectedText }}</mat-select-trigger>
1317
@for (component of components; track component.id; let i = $index) {
1418
<mat-option [value]="component" color="primary">
15-
{{ component.displayIndex }}: {{ getComponentTypeLabel(component.type) }}
19+
<span class="flex items-center gap-1">
20+
{{ component.displayIndex }}: {{ getComponentTypeLabel(component.type) }}
21+
@if (component.tags?.includes('!important')) {
22+
<mat-icon
23+
class="!m-0"
24+
color="primary"
25+
matTooltip="Bookmarked activity"
26+
matTooltipPosition="above"
27+
i18n-matTooltip
28+
>bookmark</mat-icon
29+
>
30+
}
31+
</span>
1632
</mat-option>
1733
}
1834
</mat-select>

src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/filter-components/filter-components.component.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,18 @@ function moreThanOneComponent() {
8787
} as ComponentContent
8888
]);
8989
});
90+
91+
it('should show bookmark icon for option with !important tag', async () => {
92+
component.components[0].tags = ['!important'];
93+
fixture.detectChanges();
94+
await select.open();
95+
96+
const options = await select.getOptions();
97+
const option1Text = await options[0].getText();
98+
const option2Text = await options[1].getText();
99+
100+
expect(option1Text).toContain('bookmark');
101+
expect(option2Text).not.toContain('bookmark');
102+
});
90103
});
91104
}

src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/filter-components/filter-components.component.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,19 @@ import { MatButtonModule } from '@angular/material/button';
66
import { FormsModule } from '@angular/forms';
77
import { ComponentTypeService } from '../../../../services/componentTypeService';
88
import { ComponentContent } from '../../../../common/ComponentContent';
9+
import { MatTooltipModule } from '@angular/material/tooltip';
10+
import { MatIconModule } from '@angular/material/icon';
911

1012
@Component({
11-
imports: [CommonModule, FormsModule, MatButtonModule, MatFormFieldModule, MatSelectModule],
13+
imports: [
14+
CommonModule,
15+
FormsModule,
16+
MatButtonModule,
17+
MatFormFieldModule,
18+
MatIconModule,
19+
MatSelectModule,
20+
MatTooltipModule
21+
],
1222
selector: 'filter-components',
1323
styleUrl: './filter-components.component.scss',
1424
templateUrl: './filter-components.component.html',

src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/node-grading/node-grading.component.html

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,19 @@ <h3 class="mat-body-1" i18n>Question Summaries</h3>
4242
animationDuration="0"
4343
>
4444
@for (component of components; track component; let i = $index) {
45-
<mat-tab label="{{ i + 1 }}. {{ getComponentTypeLabel(component.type) }}">
45+
<mat-tab>
46+
<ng-template mat-tab-label>
47+
{{ i + 1 }}. {{ getComponentTypeLabel(component.type) }}
48+
@if (component.tags?.includes('!important')) {
49+
&nbsp;<mat-icon
50+
color="primary"
51+
matTooltip="Bookmarked activity"
52+
matTooltipPosition="above"
53+
i18n-matTooltip
54+
>bookmark</mat-icon
55+
>
56+
}
57+
</ng-template>
4658
<ng-template matTabContent>
4759
<component-summary [component]="component" [node]="node" [periodId]="periodId" />
4860
</ng-template>

src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/node-grading/node-grading.component.spec.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ComponentServiceLookupService } from '../../../../services/componentSer
1818
import { provideHttpClient } from '@angular/common/http';
1919
import { ConfigService } from '../../../../services/configService';
2020
import { PathService } from '../../../../services/pathService';
21+
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
2122

2223
let classroomStatusService: ClassroomStatusService;
2324
let component: NodeGradingComponent;
@@ -84,7 +85,7 @@ describe('NodeGradingComponent', () => {
8485
NodeClassResponsesComponent
8586
)
8687
],
87-
imports: [NodeGradingComponent],
88+
imports: [NodeGradingComponent, NoopAnimationsModule],
8889
providers: [
8990
{ provide: AnnotationService, useClass: MockAnnotationService },
9091
{ provide: TeacherProjectService, MockProjectService },
@@ -124,8 +125,57 @@ describe('NodeGradingComponent', () => {
124125
annotationReceived_RecalculateNodeCompletion();
125126
periodChanged_RecalculateNodeCompletion();
126127
projectSaved_RecalculateNodeCompletion();
128+
componentTags_importantTag();
127129
});
128130

131+
function componentTags_importantTag() {
132+
describe('component tags', () => {
133+
it('shows a bookmark icon in the tab if the component has the !important tag', async () => {
134+
const projectService = TestBed.inject(TeacherProjectService);
135+
(projectService.getComponents as jasmine.Spy).and.returnValue([
136+
{ id: 'component1', type: 'MultipleChoice', tags: ['!important'] }
137+
]);
138+
139+
component['setFields']();
140+
component['summariesVisible'] = true;
141+
fixture.detectChanges();
142+
await fixture.whenStable();
143+
fixture.detectChanges();
144+
145+
const icons = Array.from(fixture.nativeElement.querySelectorAll('mat-icon'));
146+
let starIcon = icons.find((icon: any) => icon.textContent.trim() === 'bookmark');
147+
if (!starIcon) {
148+
starIcon = Array.from(document.querySelectorAll('mat-icon')).find(
149+
(icon: any) => (icon as Element).textContent.trim() === 'bookmark'
150+
) as any;
151+
}
152+
expect(starIcon).toBeTruthy();
153+
});
154+
155+
it('does not show a bookmark icon in the tab if the component does not have the !important tag', async () => {
156+
const projectService = TestBed.inject(TeacherProjectService);
157+
(projectService.getComponents as jasmine.Spy).and.returnValue([
158+
{ id: 'component1', type: 'MultipleChoice', tags: ['other'] }
159+
]);
160+
161+
component['setFields']();
162+
component['summariesVisible'] = true;
163+
fixture.detectChanges();
164+
await fixture.whenStable();
165+
fixture.detectChanges();
166+
167+
const icons = Array.from(fixture.nativeElement.querySelectorAll('mat-icon'));
168+
let starIcon = icons.find((icon: any) => icon.textContent.trim() === 'bookmark');
169+
if (!starIcon) {
170+
starIcon = Array.from(document.querySelectorAll('mat-icon')).find(
171+
(icon: any) => (icon as Element).textContent.trim() === 'bookmark'
172+
) as any;
173+
}
174+
expect(starIcon).toBeFalsy();
175+
});
176+
});
177+
}
178+
129179
function periodChanged_RecalculateNodeCompletion() {
130180
describe('period changed', () => {
131181
it('recalculates node completion', () => {

src/assets/wise5/classroomMonitor/classroomMonitorComponents/nodeGrading/node-grading/node-grading.component.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ComponentTypeService } from '../../../../services/componentTypeService'
1515
import { ComponentSummaryComponent } from '../../component-summary/component-summary.component';
1616
import { FormControl } from '@angular/forms';
1717
import { AnnotationService } from '../../../../services/annotationService';
18+
import { MatTooltipModule } from '@angular/material/tooltip';
1819

1920
@Component({
2021
imports: [
@@ -24,6 +25,7 @@ import { AnnotationService } from '../../../../services/annotationService';
2425
MatButtonModule,
2526
MatIconModule,
2627
MatTabsModule,
28+
MatTooltipModule,
2729
NodeClassResponsesComponent
2830
],
2931
styles: [
@@ -107,6 +109,7 @@ export class NodeGradingComponent implements OnInit, OnDestroy, OnChanges {
107109
this.visibleComponents = [this.components[0]];
108110
this.numRubrics = this.node.getNumRubrics();
109111
this.setPeriod();
112+
this.showImportantComponent();
110113
}
111114

112115
private setPeriod(): void {
@@ -129,6 +132,13 @@ export class NodeGradingComponent implements OnInit, OnDestroy, OnChanges {
129132
).completionPct;
130133
}
131134

135+
private showImportantComponent(): void {
136+
const component = this.components.find((component) => component.tags?.includes('!important'));
137+
if (component) {
138+
this.selectSummary(this.components.findIndex((c) => c.id === component.id));
139+
}
140+
}
141+
132142
protected setVisibleComponents(visibleComponents: ComponentContent[]): void {
133143
this.visibleComponents = visibleComponents;
134144
}

0 commit comments

Comments
 (0)