From a26f6f20fe51804b0cc8172e0c16b4e0bb0fd39e Mon Sep 17 00:00:00 2001 From: Geoffrey Kwan Date: Mon, 1 Jul 2024 15:06:50 -0700 Subject: [PATCH] feat(Teacher): Implement unit tagging (#1813) Co-authored-by: Jonathan Lim-Breitbart --- package-lock.json | 24 ++ package.json | 2 + src/app/domain/archiveProjectResponse.ts | 6 +- src/app/domain/project.ts | 28 +- src/app/domain/projectAndTagsResponse.ts | 6 + src/app/domain/tag.ts | 7 + .../community-library.component.html | 2 +- .../home-page-project-library.component.html | 6 +- .../home-page-project-library.component.scss | 5 + .../home-page-project-library.component.ts | 4 +- .../library-filters.component.html | 7 +- .../library-filters.component.ts | 8 +- .../library-project-details.component.html | 3 +- .../library-project-details.component.scss | 2 +- .../library-project-menu.component.spec.ts | 3 +- .../library-project-menu.component.ts | 6 +- .../library-project.component.html | 5 +- .../library-project.component.scss | 7 +- .../library-project.component.spec.ts | 22 +- .../library-project.component.ts | 55 ++- src/app/modules/library/library.module.ts | 15 +- .../library/library/library.component.scss | 1 - .../official-library.component.html | 2 +- .../personal-library.component.html | 10 +- .../personal-library.component.spec.ts | 25 +- .../personal-library.component.ts | 38 +- .../select-all-items-checkbox.component.html | 7 +- .../select-all-items-checkbox.component.scss | 3 + ...elect-all-items-checkbox.component.spec.ts | 5 +- .../select-all-items-checkbox.component.ts | 12 +- .../teacher-project-library.component.html | 10 +- .../teacher-project-library.component.scss | 6 + .../search-bar/search-bar.component.html | 11 +- .../select-menu/select-menu.component.html | 2 +- src/app/services/archive-project.service.ts | 11 +- src/app/services/tagService.spec.ts | 2 +- .../abstract-tags-menu.component.ts | 86 ++++ .../apply-tags-button.component.html | 27 ++ .../apply-tags-button.component.scss | 3 + .../apply-tags-button.component.spec.ts | 30 ++ .../apply-tags-button.component.ts | 91 +++++ .../color-chooser.component.html | 13 + .../color-chooser.component.scss | 10 + .../color-chooser.component.spec.ts | 20 + .../color-chooser/color-chooser.component.ts | 29 ++ .../teacher/edit-tag/edit-tag.component.html | 40 ++ .../teacher/edit-tag/edit-tag.component.scss | 9 + .../edit-tag/edit-tag.component.spec.ts | 25 ++ .../teacher/edit-tag/edit-tag.component.ts | 161 ++++++++ .../manage-tags-dialog.component.html | 58 +++ .../manage-tags-dialog.component.scss | 31 ++ .../manage-tags-dialog.component.spec.ts | 30 ++ .../manage-tags-dialog.component.ts | 101 +++++ .../run-menu/run-menu.component.spec.ts | 19 +- .../teacher/run-menu/run-menu.component.ts | 4 +- .../select-tags/select-tags.component.html | 31 ++ .../select-tags/select-tags.component.scss | 38 ++ .../select-tags/select-tags.component.spec.ts | 24 ++ .../select-tags/select-tags.component.ts | 47 +++ src/app/teacher/tag/tag.component.html | 15 + src/app/teacher/tag/tag.component.scss | 27 ++ src/app/teacher/tag/tag.component.spec.ts | 23 ++ src/app/teacher/tag/tag.component.ts | 29 ++ .../teacher-run-list-item.component.html | 2 +- .../teacher-run-list-item.component.spec.ts | 5 +- .../teacher-run-list-item.component.ts | 48 ++- .../teacher-run-list.component.html | 149 +++---- .../teacher-run-list.component.scss | 18 +- .../teacher-run-list.component.spec.ts | 30 +- .../teacher-run-list.component.ts | 36 +- src/app/teacher/teacher.module.ts | 18 +- src/app/teacher/teacher.service.ts | 4 + .../unit-tags/unit-tags.component.html | 7 + .../unit-tags/unit-tags.component.scss | 4 + .../unit-tags/unit-tags.component.spec.ts | 21 + .../teacher/unit-tags/unit-tags.component.ts | 16 + src/assets/wise5/services/colorService.ts | 10 + .../wise5/services/projectTagService.ts | 67 ++++ src/assets/wise5/services/tagService.ts | 2 +- src/messages.xlf | 366 +++++++++++++----- src/style/abstracts/_mixins.scss | 65 ++++ src/style/components/_dialog.scss | 2 +- src/style/styles.scss | 2 + 83 files changed, 1940 insertions(+), 321 deletions(-) create mode 100644 src/app/domain/projectAndTagsResponse.ts create mode 100644 src/app/domain/tag.ts create mode 100644 src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.scss create mode 100644 src/app/teacher/abstract-tags-menu/abstract-tags-menu.component.ts create mode 100644 src/app/teacher/apply-tags-button/apply-tags-button.component.html create mode 100644 src/app/teacher/apply-tags-button/apply-tags-button.component.scss create mode 100644 src/app/teacher/apply-tags-button/apply-tags-button.component.spec.ts create mode 100644 src/app/teacher/apply-tags-button/apply-tags-button.component.ts create mode 100644 src/app/teacher/color-chooser/color-chooser.component.html create mode 100644 src/app/teacher/color-chooser/color-chooser.component.scss create mode 100644 src/app/teacher/color-chooser/color-chooser.component.spec.ts create mode 100644 src/app/teacher/color-chooser/color-chooser.component.ts create mode 100644 src/app/teacher/edit-tag/edit-tag.component.html create mode 100644 src/app/teacher/edit-tag/edit-tag.component.scss create mode 100644 src/app/teacher/edit-tag/edit-tag.component.spec.ts create mode 100644 src/app/teacher/edit-tag/edit-tag.component.ts create mode 100644 src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.html create mode 100644 src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.scss create mode 100644 src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.spec.ts create mode 100644 src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.ts create mode 100644 src/app/teacher/select-tags/select-tags.component.html create mode 100644 src/app/teacher/select-tags/select-tags.component.scss create mode 100644 src/app/teacher/select-tags/select-tags.component.spec.ts create mode 100644 src/app/teacher/select-tags/select-tags.component.ts create mode 100644 src/app/teacher/tag/tag.component.html create mode 100644 src/app/teacher/tag/tag.component.scss create mode 100644 src/app/teacher/tag/tag.component.spec.ts create mode 100644 src/app/teacher/tag/tag.component.ts create mode 100644 src/app/teacher/unit-tags/unit-tags.component.html create mode 100644 src/app/teacher/unit-tags/unit-tags.component.scss create mode 100644 src/app/teacher/unit-tags/unit-tags.component.spec.ts create mode 100644 src/app/teacher/unit-tags/unit-tags.component.ts create mode 100644 src/assets/wise5/services/colorService.ts create mode 100644 src/assets/wise5/services/projectTagService.ts diff --git a/package-lock.json b/package-lock.json index f7cc254a553..b3b49a24dca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@aws-sdk/client-transcribe-streaming": "^3.310.0", "@aws-sdk/client-translate": "^3.310.0", "@aws-sdk/credential-provider-cognito-identity": "^3.310.0", + "@ng-select/ng-select": "^12.0.7", "@ng-web-apis/common": "^2.0.1", "@ng-web-apis/intersection-observer": "^3.0.0", "@stomp/rx-stomp": "^1.1.4", @@ -38,6 +39,7 @@ "angular-password-strength-meter": "^11.0.0", "buffer": "^6.0.3", "canvg": "^2.0.0", + "colorjs.io": "^0.5.0", "compute-covariance": "^1.0.1", "core-js": "^3.22.0", "dom-autoscroller": "^2.3.4", @@ -6300,6 +6302,23 @@ "tslib": "^2.1.0" } }, + "node_modules/@ng-select/ng-select": { + "version": "12.0.7", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-12.0.7.tgz", + "integrity": "sha512-Eht1zlLP0DJxiXcKnq3aY/EJ8odomgU0hM0BJoPY6oX3XFHndtFtdPxlZfhVtQn+FwyDEh7306rRx6digxVssA==", + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">= 16", + "npm": ">= 8" + }, + "peerDependencies": { + "@angular/common": "^17.0.0-rc.0", + "@angular/core": "^17.0.0-rc.0", + "@angular/forms": "^17.0.0-rc.0" + } + }, "node_modules/@ng-web-apis/common": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-2.2.0.tgz", @@ -11131,6 +11150,11 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/colorjs.io": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.0.tgz", + "integrity": "sha512-qekjTiBLM3F/sXKks/ih5aWaHIGu+Ftel0yKEvmpbKvmxpNOhojKgha5uiWEUOqEpRjC1Tq3nJRT7WgdBOxIGg==" + }, "node_modules/colors": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", diff --git a/package.json b/package.json index e3db88170e5..20b57a64b6d 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@aws-sdk/client-transcribe-streaming": "^3.310.0", "@aws-sdk/client-translate": "^3.310.0", "@aws-sdk/credential-provider-cognito-identity": "^3.310.0", + "@ng-select/ng-select": "^12.0.7", "@ng-web-apis/common": "^2.0.1", "@ng-web-apis/intersection-observer": "^3.0.0", "@stomp/rx-stomp": "^1.1.4", @@ -39,6 +40,7 @@ "angular-password-strength-meter": "^11.0.0", "buffer": "^6.0.3", "canvg": "^2.0.0", + "colorjs.io": "^0.5.0", "compute-covariance": "^1.0.1", "core-js": "^3.22.0", "dom-autoscroller": "^2.3.4", diff --git a/src/app/domain/archiveProjectResponse.ts b/src/app/domain/archiveProjectResponse.ts index 4f3570fe080..60b6f5130e7 100644 --- a/src/app/domain/archiveProjectResponse.ts +++ b/src/app/domain/archiveProjectResponse.ts @@ -1,9 +1,13 @@ +import { Tag } from './tag'; + export class ArchiveProjectResponse { archived: boolean; id: number; + tag: Tag; - constructor(id: number, archived: boolean) { + constructor(id: number, archived: boolean, tag: Tag) { this.id = id; this.archived = archived; + this.tag = tag; } } diff --git a/src/app/domain/project.ts b/src/app/domain/project.ts index 255f002ae2b..c50ea39e3d5 100644 --- a/src/app/domain/project.ts +++ b/src/app/domain/project.ts @@ -1,5 +1,6 @@ import { Run } from './run'; import { User } from '../domain/user'; +import { Tag } from './tag'; export class Project { archived: boolean; @@ -17,7 +18,7 @@ export class Project { run: Run; sharedOwners: User[] = []; selected: boolean; - tags: string[]; + tags: Tag[]; thumbStyle: any; uri: String; wiseVersion: number; @@ -93,16 +94,25 @@ export class Project { return metadata; } - hasTag(tag: string): boolean { - return this.tags.includes(tag); + hasTagWithText(tagText: string): boolean { + return this.tags.some((tag: Tag) => tag.text === tagText); } - updateArchivedStatus(archived: boolean): void { + hasTag(tag: Tag): boolean { + return this.tags.some((projectTag: Tag) => projectTag.id === tag.id); + } + + updateArchivedStatus(archived: boolean, tag: Tag): void { this.archived = archived; - if (archived) { - this.tags.push('archived'); - } else { - this.tags.splice(this.tags.indexOf('archived'), 1); - } + archived ? this.addTag(tag) : this.removeTag(tag); + } + + addTag(tag: Tag): void { + this.tags.push(tag); + this.tags.sort((a, b) => a.text.toLowerCase().localeCompare(b.text.toLowerCase())); + } + + removeTag(tag: Tag): void { + this.tags = this.tags.filter((projectTag: Tag) => projectTag.id !== tag.id); } } diff --git a/src/app/domain/projectAndTagsResponse.ts b/src/app/domain/projectAndTagsResponse.ts new file mode 100644 index 00000000000..4217b984a2c --- /dev/null +++ b/src/app/domain/projectAndTagsResponse.ts @@ -0,0 +1,6 @@ +import { Tag } from './tag'; + +export interface ProjectAndTagsResponse { + projectId: number; + tags: Tag[]; +} diff --git a/src/app/domain/tag.ts b/src/app/domain/tag.ts new file mode 100644 index 00000000000..a76f803c83a --- /dev/null +++ b/src/app/domain/tag.ts @@ -0,0 +1,7 @@ +export interface Tag { + color: string; + id: number; + numProjectsWithTag?: number; + selected?: boolean; + text: string; +} diff --git a/src/app/modules/library/community-library/community-library.component.html b/src/app/modules/library/community-library/community-library.component.html index 154b14457e8..7a692fb0044 100644 --- a/src/app/modules/library/community-library/community-library.component.html +++ b/src/app/modules/library/community-library/community-library.component.html @@ -19,7 +19,7 @@ fxFlex.sm="50" fxFlex.md="33" fxFlex.gt-md="25" - > + />
No community designed units found. diff --git a/src/app/modules/library/home-page-project-library/home-page-project-library.component.html b/src/app/modules/library/home-page-project-library/home-page-project-library.component.html index 15b755ddca3..b079fdf51d9 100644 --- a/src/app/modules/library/home-page-project-library/home-page-project-library.component.html +++ b/src/app/modules/library/home-page-project-library/home-page-project-library.component.html @@ -1,14 +1,14 @@
-
- +
+
Explore suggested WISE curricula for the given grade levels or search for specific units that address your needs.
- +
diff --git a/src/app/modules/library/home-page-project-library/home-page-project-library.component.scss b/src/app/modules/library/home-page-project-library/home-page-project-library.component.scss index 463ca918a0c..e1f2fafa0f0 100644 --- a/src/app/modules/library/home-page-project-library/home-page-project-library.component.scss +++ b/src/app/modules/library/home-page-project-library/home-page-project-library.component.scss @@ -18,3 +18,8 @@ max-width: none; } +.content-block { + padding: 16px; + border-radius: 0; +} + diff --git a/src/app/modules/library/home-page-project-library/home-page-project-library.component.ts b/src/app/modules/library/home-page-project-library/home-page-project-library.component.ts index ca796e1f9ce..c74264aa10b 100644 --- a/src/app/modules/library/home-page-project-library/home-page-project-library.component.ts +++ b/src/app/modules/library/home-page-project-library/home-page-project-library.component.ts @@ -3,8 +3,8 @@ import { LibraryService } from '../../../services/library.service'; @Component({ selector: 'app-home-page-project-library', - templateUrl: './home-page-project-library.component.html', - styleUrls: ['./home-page-project-library.component.scss', '../library/library.component.scss'] + styleUrls: ['./home-page-project-library.component.scss', '../library/library.component.scss'], + templateUrl: './home-page-project-library.component.html' }) export class HomePageProjectLibraryComponent { constructor(private libraryService: LibraryService) { diff --git a/src/app/modules/library/library-filters/library-filters.component.html b/src/app/modules/library/library-filters/library-filters.component.html index 5a6ef80e0fc..ba65750b5a4 100644 --- a/src/app/modules/library/library-filters/library-filters.component.html +++ b/src/app/modules/library/library-filters/library-filters.component.html @@ -32,12 +32,7 @@ (NGSS). Reset
-
+
+ />
@@ -75,6 +75,7 @@ >{{ isLast ? '' : ' / ' }}

+
; let harness: LibraryProjectMenuHarness; @@ -108,7 +109,7 @@ function showsArchiveButton() { function showsRestoreButton() { describe('project has archived tag', () => { beforeEach(() => { - component.project.tags = ['archived']; + component.project.tags = [archivedTag]; component.ngOnInit(); }); it('shows restore button', async () => { diff --git a/src/app/modules/library/library-project-menu/library-project-menu.component.ts b/src/app/modules/library/library-project-menu/library-project-menu.component.ts index 7868496e2cb..56305d4e1ee 100644 --- a/src/app/modules/library/library-project-menu/library-project-menu.component.ts +++ b/src/app/modules/library/library-project-menu/library-project-menu.component.ts @@ -10,8 +10,8 @@ import { ArchiveProjectService } from '../../../services/archive-project.service @Component({ selector: 'app-library-project-menu', - templateUrl: './library-project-menu.component.html', - styleUrls: ['./library-project-menu.component.scss'] + styleUrl: './library-project-menu.component.scss', + templateUrl: './library-project-menu.component.html' }) export class LibraryProjectMenuComponent { @Input() @@ -40,7 +40,7 @@ export class LibraryProjectMenuComponent { this.isCanShare = this.isOwner() && !this.isRun; this.editLink = `${this.configService.getContextPath()}/teacher/edit/unit/${this.project.id}`; this.isChild = this.project.isChild(); - this.archived = this.project.hasTag('archived'); + this.archived = this.project.hasTagWithText('archived'); } isOwner() { diff --git a/src/app/modules/library/library-project/library-project.component.html b/src/app/modules/library/library-project/library-project.component.html index 69946a8411b..3845f319eb6 100644 --- a/src/app/modules/library/library-project/library-project.component.html +++ b/src/app/modules/library/library-project/library-project.component.html @@ -22,7 +22,7 @@ (click)="selectProject($event)" >
- +
Shared by {{ project.owner.displayName }}

+
+ +
diff --git a/src/app/modules/library/library-project/library-project.component.scss b/src/app/modules/library/library-project/library-project.component.scss index dece9d4baab..2713de8647b 100644 --- a/src/app/modules/library/library-project/library-project.component.scss +++ b/src/app/modules/library/library-project/library-project.component.scss @@ -85,6 +85,11 @@ $config: mat.define-typography-config(); .mat-mdc-checkbox.library-project__checkbox { position: absolute; top: 0; - z-index: 1; + z-index: 100; border-end-end-radius: $button-border-radius; +} + +.tags { + max-height: 86px; + overflow-y: auto; } \ No newline at end of file diff --git a/src/app/modules/library/library-project/library-project.component.spec.ts b/src/app/modules/library/library-project/library-project.component.spec.ts index 99341300534..ec406902ce0 100644 --- a/src/app/modules/library/library-project/library-project.component.spec.ts +++ b/src/app/modules/library/library-project/library-project.component.spec.ts @@ -3,22 +3,26 @@ import { LibraryProjectComponent } from './library-project.component'; import { LibraryProject } from '../libraryProject'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { MatDialogModule } from '@angular/material/dialog'; -import { Router } from '@angular/router'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterTestingModule } from '@angular/router/testing'; import { OverlayModule } from '@angular/cdk/overlay'; +import { ProjectTagService } from '../../../../assets/wise5/services/projectTagService'; +import { provideRouter } from '@angular/router'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('LibraryProjectComponent', () => { let component: LibraryProjectComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [LibraryProjectComponent], - imports: [BrowserAnimationsModule, RouterTestingModule, OverlayModule, MatDialogModule], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [LibraryProjectComponent], + imports: [BrowserAnimationsModule, HttpClientTestingModule, MatDialogModule, OverlayModule], + providers: [ProjectTagService, provideRouter([])], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + }) + ); beforeEach(() => { fixture = TestBed.createComponent(LibraryProjectComponent); diff --git a/src/app/modules/library/library-project/library-project.component.ts b/src/app/modules/library/library-project/library-project.component.ts index 887db754a84..f1b4c3d29e0 100644 --- a/src/app/modules/library/library-project/library-project.component.ts +++ b/src/app/modules/library/library-project/library-project.component.ts @@ -5,25 +5,33 @@ import { LibraryProject } from '../libraryProject'; import { LibraryProjectDetailsComponent } from '../library-project-details/library-project-details.component'; import { flash } from '../../../animations'; import { ProjectSelectionEvent } from '../../../domain/projectSelectionEvent'; +import { Subscription } from 'rxjs'; +import { ProjectTagService } from '../../../../assets/wise5/services/projectTagService'; +import { Tag } from '../../../domain/tag'; @Component({ - selector: 'app-library-project', - templateUrl: './library-project.component.html', - styleUrls: ['./library-project.component.scss'], + animations: [flash], encapsulation: ViewEncapsulation.None, - animations: [flash] + selector: 'app-library-project', + styleUrl: './library-project.component.scss', + templateUrl: './library-project.component.html' }) export class LibraryProjectComponent implements OnInit { + animateDelay: string = '0s'; + animateDuration: string = '0s'; @Input() checked: boolean = false; @Input() myUnit: boolean = false; @Input() project: LibraryProject = new LibraryProject(); @Output() - projectSelectionEvent: EventEmitter = new EventEmitter(); - - animateDuration: string = '0s'; - animateDelay: string = '0s'; + projectSelectionEvent: EventEmitter = + new EventEmitter(); + private subscriptions: Subscription = new Subscription(); - constructor(public dialog: MatDialog, private sanitizer: DomSanitizer) {} + constructor( + private dialog: MatDialog, + private projectTagService: ProjectTagService, + private sanitizer: DomSanitizer + ) {} ngOnInit() { this.project.thumbStyle = this.getThumbStyle(this.project.projectThumb); @@ -34,6 +42,35 @@ export class LibraryProjectComponent implements OnInit { this.project.isHighlighted = false; }, 7000); } + this.subscribeToTagUpdated(); + this.subscribeToTagDeleted(); + } + + private subscribeToTagUpdated(): void { + this.subscriptions.add( + this.projectTagService.tagUpdated$.subscribe((updatedTag: Tag) => { + const projectTag = this.project.tags.find((tag: Tag) => tag.id === updatedTag.id); + if (projectTag != null) { + projectTag.text = updatedTag.text; + projectTag.color = updatedTag.color; + this.projectTagService.sortTags(this.project.tags); + } + }) + ); + } + + private subscribeToTagDeleted(): void { + this.subscriptions.add( + this.projectTagService.tagDeleted$.subscribe((tag: Tag) => { + if (this.project.hasTag(tag)) { + this.project.removeTag(tag); + } + }) + ); + } + + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); } /** diff --git a/src/app/modules/library/library.module.ts b/src/app/modules/library/library.module.ts index f0523d42c43..f1dd58dad72 100644 --- a/src/app/modules/library/library.module.ts +++ b/src/app/modules/library/library.module.ts @@ -48,10 +48,14 @@ import { ShareProjectDialogComponent } from './share-project-dialog/share-projec import { CopyProjectDialogComponent } from './copy-project-dialog/copy-project-dialog.component'; import { LibraryPaginatorIntl } from './libraryPaginatorIntl'; import { DiscourseCategoryActivityComponent } from './discourse-category-activity/discourse-category-activity.component'; -import { SelectAllItemsCheckboxComponent } from './select-all-items-checkbox/select-all-items-checkbox.component'; import { ArchiveProjectsButtonComponent } from '../../teacher/archive-projects-button/archive-projects-button.component'; import { SearchBarComponent } from '../shared/search-bar/search-bar.component'; +import { SelectAllItemsCheckboxComponent } from './select-all-items-checkbox/select-all-items-checkbox.component'; +import { ApplyTagsButtonComponent } from '../../teacher/apply-tags-button/apply-tags-button.component'; +import { SelectTagsComponent } from '../../teacher/select-tags/select-tags.component'; +import { MatChipsModule } from '@angular/material/chips'; import { SelectMenuComponent } from '../shared/select-menu/select-menu.component'; +import { UnitTagsComponent } from '../../teacher/unit-tags/unit-tags.component'; const materialModules = [ MatAutocompleteModule, @@ -59,6 +63,7 @@ const materialModules = [ MatButtonModule, MatCardModule, MatCheckboxModule, + MatChipsModule, MatDialogModule, MatDividerModule, MatExpansionModule, @@ -76,6 +81,7 @@ const materialModules = [ @NgModule({ imports: [ + ApplyTagsButtonComponent, ArchiveProjectsButtonComponent, CommonModule, DiscourseCategoryActivityComponent, @@ -86,9 +92,12 @@ const materialModules = [ RouterModule, materialModules, SearchBarComponent, + SelectAllItemsCheckboxComponent, SelectMenuComponent, + SelectTagsComponent, SharedModule, - TimelineModule + TimelineModule, + UnitTagsComponent ], declarations: [ LibraryGroupThumbsComponent, @@ -104,7 +113,6 @@ const materialModules = [ CommunityLibraryDetailsComponent, PersonalLibraryComponent, PersonalLibraryDetailsComponent, - SelectAllItemsCheckboxComponent, ShareProjectDialogComponent, CopyProjectDialogComponent ], @@ -112,6 +120,7 @@ const materialModules = [ HomePageProjectLibraryComponent, ReactiveFormsModule, TeacherProjectLibraryComponent, + UnitTagsComponent, materialModules ], providers: [LibraryService, { provide: MatPaginatorIntl, useClass: LibraryPaginatorIntl }] diff --git a/src/app/modules/library/library/library.component.scss b/src/app/modules/library/library/library.component.scss index 59bd460b4e2..2dcf4fe592f 100644 --- a/src/app/modules/library/library/library.component.scss +++ b/src/app/modules/library/library/library.component.scss @@ -8,7 +8,6 @@ .library { text-align: left; border-radius: $card-border-radius; - overflow: hidden; h2 { font-weight: 400; diff --git a/src/app/modules/library/official-library/official-library.component.html b/src/app/modules/library/official-library/official-library.component.html index 2cbc54b73d1..ab30bf0359f 100644 --- a/src/app/modules/library/official-library/official-library.component.html +++ b/src/app/modules/library/official-library/official-library.component.html @@ -27,7 +27,7 @@ fxFlex.md="{{ isSplitScreen ? 50 : 33 }}" fxFlex.gt-md="{{ isSplitScreen ? 33 : 25 }}" > - +
diff --git a/src/app/modules/library/personal-library/personal-library.component.html b/src/app/modules/library/personal-library/personal-library.component.html index 51e2f7dc4dc..7299f0f3619 100644 --- a/src/app/modules/library/personal-library/personal-library.component.html +++ b/src/app/modules/library/personal-library/personal-library.component.html @@ -2,8 +2,9 @@
What are My Units? + - View + View Active Archived @@ -14,7 +15,7 @@
+ @if (selectedProjects().length > 0) { + + }
+ />
No owned or shared units found. diff --git a/src/app/modules/library/personal-library/personal-library.component.spec.ts b/src/app/modules/library/personal-library/personal-library.component.spec.ts index 8bf70b05959..d220771a067 100644 --- a/src/app/modules/library/personal-library/personal-library.component.spec.ts +++ b/src/app/modules/library/personal-library/personal-library.component.spec.ts @@ -24,7 +24,9 @@ import { of } from 'rxjs'; import { MatPaginatorModule } from '@angular/material/paginator'; import { ArchiveProjectsButtonComponent } from '../../../teacher/archive-projects-button/archive-projects-button.component'; import { HttpClient } from '@angular/common/http'; +import { ProjectTagService } from '../../../../assets/wise5/services/projectTagService'; +const archivedTag = { id: 1, text: 'archived', color: null }; let archiveProjectService: ArchiveProjectService; let component: PersonalLibraryComponent; let fixture: ComponentFixture; @@ -52,14 +54,11 @@ describe('PersonalLibraryComponent', () => { MatPaginatorModule, MatSelectModule, MatSnackBarModule, - OverlayModule - ], - declarations: [ - LibraryProjectComponent, - PersonalLibraryComponent, + OverlayModule, SelectAllItemsCheckboxComponent ], - providers: [ArchiveProjectService, LibraryService], + declarations: [LibraryProjectComponent, PersonalLibraryComponent], + providers: [ArchiveProjectService, LibraryService, ProjectTagService], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); }) @@ -92,12 +91,12 @@ function setUpFiveProjects() { new LibraryProject({ id: projectId1, metadata: { title: 'Hello' }, - tags: ['archived'] + tags: [archivedTag] }), new LibraryProject({ id: projectId2, metadata: { title: 'Hello World' }, - tags: ['archived'] + tags: [archivedTag] }), new LibraryProject({ id: projectId3, @@ -150,7 +149,10 @@ function archiveMultipleProjects() { it('archives multiple projects', async () => { await harness.selectProjects([projectId4, projectId3]); spyOn(http, 'put').and.returnValue( - of([new ArchiveProjectResponse(4, true), new ArchiveProjectResponse(3, true)]) + of([ + new ArchiveProjectResponse(4, true, archivedTag), + new ArchiveProjectResponse(3, true, archivedTag) + ]) ); await (await harness.getArchiveButton()).click(); expect(await harness.getProjectIdsInView()).toEqual([projectId5]); @@ -166,7 +168,10 @@ function restoreMultipleProjects() { await harness.showArchivedView(); await harness.selectProjects([projectId2, projectId1]); spyOn(http, 'delete').and.returnValue( - of([new ArchiveProjectResponse(2, false), new ArchiveProjectResponse(1, false)]) + of([ + new ArchiveProjectResponse(2, false, archivedTag), + new ArchiveProjectResponse(1, false, archivedTag) + ]) ); await (await harness.getUnarchiveButton()).click(); expect(await harness.getProjectIdsInView()).toEqual([]); diff --git a/src/app/modules/library/personal-library/personal-library.component.ts b/src/app/modules/library/personal-library/personal-library.component.ts index 7945841daa4..c62e8e44f42 100644 --- a/src/app/modules/library/personal-library/personal-library.component.ts +++ b/src/app/modules/library/personal-library/personal-library.component.ts @@ -7,11 +7,13 @@ import { ProjectFilterValues } from '../../../domain/projectFilterValues'; import { ArchiveProjectService } from '../../../services/archive-project.service'; import { PageEvent } from '@angular/material/paginator'; import { ProjectSelectionEvent } from '../../../domain/projectSelectionEvent'; +import { Tag } from '../../../domain/tag'; +import { Project } from '../../../domain/project'; @Component({ selector: 'app-personal-library', - templateUrl: './personal-library.component.html', - styleUrls: ['./personal-library.component.scss'] + styleUrl: './personal-library.component.scss', + templateUrl: './personal-library.component.html' }) export class PersonalLibraryComponent extends LibraryComponent { filteredProjects: LibraryProject[] = []; @@ -25,8 +27,9 @@ export class PersonalLibraryComponent extends LibraryComponent { }, {}) ); projects: LibraryProject[] = []; - protected projectsLabel: string = $localize`units`; + protected projectsLabel: string = $localize`Select all units`; protected selectedProjects: WritableSignal = signal([]); + protected selectedTags: Tag[] = []; protected sharedProjects: LibraryProject[] = []; protected showArchivedView: boolean = false; @@ -113,8 +116,13 @@ export class PersonalLibraryComponent extends LibraryComponent { public filterUpdated(filterValues: ProjectFilterValues = null): void { super.filterUpdated(filterValues); this.filteredProjects = this.filteredProjects.filter( - (project) => project.hasTag('archived') == this.showArchivedView + (project) => project.hasTagWithText('archived') == this.showArchivedView ); + if (this.selectedTags.length > 0) { + this.filteredProjects = this.filteredProjects.filter((project: Project) => + this.selectedTags.some((tag: Tag) => project.hasTag(tag)) + ); + } this.numProjectsInView = this.getProjectsInView().length; this.unselectAllProjects(); } @@ -130,16 +138,14 @@ export class PersonalLibraryComponent extends LibraryComponent { } protected updateSelectedProjects(event: ProjectSelectionEvent): void { + const selectedProjects = this.selectedProjects(); if (event.selected) { - this.selectedProjects.update((selectedProjects) => { - return [...selectedProjects, event.project]; - }); + selectedProjects.push(event.project); } else { - this.selectedProjects.update((selectedProjects) => { - selectedProjects.splice(selectedProjects.indexOf(event.project), 1); - return [...selectedProjects]; - }); + selectedProjects.splice(selectedProjects.indexOf(event.project), 1); } + // create a new array to trigger change detection + this.selectedProjects.set([...selectedProjects]); } protected unselectAllProjects(): void { @@ -162,6 +168,16 @@ export class PersonalLibraryComponent extends LibraryComponent { protected archiveProjects(archive: boolean): void { this.archiveProjectService.archiveProjects(this.selectedProjects(), archive); } + + protected selectTags(tags: Tag[]): void { + this.selectedTags = tags; + this.filterUpdated(); + } + + protected removeTag(tag: Tag): void { + this.selectedTags = this.selectedTags.filter((selectedTag: Tag) => selectedTag.id !== tag.id); + this.filterUpdated(); + } } @Component({ diff --git a/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.html b/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.html index e5451dcd2d7..f518102e148 100644 --- a/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.html +++ b/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.html @@ -1,9 +1,12 @@ +> + + diff --git a/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.scss b/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.scss new file mode 100644 index 00000000000..0b3fa2aa38b --- /dev/null +++ b/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.scss @@ -0,0 +1,3 @@ +.checkbox { + width: 100%; +} diff --git a/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.spec.ts b/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.spec.ts index e484654b25b..3eb2c99fef3 100644 --- a/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.spec.ts +++ b/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.spec.ts @@ -1,7 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SelectAllItemsCheckboxComponent } from './select-all-items-checkbox.component'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatTooltipModule } from '@angular/material/tooltip'; describe('SelectAllItemsCheckboxComponent', () => { let component: SelectAllItemsCheckboxComponent; @@ -9,8 +7,7 @@ describe('SelectAllItemsCheckboxComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ - declarations: [SelectAllItemsCheckboxComponent], - imports: [MatCheckboxModule, MatTooltipModule] + imports: [SelectAllItemsCheckboxComponent] }); fixture = TestBed.createComponent(SelectAllItemsCheckboxComponent); component = fixture.componentInstance; diff --git a/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.ts b/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.ts index 7243ca97307..179c6453f65 100644 --- a/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.ts +++ b/src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.ts @@ -1,20 +1,24 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox'; +import { MAT_CHECKBOX_DEFAULT_OPTIONS, MatCheckboxModule } from '@angular/material/checkbox'; +import { MatTooltipModule } from '@angular/material/tooltip'; type SelectAllItemsStatus = 'none' | 'some' | 'all'; @Component({ + imports: [MatCheckboxModule, MatTooltipModule], + providers: [{ provide: MAT_CHECKBOX_DEFAULT_OPTIONS, useValue: { clickAction: 'noop' } }], selector: 'select-all-items-checkbox', - templateUrl: './select-all-items-checkbox.component.html', - providers: [{ provide: MAT_CHECKBOX_DEFAULT_OPTIONS, useValue: { clickAction: 'noop' } }] + standalone: true, + styleUrl: 'select-all-items-checkbox.component.scss', + templateUrl: './select-all-items-checkbox.component.html' }) export class SelectAllItemsCheckboxComponent { @Output() allSelectedEvent: EventEmitter = new EventEmitter(); - @Input() label: string = $localize`items`; @Output() noneSelectedEvent: EventEmitter = new EventEmitter(); @Input() numAllItems: number; @Input() numSelectedItems: number; protected status: SelectAllItemsStatus; + @Input() tooltip: string; ngOnChanges(): void { if (this.numSelectedItems == 0) { diff --git a/src/app/modules/library/teacher-project-library/teacher-project-library.component.html b/src/app/modules/library/teacher-project-library/teacher-project-library.component.html index 1a14a7599d1..7a9b24fdf64 100644 --- a/src/app/modules/library/teacher-project-library/teacher-project-library.component.html +++ b/src/app/modules/library/teacher-project-library/teacher-project-library.component.html @@ -1,6 +1,6 @@
-
- +
+
- - - + + +
diff --git a/src/app/modules/library/teacher-project-library/teacher-project-library.component.scss b/src/app/modules/library/teacher-project-library/teacher-project-library.component.scss index 340cbe07ed5..9f68504ab1a 100644 --- a/src/app/modules/library/teacher-project-library/teacher-project-library.component.scss +++ b/src/app/modules/library/teacher-project-library/teacher-project-library.component.scss @@ -12,6 +12,12 @@ } } +.filters { + padding: 16px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + .library-teacher__header { h3 { font-weight: 500; diff --git a/src/app/modules/shared/search-bar/search-bar.component.html b/src/app/modules/shared/search-bar/search-bar.component.html index 3aac8ef63b1..12762f5a3d8 100644 --- a/src/app/modules/shared/search-bar/search-bar.component.html +++ b/src/app/modules/shared/search-bar/search-bar.component.html @@ -1,12 +1,17 @@ - + {{ placeholderText }} search - + + + + + @for (tag of filteredTags; track tag.id) { +
+ + + +
+ } + +
Manage Tags
+
diff --git a/src/app/teacher/apply-tags-button/apply-tags-button.component.scss b/src/app/teacher/apply-tags-button/apply-tags-button.component.scss new file mode 100644 index 00000000000..52d7a8062b3 --- /dev/null +++ b/src/app/teacher/apply-tags-button/apply-tags-button.component.scss @@ -0,0 +1,3 @@ +.menu-info { + margin: 16px; +} diff --git a/src/app/teacher/apply-tags-button/apply-tags-button.component.spec.ts b/src/app/teacher/apply-tags-button/apply-tags-button.component.spec.ts new file mode 100644 index 00000000000..3937e833c83 --- /dev/null +++ b/src/app/teacher/apply-tags-button/apply-tags-button.component.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ApplyTagsButtonComponent } from './apply-tags-button.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; + +describe('ApplyTagsButtonComponent', () => { + let component: ApplyTagsButtonComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + ApplyTagsButtonComponent, + BrowserAnimationsModule, + HttpClientTestingModule, + MatSnackBarModule + ], + providers: [ProjectTagService] + }); + fixture = TestBed.createComponent(ApplyTagsButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/teacher/apply-tags-button/apply-tags-button.component.ts b/src/app/teacher/apply-tags-button/apply-tags-button.component.ts new file mode 100644 index 00000000000..5c01a175a68 --- /dev/null +++ b/src/app/teacher/apply-tags-button/apply-tags-button.component.ts @@ -0,0 +1,91 @@ +import { Component, Input } from '@angular/core'; +import { Project } from '../../domain/project'; +import { Tag } from '../../domain/tag'; +import { MAT_CHECKBOX_DEFAULT_OPTIONS } from '@angular/material/checkbox'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatMenuModule } from '@angular/material/menu'; +import { MatIconModule } from '@angular/material/icon'; +import { MatDividerModule } from '@angular/material/divider'; +import { SelectAllItemsCheckboxComponent } from '../../modules/library/select-all-items-checkbox/select-all-items-checkbox.component'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { AbstractTagsMenuComponent } from '../abstract-tags-menu/abstract-tags-menu.component'; +import { SearchBarComponent } from '../../modules/shared/search-bar/search-bar.component'; +import { TagComponent } from '../tag/tag.component'; +import { MatDialog } from '@angular/material/dialog'; +import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { MatSnackBar } from '@angular/material/snack-bar'; + +@Component({ + imports: [ + CommonModule, + MatButtonModule, + MatDividerModule, + MatIconModule, + MatMenuModule, + MatTooltipModule, + SearchBarComponent, + SelectAllItemsCheckboxComponent, + TagComponent + ], + providers: [{ provide: MAT_CHECKBOX_DEFAULT_OPTIONS, useValue: { clickAction: 'noop' } }], + selector: 'apply-tags-button', + standalone: true, + styleUrl: './apply-tags-button.component.scss', + templateUrl: './apply-tags-button.component.html' +}) +export class ApplyTagsButtonComponent extends AbstractTagsMenuComponent { + @Input() selectedProjects: Project[] = []; + + constructor( + dialog: MatDialog, + protected projectTagService: ProjectTagService, + private snackBar: MatSnackBar + ) { + super(dialog, projectTagService); + } + + ngOnChanges(): void { + this.updateAllTagsCheckedValues(); + } + + protected afterRetrieveUserTags(): void { + this.updateAllTagsCheckedValues(); + } + + private updateAllTagsCheckedValues(): void { + for (const tag of this.tags) { + this.updateTagCheckedValue(tag); + } + } + + protected afterNewTag(tag: Tag): void { + this.updateTagCheckedValue(tag); + } + + private updateTagCheckedValue(tag: Tag): void { + tag.numProjectsWithTag = this.selectedProjects.filter((project) => + project.tags.some((projectTag) => projectTag.id === tag.id) + ).length; + } + + protected addTagToProjects(tag: Tag): void { + this.projectTagService.applyTagToProjects(tag, this.selectedProjects).subscribe(() => { + for (const project of this.selectedProjects) { + project.addTag(tag); + } + this.updateTagCheckedValue(tag); + this.snackBar.open($localize`Successfully applied tag`); + }); + } + + protected removeTagFromProjects(tag: Tag): void { + this.projectTagService.removeTagFromProjects(tag, this.selectedProjects).subscribe(() => { + for (const project of this.selectedProjects) { + project.tags = project.tags.filter((projectTag: Tag) => projectTag.id !== tag.id); + } + this.updateTagCheckedValue(tag); + this.snackBar.open($localize`Successfully removed tag`); + }); + } +} diff --git a/src/app/teacher/color-chooser/color-chooser.component.html b/src/app/teacher/color-chooser/color-chooser.component.html new file mode 100644 index 00000000000..7377628800e --- /dev/null +++ b/src/app/teacher/color-chooser/color-chooser.component.html @@ -0,0 +1,13 @@ + + @for (color of colors; track color) { + +
+
+ } +
diff --git a/src/app/teacher/color-chooser/color-chooser.component.scss b/src/app/teacher/color-chooser/color-chooser.component.scss new file mode 100644 index 00000000000..5854076152a --- /dev/null +++ b/src/app/teacher/color-chooser/color-chooser.component.scss @@ -0,0 +1,10 @@ +@import 'style/abstracts/variables'; + +.color-choice { + border-radius: $button-border-radius; + width: 32px; +} + +.color-option-div { + width: 10px; +} diff --git a/src/app/teacher/color-chooser/color-chooser.component.spec.ts b/src/app/teacher/color-chooser/color-chooser.component.spec.ts new file mode 100644 index 00000000000..a5c95929ac0 --- /dev/null +++ b/src/app/teacher/color-chooser/color-chooser.component.spec.ts @@ -0,0 +1,20 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ColorChooserComponent } from './color-chooser.component'; + +describe('ColorChooserComponent', () => { + let component: ColorChooserComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ColorChooserComponent] + }); + fixture = TestBed.createComponent(ColorChooserComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/teacher/color-chooser/color-chooser.component.ts b/src/app/teacher/color-chooser/color-chooser.component.ts new file mode 100644 index 00000000000..71617fc074c --- /dev/null +++ b/src/app/teacher/color-chooser/color-chooser.component.ts @@ -0,0 +1,29 @@ +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatChipsModule } from '@angular/material/chips'; + +@Component({ + imports: [CommonModule, MatChipsModule], + selector: 'color-chooser', + standalone: true, + styleUrl: './color-chooser.component.scss', + templateUrl: './color-chooser.component.html' +}) +export class ColorChooserComponent { + @Output() chooseColorEvent: EventEmitter = new EventEmitter(); + @Input() chosenColor: string; + protected colors: string[] = [ + '#66BB6A', + '#009688', + '#00B0FF', + '#1565C0', + '#673AB7', + '#AB47BC', + '#E91E63', + '#D50000', + '#F57C00', + '#FBC02D', + '#795548', + '#757575' + ]; +} diff --git a/src/app/teacher/edit-tag/edit-tag.component.html b/src/app/teacher/edit-tag/edit-tag.component.html new file mode 100644 index 00000000000..e14be165b6a --- /dev/null +++ b/src/app/teacher/edit-tag/edit-tag.component.html @@ -0,0 +1,40 @@ +
+
+ +
+
+ + Tag Name + + @if (nameControl.errors?.required) { + Required + } @else if (nameControl.errors?.archivedNotAllowed) { + Archived tag not allowed + } @else if (nameControl.errors?.tagAlreadyExists) { + Tag already exists + } + + + Color + + @if (colorControl.errors?.required) { + Required + } + +
+
+
+ + +
+ + +
+
diff --git a/src/app/teacher/edit-tag/edit-tag.component.scss b/src/app/teacher/edit-tag/edit-tag.component.scss new file mode 100644 index 00000000000..17f523357f6 --- /dev/null +++ b/src/app/teacher/edit-tag/edit-tag.component.scss @@ -0,0 +1,9 @@ +@import 'style/abstracts/variables'; + +.tag-preview { + margin-bottom: 12px; +} + +.color-input { + width: 120px; +} diff --git a/src/app/teacher/edit-tag/edit-tag.component.spec.ts b/src/app/teacher/edit-tag/edit-tag.component.spec.ts new file mode 100644 index 00000000000..6d1d76cce87 --- /dev/null +++ b/src/app/teacher/edit-tag/edit-tag.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { EditTagComponent } from './edit-tag.component'; +import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { ColorService } from '../../../assets/wise5/services/colorService'; + +describe('EditTagComponent', () => { + let component: EditTagComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [BrowserAnimationsModule, EditTagComponent, HttpClientTestingModule], + providers: [ColorService, ProjectTagService] + }); + fixture = TestBed.createComponent(EditTagComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/teacher/edit-tag/edit-tag.component.ts b/src/app/teacher/edit-tag/edit-tag.component.ts new file mode 100644 index 00000000000..5265237f18e --- /dev/null +++ b/src/app/teacher/edit-tag/edit-tag.component.ts @@ -0,0 +1,161 @@ +import { CommonModule } from '@angular/common'; +import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { + AbstractControl, + FormControl, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + ValidatorFn, + Validators +} from '@angular/forms'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { Tag } from '../../domain/tag'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { MatButtonModule } from '@angular/material/button'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { ColorChooserComponent } from '../color-chooser/color-chooser.component'; +import { TagComponent } from '../tag/tag.component'; + +@Component({ + imports: [ + ColorChooserComponent, + CommonModule, + FormsModule, + FlexLayoutModule, + MatButtonModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule, + TagComponent + ], + selector: 'edit-tag', + standalone: true, + styleUrl: './edit-tag.component.scss', + templateUrl: './edit-tag.component.html' +}) +export class EditTagComponent { + @Output() closeEvent: EventEmitter = new EventEmitter(); + @ViewChild('nameInput') nameInput: ElementRef; + protected submitLabel: string = $localize`Create`; + @Input() tag: Tag; + private tags: Tag[] = []; + + protected colorControl = new FormControl('', [Validators.required]); + protected nameControl = new FormControl('', [ + Validators.required, + this.createArchivedTagValidator(), + this.createUniqueTagValidator() + ]); + + constructor(private projectTagService: ProjectTagService, private snackBar: MatSnackBar) {} + + ngOnInit(): void { + if (this.tag != null) { + this.nameControl.setValue(this.tag.text); + this.colorControl.setValue(this.tag.color); + this.submitLabel = $localize`Save`; + } + this.projectTagService.retrieveUserTags().subscribe((tags: Tag[]) => { + this.tags = tags; + if (this.tag != null) { + this.tags = this.tags.filter((tag: Tag) => tag.id !== this.tag.id); + } + }); + } + + ngAfterViewInit(): void { + setTimeout(() => { + this.nameInput.nativeElement.focus(); + }); + } + + private createArchivedTagValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (control.value?.toLowerCase() === 'archived') { + control.markAsTouched(); + return { archivedNotAllowed: true }; + } else { + return null; + } + }; + } + + private createUniqueTagValidator(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (this.doesTagAlreadyExist(control.value)) { + control.markAsTouched(); + return { tagAlreadyExists: true }; + } else { + return null; + } + }; + } + private doesTagAlreadyExist(tagText: string): boolean { + return this.tags.some((tag: Tag) => tag.text.toLowerCase() === tagText.toLowerCase().trim()); + } + + protected enterKeyPressed(): void { + if (this.nameControl.valid) { + this.submit(); + } + } + + protected chooseColor(color: string): void { + this.colorControl.setValue(color); + } + + protected submit(): void { + if (this.tag == null) { + this.createTag(); + } else { + this.updateTag(); + } + } + + private getNameValue(): string { + return this.nameControl.value.trim(); + } + + private getColorValue(): string { + return this.colorControl.value.trim(); + } + + private createTag(): void { + this.projectTagService.createTag(this.getNameValue(), this.getColorValue()).subscribe({ + next: () => { + this.snackBar.open($localize`Tag created`); + this.close(); + }, + error: ({ error }) => { + this.handleError(error); + } + }); + } + + private updateTag(): void { + this.tag.text = this.getNameValue(); + this.tag.color = this.getColorValue(); + this.projectTagService.updateTag(this.tag).subscribe({ + next: () => { + this.snackBar.open($localize`Tag updated`); + this.close(); + }, + error: ({ error }) => { + this.handleError(error); + } + }); + } + + private handleError(error: any): void { + if (error.messageCode === 'tagAlreadyExists') { + this.nameControl.setErrors({ tagAlreadyExists: true }); + } + } + + protected close(): void { + this.closeEvent.emit(); + } +} diff --git a/src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.html b/src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.html new file mode 100644 index 00000000000..2ffaeb14d09 --- /dev/null +++ b/src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.html @@ -0,0 +1,58 @@ +

Manage Tags

+ +
+ @if (showCreateTag) { + + } @else { +
+ +
+ } + + @for (tag of tags; track tag.id; let tagIndex = $index; let last = $last) { +
+ +
+ @if (idToEditing[tag.id]) { +
+ +
+ } @else { + + + + + } +
+
+ @if (!last) { + + } +
+ } +
+
+
+ + + diff --git a/src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.scss b/src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.scss new file mode 100644 index 00000000000..1c29f635cc5 --- /dev/null +++ b/src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.scss @@ -0,0 +1,31 @@ +@import 'style/abstracts/variables'; + +.edit-tag { + width: 100%; + padding: 16px 0; +} + +.tag-name { + border-radius: $card-border-radius; + padding: 12px; +} + +.mat-icon { + margin: 0px; +} + +.mdc-list { + padding: 0; +} + +.mdc-list-item { + padding: 0 12px; +} + +.mat-divider { + margin: 0; +} + +.mdc-list-item.mdc-list-item--with-one-line { + height: auto; +} diff --git a/src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.spec.ts b/src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.spec.ts new file mode 100644 index 00000000000..47f9491c32d --- /dev/null +++ b/src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ManageTagsDialogComponent } from './manage-tags-dialog.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { StudentTeacherCommonServicesModule } from '../../student-teacher-common-services.module'; +import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { TeacherService } from '../teacher.service'; +import { ColorService } from '../../../assets/wise5/services/colorService'; + +describe('ManageTagsDialogComponent', () => { + let component: ManageTagsDialogComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + ManageTagsDialogComponent, + StudentTeacherCommonServicesModule + ], + providers: [ColorService, ProjectTagService, TeacherService] + }); + fixture = TestBed.createComponent(ManageTagsDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.ts b/src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.ts new file mode 100644 index 00000000000..f6e1d39fe8c --- /dev/null +++ b/src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.ts @@ -0,0 +1,101 @@ +import { Component, OnInit } from '@angular/core'; +import { Tag } from '../../domain/tag'; +import { Subject, Subscription } from 'rxjs'; +import { CommonModule } from '@angular/common'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import { FormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { MatIconModule } from '@angular/material/icon'; +import { TeacherService } from '../teacher.service'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { EditTagComponent } from '../edit-tag/edit-tag.component'; +import { Project } from '../../domain/project'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatListModule } from '@angular/material/list'; +import { TagComponent } from '../tag/tag.component'; + +@Component({ + imports: [ + CommonModule, + EditTagComponent, + FlexLayoutModule, + FormsModule, + MatButtonModule, + MatDialogModule, + MatDividerModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatListModule, + MatTooltipModule, + TagComponent + ], + selector: 'manage-tags-dialog', + standalone: true, + styleUrl: './manage-tags-dialog.component.scss', + templateUrl: './manage-tags-dialog.component.html' +}) +export class ManageTagsDialogComponent implements OnInit { + protected idToEditing: { [id: string]: boolean } = {}; + protected inputChanged: Subject = new Subject(); + private projects: Project[]; + protected showCreateTag: boolean; + private subscriptions: Subscription = new Subscription(); + protected tags: Tag[] = []; + + constructor( + private projectTagService: ProjectTagService, + private snackBar: MatSnackBar, + private teacherService: TeacherService + ) {} + + ngOnInit(): void { + this.teacherService.getPersonalAndSharedProjects().subscribe((projects) => { + this.projects = projects; + }); + this.subscriptions.add( + this.projectTagService.retrieveUserTags().subscribe((tags: Tag[]) => { + this.tags = tags; + }) + ); + this.subscriptions.add( + this.projectTagService.newTag$.subscribe((tag: Tag) => { + this.tags.push(tag); + this.projectTagService.sortTags(this.tags); + }) + ); + } + + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + } + + protected cancelEditing(tag: Tag): void { + this.idToEditing[tag.id] = false; + } + + protected delete(tag: Tag): void { + if (confirm(this.getDeleteMessage(tag))) { + this.projectTagService.deleteTag(tag).subscribe((tag: Tag) => { + this.tags = this.tags.filter((t) => t.id !== tag.id); + this.snackBar.open($localize`Tag deleted`); + }); + } + } + + private getDeleteMessage(tag: Tag): string { + const numProjectsWithTag = this.projects.filter((project: Project) => + project.tags.some((projectTag: Tag) => projectTag.id === tag.id) + ).length; + const numberOfProjectsMessage = + numProjectsWithTag === 1 + ? $localize`There is ${numProjectsWithTag} unit with this tag.` + : $localize`There are ${numProjectsWithTag} units with this tag.`; + return $localize`Are you sure you want to delete this tag? ` + numberOfProjectsMessage; + } +} diff --git a/src/app/teacher/run-menu/run-menu.component.spec.ts b/src/app/teacher/run-menu/run-menu.component.spec.ts index 647e2aa2078..d05d420938d 100644 --- a/src/app/teacher/run-menu/run-menu.component.spec.ts +++ b/src/app/teacher/run-menu/run-menu.component.spec.ts @@ -67,6 +67,7 @@ export class MockConfigService { } } +const archivedTag = { id: 1, text: 'archived', color: null }; let archiveProjectService: ArchiveProjectService; let component: RunMenuComponent; let fixture: ComponentFixture; @@ -137,20 +138,22 @@ function setRun(archived: boolean): void { function archive() { describe('archive()', () => { it('should archive a run', async () => { - spyOn(http, 'put').and.returnValue(of(new ArchiveProjectResponse(runId1, true))); + spyOn(http, 'put').and.returnValue(of(new ArchiveProjectResponse(runId1, true, archivedTag))); await runMenuHarness.clickArchiveMenuButton(); expect(component.run.project.archived).toEqual(true); const snackBar = await getSnackBar(); expect(await snackBar.getMessage()).toEqual('Successfully archived unit.'); }); it('should archive a run and then undo', async () => { - spyOn(http, 'put').and.returnValue(of(new ArchiveProjectResponse(runId1, true))); + spyOn(http, 'put').and.returnValue(of(new ArchiveProjectResponse(runId1, true, archivedTag))); await runMenuHarness.clickArchiveMenuButton(); expect(component.run.project.archived).toEqual(true); let snackBar = await getSnackBar(); expect(await snackBar.getMessage()).toEqual('Successfully archived unit.'); expect(await snackBar.getActionDescription()).toEqual('Undo'); - spyOn(http, 'delete').and.returnValue(of(new ArchiveProjectResponse(runId1, false))); + spyOn(http, 'delete').and.returnValue( + of(new ArchiveProjectResponse(runId1, false, archivedTag)) + ); await snackBar.dismissWithAction(); expect(component.run.project.archived).toEqual(false); snackBar = await getSnackBar(); @@ -164,7 +167,9 @@ function unarchive() { it('should unarchive a run', async () => { setRun(true); component.ngOnInit(); - spyOn(http, 'delete').and.returnValue(of(new ArchiveProjectResponse(runId1, false))); + spyOn(http, 'delete').and.returnValue( + of(new ArchiveProjectResponse(runId1, false, archivedTag)) + ); await runMenuHarness.clickUnarchiveMenuButton(); expect(component.run.project.archived).toEqual(false); const snackBar = await getSnackBar(); @@ -173,13 +178,15 @@ function unarchive() { it('should unarchive a run and then undo', async () => { setRun(true); component.ngOnInit(); - spyOn(http, 'delete').and.returnValue(of(new ArchiveProjectResponse(runId1, false))); + spyOn(http, 'delete').and.returnValue( + of(new ArchiveProjectResponse(runId1, false, archivedTag)) + ); await runMenuHarness.clickUnarchiveMenuButton(); expect(component.run.project.archived).toEqual(false); let snackBar = await getSnackBar(); expect(await snackBar.getMessage()).toEqual('Successfully restored unit.'); expect(await snackBar.getActionDescription()).toEqual('Undo'); - spyOn(http, 'put').and.returnValue(of(new ArchiveProjectResponse(runId1, true))); + spyOn(http, 'put').and.returnValue(of(new ArchiveProjectResponse(runId1, true, archivedTag))); await snackBar.dismissWithAction(); expect(component.run.project.archived).toEqual(true); snackBar = await getSnackBar(); diff --git a/src/app/teacher/run-menu/run-menu.component.ts b/src/app/teacher/run-menu/run-menu.component.ts index 2bb73952461..e75fb35309a 100644 --- a/src/app/teacher/run-menu/run-menu.component.ts +++ b/src/app/teacher/run-menu/run-menu.component.ts @@ -12,8 +12,8 @@ import { ArchiveProjectService } from '../../services/archive-project.service'; @Component({ selector: 'app-run-menu', - templateUrl: './run-menu.component.html', - styleUrls: ['./run-menu.component.scss'] + styleUrl: './run-menu.component.scss', + templateUrl: './run-menu.component.html' }) export class RunMenuComponent implements OnInit { private editLink: string = ''; diff --git a/src/app/teacher/select-tags/select-tags.component.html b/src/app/teacher/select-tags/select-tags.component.html new file mode 100644 index 00000000000..f22c13de485 --- /dev/null +++ b/src/app/teacher/select-tags/select-tags.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/src/app/teacher/select-tags/select-tags.component.scss b/src/app/teacher/select-tags/select-tags.component.scss new file mode 100644 index 00000000000..975127ea8d0 --- /dev/null +++ b/src/app/teacher/select-tags/select-tags.component.scss @@ -0,0 +1,38 @@ +.ng-select { + padding-bottom: 0; + + &.select-tags { + &.ng-select-multiple .ng-select-container { + .ng-value-container { + .ng-value { + background-color: transparent; + padding: 2px 0; + } + } + } + + .ng-select-container { + width: auto; + } + + div { + box-sizing: unset; + } + } +} + +.ng-dropdown-panel { + &.ng-select-bottom { + top: 100%; + width: auto; + } + + &.ng-select-top { + bottom: calc(100% - .04375em); + width: auto; + } + + .mdc-checkbox .mdc-checkbox__background { + box-sizing: border-box; + } +} diff --git a/src/app/teacher/select-tags/select-tags.component.spec.ts b/src/app/teacher/select-tags/select-tags.component.spec.ts new file mode 100644 index 00000000000..0b1a2491b22 --- /dev/null +++ b/src/app/teacher/select-tags/select-tags.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SelectTagsComponent } from './select-tags.component'; +import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +describe('SelectTagsComponent', () => { + let component: SelectTagsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [BrowserAnimationsModule, HttpClientTestingModule, SelectTagsComponent], + providers: [ProjectTagService] + }); + fixture = TestBed.createComponent(SelectTagsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/teacher/select-tags/select-tags.component.ts b/src/app/teacher/select-tags/select-tags.component.ts new file mode 100644 index 00000000000..e15f4a1fa6e --- /dev/null +++ b/src/app/teacher/select-tags/select-tags.component.ts @@ -0,0 +1,47 @@ +import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatDividerModule } from '@angular/material/divider'; +import { Tag } from '../../domain/tag'; +import { AbstractTagsMenuComponent } from '../abstract-tags-menu/abstract-tags-menu.component'; +import { SearchBarComponent } from '../../modules/shared/search-bar/search-bar.component'; +import { MatSelectModule } from '@angular/material/select'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatButtonModule } from '@angular/material/button'; +import { NgSelectModule } from '@ng-select/ng-select'; +import { TagComponent } from '../tag/tag.component'; +import { FormsModule } from '@angular/forms'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { FlexLayoutModule } from '@angular/flex-layout'; + +@Component({ + imports: [ + CommonModule, + FlexLayoutModule, + FormsModule, + MatButtonModule, + MatCheckboxModule, + MatDividerModule, + MatFormFieldModule, + MatSelectModule, + NgSelectModule, + SearchBarComponent, + TagComponent + ], + encapsulation: ViewEncapsulation.None, + selector: 'select-tags', + standalone: true, + styleUrl: './select-tags.component.scss', + templateUrl: './select-tags.component.html' +}) +export class SelectTagsComponent extends AbstractTagsMenuComponent { + @Input() selectedTags: Tag[] = []; + @Output() selectTagEvent: EventEmitter = new EventEmitter(); + + protected tagSearch(term: string, item: Tag): boolean { + return item.text.toLowerCase().includes(term.toLowerCase()); + } + + protected isSelected(tag: Tag): boolean { + return this.selectedTags.some((selectedTag) => selectedTag.text === tag.text); + } +} diff --git a/src/app/teacher/tag/tag.component.html b/src/app/teacher/tag/tag.component.html new file mode 100644 index 00000000000..0410491d97c --- /dev/null +++ b/src/app/teacher/tag/tag.component.html @@ -0,0 +1,15 @@ + + {{ text }} + @if (allowRemove) { + + + } + diff --git a/src/app/teacher/tag/tag.component.scss b/src/app/teacher/tag/tag.component.scss new file mode 100644 index 00000000000..0fa5e34605c --- /dev/null +++ b/src/app/teacher/tag/tag.component.scss @@ -0,0 +1,27 @@ +.tag { + padding: .3rem .6rem; + border-radius: 1rem; + min-height: 1rem; + font-weight: 400; + display: inline-flex; + align-items: center; +} + +.divider { + margin: 0 .2rem; +} + +.remove-tag { + padding: 0; + height: 16px; + width: 16px; +} + +.remove-icon { + width: 16px; + height: 16px; + font-size: 16px; + position: absolute; + top: 0; + left: 0; +} \ No newline at end of file diff --git a/src/app/teacher/tag/tag.component.spec.ts b/src/app/teacher/tag/tag.component.spec.ts new file mode 100644 index 00000000000..5f0cbffa5ae --- /dev/null +++ b/src/app/teacher/tag/tag.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TagComponent } from './tag.component'; +import { ColorService } from '../../../assets/wise5/services/colorService'; + +describe('TagComponent', () => { + let component: TagComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TagComponent], + providers: [ColorService] + }).compileComponents(); + + fixture = TestBed.createComponent(TagComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/teacher/tag/tag.component.ts b/src/app/teacher/tag/tag.component.ts new file mode 100644 index 00000000000..880f64a604e --- /dev/null +++ b/src/app/teacher/tag/tag.component.ts @@ -0,0 +1,29 @@ +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { ColorService } from '../../../assets/wise5/services/colorService'; +import { CommonModule } from '@angular/common'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDividerModule } from '@angular/material/divider'; + +@Component({ + imports: [CommonModule, MatButtonModule, MatDividerModule, MatIconModule], + selector: 'tag', + standalone: true, + templateUrl: './tag.component.html', + styleUrl: './tag.component.scss' +}) +export class TagComponent implements OnChanges { + @Input() allowRemove: boolean; + @Input() color: string; + @Output() removeTagEvent: EventEmitter = new EventEmitter(); + @Input() text: string; + protected textColor: string; + + constructor(private colorService: ColorService) {} + + ngOnChanges(changes: SimpleChanges): void { + if (changes.color?.currentValue) { + this.textColor = this.colorService.getContrastColor(this.color); + } + } +} diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html index f979f26c9a7..3edf02dcf48 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.html @@ -47,8 +47,8 @@
Shared by {{ run.owner.firstName }} {{ run.owner.lastName }}
+ - ; let http: HttpClient; @@ -64,6 +66,7 @@ describe('TeacherRunListItemComponent', () => { providers: [ ArchiveProjectService, { provide: ConfigService, useClass: MockConfigService }, + ProjectTagService, { provide: TeacherService, useClass: MockTeacherService }, UserService ], @@ -119,7 +122,7 @@ function runArchiveStatusChanged() { describe('run is not archived and archive menu button is clicked', () => { it('should archive run and emit events', async () => { expect(await runListItemHarness.isArchived()).toBeFalse(); - spyOn(http, 'put').and.returnValue(of(new ArchiveProjectResponse(1, true))); + spyOn(http, 'put').and.returnValue(of(new ArchiveProjectResponse(1, true, archivedTag))); await runListItemHarness.clickArchiveMenuButton(); expect(await runListItemHarness.isArchived()).toBeTrue(); }); diff --git a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts index 912f7973e85..e2facc631f8 100644 --- a/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts +++ b/src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts @@ -7,12 +7,15 @@ import { flash } from '../../animations'; import { Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; import { ShareRunCodeDialogComponent } from '../share-run-code-dialog/share-run-code-dialog.component'; +import { Subscription } from 'rxjs'; +import { ProjectTagService } from '../../../assets/wise5/services/projectTagService'; +import { Tag } from '../../domain/tag'; @Component({ + animations: [flash], selector: 'app-teacher-run-list-item', - templateUrl: './teacher-run-list-item.component.html', - styleUrls: ['./teacher-run-list-item.component.scss'], - animations: [flash] + styleUrl: './teacher-run-list-item.component.scss', + templateUrl: './teacher-run-list-item.component.html' }) export class TeacherRunListItemComponent implements OnInit { protected animateDelay: string = '0s'; @@ -22,6 +25,7 @@ export class TeacherRunListItemComponent implements OnInit { @Input() run: TeacherRun = new TeacherRun(); @Output() runArchiveStatusChangedEvent: EventEmitter = new EventEmitter(); @Output() runSelectedStatusChangedEvent: EventEmitter = new EventEmitter(); + private subscriptions: Subscription = new Subscription(); protected thumbStyle: SafeStyle; constructor( @@ -29,10 +33,11 @@ export class TeacherRunListItemComponent implements OnInit { private configService: ConfigService, private router: Router, private elRef: ElementRef, - private dialog: MatDialog + private dialog: MatDialog, + private projectTagService: ProjectTagService ) {} - ngOnInit() { + ngOnInit(): void { this.run.project.thumbStyle = this.getThumbStyle(); this.manageStudentsLink = `${this.configService.getContextPath()}/teacher/manage/unit/${ this.run.id @@ -44,18 +49,47 @@ export class TeacherRunListItemComponent implements OnInit { this.run.highlighted = false; }, 7000); } + this.subscribeToTagUpdated(); + this.subscribeToTagDeleted(); } - ngAfterViewInit() { + private subscribeToTagUpdated(): void { + this.subscriptions.add( + this.projectTagService.tagUpdated$.subscribe((updatedTag: Tag) => { + const projectTag = this.run.project.tags.find((tag: Tag) => tag.id === updatedTag.id); + if (projectTag != null) { + projectTag.text = updatedTag.text; + projectTag.color = updatedTag.color; + this.projectTagService.sortTags(this.run.project.tags); + } + }) + ); + } + + private subscribeToTagDeleted(): void { + this.subscriptions.add( + this.projectTagService.tagDeleted$.subscribe((tag: Tag) => { + if (this.run.project.hasTag(tag)) { + this.run.project.removeTag(tag); + } + }) + ); + } + + ngAfterViewInit(): void { if (this.run.highlighted) { this.elRef.nativeElement.querySelector('mat-card').scrollIntoView(); } } - ngOnChanges() { + ngOnChanges(): void { this.periodsTooltipText = this.getPeriodsTooltipText(); } + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + } + private getThumbStyle() { const DEFAULT_THUMB = 'assets/img/default-picture.svg'; const STYLE = `url(${this.run.project.projectThumb}), url(${DEFAULT_THUMB})`; diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.html b/src/app/teacher/teacher-run-list/teacher-run-list.component.html index 6b7215fc21d..8750ffd76a8 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.html +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.html @@ -1,23 +1,21 @@ -
-
- - - +
+ + +
+ View Active @@ -25,58 +23,71 @@
-
-

- Units found: {{ filteredRuns.length }} - - Archived classroom units: - Active classroom units: - {{ filteredRuns.length }} - - - ({{ completedTotal() }} completed, - - {{ activeTotal() }} running - , {{ scheduledTotal() }} scheduled) - - - | - Clear filters +

+
+ +
+

+ Units found: {{ filteredRuns.length }} - -

+ + Archived classroom units: + Active classroom units: + {{ filteredRuns.length }} + + + ({{ completedTotal() }} completed, + + {{ activeTotal() }} running + , {{ scheduledTotal() }} scheduled) + + + | + Clear filters + +

+
- -
-

Hey there! Looks like you don't have any active classroom units.

-

Browse the "Unit Library" to find titles to use with your students.

-
-
-

Looks like you don't have any archived classroom units.

-
-
- +
+

Hey there! Looks like you don't have any active classroom units.

+

Browse the "Unit Library" to find titles to use with your students.

+
+
+

Looks like you don't have any archived classroom units.

+
+
+ + @if (selectedProjects.length > 0) { + + } +
{ providers: [ ArchiveProjectService, ConfigService, + ProjectTagService, provideRouter([ { path: 'teacher/home/schedule', component: TeacherScheduleStubComponent } ]), @@ -156,7 +160,10 @@ function archiveSelectedRuns(): void { await (await runListHarness.getRunListItem(run3Title)).checkCheckbox(); await (await runListHarness.getRunListItem(run2Title)).checkCheckbox(); spyOn(http, 'put').and.returnValue( - of([new ArchiveProjectResponse(runId3, true), new ArchiveProjectResponse(runId2, true)]) + of([ + new ArchiveProjectResponse(runId3, true, archivedTag), + new ArchiveProjectResponse(runId2, true, archivedTag) + ]) ); await runListHarness.clickArchiveButton(); expect(await runListHarness.getNumRunListItems()).toEqual(1); @@ -171,8 +178,8 @@ function unarchiveSelectedRuns(): void { getRunsSpy.and.returnValue( of([ new TeacherRunStub(runId1, run1StartTime, null, run1Title), - new TeacherRunStub(runId2, run2StartTime, null, run2Title, ['archived']), - new TeacherRunStub(runId3, run3StartTime, null, run3Title, ['archived']) + new TeacherRunStub(runId2, run2StartTime, null, run2Title, [archivedTag]), + new TeacherRunStub(runId3, run3StartTime, null, run3Title, [archivedTag]) ]) ); component.ngOnInit(); @@ -181,7 +188,10 @@ function unarchiveSelectedRuns(): void { await (await runListHarness.getRunListItem(run3Title)).checkCheckbox(); await (await runListHarness.getRunListItem(run2Title)).checkCheckbox(); spyOn(http, 'delete').and.returnValue( - of([new ArchiveProjectResponse(runId3, false), new ArchiveProjectResponse(runId2, false)]) + of([ + new ArchiveProjectResponse(runId3, false, archivedTag), + new ArchiveProjectResponse(runId2, false, archivedTag) + ]) ); await runListHarness.clickUnarchiveButton(); expect(await runListHarness.getNumRunListItems()).toEqual(0); @@ -310,7 +320,7 @@ function archiveRunNoLongerInActiveView() { it('it should no longer be displayed in the active view', async () => { expect(await runListHarness.isShowingArchived()).toBeFalse(); expect(await runListHarness.getNumRunListItems()).toEqual(3); - spyOn(http, 'put').and.returnValue(of(new ArchiveProjectResponse(runId2, true))); + spyOn(http, 'put').and.returnValue(of(new ArchiveProjectResponse(runId2, true, archivedTag))); const runListItem = await runListHarness.getRunListItem(run2Title); await runListItem.clickArchiveMenuButton(); expect(await runListHarness.isShowingArchived()).toBeFalse(); @@ -326,7 +336,7 @@ function unarchiveRunNoLongerInArchivedView() { getRunsSpy.and.returnValue( of([ new TeacherRunStub(runId1, run1StartTime, null, run1Title), - new TeacherRunStub(runId2, run2StartTime, null, run2Title, ['archived']), + new TeacherRunStub(runId2, run2StartTime, null, run2Title, [archivedTag]), new TeacherRunStub(runId3, run3StartTime, null, run3Title) ]) ); @@ -335,7 +345,9 @@ function unarchiveRunNoLongerInArchivedView() { expect(await runListHarness.isShowingArchived()).toBeTrue(); expect(await runListHarness.getNumRunListItems()).toEqual(1); await expectRunTitles([run2Title]); - spyOn(http, 'delete').and.returnValue(of(new ArchiveProjectResponse(runId2, false))); + spyOn(http, 'delete').and.returnValue( + of(new ArchiveProjectResponse(runId2, false, archivedTag)) + ); const runListItem = await runListHarness.getRunListItem(run2Title); await runListItem.clickUnarchiveMenuButton(); expect(await runListHarness.isShowingArchived()).toBeTrue(); diff --git a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts index 9d479215b35..40a738311ba 100644 --- a/src/app/teacher/teacher-run-list/teacher-run-list.component.ts +++ b/src/app/teacher/teacher-run-list/teacher-run-list.component.ts @@ -11,11 +11,12 @@ import { runSpansDays } from '../../../assets/wise5/common/datetime/datetime'; import { SelectRunsOption } from '../select-runs-controls/select-runs-option'; import { sortByRunStartTimeDesc } from '../../domain/run'; import { Project } from '../../domain/project'; +import { Tag } from '../../domain/tag'; @Component({ selector: 'app-teacher-run-list', - templateUrl: './teacher-run-list.component.html', - styleUrls: ['./teacher-run-list.component.scss'] + styleUrl: './teacher-run-list.component.scss', + templateUrl: './teacher-run-list.component.html' }) export class TeacherRunListComponent implements OnInit { private MAX_RECENT_RUNS = 10; @@ -28,6 +29,8 @@ export class TeacherRunListComponent implements OnInit { protected runChangedEventEmitter: EventEmitter = new EventEmitter(); protected runs: TeacherRun[] = []; protected searchValue: string = ''; + protected selectedProjects: Project[] = []; + protected selectedTags: Tag[] = []; protected showAll: boolean = false; protected showArchivedView: boolean = false; private subscriptions: Subscription = new Subscription(); @@ -78,7 +81,7 @@ export class TeacherRunListComponent implements OnInit { this.runs = runs.map((run) => { const teacherRun = new TeacherRun(run); teacherRun.shared = !teacherRun.isOwner(userId); - teacherRun.project.archived = teacherRun.project.tags.includes('archived'); + teacherRun.project.archived = teacherRun.project.hasTagWithText('archived'); return teacherRun; }); this.filteredRuns = this.runs; @@ -153,12 +156,27 @@ export class TeacherRunListComponent implements OnInit { this.performSearchAndFilter(); } + protected selectTags(tags: Tag[]): void { + this.selectedTags = tags; + this.performSearchAndFilter(); + } + + protected removeTag(tag: Tag): void { + this.selectedTags = this.selectedTags.filter((selectedTag: Tag) => selectedTag.id !== tag.id); + this.performSearchAndFilter(); + } + private performFilter(): void { this.filteredRuns = this.filteredRuns.filter( (run: TeacherRun) => (!this.showArchivedView && !run.project.archived) || (this.showArchivedView && run.project.archived) ); + if (this.selectedTags.length > 0) { + this.filteredRuns = this.filteredRuns.filter((run: TeacherRun) => + this.selectedTags.some((tag: Tag) => run.project.hasTag(tag)) + ); + } } private performSearch(searchValue: string): TeacherRun[] { @@ -185,6 +203,7 @@ export class TeacherRunListComponent implements OnInit { protected reset(): void { this.searchValue = ''; this.filterValue = ''; + this.selectedTags = []; this.performSearchAndFilter(); } @@ -237,15 +256,24 @@ export class TeacherRunListComponent implements OnInit { private runSelectedStatusChanged(): void { this.runChangedEventEmitter.emit(); + this.selectedProjects = this.getSelectedProjects(); } protected archiveProjects(archive: boolean): void { this.archiveProjectService.archiveProjects(this.getSelectedProjects(), archive); } - private getSelectedProjects(): Project[] { + protected getSelectedProjects(): Project[] { return this.filteredRuns .filter((run: TeacherRun) => run.project.selected) .map((run: TeacherRun) => run.project); } + + protected getNumActiveRuns(): number { + return this.runs.filter((run: TeacherRun) => !run.project.archived).length; + } + + protected getNumArchivedRuns(): number { + return this.runs.filter((run: TeacherRun) => run.project.archived).length; + } } diff --git a/src/app/teacher/teacher.module.ts b/src/app/teacher/teacher.module.ts index c6f9320236c..2d8b2f725d4 100644 --- a/src/app/teacher/teacher.module.ts +++ b/src/app/teacher/teacher.module.ts @@ -43,6 +43,12 @@ import { MatListModule } from '@angular/material/list'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { SelectRunsControlsModule } from './select-runs-controls/select-runs-controls.module'; import { SearchBarComponent } from '../modules/shared/search-bar/search-bar.component'; +import { ApplyTagsButtonComponent } from './apply-tags-button/apply-tags-button.component'; +import { ProjectTagService } from '../../assets/wise5/services/projectTagService'; +import { SelectTagsComponent } from './select-tags/select-tags.component'; +import { UnitTagsComponent } from './unit-tags/unit-tags.component'; +import { ColorService } from '../../assets/wise5/services/colorService'; +import { NgSelectModule } from '@ng-select/ng-select'; const materialModules = [ MatAutocompleteModule, @@ -67,18 +73,22 @@ const materialModules = [ ]; @NgModule({ imports: [ + ApplyTagsButtonComponent, CommonModule, DiscourseRecentActivityComponent, FlexLayoutModule, FormsModule, LibraryModule, materialModules, + NgSelectModule, SearchBarComponent, - SharedModule, SelectRunsControlsModule, + SelectTagsComponent, + SharedModule, TeacherRoutingModule, TimelineModule, - ClipboardModule + ClipboardModule, + UnitTagsComponent ], declarations: [ CreateRunDialogComponent, @@ -95,7 +105,7 @@ const materialModules = [ TeacherRunListComponent, TeacherRunListItemComponent ], - providers: [AuthGuard], - exports: [TeacherComponent, materialModules] + providers: [AuthGuard, ColorService, ProjectTagService], + exports: [TeacherComponent, UnitTagsComponent, materialModules] }) export class TeacherModule {} diff --git a/src/app/teacher/teacher.service.ts b/src/app/teacher/teacher.service.ts index 0bcada405e6..a691bad2e3d 100644 --- a/src/app/teacher/teacher.service.ts +++ b/src/app/teacher/teacher.service.ts @@ -65,6 +65,10 @@ export class TeacherService { return this.http.get(`${this.lastRunUrl}/${projectId}`); } + getPersonalAndSharedProjects(): Observable { + return this.http.get('/api/project/personal-and-shared'); + } + registerTeacherAccount(teacherUser: Teacher): Observable { const headers = { 'Content-Type': 'application/json' diff --git a/src/app/teacher/unit-tags/unit-tags.component.html b/src/app/teacher/unit-tags/unit-tags.component.html new file mode 100644 index 00000000000..c97bddb2e90 --- /dev/null +++ b/src/app/teacher/unit-tags/unit-tags.component.html @@ -0,0 +1,7 @@ +
+ @for (tag of tags; track tag) { + @if (tag.text !== 'archived') { + + } + } +
diff --git a/src/app/teacher/unit-tags/unit-tags.component.scss b/src/app/teacher/unit-tags/unit-tags.component.scss new file mode 100644 index 00000000000..0dda090c8dd --- /dev/null +++ b/src/app/teacher/unit-tags/unit-tags.component.scss @@ -0,0 +1,4 @@ +tag { + display: inline-block; + margin-top: 8px; +} \ No newline at end of file diff --git a/src/app/teacher/unit-tags/unit-tags.component.spec.ts b/src/app/teacher/unit-tags/unit-tags.component.spec.ts new file mode 100644 index 00000000000..59093c6e52c --- /dev/null +++ b/src/app/teacher/unit-tags/unit-tags.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { UnitTagsComponent } from './unit-tags.component'; + +describe('UnitTagsComponent', () => { + let component: UnitTagsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UnitTagsComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(UnitTagsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/teacher/unit-tags/unit-tags.component.ts b/src/app/teacher/unit-tags/unit-tags.component.ts new file mode 100644 index 00000000000..b7593b32059 --- /dev/null +++ b/src/app/teacher/unit-tags/unit-tags.component.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core'; +import { TagComponent } from '../tag/tag.component'; +import { Tag } from '../../domain/tag'; +import { CommonModule } from '@angular/common'; +import { FlexLayoutModule } from '@angular/flex-layout'; + +@Component({ + selector: 'unit-tags', + standalone: true, + imports: [CommonModule, FlexLayoutModule, TagComponent], + templateUrl: './unit-tags.component.html', + styleUrl: './unit-tags.component.scss' +}) +export class UnitTagsComponent { + @Input() tags: Tag[]; +} diff --git a/src/assets/wise5/services/colorService.ts b/src/assets/wise5/services/colorService.ts new file mode 100644 index 00000000000..e6ff68dca14 --- /dev/null +++ b/src/assets/wise5/services/colorService.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core'; +import Color from 'colorjs.io/dist/color'; + +@Injectable() +export class ColorService { + getContrastColor(color: string): string { + const colorObj = new Color(color); + return colorObj.contrast('#FFFFFF', 'WCAG21') < 4.5 ? '#000000' : '#FFFFFF'; + } +} diff --git a/src/assets/wise5/services/projectTagService.ts b/src/assets/wise5/services/projectTagService.ts new file mode 100644 index 00000000000..4cb343c10e4 --- /dev/null +++ b/src/assets/wise5/services/projectTagService.ts @@ -0,0 +1,67 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Subject, Observable, tap, map } from 'rxjs'; +import { Project } from '../../../app/domain/project'; +import { Tag } from '../../../app/domain/tag'; +import { ProjectAndTagsResponse } from '../../../app/domain/projectAndTagsResponse'; + +@Injectable() +export class ProjectTagService { + private newTagSource: Subject = new Subject(); + public newTag$: Observable = this.newTagSource.asObservable(); + private tagDeletedSource: Subject = new Subject(); + public tagDeleted$: Observable = this.tagDeletedSource.asObservable(); + private tagUpdatedSource: Subject = new Subject(); + public tagUpdated$: Observable = this.tagUpdatedSource.asObservable(); + + constructor(protected http: HttpClient) {} + + retrieveUserTags(): Observable { + return this.http + .get(`/api/user/tags`) + .pipe(map((tags) => tags.filter((tag) => tag.text !== 'archived'))); + } + + applyTagToProjects(tag: Tag, projects: Project[]): Observable { + const projectIds = projects.map((project) => project.id); + return this.http.put(`/api/projects/tag/${tag.id}`, projectIds); + } + + removeTagFromProjects(tag: Tag, projects: Project[]): Observable { + let params = new HttpParams(); + for (const project of projects) { + params = params.append('projectIds', project.id); + } + return this.http.delete(`/api/projects/tag/${tag.id}`, { + params: params + }); + } + + updateTag(tag: Tag): Observable { + return this.http.put(`/api/user/tag/${tag.id}`, tag).pipe( + tap((tag) => { + this.tagUpdatedSource.next(tag); + }) + ); + } + + createTag(tagName: string, color: string): Observable { + return this.http.post(`/api/user/tag`, { text: tagName, color: color }).pipe( + tap((tag: Tag) => { + this.newTagSource.next(tag); + }) + ); + } + + sortTags(tags: Tag[]): Tag[] { + return tags.sort((a, b) => a.text.toLowerCase().localeCompare(b.text.toLowerCase())); + } + + deleteTag(tag: Tag): Observable { + return this.http.delete(`/api/user/tag/${tag.id}`).pipe( + tap((tag: Tag) => { + this.tagDeletedSource.next(tag); + }) + ); + } +} diff --git a/src/assets/wise5/services/tagService.ts b/src/assets/wise5/services/tagService.ts index f6166dfee56..dad97205668 100644 --- a/src/assets/wise5/services/tagService.ts +++ b/src/assets/wise5/services/tagService.ts @@ -8,7 +8,7 @@ import { ProjectService } from './projectService'; @Injectable() export class TagService { - tags: any[] = []; + private tags: any[] = []; constructor( protected http: HttpClient, diff --git a/src/messages.xlf b/src/messages.xlf index a665769d0de..0164e979a15 100644 --- a/src/messages.xlf +++ b/src/messages.xlf @@ -279,7 +279,7 @@ src/app/modules/library/library-project-details/library-project-details.component.html - 131 + 132 src/app/modules/library/official-library/official-library-details.html @@ -289,6 +289,10 @@ src/app/modules/library/personal-library/personal-library-details.html 15 + + src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.html + 57 + src/app/teacher/share-run-code-dialog/share-run-code-dialog.component.html 61 @@ -431,6 +435,10 @@ src/app/teacher/edit-run-warning-dialog/edit-run-warning-dialog.component.html 20 + + src/app/teacher/edit-tag/edit-tag.component.html + 30 + src/app/teacher/list-classroom-courses-dialog/list-classroom-courses-dialog.component.html 52 @@ -693,6 +701,10 @@ src/app/authoring-tool/edit-connected-component-delete-button/edit-connected-component-delete-button.component.html 5 + + src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.html + 39 + src/assets/wise5/authoringTool/project-asset-authoring/project-asset-authoring.component.html 85 @@ -1013,6 +1025,10 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/app/authoring-tool/edit-component-tags/edit-component-tags.component.html 23 + + src/app/teacher/edit-tag/edit-tag.component.html + 7 + Are you sure you want to delete this tag? @@ -2925,7 +2941,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 14 + 11 src/assets/wise5/authoringTool/addNode/choose-simulation/choose-simulation.component.html @@ -5410,21 +5426,21 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Discipline src/app/modules/library/library-filters/library-filters.component.html - 50 + 45 DCI Arrangement src/app/modules/library/library-filters/library-filters.component.html - 67 + 62 Performance Expectation src/app/modules/library/library-filters/library-filters.component.html - 84 + 79 @@ -5495,49 +5511,49 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.This unit is licensed under CC BY-SA. src/app/modules/library/library-project-details/library-project-details.component.html - 87,88 + 88,89 This unit is licensed under CC BY-SA by . src/app/modules/library/library-project-details/library-project-details.component.html - 91,93 + 92,94 This unit is a copy of (used under CC BY-SA). src/app/modules/library/library-project-details/library-project-details.component.html - 98,100 + 99,101 This unit is a copy of by (used under CC BY-SA). src/app/modules/library/library-project-details/library-project-details.component.html - 103,106 + 104,107 More src/app/modules/library/library-project-details/library-project-details.component.html - 117 + 118 View License src/app/modules/library/library-project-details/library-project-details.component.html - 119 + 120 Use with Class src/app/modules/library/library-project-details/library-project-details.component.html - 139 + 140 src/app/teacher/create-run-dialog/create-run-dialog.component.html @@ -5548,7 +5564,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Preview src/app/modules/library/library-project-details/library-project-details.component.html - 143 + 144 src/app/teacher/run-menu/run-menu.component.html @@ -5599,6 +5615,10 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.src/app/modules/library/library-project-menu/library-project-menu.component.html 14 + + src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.html + 29 + src/assets/wise5/authoringTool/peer-grouping/select-peer-grouping-option/select-peer-grouping-option.component.html 9 @@ -5689,7 +5709,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Unit Details src/app/modules/library/library-project/library-project.component.ts - 53 + 90 @@ -5793,54 +5813,54 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.3 + + View + + src/app/modules/library/personal-library/personal-library.component.html + 7 + + Active src/app/modules/library/personal-library/personal-library.component.html - 8 + 9 src/app/teacher/teacher-run-list/teacher-run-list.component.html - 23 + 21 Archived src/app/modules/library/personal-library/personal-library.component.html - 9 + 10 src/app/teacher/teacher-run-list/teacher-run-list.component.html - 24 + 22 selected src/app/modules/library/personal-library/personal-library.component.html - 23,25 + 24,26 No owned or shared units found. src/app/modules/library/personal-library/personal-library.component.html - 56,58 + 60,62 - - units + + Select all units src/app/modules/library/personal-library/personal-library.component.ts - 28 - - - - items - - src/app/modules/library/select-all-items-checkbox/select-all-items-checkbox.component.ts - 13 + 30 @@ -5915,7 +5935,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/modules/shared/search-bar/search-bar.component.html - 11 + 16 src/app/teacher/share-run-dialog/share-run-dialog.component.html @@ -6242,7 +6262,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 104 + 115 @@ -7408,82 +7428,82 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Successfully unit. src/app/services/archive-project.service.ts - 42 + 43 Undo src/app/services/archive-project.service.ts - 42 + 43 src/app/services/archive-project.service.ts - 123 + 124 Action undone. src/app/services/archive-project.service.ts - 56 + 57 src/app/services/archive-project.service.ts - 139 + 140 Error undoing action. src/app/services/archive-project.service.ts - 59 + 60 src/app/services/archive-project.service.ts - 142 + 143 Error archiving unit. src/app/services/archive-project.service.ts - 66 + 67 Error restoring unit. src/app/services/archive-project.service.ts - 66 + 67 Successfully archived unit(s). src/app/services/archive-project.service.ts - 121 + 122 Successfully restored unit(s). src/app/services/archive-project.service.ts - 122 + 123 Error archiving unit(s). src/app/services/archive-project.service.ts - 149 + 150 Error restoring unit(s). src/app/services/archive-project.service.ts - 149 + 150 @@ -7769,7 +7789,7 @@ Click "Cancel" to keep the invalid JSON open so you can fix it. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 31 + 38 @@ -8010,6 +8030,46 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.121,123 + + Apply tags + + src/app/teacher/apply-tags-button/apply-tags-button.component.html + 4 + + + src/app/teacher/apply-tags-button/apply-tags-button.component.html + 11 + + + + Manage Tags + + src/app/teacher/apply-tags-button/apply-tags-button.component.html + 26 + + + src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.html + 1 + + + src/app/teacher/select-tags/select-tags.component.html + 29 + + + + Successfully applied tag + + src/app/teacher/apply-tags-button/apply-tags-button.component.ts + 78 + + + + Successfully removed tag + + src/app/teacher/apply-tags-button/apply-tags-button.component.ts + 88 + + 1. Choose Periods @@ -8232,6 +8292,102 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.25 + + Required + + src/app/teacher/edit-tag/edit-tag.component.html + 10 + + + src/app/teacher/edit-tag/edit-tag.component.html + 21 + + + src/assets/wise5/authoringTool/node/advanced/required-error-label/required-error-label.component.html + 3 + + + + Archived tag not allowed + + src/app/teacher/edit-tag/edit-tag.component.html + 12 + + + + Tag already exists + + src/app/teacher/edit-tag/edit-tag.component.html + 14 + + + + Color + + src/app/teacher/edit-tag/edit-tag.component.html + 18 + + + src/assets/wise5/components/conceptMap/concept-map-authoring/concept-map-authoring.component.html + 249 + + + src/assets/wise5/components/graph/edit-graph-advanced/edit-graph-advanced.component.html + 107 + + + src/assets/wise5/components/graph/edit-graph-advanced/edit-graph-advanced.component.html + 148 + + + src/assets/wise5/components/graph/graph-authoring/graph-authoring.component.html + 191 + + + src/assets/wise5/components/graph/graph-authoring/graph-authoring.component.html + 250 + + + src/assets/wise5/components/graph/graph-authoring/graph-authoring.component.html + 431 + + + src/assets/wise5/components/label/label-authoring/label-authoring.component.html + 147 + + + src/assets/wise5/components/summary/summary-authoring/summary-authoring.component.html + 146 + + + + Create + + src/app/teacher/edit-tag/edit-tag.component.ts + 42 + + + + Save + + src/app/teacher/edit-tag/edit-tag.component.ts + 59 + + + + Tag created + + src/app/teacher/edit-tag/edit-tag.component.ts + 129 + + + + Tag updated + + src/app/teacher/edit-tag/edit-tag.component.ts + 143 + + Share to Google Classroom @@ -8345,6 +8501,41 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.123 + + + New Tag + + src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.html + 8,10 + + + + Tag deleted + + src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.ts + 86 + + + + There is unit with this tag. + + src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.ts + 97 + + + + There are units with this tag. + + src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.ts + 98 + + + + Are you sure you want to delete this tag? + + src/app/teacher/manage-tags-dialog/manage-tags-dialog.component.ts + 99 + + Teams @@ -8708,6 +8899,13 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.28 + + Filter by tag + + src/app/teacher/select-tags/select-tags.component.html + 6 + + Share with Students @@ -8868,6 +9066,13 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.172 + + Remove tag + + src/app/teacher/tag/tag.component.html + 9 + + Teacher home navigation @@ -8987,70 +9192,77 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.Class Periods: src/app/teacher/teacher-run-list-item/teacher-run-list-item.component.ts - 82 + 116 Archived classroom units: src/app/teacher/teacher-run-list/teacher-run-list.component.html - 34 + 41 Active classroom units: src/app/teacher/teacher-run-list/teacher-run-list.component.html - 35 + 42 completed src/app/teacher/teacher-run-list/teacher-run-list.component.html - 40 + 47 running src/app/teacher/teacher-run-list/teacher-run-list.component.html - 43 + 50 scheduled src/app/teacher/teacher-run-list/teacher-run-list.component.html - 46 + 53 Clear filters src/app/teacher/teacher-run-list/teacher-run-list.component.html - 57 + 64 Hey there! Looks like you don't have any active classroom units. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 65 + 72 Browse the "Unit Library" to find titles to use with your students. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 66 + 73 Looks like you don't have any archived classroom units. src/app/teacher/teacher-run-list/teacher-run-list.component.html - 69 + 76 + + + + Unit tags + + src/app/teacher/unit-tags/unit-tags.component.html + 1 @@ -11508,13 +11720,6 @@ Click "Cancel" to keep the invalid JSON open so you can fix it.203 - - Required - - src/assets/wise5/authoringTool/node/advanced/required-error-label/required-error-label.component.html - 3 - - Choose the new location by clicking one of the buttons below @@ -16718,41 +16923,6 @@ Are you ready to receive feedback on this answer? 245 - - Color - - src/assets/wise5/components/conceptMap/concept-map-authoring/concept-map-authoring.component.html - 249 - - - src/assets/wise5/components/graph/edit-graph-advanced/edit-graph-advanced.component.html - 107 - - - src/assets/wise5/components/graph/edit-graph-advanced/edit-graph-advanced.component.html - 148 - - - src/assets/wise5/components/graph/graph-authoring/graph-authoring.component.html - 191 - - - src/assets/wise5/components/graph/graph-authoring/graph-authoring.component.html - 250 - - - src/assets/wise5/components/graph/graph-authoring/graph-authoring.component.html - 431 - - - src/assets/wise5/components/label/label-authoring/label-authoring.component.html - 147 - - - src/assets/wise5/components/summary/summary-authoring/summary-authoring.component.html - 146 - - Are you sure you want to delete this node? diff --git a/src/style/abstracts/_mixins.scss b/src/style/abstracts/_mixins.scss index 388c1bbf6d0..8346c0d37bf 100644 --- a/src/style/abstracts/_mixins.scss +++ b/src/style/abstracts/_mixins.scss @@ -63,6 +63,10 @@ } } + .mat-mdc-standard-chip .mdc-evolution-chip__text-label { + font-size: mat.font-size($wise-typography, 'button'); + } + .mat-divider-horizontal { margin: 16px 0; } @@ -286,6 +290,67 @@ border-color: $value; } } + + // ng-select + .ng-select { + .ng-select-container { + padding: 0 12px; + background-color: rgba(0, 0, 0, 0.04); + border-radius: $card-border-radius; + + .ng-value-container { + padding: .2875em 0; + + .ng-value { + margin: 0.21875em 0.4375em 0.21875em 0; + } + + .ng-input>input { + height: 100%; + } + } + + &.ng-appearance-outline { + min-height: 56px; + + .ng-placeholder { + background-color: transparent; + } + + &:after { + border-radius: $card-border-radius; + border: 0 none; + } + + &:hover:after { + border-bottom: 1px solid; + } + + &.ng-has-value { + .ng-placeholder { + margin-top: 0.4375em; + } + } + } + } + + &.ng-select-focused { + .ng-select-container { + background-color: rgba(0, 0, 0, 0.08); + + &.ng-appearance-outline { + &:after { + border-bottom: 2px solid map.get($colors, 'primary'); + } + + .ng-placeholder { + color: map.get($colors, 'primary'); + margin-top: 0.4375em; + } + } + } + } + } } // Set Angular Material icon size diff --git a/src/style/components/_dialog.scss b/src/style/components/_dialog.scss index ffa66c00af1..ab2dddc6731 100644 --- a/src/style/components/_dialog.scss +++ b/src/style/components/_dialog.scss @@ -35,7 +35,7 @@ mat-dialog-container.mat-mdc-dialog-container { } } - .dialog-content-scroll { + .mat-mdc-dialog-title+.mat-mdc-dialog-content.dialog-content-scroll { padding-top: 16px; padding-bottom: 16px; } diff --git a/src/style/styles.scss b/src/style/styles.scss index a9ff04bd1ed..41996d3e618 100644 --- a/src/style/styles.scss +++ b/src/style/styles.scss @@ -64,6 +64,8 @@ @import 'themes/author'; @import 'themes/monitor'; +@import "~@ng-select/ng-select/themes/material.theme.css"; + // TODO: remove/merge after upgrading apps to Angular @import 'themes/apps'; .app-styles {