Skip to content

Commit cf085c0

Browse files
hirokiterashimaAaron-Detrebreity
authored
feat(Library): Add Resource Units (#2131)
Co-authored-by: Aaron Detre <aarondetre@gmail.com> Co-authored-by: Jonathan Lim-Breitbart <breity10@gmail.com>
1 parent 1c0a0b7 commit cf085c0

33 files changed

Lines changed: 1082 additions & 150 deletions

File tree

src/app/modules/library/library-project-details/library-project-details.component.html

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,17 @@
9090
}
9191
</p>
9292
}
93+
@if (project.metadata.resources?.length > 0) {
94+
<p>
95+
<strong i18n>Resources:</strong>&nbsp;
96+
@for (resource of project.metadata.resources; track resource.name; let last = $last) {
97+
<a href="{{ resource.url }}" target="_blank" matTooltip="{{ resource.name }}">{{
98+
resource.name
99+
}}</a>
100+
{{ last ? '' : ' | ' }}
101+
}
102+
</p>
103+
}
93104
<unit-tags [tags]="project.tags" />
94105
</div>
95106
@if (project.metadata.discourseCategoryURL) {
@@ -148,14 +159,21 @@
148159
</div>
149160
<div mat-dialog-actions class="flex flex-col sm:flex-row gap-2 sm:gap-4 !items-stretch" align="end">
150161
<button mat-button cdkFocusInitial (click)="close()" i18n>Close</button>
151-
@if (isTeacher && !isRunProject && project.wiseVersion !== 4) {
162+
@if (
163+
isTeacher &&
164+
!isRunProject &&
165+
project.wiseVersion !== 4 &&
166+
project.metadata.unitType === 'Platform'
167+
) {
152168
<button mat-flat-button color="accent" (click)="runProject()">
153169
<mat-icon>supervised_user_circle</mat-icon>&nbsp;<ng-container i18n
154170
>Use with Class</ng-container
155171
>
156172
</button>
157173
}
158-
<button mat-flat-button color="primary" (click)="previewProject()">
159-
<mat-icon>preview</mat-icon>&nbsp;<ng-container i18n>Preview</ng-container>
160-
</button>
174+
@if (project.metadata.unitType === 'Platform') {
175+
<button mat-flat-button color="primary" (click)="previewProject()">
176+
<mat-icon>preview</mat-icon>&nbsp;<ng-container i18n>Preview</ng-container>
177+
</button>
178+
}
161179
</div>

src/app/modules/library/library-project-details/library-project-details.component.spec.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ import { Project } from '../../../domain/project';
66
import { NGSSStandards } from '../ngssStandards';
77
import { ConfigService } from '../../../services/config.service';
88
import { ParentProject } from '../../../domain/parentProject';
9-
import { MockProviders } from 'ng-mocks';
9+
import { MockComponent, MockProviders } from 'ng-mocks';
10+
import { By } from '@angular/platform-browser';
11+
import { LibraryProjectMenuComponent } from '../library-project-menu/library-project-menu.component';
1012

13+
let component: LibraryProjectDetailsComponent;
14+
let fixture: ComponentFixture<LibraryProjectDetailsComponent>;
1115
describe('LibraryProjectDetailsComponent', () => {
12-
let component: LibraryProjectDetailsComponent;
13-
let fixture: ComponentFixture<LibraryProjectDetailsComponent>;
14-
1516
beforeEach(() => {
1617
TestBed.configureTestingModule({
18+
declarations: [MockComponent(LibraryProjectMenuComponent)],
1719
imports: [LibraryProjectDetailsComponent],
1820
providers: [
1921
MockProviders(ConfigService, MatDialog, MatDialogRef, UserService),
@@ -30,11 +32,13 @@ describe('LibraryProjectDetailsComponent', () => {
3032
grades: ['7'],
3133
title: 'Photosynthesis & Cellular Respiration',
3234
summary: 'A really great unit.',
35+
unitType: 'Platform',
3336
totalTime: '6-7 hours',
3437
authors: [
3538
{ id: 10, firstName: 'Spaceman', lastName: 'Spiff', username: 'SpacemanSpiff' },
3639
{ id: 12, firstName: 'Captain', lastName: 'Napalm', username: 'CaptainNapalm' }
37-
]
40+
],
41+
resources: [{ name: 'Resource 1', uri: 'http://example.com/resource1' }]
3842
};
3943
const ngssObject: any = {
4044
disciplines: [
@@ -90,6 +94,11 @@ describe('LibraryProjectDetailsComponent', () => {
9094
expect(compiled.textContent).toContain('by Spaceman Spiff, Captain Napalm');
9195
});
9296

97+
it('should show project resources', () => {
98+
const compiled = fixture.debugElement.nativeElement;
99+
expect(compiled.textContent).toContain('Resource 1');
100+
});
101+
93102
it('should show copied project info', () => {
94103
component['project'].metadata.authors = [];
95104
component['parentProject'] = new ParentProject({
@@ -103,4 +112,33 @@ describe('LibraryProjectDetailsComponent', () => {
103112
const compiled = fixture.debugElement.nativeElement;
104113
expect(compiled.textContent).toContain('is a copy of Photosynthesis');
105114
});
115+
116+
it('should show use with class and preview buttons', () => {
117+
component['isTeacher'] = true;
118+
fixture.detectChanges();
119+
expect(getButtonWithText('Use with Class')).toBeTruthy();
120+
expect(getButtonWithText('Preview')).toBeTruthy();
121+
});
122+
123+
isResourceUnitType_HideButtons();
106124
});
125+
126+
function isResourceUnitType_HideButtons() {
127+
describe('is not Resource unit type', () => {
128+
beforeEach(() => {
129+
component['project'].metadata.unitType = 'Resource';
130+
fixture.detectChanges();
131+
});
132+
133+
it('should hide buttons when unit type is Resource', () => {
134+
expect(getButtonWithText('Use with Class')).toBeFalsy();
135+
expect(getButtonWithText('Preview')).toBeFalsy();
136+
});
137+
});
138+
}
139+
140+
function getButtonWithText(text: string) {
141+
return fixture.debugElement
142+
.queryAll(By.css('button'))
143+
.find((el) => el.nativeElement.textContent.includes(text));
144+
}

src/app/teacher/authoring-tool.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ import { AddComponentComponent } from '../../assets/wise5/authoringTool/node/add
6161
import { SideMenuComponent } from '../../assets/wise5/common/side-menu/side-menu.component';
6262
import { MainMenuComponent } from '../../assets/wise5/common/main-menu/main-menu.component';
6363
import { ChooseImportComponentComponent } from '../../assets/wise5/authoringTool/importComponent/choose-import-component/choose-import-component.component';
64+
import { EditUnitResourcesComponent } from '../../assets/wise5/authoringTool/edit-unit-resources/edit-unit-resources.component';
65+
import { EditUnitTypeComponent } from '../../assets/wise5/authoringTool/edit-unit-type/edit-unit-type.component';
6466

6567
@NgModule({
6668
declarations: [
@@ -106,6 +108,8 @@ import { ChooseImportComponentComponent } from '../../assets/wise5/authoringTool
106108
CreateBranchComponent,
107109
EditBranchComponent,
108110
EditNodeTitleComponent,
111+
EditUnitResourcesComponent,
112+
EditUnitTypeComponent,
109113
MatBadgeModule,
110114
MatChipsModule,
111115
MatExpansionModule,
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<ng-template #addButton let-addToTop="addToTop">
2+
<button
3+
mat-icon-button
4+
color="primary"
5+
matTooltip="Add a new resource"
6+
i18n-matTooltip
7+
matTooltipPosition="after"
8+
(click)="addNewResource(addToTop)"
9+
>
10+
<mat-icon>add_circle</mat-icon>
11+
</button>
12+
</ng-template>
13+
<div class="resource-descriptions notice-bg-bg">
14+
<h5 class="flex flex-row items-center gap-1 !text-xl">
15+
<span i18n>Resources</span>
16+
<ng-container *ngTemplateOutlet="addButton; context: { addToTop: true }" />
17+
<span class="flex-1"></span>
18+
</h5>
19+
<ul>
20+
@for (
21+
resource of resources;
22+
track $index;
23+
let resourceIndex = $index, first = $first, last = $last
24+
) {
25+
<li class="resource">
26+
<mat-card appearance="outlined" class="resource-content">
27+
<div class="flex flex-row flex-wrap gap-2">
28+
<div class="text-secondary flex flex-col items-center">
29+
<span class="mat-subtitle-1">{{ resourceIndex + 1 }}</span>
30+
</div>
31+
<div class="flex flex-col items-start gap-2 flex-1">
32+
<mat-form-field class="resource-input form-field-no-hint" appearance="fill">
33+
<mat-label i18n>Resource Name</mat-label>
34+
<input
35+
matInput
36+
[(ngModel)]="resource.name"
37+
(ngModelChange)="inputChanged.next($event)"
38+
/>
39+
</mat-form-field>
40+
<mat-form-field class="resource-input form-field-no-hint" appearance="fill">
41+
<mat-label i18n>Resource URL</mat-label>
42+
<textarea
43+
matInput
44+
[(ngModel)]="resource.url"
45+
(ngModelChange)="inputChanged.next($event)"
46+
cdkTextareaAutosize
47+
>
48+
</textarea>
49+
</mat-form-field>
50+
</div>
51+
<div class="flex flex-col items-center">
52+
<button
53+
mat-icon-button
54+
i18n-matTooltip
55+
matTooltip="Delete resource"
56+
matTooltipPosition="before"
57+
(click)="deleteResource(resourceIndex)"
58+
>
59+
<mat-icon>clear</mat-icon>
60+
</button>
61+
</div>
62+
</div>
63+
</mat-card>
64+
</li>
65+
}
66+
</ul>
67+
<div id="add-new-resource-bottom-button" [hidden]="resources.length === 0">
68+
<ng-container *ngTemplateOutlet="addButton; context: { addToTop: false }" />
69+
</div>
70+
</div>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
@import 'style/abstracts/variables';
2+
3+
.resource-descriptions {
4+
padding: 16px;
5+
border-radius: $card-border-radius;
6+
}
7+
8+
h5 {
9+
margin-top: 0;
10+
}
11+
12+
ul {
13+
margin: 16px 0 0 0;
14+
padding: 0 0 16px;
15+
}
16+
17+
li {
18+
list-style-type: none;
19+
}
20+
21+
.resource {
22+
position: relative;
23+
}
24+
25+
.resource-content {
26+
width: 100%;
27+
padding: 8px;
28+
margin-bottom: 8px;
29+
}
30+
31+
.resource-input {
32+
width: 100%;
33+
}
34+
35+
.mat-subtitle-1 {
36+
margin: 0;
37+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { EditUnitResourcesComponent } from './edit-unit-resources.component';
3+
import { MockProvider } from 'ng-mocks';
4+
import { TeacherProjectService } from '../../services/teacherProjectService';
5+
import { By } from '@angular/platform-browser';
6+
7+
let component: EditUnitResourcesComponent;
8+
let fixture: ComponentFixture<EditUnitResourcesComponent>;
9+
describe('EditUnitResourcesComponent', () => {
10+
beforeEach(async () => {
11+
await TestBed.configureTestingModule({
12+
imports: [EditUnitResourcesComponent],
13+
providers: [MockProvider(TeacherProjectService)]
14+
}).compileComponents();
15+
16+
fixture = TestBed.createComponent(EditUnitResourcesComponent);
17+
component = fixture.componentInstance;
18+
component.resources = [
19+
{ name: 'Resource 1', url: 'http://example.com/resource1' },
20+
{ name: 'Resource 2', url: 'http://example.com/resource2' }
21+
];
22+
fixture.detectChanges();
23+
});
24+
25+
it('should show the correct number of resources', () => {
26+
const resourceElements = fixture.debugElement.queryAll(By.css('input'));
27+
expect(resourceElements.length).toBe(2);
28+
expect(resourceElements[0].nativeElement.value).toContain('Resource 1');
29+
expect(resourceElements[1].nativeElement.value).toContain('Resource 2');
30+
});
31+
32+
clickTopAddButton_addNewResourceAtTheBeginning();
33+
clickBottomTopButton_addNewResourceAtTheEnd();
34+
});
35+
36+
function clickTopAddButton_addNewResourceAtTheBeginning() {
37+
describe('Clicking on the top Add Resource button', () => {
38+
let initialLength = 0;
39+
beforeEach(() => {
40+
initialLength = component.resources.length;
41+
fixture.debugElement.queryAll(By.css('button'))[0].nativeElement.click();
42+
fixture.detectChanges();
43+
});
44+
it('should add a new resource to the beginning of the list', () => {
45+
expect(component.resources.length).toBe(initialLength + 1);
46+
expect(component.resources[0].name).toEqual('');
47+
expect(component.resources[0].url).toEqual('');
48+
});
49+
});
50+
}
51+
52+
function clickBottomTopButton_addNewResourceAtTheEnd() {
53+
describe('Clicking on the bottom Add Resource button', () => {
54+
let initialLength = 0;
55+
beforeEach(() => {
56+
initialLength = component.resources.length;
57+
const allButtons = fixture.debugElement.queryAll(By.css('button'));
58+
allButtons[allButtons.length - 1].nativeElement.click();
59+
fixture.detectChanges();
60+
});
61+
it('should add a new resource to the end of the list', () => {
62+
expect(component.resources.length).toBe(initialLength + 1);
63+
expect(component.resources.at(-1).name).toEqual('');
64+
expect(component.resources.at(-1).url).toEqual('');
65+
});
66+
});
67+
}

0 commit comments

Comments
 (0)