From d4d294971028a8ae41ab590e467c3f362336b0b7 Mon Sep 17 00:00:00 2001 From: zara1504 Date: Thu, 27 Mar 2025 14:21:08 +1100 Subject: [PATCH 01/20] chore: remove media-service.coffee and modify doubtfire-angularjs.module.ts --- src/app/common/services/media-service.coffee | 20 -------------------- src/app/doubtfire-angularjs.module.ts | 1 - 2 files changed, 21 deletions(-) delete mode 100644 src/app/common/services/media-service.coffee diff --git a/src/app/common/services/media-service.coffee b/src/app/common/services/media-service.coffee deleted file mode 100644 index 21c6cefa08..0000000000 --- a/src/app/common/services/media-service.coffee +++ /dev/null @@ -1,20 +0,0 @@ -angular.module("doubtfire.common.services.media-service", []) -# -# Services for working with media APIs -# -.factory("mediaService", ($rootScope, $timeout, $sce) -> - mediaService = {} - - mediaService.audioCtx = mediaService.audioCtx? || (new (window.AudioContext || webkitAudioContext)()) - - mediaService.getMimeType = () -> - mimeType = 'audio/webm' - if !MediaRecorder.isTypeSupported(mimeType) - if navigator.userAgent.toLowerCase().indexOf('firefox') > -1 - mimeType = 'audio/ogg' - else - mimeType = '' - mimeType - - mediaService -) diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 868e381213..d3a55dbdd3 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -129,7 +129,6 @@ import 'build/src/app/common/services/listener-service.js'; import 'build/src/app/common/services/outcome-service.js'; import 'build/src/app/common/services/services.js'; import 'build/src/app/common/services/recorder-service.js'; -import 'build/src/app/common/services/media-service.js'; import 'build/src/app/common/services/analytics-service.js'; import 'build/src/app/common/services/date-service.js'; import 'build/src/app/sessions/auth/http-auth-injector.js'; From 2a67173273662ebd7ac9ccdb524f8f56240e2909 Mon Sep 17 00:00:00 2001 From: Jason Vellucci Date: Wed, 10 Sep 2025 12:01:24 +1000 Subject: [PATCH 02/20] Migrate/group set selector (#981) * chore: migrate group-set-selector (WIP) - define and declare new component files - import new component into app - downgrade component * chore: migrate group-set-selector - convert AngularJS template syntax to Angular syntax - finish implementing `group-set-selector.component.ts` - remove redundant Coffeescript file * chore: migrate group-set-selector - finish defining Typescript logic - update parent component to use Angular syntax for child component declaration * chore: cleanup and documentation - remove redundant Coffeescript file - adjust frontend migration todo list * chore: remove redundant file - remove redundant Coffeescript file * fix: attribute and event binding - fix attribute and event binding syntax - remove redundant template file * refactor: two-way binding - remove redundant directive attributes - rename EventEmitter var name in child component * Update README.md * Update README.md * refactor: replace Bootstrap components with Angular Material - replace Bootstrap components with Angular Material components & directives - refactor to use modern Angular structural directives --- README.md | 4 +-- src/app/doubtfire-angular.module.ts | 2 ++ src/app/doubtfire-angularjs.module.ts | 7 ++++- .../group-selector/group-selector.tpl.html | 8 ++--- .../group-set-selector.coffee | 18 ----------- .../group-set-selector.component.html | 10 ++++++ ...scss => group-set-selector.component.scss} | 0 .../group-set-selector.component.ts | 31 +++++++++++++++++++ .../group-set-selector.tpl.html | 6 ---- src/app/groups/groups.coffee | 1 - 10 files changed, 55 insertions(+), 32 deletions(-) delete mode 100644 src/app/groups/group-set-selector/group-set-selector.coffee create mode 100644 src/app/groups/group-set-selector/group-set-selector.component.html rename src/app/groups/group-set-selector/{group-set-selector.scss => group-set-selector.component.scss} (100%) create mode 100644 src/app/groups/group-set-selector/group-set-selector.component.ts delete mode 100644 src/app/groups/group-set-selector/group-set-selector.tpl.html diff --git a/README.md b/README.md index cf49e28efd..0c4c84ab2d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A modern, lightweight learning management system. SUMMARY: -73 / 132 components migrated +74 / 132 components migrated MIGRATED: @@ -91,6 +91,7 @@ MIGRATED: - [x] ./src/app/common/services/alert.service.ts - [x] ./src/app/sessions/states/sign-in/sign-in.component.ts - [x] ./src/app/account/edit-profile/edit-profile.component.ts +- [x] ./src/app/groups/group-set-selector/group-set-selector.component.ts - [x] ./src/app/admin/modals/create-unit-modal/create-unit-modal.coffee TODO: @@ -164,7 +165,6 @@ TODO: - [ ] ./src/app/groups/group-set-manager/group-set-manager.coffee - [ ] ./src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.coffee - [ ] ./src/app/groups/group-member-list/group-member-list.coffee -- [ ] ./src/app/groups/group-set-selector/group-set-selector.coffee - [ ] ./src/app/groups/tutor-group-manager/tutor-group-manager.coffee - [ ] ./src/app/groups/groups.coffee - [ ] ./src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.coffee diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index c714ff0e5a..7290c52090 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -253,6 +253,7 @@ import {ScormExtensionModalComponent} from './common/modals/scorm-extension-moda import { GradeIconComponent } from './common/grade-icon/grade-icon.component'; import { GradeTaskModalComponent } from './tasks/modals/grade-task-modal/grade-task-modal.component'; import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; +import { GroupSetSelectorComponent } from './groups/group-set-selector/group-set-selector.component'; // See https://stackoverflow.com/questions/55721254/how-to-change-mat-datepicker-date-format-to-dd-mm-yyyy-in-simplest-way/58189036#58189036 const MY_DATE_FORMAT = { @@ -389,6 +390,7 @@ import { UnitStudentEnrolmentModalComponent } from './units/modals/unit-student- TaskScormCardComponent, ScormExtensionCommentComponent, ScormExtensionModalComponent, + GroupSetSelectorComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index d9245086aa..d2577e961a 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -86,7 +86,6 @@ import 'build/src/app/groups/group-set-manager/group-set-manager.js'; import 'build/src/app/groups/groups.js'; import 'build/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.js'; import 'build/src/app/groups/group-member-list/group-member-list.js'; -import 'build/src/app/groups/group-set-selector/group-set-selector.js'; import 'build/src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.js'; import 'build/src/app/units/modals/modals.js'; import 'build/src/app/units/units.js'; @@ -222,6 +221,7 @@ import {GradeService} from './common/services/grade.service'; import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component'; import { UnitStudentEnrolmentModalService } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; +import { GroupSetSelectorComponent } from './groups/group-set-selector/group-set-selector.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -493,3 +493,8 @@ DoubtfireAngularJSModule.directive( 'fTaskVisualisation', downgradeComponent({ component: TaskVisualisationComponent }) ); + +DoubtfireAngularJSModule.directive( + 'groupSetSelector', + downgradeComponent({ component: GroupSetSelectorComponent }) +); diff --git a/src/app/groups/group-selector/group-selector.tpl.html b/src/app/groups/group-selector/group-selector.tpl.html index 3e52cc4402..e0a9446a14 100644 --- a/src/app/groups/group-selector/group-selector.tpl.html +++ b/src/app/groups/group-selector/group-selector.tpl.html @@ -3,10 +3,10 @@

Groups for {{selectedGroupSet.name}}

diff --git a/src/app/groups/group-set-selector/group-set-selector.coffee b/src/app/groups/group-set-selector/group-set-selector.coffee deleted file mode 100644 index 62e99b74d0..0000000000 --- a/src/app/groups/group-set-selector/group-set-selector.coffee +++ /dev/null @@ -1,18 +0,0 @@ -angular.module('doubtfire.groups.group-set-selector', []) - -# -# Directive that can switch context of a specific group set -# -.directive('groupSetSelector', -> - restrict: 'E' - templateUrl: 'groups/group-set-selector/group-set-selector.tpl.html' - scope: - unit: '=' - selectedGroupSet: '=ngModel' - onSelectGroupSet: '=onChange' - controller: ($scope) -> - unless $scope.unit? - throw Error "Unit not supplied to group set selector" - $scope.selectGroupSet = -> - $scope.onSelectGroupSet?($scope.selectedGroupSet) -) diff --git a/src/app/groups/group-set-selector/group-set-selector.component.html b/src/app/groups/group-set-selector/group-set-selector.component.html new file mode 100644 index 0000000000..46486cf3f8 --- /dev/null +++ b/src/app/groups/group-set-selector/group-set-selector.component.html @@ -0,0 +1,10 @@ + + + @for (gs of unit.groupSets; track gs.id) { + {{gs.name}} + } + + diff --git a/src/app/groups/group-set-selector/group-set-selector.scss b/src/app/groups/group-set-selector/group-set-selector.component.scss similarity index 100% rename from src/app/groups/group-set-selector/group-set-selector.scss rename to src/app/groups/group-set-selector/group-set-selector.component.scss diff --git a/src/app/groups/group-set-selector/group-set-selector.component.ts b/src/app/groups/group-set-selector/group-set-selector.component.ts new file mode 100644 index 0000000000..60ece272be --- /dev/null +++ b/src/app/groups/group-set-selector/group-set-selector.component.ts @@ -0,0 +1,31 @@ +import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; +import { Unit, GroupSet } from 'src/app/api/models/doubtfire-model'; + +@Component({ + selector: 'group-set-selector', + templateUrl: './group-set-selector.component.html', + styleUrls: ['./group-set-selector.component.scss'] +}) +export class GroupSetSelectorComponent implements OnInit { + @Input() unit: Unit; + @Input() selectedGroupSet: GroupSet; + @Output() selectedGroupSetChange = new EventEmitter(); + + ngOnInit(): void { + if (!this.unit) { + throw new Error('Unit not supplied to group set selector'); + } + } + + /** + * Emits the selected group set and updates the parent component. + * + * Also updates the local state. + * + * @param {GroupSet} groupSet + */ + selectGroupSet(groupSet: GroupSet): void { + this.selectedGroupSet = groupSet; + this.selectedGroupSetChange.emit(this.selectedGroupSet); + } +} diff --git a/src/app/groups/group-set-selector/group-set-selector.tpl.html b/src/app/groups/group-set-selector/group-set-selector.tpl.html deleted file mode 100644 index e735686bb4..0000000000 --- a/src/app/groups/group-set-selector/group-set-selector.tpl.html +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/src/app/groups/groups.coffee b/src/app/groups/groups.coffee index 031080f0b1..391a78d492 100644 --- a/src/app/groups/groups.coffee +++ b/src/app/groups/groups.coffee @@ -3,5 +3,4 @@ angular.module('doubtfire.groups', [ 'doubtfire.groups.group-member-list' 'doubtfire.groups.group-selector' 'doubtfire.groups.group-set-manager' - 'doubtfire.groups.group-set-selector' ]) From 4311f3840744baf9ec202876986bfa55542db3d3 Mon Sep 17 00:00:00 2001 From: Jason Vellucci Date: Thu, 11 Sep 2025 13:13:47 +1000 Subject: [PATCH 03/20] refactor: migrate/date service (#931) * chore: date-service.coffee to TS - add `date.service.ts` file - add `date.service.spec.ts` unit test - downgrade TS module such that it works within AngularJS * chore: unlink date-service.coffee - complete unlinking process of `date-service.coffee` after migrating to Typescript - manual linting/comment formatting * chore: delete redundant coffeescript file - delete `date-service.coffee` file after migration * chore: formatting - format code with prettier --- README.md | 2 +- src/app/common/services/date-service.coffee | 31 --------- src/app/common/services/date.service.spec.ts | 15 +++++ src/app/common/services/date.service.ts | 69 ++++++++++++++++++++ src/app/common/services/services.coffee | 1 - src/app/doubtfire-angularjs.module.ts | 3 +- src/app/home/states/home/home.component.ts | 5 +- 7 files changed, 90 insertions(+), 36 deletions(-) delete mode 100644 src/app/common/services/date-service.coffee create mode 100644 src/app/common/services/date.service.spec.ts create mode 100644 src/app/common/services/date.service.ts diff --git a/README.md b/README.md index 0c4c84ab2d..fc38c9d607 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ MIGRATED: - [x] ./src/app/account/edit-profile/edit-profile.component.ts - [x] ./src/app/groups/group-set-selector/group-set-selector.component.ts - [x] ./src/app/admin/modals/create-unit-modal/create-unit-modal.coffee +- [x] ./src/app/common/services/date.service.ts TODO: @@ -209,7 +210,6 @@ TODO: - [ ] ./src/app/common/file-uploader/file-uploader.coffee - [ ] ./src/app/common/common.coffee - [ ] ./src/app/common/services/grade-service.coffee -- [ ] ./src/app/common/services/date-service.coffee - [ ] ./src/app/common/services/alert-service.coffee - [ ] ./src/app/common/services/media-service.coffee - [ ] ./src/app/common/services/recorder-service.coffee diff --git a/src/app/common/services/date-service.coffee b/src/app/common/services/date-service.coffee deleted file mode 100644 index 10cffe1980..0000000000 --- a/src/app/common/services/date-service.coffee +++ /dev/null @@ -1,31 +0,0 @@ -angular.module("doubtfire.common.services.dates", []) -# -# Services for making alerts -# -.factory("dateService", -> - - dateService = {} - - monthNames = [ - "Jan", "Feb", "Mar", - "Apr", "May", "Jun", "Jul", - "Aug", "Sep", "Oct", - "Nov", "Dec" - ] - - dateService.showDate = (dateValue) -> - if (dateValue?) - date = new Date(dateValue) - "#{monthNames[date.getMonth()]} #{date.getFullYear()}" - else - "-" - - dateService.showFullDate = (dateValue) -> - if (dateValue?) - date = new Date(dateValue) - "#{date.getDate()} #{monthNames[date.getMonth()]} #{date.getFullYear()}" - else - "-" - - dateService -) diff --git a/src/app/common/services/date.service.spec.ts b/src/app/common/services/date.service.spec.ts new file mode 100644 index 0000000000..7421b9d308 --- /dev/null +++ b/src/app/common/services/date.service.spec.ts @@ -0,0 +1,15 @@ +import {TestBed} from '@angular/core/testing'; +import {DateService} from './date.service'; + +describe('DateService', () => { + let service: DateService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DateService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/src/app/common/services/date.service.ts b/src/app/common/services/date.service.ts new file mode 100644 index 0000000000..915fd96fcc --- /dev/null +++ b/src/app/common/services/date.service.ts @@ -0,0 +1,69 @@ +import {Injectable} from '@angular/core'; + +@Injectable({ + providedIn: 'root', // Available throughout the app. +}) +export class DateService { + private monthNames: string[] = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + + constructor() { + // Bind the methods to ensure `this` context is correct + this.showDate = this.showDate.bind(this); + this.showFullDate = this.showFullDate.bind(this); + } + + /** + * Returns a dateString for the passed-in `date`, + * in the format of `MMM-YYYY`. + * + * Note: If you are going to pass in a `dateString`, please ensure it is in + * `ISO 8601` format. + * + * @param {string | Date} dateValue + * + * @returns {string} + */ + showDate(dateValue?: string | Date): string { + if (dateValue) { + const date = new Date(dateValue); + + return `${this.monthNames[date.getMonth()]} ${date.getFullYear()}`; + } else { + return '-'; + } + } + + /** + * Returns a dateString for the passed-in `date`, + * in the format of `D-MM-YYYY`. + * + * Note: If you are going to pass in a `dateString`, please ensure it is in + * `ISO 8601` format. + * + * @param {string | Date} dateValue + * + * @returns {string} + */ + showFullDate(dateValue?: string | Date): string { + if (dateValue) { + const date = new Date(dateValue); + + return `${date.getDate()} ${this.monthNames[date.getMonth()]} ${date.getFullYear()}`; + } else { + return '-'; + } + } +} diff --git a/src/app/common/services/services.coffee b/src/app/common/services/services.coffee index e88e7a7bdc..086ea3536f 100644 --- a/src/app/common/services/services.coffee +++ b/src/app/common/services/services.coffee @@ -1,7 +1,6 @@ angular.module("doubtfire.common.services", [ 'doubtfire.common.services.outcome-service' 'doubtfire.common.services.analytics' - 'doubtfire.common.services.dates' 'doubtfire.common.services.listener' 'doubtfire.common.services.recorder-service' ]) diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index d2577e961a..6ec1d54703 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -121,7 +121,6 @@ import 'build/src/app/common/services/services.js'; import 'build/src/app/common/services/recorder-service.js'; import 'build/src/app/common/services/media-service.js'; import 'build/src/app/common/services/analytics-service.js'; -import 'build/src/app/common/services/date-service.js'; import 'build/src/app/sessions/auth/http-auth-injector.js'; import 'build/src/app/sessions/sessions.js'; import 'build/src/app/errors/errors.js'; @@ -155,6 +154,7 @@ import {TutorialStreamService} from './api/services/tutorial-stream.service'; import {UnitStudentsEditorComponent} from './units/states/edit/directives/unit-students-editor/unit-students-editor.component'; import {CampusService} from './api/services/campus.service'; import {WebcalService} from './api/services/webcal.service'; +import {DateService} from './common/services/date.service'; import {StudentTutorialSelectComponent} from './units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component'; import {StudentCampusSelectComponent} from './units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component'; import {EmojiService} from './common/services/emoji.service'; @@ -298,6 +298,7 @@ DoubtfireAngularJSModule.factory( 'EditProfileService', downgradeInjectable(EditProfileDialogService), ); +DoubtfireAngularJSModule.factory('dateService', downgradeInjectable(DateService)); DoubtfireAngularJSModule.factory('CreateNewUnitModal', downgradeInjectable(CreateNewUnitModal)); DoubtfireAngularJSModule.factory('GradeTaskModal', downgradeInjectable(GradeTaskModalService)); DoubtfireAngularJSModule.factory('UnitStudentEnrolmentModal', downgradeInjectable(UnitStudentEnrolmentModalService)); diff --git a/src/app/home/states/home/home.component.ts b/src/app/home/states/home/home.component.ts index b778d06460..3d6ab49bb8 100644 --- a/src/app/home/states/home/home.component.ts +++ b/src/app/home/states/home/home.component.ts @@ -1,6 +1,7 @@ import {Component, Inject, OnDestroy, OnInit, Renderer2} from '@angular/core'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; -import {analyticsService, dateService} from 'src/app/ajs-upgraded-providers'; +import {analyticsService} from 'src/app/ajs-upgraded-providers'; +import {DateService} from 'src/app/common/services/date.service'; import {UIRouter} from '@uirouter/angular'; import {GlobalStateService, ViewType} from 'src/app/projects/states/index/global-state.service'; import {Project, UnitRole, User, UserService} from 'src/app/api/models/doubtfire-model'; @@ -28,7 +29,7 @@ export class HomeComponent implements OnInit, OnDestroy { private globalState: GlobalStateService, private userService: UserService, @Inject(analyticsService) private AnalyticsService: any, - @Inject(dateService) private DateService: any, + @Inject(DateService) private DateService: DateService, @Inject(UIRouter) private router: UIRouter, ) { // this.renderer.setStyle(document.body, 'background-color', '#f0f2f5'); From 7f8ee94f3d81da299014113a36c16f51d1aa3110 Mon Sep 17 00:00:00 2001 From: Lachlan Date: Thu, 11 Sep 2025 14:42:36 +1000 Subject: [PATCH 04/20] refactor: migrate progress dashboard (#927) * chore: 9.x windows * chore: create new component files * chore: unlink old component + link new component * chore: migrate functionality + update to tailwindcss and material UI * chore: remove old component files * chore: responsive columns for visualisations * Update package-lock.json * Update package.json * Update package.json * Update package-lock.json * chore: update styling to better match original * chore: add custom breakpoint for responsive change * chore: tweak breakpoint for better visualisation visibility * chore: update tutor view functionality + tweak event emitter name --------- Co-authored-by: Boink <40929320+b0ink@users.noreply.github.com> --- src/app/doubtfire-angular.module.ts | 64 +++++++------ src/app/doubtfire-angularjs.module.ts | 91 ++++++++++--------- .../states/dashboard/dashboard.tpl.html | 8 +- .../dashboard/directives/directives.coffee | 1 - .../progress-dashboard.coffee | 44 --------- .../progress-dashboard.component.html | 74 +++++++++++++++ ...scss => progress-dashboard.component.scss} | 0 .../progress-dashboard.component.ts | 67 ++++++++++++++ .../progress-dashboard.tpl.html | 58 ------------ 9 files changed, 230 insertions(+), 177 deletions(-) delete mode 100644 src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.coffee create mode 100644 src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.html rename src/app/projects/states/dashboard/directives/progress-dashboard/{progress-dashboard.scss => progress-dashboard.component.scss} (100%) create mode 100644 src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.ts delete mode 100644 src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 7290c52090..c5903c03a2 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -1,16 +1,17 @@ import {interval} from 'rxjs'; import {take} from 'rxjs/operators'; -import { NgModule, Injector, DoBootstrap } from '@angular/core'; -import { BrowserModule, DomSanitizer, Title } from '@angular/platform-browser'; -import { UpgradeModule } from '@angular/upgrade/static'; -import { AppInjector, setAppInjector } from './app-injector'; -import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NgxChartsModule } from '@swimlane/ngx-charts'; +import {NgModule, Injector, DoBootstrap} from '@angular/core'; +import {BrowserModule, DomSanitizer, Title} from '@angular/platform-browser'; +import {UpgradeModule} from '@angular/upgrade/static'; +import {AppInjector, setAppInjector} from './app-injector'; +import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {NgxChartsModule} from '@swimlane/ngx-charts'; // Lottie animation module // import {LottieModule, LottieCacheModule} from 'ngx-lottie'; +import {ProgressDashboardComponent} from './projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component'; import {provideLottieOptions, LottieComponent} from 'ngx-lottie'; import player from 'lottie-web'; import {ClipboardModule} from '@angular/cdk/clipboard'; @@ -98,12 +99,16 @@ import {ExtensionModalComponent} from './common/modals/extension-modal/extension import {CalendarModalComponent} from './common/modals/calendar-modal/calendar-modal.component'; import {MatRadioModule} from '@angular/material/radio'; import {MatButtonToggleModule} from '@angular/material/button-toggle'; -import {DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatOptionModule} from '@angular/material/core'; +import { + DateAdapter, + MAT_DATE_FORMATS, + MAT_DATE_LOCALE, + MatOptionModule, +} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; -import { DateFnsAdapter } from '@angular/material-date-fns-adapter'; -import { enAU } from 'date-fns/locale'; - +import {DateFnsAdapter} from '@angular/material-date-fns-adapter'; +import {enAU} from 'date-fns/locale'; import {doubtfireStates} from './doubtfire.states'; import {MatTableModule} from '@angular/material/table'; @@ -226,23 +231,23 @@ import { TeachingPeriodUnitImportDialogComponent, TeachingPeriodUnitImportService, } from './admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog'; -import { UnauthorisedComponent } from './errors/states/unauthorised/unauthorised.component'; -import { AcceptEulaComponent } from './eula/accept-eula/accept-eula.component'; -import { TiiActionLogComponent } from './admin/tii-action-log/tii-action-log.component'; -import { TiiActionService } from './api/services/tii-action.service'; -import { FUnitsComponent } from './admin/states/units/units.component'; -import { FUnitTaskListComponent } from './units/task-viewer/directives/unit-task-list/unit-task-list.component'; -import { FTaskDetailsViewComponent } from './units/task-viewer/directives/task-details-view/task-details-view.component'; -import { FTaskSheetViewComponent } from './units/task-viewer/directives/task-sheet-view/task-sheet-view.component'; -import { UnitCodeComponent } from './common/unit-code/unit-code.component'; -import { GradeService } from './common/services/grade.service'; -import { UnitRootStateComponent } from './units/unit-root-state.component'; -import { TaskViewerStateComponent } from './units/task-viewer/task-viewer-state.component'; -import { ProjectRootStateComponent } from './projects/states/project-root-state.component'; -import { ProjectProgressDashboardComponent } from './projects/project-progress-dashboard/project-progress-dashboard.component'; -import { ProgressBurndownChartComponent } from './visualisations/progress-burndown-chart/progressburndownchart.component'; -import { TaskVisualisationComponent } from './visualisations/task-visualisation/taskvisualisation.component'; -import { ChartBaseComponent } from './common/chart-base/chart-base-component/chart-base-component.component'; +import {UnauthorisedComponent} from './errors/states/unauthorised/unauthorised.component'; +import {AcceptEulaComponent} from './eula/accept-eula/accept-eula.component'; +import {TiiActionLogComponent} from './admin/tii-action-log/tii-action-log.component'; +import {TiiActionService} from './api/services/tii-action.service'; +import {FUnitsComponent} from './admin/states/units/units.component'; +import {FUnitTaskListComponent} from './units/task-viewer/directives/unit-task-list/unit-task-list.component'; +import {FTaskDetailsViewComponent} from './units/task-viewer/directives/task-details-view/task-details-view.component'; +import {FTaskSheetViewComponent} from './units/task-viewer/directives/task-sheet-view/task-sheet-view.component'; +import {UnitCodeComponent} from './common/unit-code/unit-code.component'; +import {GradeService} from './common/services/grade.service'; +import {UnitRootStateComponent} from './units/unit-root-state.component'; +import {TaskViewerStateComponent} from './units/task-viewer/task-viewer-state.component'; +import {ProjectRootStateComponent} from './projects/states/project-root-state.component'; +import {ProjectProgressDashboardComponent} from './projects/project-progress-dashboard/project-progress-dashboard.component'; +import {ProgressBurndownChartComponent} from './visualisations/progress-burndown-chart/progressburndownchart.component'; +import {TaskVisualisationComponent} from './visualisations/task-visualisation/taskvisualisation.component'; +import {ChartBaseComponent} from './common/chart-base/chart-base-component/chart-base-component.component'; import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; import {ScormAdapterService} from './api/services/scorm-adapter.service'; import {ScormCommentComponent} from './tasks/task-comments-viewer/scorm-comment/scorm-comment.component'; @@ -267,12 +272,13 @@ const MY_DATE_FORMAT = { monthYearA11yLabel: 'MMMM yyyy', }, }; -import { UnitStudentEnrolmentModalComponent } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component'; +import {UnitStudentEnrolmentModalComponent} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component'; @NgModule({ // Components we declare declarations: [ AlertComponent, + ProgressDashboardComponent, UnitStudentEnrolmentModalComponent, AboutDoubtfireModalContent, TeachingPeriodUnitImportDialogComponent, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 6ec1d54703..04ee74f3e1 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -63,7 +63,6 @@ import 'build/src/app/projects/project-progress-dashboard/project-progress-dashb import 'build/src/app/projects/states/groups/groups.js'; import 'build/src/app/projects/states/feedback/feedback.js'; import 'build/src/app/projects/states/states.js'; -import 'build/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.js'; import 'build/src/app/projects/states/dashboard/directives/student-task-list/student-task-list.js'; import 'build/src/app/projects/states/dashboard/directives/directives.js'; import 'build/src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.js'; @@ -178,41 +177,42 @@ import { UnitService, UserService, } from './api/models/doubtfire-model'; -import { UnauthorisedComponent } from './errors/states/unauthorised/unauthorised.component'; -import { FileDownloaderService } from './common/file-downloader/file-downloader.service'; -import { CheckForUpdateService } from './sessions/service-worker-updater/check-for-update.service'; -import { TaskSubmissionService } from './common/services/task-submission.service'; -import { TaskAssessmentModalService } from './common/modals/task-assessment-modal/task-assessment-modal.service'; -import { TaskSubmissionHistoryComponent } from './tasks/task-submission-history/task-submission-history.component'; -import { HeaderComponent } from './common/header/header.component'; -import { SplashScreenComponent } from './home/splash-screen/splash-screen.component'; -import { GlobalStateService } from './projects/states/index/global-state.service'; -import { TransitionHooksService } from './sessions/transition-hooks.service'; -import { GradeIconComponent } from './common/grade-icon/grade-icon.component'; -import { GradeTaskModalService } from './tasks/modals/grade-task-modal/grade-task-modal.service'; -import { AuthenticationService } from './api/services/authentication.service'; -import { ProjectService } from './api/services/project.service'; -import { ObjectSelectComponent } from './common/obect-select/object-select.component'; -import { TaskDefinitionService } from './api/services/task-definition.service'; -import { EditProfileDialogService } from './common/modals/edit-profile-dialog/edit-profile-dialog.service'; -import { GroupService } from './api/services/group.service'; -import { UserBadgeComponent } from './common/user-badge/user-badge.component'; -import { TaskStatusCardComponent } from './projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component'; -import { TaskDueCardComponent } from './projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component'; -import { FooterComponent } from './common/footer/footer.component'; -import { TaskAssessmentCardComponent } from './projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component'; -import { TaskSubmissionCardComponent } from './projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component'; -import { InboxComponent } from './units/states/tasks/inbox/inbox.component'; -import { TaskDefinitionEditorComponent } from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component'; -import { UnitAnalyticsComponent } from './units/states/analytics/unit-analytics-route.component'; -import { UnitTaskEditorComponent } from './units/states/edit/directives/unit-tasks-editor/unit-task-editor.component'; -import { CreateNewUnitModal } from './admin/modals/create-new-unit-modal/create-new-unit-modal.component'; -import { FUsersComponent } from './admin/states/users/users.component'; -import { FUnitTaskListComponent } from './units/task-viewer/directives/unit-task-list/unit-task-list.component'; -import { FTaskDetailsViewComponent } from './units/task-viewer/directives/task-details-view/task-details-view.component'; -import { FTaskSheetViewComponent } from './units/task-viewer/directives/task-sheet-view/task-sheet-view.component'; -import { ProgressBurndownChartComponent } from './visualisations/progress-burndown-chart/progressburndownchart.component'; -import { TaskVisualisationComponent } from './visualisations/task-visualisation/taskvisualisation.component'; +import {UnauthorisedComponent} from './errors/states/unauthorised/unauthorised.component'; +import {FileDownloaderService} from './common/file-downloader/file-downloader.service'; +import {CheckForUpdateService} from './sessions/service-worker-updater/check-for-update.service'; +import {TaskSubmissionService} from './common/services/task-submission.service'; +import {TaskAssessmentModalService} from './common/modals/task-assessment-modal/task-assessment-modal.service'; +import {TaskSubmissionHistoryComponent} from './tasks/task-submission-history/task-submission-history.component'; +import {HeaderComponent} from './common/header/header.component'; +import {SplashScreenComponent} from './home/splash-screen/splash-screen.component'; +import {GlobalStateService} from './projects/states/index/global-state.service'; +import {TransitionHooksService} from './sessions/transition-hooks.service'; +import {GradeIconComponent} from './common/grade-icon/grade-icon.component'; +import {GradeTaskModalService} from './tasks/modals/grade-task-modal/grade-task-modal.service'; +import {AuthenticationService} from './api/services/authentication.service'; +import {ProjectService} from './api/services/project.service'; +import {ObjectSelectComponent} from './common/obect-select/object-select.component'; +import {TaskDefinitionService} from './api/services/task-definition.service'; +import {EditProfileDialogService} from './common/modals/edit-profile-dialog/edit-profile-dialog.service'; +import {GroupService} from './api/services/group.service'; +import {UserBadgeComponent} from './common/user-badge/user-badge.component'; +import {TaskStatusCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component'; +import {TaskDueCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component'; +import {FooterComponent} from './common/footer/footer.component'; +import {TaskAssessmentCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component'; +import {TaskSubmissionCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component'; +import {InboxComponent} from './units/states/tasks/inbox/inbox.component'; +import {TaskDefinitionEditorComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component'; +import {UnitAnalyticsComponent} from './units/states/analytics/unit-analytics-route.component'; +import {UnitTaskEditorComponent} from './units/states/edit/directives/unit-tasks-editor/unit-task-editor.component'; +import {CreateNewUnitModal} from './admin/modals/create-new-unit-modal/create-new-unit-modal.component'; +import {FUsersComponent} from './admin/states/users/users.component'; +import {FUnitTaskListComponent} from './units/task-viewer/directives/unit-task-list/unit-task-list.component'; +import {FTaskDetailsViewComponent} from './units/task-viewer/directives/task-details-view/task-details-view.component'; +import {FTaskSheetViewComponent} from './units/task-viewer/directives/task-sheet-view/task-sheet-view.component'; +import {ProgressBurndownChartComponent} from './visualisations/progress-burndown-chart/progressburndownchart.component'; +import {TaskVisualisationComponent} from './visualisations/task-visualisation/taskvisualisation.component'; +import {ProgressDashboardComponent} from './projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component'; import {FUnitsComponent} from './admin/states/units/units.component'; import {AlertService} from './common/services/alert.service'; @@ -301,10 +301,17 @@ DoubtfireAngularJSModule.factory( DoubtfireAngularJSModule.factory('dateService', downgradeInjectable(DateService)); DoubtfireAngularJSModule.factory('CreateNewUnitModal', downgradeInjectable(CreateNewUnitModal)); DoubtfireAngularJSModule.factory('GradeTaskModal', downgradeInjectable(GradeTaskModalService)); -DoubtfireAngularJSModule.factory('UnitStudentEnrolmentModal', downgradeInjectable(UnitStudentEnrolmentModalService)); +DoubtfireAngularJSModule.factory( + 'UnitStudentEnrolmentModal', + downgradeInjectable(UnitStudentEnrolmentModalService), +); DoubtfireAngularJSModule.factory('PrivacyPolicy', downgradeInjectable(PrivacyPolicy)); // directive -> component +DoubtfireAngularJSModule.directive( + 'fProgressDashboard', + downgradeComponent({component: ProgressDashboardComponent}), +); DoubtfireAngularJSModule.directive( 'fProjectTasksList', downgradeComponent({component: ProjectTasksListComponent}), @@ -469,7 +476,10 @@ DoubtfireAngularJSModule.directive( ); DoubtfireAngularJSModule.directive('newFUnits', downgradeComponent({component: FUnitsComponent})); -DoubtfireAngularJSModule.directive('unauthorised', downgradeComponent({ component: UnauthorisedComponent })); +DoubtfireAngularJSModule.directive( + 'unauthorised', + downgradeComponent({component: UnauthorisedComponent}), +); // Global configuration @@ -484,15 +494,14 @@ const otherwiseConfigBlock = [ ]; DoubtfireAngularJSModule.config(otherwiseConfigBlock); - DoubtfireAngularJSModule.directive( 'fProgressBurndownChart', - downgradeComponent({ component: ProgressBurndownChartComponent }) + downgradeComponent({component: ProgressBurndownChartComponent}), ); DoubtfireAngularJSModule.directive( 'fTaskVisualisation', - downgradeComponent({ component: TaskVisualisationComponent }) + downgradeComponent({component: TaskVisualisationComponent}), ); DoubtfireAngularJSModule.directive( diff --git a/src/app/projects/states/dashboard/dashboard.tpl.html b/src/app/projects/states/dashboard/dashboard.tpl.html index e79f1af1ca..2171877d44 100644 --- a/src/app/projects/states/dashboard/dashboard.tpl.html +++ b/src/app/projects/states/dashboard/dashboard.tpl.html @@ -7,14 +7,14 @@ style="padding: 0 8px 0 8px" > - - - restrict: 'E' - templateUrl: 'projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html' - scope: - project: '=' - onUpdateTargetGrade: '=' - controller: ($scope, $stateParams, newProjectService, gradeService, analyticsService, alertService) -> - # Is the current user a tutor? - $scope.tutor = $stateParams.tutor - # Number of tasks completed and remaining - updateTaskCompletionValues = -> - completedTasks = $scope.project.numberTasks("complete") - $scope.numberOfTasks = - completed: completedTasks - remaining: $scope.project.activeTasks().length - completedTasks - updateTaskCompletionValues() - - # Expose grade names and values - $scope.grades = - names: gradeService.grades - values: gradeService.gradeValues - - $scope.updateTargetGrade = (newGrade) -> - $scope.project.targetGrade = newGrade - newProjectService.update($scope.project).subscribe( - (project) -> - project.refreshBurndownChartData() - - # Update task completions and re-render task status graph - updateTaskCompletionValues() - $scope.renderTaskStatusPieChart?() - $scope.onUpdateTargetGrade?() - analyticsService.event("Student Project View - Progress Dashboard", "Grade Changed", $scope.grades.names[newGrade]) - alertService.success( "Updated target grade successfully", 2000) - - (failure) -> - alertService.error( "Failed to update target grade", 4000) - ) -) diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.html b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.html new file mode 100644 index 0000000000..2fa31e0c7f --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.html @@ -0,0 +1,74 @@ +
+
+

+ Progress Dashboard for {{ project.student.name }} +

+
+
+ +
+
+
+

Target Grade

+
+
+
+ + Select Target Grade + + + {{ grades.names[grade] }} + + + +
+
+ + +
+
+
+

Progress Burndown

+

+ The burndown chart shows how much work remains for you to achieve your target grade. +

+
+
+
+ +
+
+ Aim to keep your + Complete + line close to or ahead of the + Target + line to keep on track. +
+
+ + +
+
+
+

Task Statuses

+

+ Breakdown summary of each of your task statuses. +

+
+
+
+ +
+
+
+
diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.scss b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.scss similarity index 100% rename from src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.scss rename to src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.scss diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.ts b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.ts new file mode 100644 index 0000000000..ab9e3e6264 --- /dev/null +++ b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component.ts @@ -0,0 +1,67 @@ +import {Component, Input, Output, EventEmitter, OnInit, Inject} from '@angular/core'; +import {GradeService} from 'src/app/common/services/grade.service'; +import {analyticsService} from 'src/app/ajs-upgraded-providers'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {ProjectService} from 'src/app/api/services/project.service'; +import {Project} from 'src/app/api/models/project'; + +@Component({ + selector: 'f-progress-dashboard', + templateUrl: './progress-dashboard.component.html', + styleUrls: ['./progress-dashboard.component.scss'], +}) +export class ProgressDashboardComponent implements OnInit { + @Input() project: Project; + @Output() doUpdateTargetGrade = new EventEmitter(); + + tutor: boolean; + grades = { + names: this.gradeService.grades, + values: this.gradeService.gradeValues, + }; + numberOfTasks = { + completed: 0, + remaining: 0, + }; + + constructor( + private gradeService: GradeService, + private projectService: ProjectService, + @Inject(analyticsService) private AnalyticsService, + private alertService: AlertService, + ) {} + + ngOnInit(): void { + this.updateTaskCompletionValues(); + this.tutor = this.project.myRole === 'Tutor' ? true : false; + } + + updateTargetGrade(newGrade: number): void { + this.project.targetGrade = newGrade; + this.projectService.update(this.project).subscribe( + (project) => { + project.refreshBurndownChartData(); + this.updateTaskCompletionValues(); + this.doUpdateTargetGrade.emit(); + this.AnalyticsService.event( + 'Student Project View - Progress Dashboard', + 'Grade Changed', + this.grades.names[newGrade], + ); + this.alertService.success('Updated target grade successfully', 2000); + }, + (error) => { + console.error('Error updating target grade:', error); + this.alertService.error('Failed to update target grade', 4000); + }, + ); + } + + private updateTaskCompletionValues(): void { + const completedTasks = this.project.numberTasks('complete'); + this.numberOfTasks = { + completed: completedTasks, + remaining: this.project.activeTasks().length - completedTasks, + }; + } +} diff --git a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html b/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html deleted file mode 100644 index 6cbfd88f44..0000000000 --- a/src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.tpl.html +++ /dev/null @@ -1,58 +0,0 @@ -
-
-

- Progress Dashboard for {{project.student.name}} -

-
-
-
-
-
-

Target Grade

-
-
- -
-
-
-
-

Progress Burndown

-
- The burndown chart shows how much work remains for you to achieve your target grade. -
-
-
- -
- -
-
-
-
-
-

Task Statuses

-
- Breakdown summary of each of your task statuses. -
-
-
- - -
-
-
-
-
From 8487b8d20e838fa1b2a2238bb856f73edc86442c Mon Sep 17 00:00:00 2001 From: Lachlan Date: Thu, 11 Sep 2025 14:50:06 +1000 Subject: [PATCH 05/20] fix: burndown chart visualisation (#942) * fix: update burndown data to render series of different lengths * chore: cleanup imports * chore: remove leftover testing comment from previous contributor --- .../progressburndownchart.component.html | 4 +- .../progressburndownchart.component.ts | 97 +++++++++---------- 2 files changed, 49 insertions(+), 52 deletions(-) diff --git a/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.html b/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.html index 3f51c0d7dd..374cee4ce8 100644 --- a/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.html +++ b/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.html @@ -13,7 +13,9 @@ [yAxisLabel]="yAxisLabel" [yAxisTickFormatting]="formatPerc" [scheme]="colorScheme" + [yScaleMin]="yScaleMin" + [yScaleMax]="yScaleMax" (select)="onSelect($event)" - > + > diff --git a/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts b/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts index 4c1920154a..e49ca853dc 100644 --- a/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts +++ b/src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts @@ -1,14 +1,13 @@ -import { Component, OnInit, Input, SimpleChanges, LOCALE_ID, ViewContainerRef } from '@angular/core'; -import { Project, Unit } from 'src/app/api/models/doubtfire-model'; -import { formatDate } from '@angular/common'; -import { MappingFunctions } from 'src/app/api/services/mapping-fn'; -import { AppInjector } from 'src/app/app-injector'; -import { ChartBaseComponent } from 'src/app/common/chart-base/chart-base-component/chart-base-component.component'; +import {Component, OnInit, Input, SimpleChanges, LOCALE_ID, ViewContainerRef} from '@angular/core'; +import {Project, Unit} from 'src/app/api/models/doubtfire-model'; +import {formatDate} from '@angular/common'; +import {AppInjector} from 'src/app/app-injector'; +import {ChartBaseComponent} from 'src/app/common/chart-base/chart-base-component/chart-base-component.component'; @Component({ selector: 'f-progress-burndown-chart', templateUrl: './progressburndownchart.component.html', - styleUrls: ['./progressburndownchart.component.scss'] + styleUrls: ['./progressburndownchart.component.scss'], }) export class ProgressBurndownChartComponent extends ChartBaseComponent implements OnInit { @Input() project: Project; @@ -28,9 +27,11 @@ export class ProgressBurndownChartComponent extends ChartBaseComponent implement showXAxisLabel: boolean = true; xAxisLabel: string = 'Time'; yAxisLabel: string = 'Tasks Remaining'; - colorScheme = { domain: ['#AAAAAA', '#777777', '#0079d8', '#E01B5D'] }; + colorScheme = {domain: ['#AAAAAA', '#777777', '#0079d8', '#E01B5D', 'transparent']}; + yScaleMin: number = 0; + yScaleMax: number = 100; - private seriesVisibility: { [key: string]: boolean } = {}; + private seriesVisibility: {[key: string]: boolean} = {}; constructor(public viewContainerRef: ViewContainerRef) { super(viewContainerRef); @@ -39,9 +40,6 @@ export class ProgressBurndownChartComponent extends ChartBaseComponent implement } ngOnInit(): void { - console.log('ProgressBurndownChartComponent: ngOnInit'); - console.log(this.project); - this.project.refreshBurndownChartData(); this.updateData(); this.data.forEach((item) => { @@ -56,49 +54,46 @@ export class ProgressBurndownChartComponent extends ChartBaseComponent implement } } - generateDates() { - const startDate: Date = this.project.unit.startDate; - const endDate: Date = this.project.unit.endDate; - const locale: string = AppInjector.get(LOCALE_ID); - const numberPoints = 10; - // Get the number of days between dates - const totalDays = MappingFunctions.daysBetween(startDate, endDate); - const interval = totalDays / (numberPoints - 1); // get gaps between points - - const dates = []; - for (let i = 0; i < numberPoints; i++) { - const date = MappingFunctions.daysAfter(startDate, interval * i); - dates.push(formatDate(date, 'd MMM', locale)); - } - - return dates; - } - updateData(): void { const chartData = this.project?.burndownChartData; - const dates = this.generateDates(); - - const formattedData = chartData.map((dataset) => { - const values = Array(10) - .fill(0) - .map((_, index) => dataset.values[index] || 0); - - const series = dates.map((date, index) => { - let value = values[index][1] ?? 0; - value = value * 100; - - if (value < 0) { - value = 0; - } + const locale: string = AppInjector.get(LOCALE_ID); + const startDate: Date = this.project.unit.startDate; + const endDate: Date = this.project.unit.endDate; - return { name: date, value }; - }); + if (!chartData) { + this.data = []; + return; + } - return { - name: dataset.key, - series, - }; - }); + const formattedData = chartData.map((series) => ({ + name: series.key, // Use the "key" as the "name" + series: series.values + .filter((value) => value[0] >= startDate.getTime() && value[0] <= endDate.getTime()) // Filter values based on the date range + .map((value) => { + if (value[1] < 0) { + value[1] = 0; // If the value is negative, set it to 0 + } + value[1] = Math.round(value[1] * 100); // Round the value to 2 decimal places + return { + name: formatDate(new Date(value[0]), 'd MMM', locale), // Format the timestamp as a date + value: value[1], + }; + }), + })); + + // Hack to get around yScaleMin and yScaleMax not working. + const target = formattedData.find((series) => series.name === 'Target'); + if (target) { + const start = target.series.find( + (point) => point.name === formatDate(new Date(startDate), 'd MMM', locale), + ); + const end = target.series.find( + (point) => point.name === formatDate(new Date(endDate), 'd MMM', locale), + ); + + if (start) start.value = 100; // Update start + if (end) end.value = 0; // Update end + } this.temp = JSON.parse(JSON.stringify(formattedData)); this.data = formattedData; From d010c4ee0483bd6622168aa7cf1330ed1c1ebe74 Mon Sep 17 00:00:00 2001 From: Lachlan Date: Mon, 15 Sep 2025 11:33:43 +1000 Subject: [PATCH 06/20] refactor: migrate task status pie chart (#928) * chore: migrate pie chart to ngxchart * chore: remove testing console log * chore: restore package and lock to 9.x * chore: remove testing comments * chore: resolve merge conflicts * chore: remove testing console log * chore: resolve futher conflicts * chore: exlude packge-lock * chore: exclude package * chore: delete migrated progress dashboard html --------- Co-authored-by: Boink <40929320+b0ink@users.noreply.github.com> --- src/app/doubtfire-angular.module.ts | 10 ++- src/app/doubtfire-angularjs.module.ts | 13 +++- .../project-progress-dashboard.component.html | 13 +++- .../taskstatuspiechart.component.html | 12 +++ .../taskstatuspiechart.component.scss | 0 .../taskstatuspiechart.component.ts | 76 +++++++++++++++++++ 6 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.html create mode 100644 src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.scss create mode 100644 src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.ts diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index c5903c03a2..7e448e7f07 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -255,10 +255,10 @@ import {TaskScormCardComponent} from './projects/states/dashboard/directives/tas import {TestAttemptService} from './api/services/test-attempt.service'; import {ScormExtensionCommentComponent} from './tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component'; import {ScormExtensionModalComponent} from './common/modals/scorm-extension-modal/scorm-extension-modal.component'; -import { GradeIconComponent } from './common/grade-icon/grade-icon.component'; -import { GradeTaskModalComponent } from './tasks/modals/grade-task-modal/grade-task-modal.component'; -import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; -import { GroupSetSelectorComponent } from './groups/group-set-selector/group-set-selector.component'; +import {GradeIconComponent} from './common/grade-icon/grade-icon.component'; +import {GradeTaskModalComponent} from './tasks/modals/grade-task-modal/grade-task-modal.component'; +import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; +import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; // See https://stackoverflow.com/questions/55721254/how-to-change-mat-datepicker-date-format-to-dd-mm-yyyy-in-simplest-way/58189036#58189036 const MY_DATE_FORMAT = { @@ -273,10 +273,12 @@ const MY_DATE_FORMAT = { }, }; import {UnitStudentEnrolmentModalComponent} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component'; +import {TaskStatusPieChartComponent} from './visualisations/task-status-pie-chart/taskstatuspiechart.component'; @NgModule({ // Components we declare declarations: [ + TaskStatusPieChartComponent, AlertComponent, ProgressDashboardComponent, UnitStudentEnrolmentModalComponent, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 66ad40a1ab..1cacad7583 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -165,6 +165,7 @@ import {fPdfViewerComponent} from './common/pdf-viewer/pdf-viewer.component'; import {PdfViewerPanelComponent} from './common/pdf-viewer-panel/pdf-viewer-panel.component'; import {StaffTaskListComponent} from './units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component'; import {StatusIconComponent} from './common/status-icon/status-icon.component'; +import {TaskStatusPieChartComponent} from './visualisations/task-status-pie-chart/taskstatuspiechart.component'; import { GroupSetService, LearningOutcomeService, @@ -218,9 +219,9 @@ import {AlertService} from './common/services/alert.service'; import {GradeService} from './common/services/grade.service'; import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component'; -import { UnitStudentEnrolmentModalService } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; -import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; -import { GroupSetSelectorComponent } from './groups/group-set-selector/group-set-selector.component'; +import {UnitStudentEnrolmentModalService} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; +import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; +import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -307,6 +308,10 @@ DoubtfireAngularJSModule.factory( DoubtfireAngularJSModule.factory('PrivacyPolicy', downgradeInjectable(PrivacyPolicy)); // directive -> component +DoubtfireAngularJSModule.directive( + 'fTaskStatusPieChart', + downgradeComponent({component: TaskStatusPieChartComponent}), +); DoubtfireAngularJSModule.directive( 'fProgressDashboard', downgradeComponent({component: ProgressDashboardComponent}), @@ -505,5 +510,5 @@ DoubtfireAngularJSModule.directive( DoubtfireAngularJSModule.directive( 'groupSetSelector', - downgradeComponent({ component: GroupSetSelectorComponent }) + downgradeComponent({component: GroupSetSelectorComponent}), ); diff --git a/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.html b/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.html index c1cd983626..812ba36201 100644 --- a/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.html +++ b/src/app/projects/project-progress-dashboard/project-progress-dashboard.component.html @@ -46,11 +46,16 @@

Targetting

- - - + [unit]="project.unit" + [grade]="project.targetGrade" + > +
diff --git a/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.html b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.html new file mode 100644 index 0000000000..02f2e78dee --- /dev/null +++ b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.html @@ -0,0 +1,12 @@ +
+ + +
diff --git a/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.scss b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.ts b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.ts new file mode 100644 index 0000000000..1e9bb142d8 --- /dev/null +++ b/src/app/visualisations/task-status-pie-chart/taskstatuspiechart.component.ts @@ -0,0 +1,76 @@ +import {Component, OnInit, Input, SimpleChanges} from '@angular/core'; +import {Project, TaskStatus} from 'src/app/api/models/doubtfire-model'; +import {ChartBaseComponent} from 'src/app/common/chart-base/chart-base-component/chart-base-component.component'; + +@Component({ + selector: 'f-task-status-pie-chart', + templateUrl: './taskstatuspiechart.component.html', + styleUrls: ['./taskstatuspiechart.component.scss'], +}) +export class TaskStatusPieChartComponent extends ChartBaseComponent implements OnInit { + @Input() project: Project; + @Input() grade: number; + + data: {name: string; value: number}[] = []; + colors: {name: string; value: string}[]; + view: number[] = [700, 400]; + + ngOnInit(): void { + this.updateData(); + } + + ngOnChanges(changes: SimpleChanges): void { + if ('grade' in changes && changes.grade.currentValue !== undefined) { + this.updateData(); + } + } + + updateData(): void { + if (this.project) { + const taskCounts = new Map(TaskStatus.STATUS_KEYS.map((status) => [status, 0])); + const activeTasks = this.project.activeTasks(); + activeTasks.forEach((task) => { + if (task.status) { + taskCounts.set(task.status, (taskCounts.get(task.status) || 0) + 1); + } + }); + + const sortOrder = [ + 'not_started', + 'feedback_exceeded', + 'redo', + 'need_help', + 'working_on_it', + 'fix_and_resubmit', + 'ready_for_feedback', + 'discuss', + 'demonstrate', + 'complete', + 'fail', + 'time_exceeded', + ]; + + this.data = Array.from(taskCounts) + .map(([status, count]) => { + return { + name: TaskStatus.STATUS_LABELS.get(status), + value: count, + }; + }) + .filter((task) => task.value > 0 || sortOrder.includes(task.name)) + .sort((a, b) => { + let aIndex = sortOrder.indexOf(a.name); + let bIndex = sortOrder.indexOf(b.name); + + aIndex = aIndex === -1 ? sortOrder.length : aIndex; + bIndex = bIndex === -1 ? sortOrder.length : bIndex; + + return aIndex - bIndex; + }); + + this.colors = Array.from(TaskStatus.STATUS_COLORS).map(([status, color]) => { + return {name: TaskStatus.STATUS_LABELS.get(status), value: color}; + }); + } + } +} From ac8a82b978a3a775504d7dd6d15d8fc6291b9b48 Mon Sep 17 00:00:00 2001 From: Jason Vellucci Date: Mon, 22 Sep 2025 11:18:15 +1000 Subject: [PATCH 07/20] refactor: migrate/unit staff editor 9.x (#933) * chore: migrate/unit-staff-editor - delete old Coffeescript file - delete old template - remove reference to old Coffeescript file - link new component - downgrade new component * chore: migrate/unit-staff-editor - port `unit-staff-editor-component` to Typescript from Coffeescript - port `unit-staff-editor` template to Angular template syntax w/ Angular Material - adjust parent component `Inputs` and attribute bindings * Update README.md * Update README.md --------- Co-authored-by: Boink <40929320+b0ink@users.noreply.github.com> --- README.md | 2 +- src/app/doubtfire-angular.module.ts | 8 +- src/app/doubtfire-angularjs.module.ts | 7 +- .../states/edit/directives/directives.coffee | 1 - .../unit-staff-editor.component.html | 116 +++++++++++++++ .../unit-staff-editor.component.ts | 135 ++++++++++++++++++ .../unit-staff-editor.tpl.html | 101 ------------- src/app/units/states/edit/edit.tpl.html | 2 +- 8 files changed, 262 insertions(+), 110 deletions(-) create mode 100644 src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html create mode 100644 src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts delete mode 100644 src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html diff --git a/README.md b/README.md index fc38c9d607..34fcf6a733 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ MIGRATED: - [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.ts - [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-general/task-definition-general.component.ts - [x] ./src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-upload/task-definition-upload.component.ts +- [x] ./src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts - [x] ./src/app/units/states/analytics/unit-analytics-route.component.ts - [x] ./src/app/common/footer/footer.component.ts - [x] ./src/app/common/audio-recorder/audio/audio-comment-recorder/audio-comment-recorder.ts @@ -189,7 +190,6 @@ TODO: - [ ] ./src/app/units/states/edit/directives/directives.coffee - [ ] ./src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.coffee - [ ] ./src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee -- [ ] ./src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee - [ ] ./src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.coffee - [ ] ./src/app/units/states/edit/edit.coffee - [ ] ./src/app/units/states/rollover/directives/directives.coffee diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 7e448e7f07..338e6591f4 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -255,9 +255,10 @@ import {TaskScormCardComponent} from './projects/states/dashboard/directives/tas import {TestAttemptService} from './api/services/test-attempt.service'; import {ScormExtensionCommentComponent} from './tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component'; import {ScormExtensionModalComponent} from './common/modals/scorm-extension-modal/scorm-extension-modal.component'; -import {GradeIconComponent} from './common/grade-icon/grade-icon.component'; -import {GradeTaskModalComponent} from './tasks/modals/grade-task-modal/grade-task-modal.component'; -import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; +import { GradeIconComponent } from './common/grade-icon/grade-icon.component'; +import { GradeTaskModalComponent } from './tasks/modals/grade-task-modal/grade-task-modal.component'; +import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; +import { UnitStaffEditorComponent } from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; // See https://stackoverflow.com/questions/55721254/how-to-change-mat-datepicker-date-format-to-dd-mm-yyyy-in-simplest-way/58189036#58189036 @@ -398,6 +399,7 @@ import {TaskStatusPieChartComponent} from './visualisations/task-status-pie-char TaskScormCardComponent, ScormExtensionCommentComponent, ScormExtensionModalComponent, + UnitStaffEditorComponent, GroupSetSelectorComponent, ], // Services we provide diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 1cacad7583..bd638cbea8 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -96,7 +96,6 @@ import 'build/src/app/units/states/groups/groups.js'; import 'build/src/app/units/states/states.js'; import 'build/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.js'; import 'build/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.js'; -import 'build/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.js'; import 'build/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.js'; import 'build/src/app/units/states/edit/directives/directives.js'; import 'build/src/app/units/states/edit/edit.js'; @@ -219,8 +218,9 @@ import {AlertService} from './common/services/alert.service'; import {GradeService} from './common/services/grade.service'; import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component'; -import {UnitStudentEnrolmentModalService} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; -import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; +import { UnitStudentEnrolmentModalService } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; +import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; +import { UnitStaffEditorComponent } from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ @@ -480,6 +480,7 @@ DoubtfireAngularJSModule.directive( ); DoubtfireAngularJSModule.directive('newFUnits', downgradeComponent({component: FUnitsComponent})); +DoubtfireAngularJSModule.directive('unitStaffEditor', downgradeComponent({ component: UnitStaffEditorComponent })); DoubtfireAngularJSModule.directive( 'unauthorised', downgradeComponent({component: UnauthorisedComponent}), diff --git a/src/app/units/states/edit/directives/directives.coffee b/src/app/units/states/edit/directives/directives.coffee index bfb12ed317..fe06bf4510 100644 --- a/src/app/units/states/edit/directives/directives.coffee +++ b/src/app/units/states/edit/directives/directives.coffee @@ -2,5 +2,4 @@ angular.module('doubtfire.units.states.edit.directives', [ 'doubtfire.units.states.edit.directives.unit-details-editor' 'doubtfire.units.states.edit.directives.unit-group-set-editor' 'doubtfire.units.states.edit.directives.unit-ilo-editor' - 'doubtfire.units.states.edit.directives.unit-staff-editor' ]) diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html new file mode 100644 index 0000000000..84aa983f0a --- /dev/null +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html @@ -0,0 +1,116 @@ +
+ + + + + + + + +
+
+

Modify Unit Staff

+

Add staff members to the unit, assigning them a convenor or tutor role.

+
+ +
+
+ +
+ This unit has no staff assigned. +
+ + +
+ + + + + + + + + + + + + + + + + + +
NameRoleMain ConvenorActions
+ + {{ staff.user.name }} +
+ + +
+
+ + + +
+
+
+
+ +
+
diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts new file mode 100644 index 0000000000..167ed3210f --- /dev/null +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts @@ -0,0 +1,135 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {AlertService} from 'src/app/common/services/alert.service'; +import {UnitRoleService} from 'src/app/api/services/unit-role.service'; +import {Unit} from 'src/app/api/models/unit'; +import {User} from 'src/app/api/models/doubtfire-model'; +import {UnitRole} from 'src/app/api/models/unit-role'; + +@Component({ + selector: 'unit-staff-editor', + templateUrl: 'unit-staff-editor.component.html', +}) +export class UnitStaffEditorComponent implements OnInit { + @Input() unit: Unit; + @Input() staff: User[]; + + temp = []; + users = []; + unitStaff: UnitRole[]; + filteredStaff: User[] = []; // Filtered staff members + searchTerm: string = ''; // Search term entered by the user + + // Inject services here + constructor( + private alertService: AlertService, + private unitRoleService: UnitRoleService, + ) {} + + ngOnInit(): void { + // Subscribe to staff cache + this.unit.staffCache.values.subscribe((staff: UnitRole[]) => { + this.unitStaff = staff; + }); + } + + /** + * Changes the role of a staff member. + * + * @param UnitRole unitRole + * @param number role_id + * + * @returns void + */ + changeRole(unitRole: UnitRole, role_id: number) { + unitRole.roleId = role_id; + this.unitRoleService.update(unitRole).subscribe({ + next: (response) => this.alertService.success('Role changed', 2000), + error: (response) => this.alertService.error(response, 6000), + }); + } + + /** + * Changes who the `Main Convenor` of the unit is. + * + * @param UnitRole staff + * + * @returns void + */ + changeMainConvenor(staff: UnitRole) { + this.unit.changeMainConvenor(staff).subscribe({ + next: (response) => this.alertService.success('Main convenor changed', 2000), + error: (response) => this.alertService.error(response, 6000), + }); + } + + /** + * Adds a staff member to the unit. + * + * @param User selectedStaff + * + * @returns void + */ + addSelectedStaff(selectedStaff: User) { + if (selectedStaff?.id) { + this.unit.addStaff(selectedStaff).subscribe({ + next: () => { + this.alertService.success('Staff member added', 2000); + this.searchTerm = ''; // Clear the input field + this.filterStaffList(); // Refilter the list + }, + error: (response) => this.alertService.error(response, 6000), + }); + } else { + this.alertService.error( + 'Unable to add staff member. Ensure they have a tutor or convenor account in User admin first', + ); + } + } + + /** + * Used in filtering the staff list. The `searchTerm` is bound to the auto-complete input in this class's template. + * + * @returns void + */ + filterStaffList(): void { + // `this.searchTerm` holds the selected staff member object from the dropdown OR the auto-complete input searchTerm (never at the same time). + // Thus, check the type here and exit early if string filtering is not needed. + if (typeof this.searchTerm !== 'string') { + return; + } + this.filteredStaff = this.staff.filter( + (staff) => + staff.name.toLowerCase().includes(this.searchTerm.toLowerCase()) && // Find by name + !this.unit.staff.find((listStaff) => staff.id === listStaff.user.id), // Not already assigned to the unit + ); + } + + /** + * Generates a human-readable name made up of the passed-in staff member's `first` and `last` names. + * + * @param User staff + * + * @returns void + */ + displayStaffName(staff: User): string { + return staff ? staff.name : ''; + } + + /** + * Removes a staff member from the unit. + * + * @param UnitRole staff + * + * @returns void + */ + removeStaff(staff: UnitRole) { + this.unitRoleService.delete(staff, {cache: this.unit.staffCache}).subscribe({ + next: (response) => this.alertService.success('Staff member removed', 2000), + error: (response) => this.alertService.error(response, 6000), + }); + } + + groupSetName(id: number) { + this.unit.groupSetsCache.get(id).name || 'Individual Work'; + } +} diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html deleted file mode 100644 index f8b38cecb0..0000000000 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html +++ /dev/null @@ -1,101 +0,0 @@ -
- -
-
-

Modify Unit Staff

- Add staff members to the unit, assigning them a convenor or tutor role. -
-
-
-
This unit has no staff assigned
-
- - - - - - - - - - - - - - - - - - -
NameRoleMain ConvenorActions
- - {{staff.user.name}} -
- - -
-
- - - -
-
-
-
- -
-
diff --git a/src/app/units/states/edit/edit.tpl.html b/src/app/units/states/edit/edit.tpl.html index 3ba73129a5..a49ba3561d 100644 --- a/src/app/units/states/edit/edit.tpl.html +++ b/src/app/units/states/edit/edit.tpl.html @@ -6,7 +6,7 @@ - + From d510b3b54340c225abb4db51b06bddc4404b9306 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:44:53 +1000 Subject: [PATCH 08/20] refactor: unit staff editor material ui (#1001) * fix: revert staff role on error * fix: ensure full search for unit role * chore: fix casing * refactor: use mat table for staff editor * chore: remove unused variables * fix: null check * refactor: use icon button * chore: remove unit staff editor coffeescript file * chore: add back staff editor tooltips --- src/app/common/header/header.component.ts | 3 +- .../unit-staff-editor.coffee | 58 ------ .../unit-staff-editor.component.html | 197 ++++++++---------- .../unit-staff-editor.component.ts | 51 ++++- 4 files changed, 127 insertions(+), 182 deletions(-) delete mode 100644 src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee diff --git a/src/app/common/header/header.component.ts b/src/app/common/header/header.component.ts index b5e1960c8a..842287b92e 100644 --- a/src/app/common/header/header.component.ts +++ b/src/app/common/header/header.component.ts @@ -104,8 +104,7 @@ export class HeaderComponent implements OnInit, OnDestroy { } isUniqueRole = (unit) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const units = this.unitRoles.filter((role: any) => role.unit.id === unit.unit.id); + const units = this.unitRoles.filter((role: UnitRole) => role.unit?.id === unit.unit?.id); return units.length == 1 || unit.role == 'Tutor'; }; diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee deleted file mode 100644 index 3856daefe5..0000000000 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee +++ /dev/null @@ -1,58 +0,0 @@ -angular.module('doubtfire.units.states.edit.directives.unit-staff-editor', []) - -# -# Editor for adding new staff to a unit and assigning those staff -# members new unit roles within the unit -# -.directive('unitStaffEditor', -> - replace: true - restrict: 'E' - templateUrl: 'units/states/edit/directives/unit-staff-editor/unit-staff-editor.tpl.html' - controller: ($scope, $rootScope, alertService, newUnitService, newUnitRoleService) -> - temp = [] - users = [] - - $scope.unit.staffCache.values.subscribe( (staff) -> $scope.unitStaff = staff ) - - $scope.changeRole = (unitRole, role_id) -> - unitRole.roleId = role_id - newUnitRoleService.update(unitRole).subscribe({ - next: (response) -> alertService.success( "Role changed", 2000) - error: (response) -> alertService.error( response, 6000) - }) - - $scope.changeMainConvenor = (staff) -> - $scope.unit.changeMainConvenor(staff).subscribe({ - next: (response) -> - alertService.success( "Main convenor changed", 2000) - error: (response) -> - alertService.error( response, 6000) - }) - - $scope.addSelectedStaff = -> - staff = $scope.selectedStaff - $scope.selectedStaff = null - $scope.unit.staff = [] unless $scope.unit.staff - - if staff.id? - $scope.unit.addStaff(staff).subscribe({ - next: (response) -> alertService.success( "Staff member added", 2000) - error: (response) -> alertService.error( response, 6000) - }) - else - alertService.error( "Unable to add staff member. Ensure they have a tutor or convenor account in User admin first.", 6000) - - # Used in the typeahead to filter staff already in unit - $scope.filterStaff = (staff) -> - not _.find($scope.unit.staff, (listStaff) -> staff.id == listStaff.user.id) - - $scope.removeStaff = (staff) -> - newUnitRoleService.delete(staff, {cache: $scope.unit.staffCache}).subscribe({ - next: (response) -> alertService.success( "Staff member removed", 2000) - error: (response) -> alertService.error( response, 6000) - }) - - $scope.groupSetName = (id) -> - $scope.unit.groupSetsCache.get(id)?.name || "Individual Work" - -) diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html index 84aa983f0a..82938cde3f 100644 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html @@ -1,116 +1,89 @@ -
- - - - - - - - -
-
-

Modify Unit Staff

-

Add staff members to the unit, assigning them a convenor or tutor role.

-
- -
-
- -
- This unit has no staff assigned. -
+
+
+

Unit Staff

+

Manage unit staff by adding members and assigning them as convenors or tutors.

+
+ + + - -
-
Name
- - - - - - - - - - - - - - - - - -
NameRoleMain ConvenorActions
- - {{ staff.user.name }} -
- - -
-
- - - -
+ +
+ {{ unitRole.user.name }}
-
-
- -
+ Convenor + + + + + Main Convenor + + @if (unitRole?.role === 'Convenor') { + + } + + + + Actions + + + + + + + + + + + + {{ staff.name }} + + +
diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts index 167ed3210f..7c53a04671 100644 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.ts @@ -4,6 +4,9 @@ import {UnitRoleService} from 'src/app/api/services/unit-role.service'; import {Unit} from 'src/app/api/models/unit'; import {User} from 'src/app/api/models/doubtfire-model'; import {UnitRole} from 'src/app/api/models/unit-role'; +import {MatTableDataSource} from '@angular/material/table'; +import {MatButtonToggleChange} from '@angular/material/button-toggle'; +import {ConfirmationModalService} from 'src/app/common/modals/confirmation-modal/confirmation-modal.service'; @Component({ selector: 'unit-staff-editor', @@ -19,19 +22,32 @@ export class UnitStaffEditorComponent implements OnInit { filteredStaff: User[] = []; // Filtered staff members searchTerm: string = ''; // Search term entered by the user + displayedColumns: string[] = ['name', 'role', 'main-convenor', 'actions']; + dataSource = new MatTableDataSource(); + // Inject services here constructor( private alertService: AlertService, private unitRoleService: UnitRoleService, + private confirmationModalService: ConfirmationModalService, ) {} ngOnInit(): void { // Subscribe to staff cache this.unit.staffCache.values.subscribe((staff: UnitRole[]) => { this.unitStaff = staff; + this.dataSource.data = staff; }); } + onRoleChange(unitRole: UnitRole, event: MatButtonToggleChange) { + const role = event.value; + if (role !== 'Tutor' && role !== 'Convenor') { + return; + } + const roleId = role === 'Tutor' ? 2 : 3; // map however you like + this.changeRole(unitRole, roleId, role); + } /** * Changes the role of a staff member. * @@ -40,11 +56,20 @@ export class UnitStaffEditorComponent implements OnInit { * * @returns void */ - changeRole(unitRole: UnitRole, role_id: number) { - unitRole.roleId = role_id; + changeRole(unitRole: UnitRole, roleId: number, role: string) { + const previousRoleId = unitRole.roleId; + const previousRole = unitRole.role; + + unitRole.roleId = roleId; + unitRole.role = role; this.unitRoleService.update(unitRole).subscribe({ - next: (response) => this.alertService.success('Role changed', 2000), - error: (response) => this.alertService.error(response, 6000), + next: () => this.alertService.success('Role changed', 2000), + error: (response) => { + // Revert changes on error + unitRole.roleId = previousRoleId; + unitRole.role = previousRole; + this.alertService.error(response, 6000); + }, }); } @@ -56,10 +81,16 @@ export class UnitStaffEditorComponent implements OnInit { * @returns void */ changeMainConvenor(staff: UnitRole) { - this.unit.changeMainConvenor(staff).subscribe({ - next: (response) => this.alertService.success('Main convenor changed', 2000), - error: (response) => this.alertService.error(response, 6000), - }); + this.confirmationModalService.show( + 'Set Main Convenor', + `Do you want to make ${staff.user.name} the main convenor for this unit?`, + () => { + this.unit.changeMainConvenor(staff).subscribe({ + next: (_response) => this.alertService.success('Main convenor changed', 2000), + error: (response) => this.alertService.error(response, 6000), + }); + }, + ); } /** @@ -99,7 +130,7 @@ export class UnitStaffEditorComponent implements OnInit { } this.filteredStaff = this.staff.filter( (staff) => - staff.name.toLowerCase().includes(this.searchTerm.toLowerCase()) && // Find by name + staff.matches(this.searchTerm.toLowerCase()) && // Find by name !this.unit.staff.find((listStaff) => staff.id === listStaff.user.id), // Not already assigned to the unit ); } @@ -124,7 +155,7 @@ export class UnitStaffEditorComponent implements OnInit { */ removeStaff(staff: UnitRole) { this.unitRoleService.delete(staff, {cache: this.unit.staffCache}).subscribe({ - next: (response) => this.alertService.success('Staff member removed', 2000), + next: () => this.alertService.success('Staff member removed', 2000), error: (response) => this.alertService.error(response, 6000), }); } From e3c4e8cb61739b43f4a7f4a33972e9ac17e10d38 Mon Sep 17 00:00:00 2001 From: Andrew Cain Date: Thu, 3 Apr 2025 17:38:51 +1100 Subject: [PATCH 09/20] Merge branch 'migrate/confirmation-modal' of https://github.com/b0ink/doubtfire-web into b0ink-migrate/confirmation-modal --- .../confirmation-modal.coffee | 35 -------------- .../confirmation-modal.component.html | 20 ++++++++ .../confirmation-modal.component.scss | 0 .../confirmation-modal.component.ts | 47 +++++++++++++++++++ .../confirmation-modal.scss | 3 -- .../confirmation-modal.service.ts | 26 ++++++++++ .../confirmation-modal.tpl.html | 22 --------- src/app/common/modals/modals.coffee | 1 - src/app/doubtfire-angular.module.ts | 2 + src/app/doubtfire-angularjs.module.ts | 3 +- .../unit-task-editor.component.ts | 4 +- 11 files changed, 98 insertions(+), 65 deletions(-) delete mode 100644 src/app/common/modals/confirmation-modal/confirmation-modal.coffee create mode 100644 src/app/common/modals/confirmation-modal/confirmation-modal.component.html create mode 100644 src/app/common/modals/confirmation-modal/confirmation-modal.component.scss create mode 100644 src/app/common/modals/confirmation-modal/confirmation-modal.component.ts delete mode 100644 src/app/common/modals/confirmation-modal/confirmation-modal.scss create mode 100644 src/app/common/modals/confirmation-modal/confirmation-modal.service.ts delete mode 100644 src/app/common/modals/confirmation-modal/confirmation-modal.tpl.html diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.coffee b/src/app/common/modals/confirmation-modal/confirmation-modal.coffee deleted file mode 100644 index c7999098cf..0000000000 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.coffee +++ /dev/null @@ -1,35 +0,0 @@ -angular.module("doubtfire.common.modals.confirmation-modal", []) - -.factory("ConfirmationModal", ($modal) -> - ConfirmationModal = {} - - # - # Show a modal asking the user to confirm their indicated action. - # - ConfirmationModal.show = (title, message, action) -> - modalInstance = $modal.open - templateUrl: 'common/modals/confirmation-modal/confirmation-modal.tpl.html' - controller: 'ConfirmationModalCtrl' - resolve: - title: -> title - message: -> message - action: -> action - - ConfirmationModal -) - -# -# Controller for confirmation modal -# -.controller('ConfirmationModalCtrl', ($scope, $modalInstance, title, message, action, alertService) -> - $scope.title = title - $scope.message = message - - $scope.confirmAction = -> - action() - $modalInstance.dismiss() - - $scope.cancelAction = -> - alertService.message "#{title} action cancelled", 3000 - $modalInstance.dismiss() -) diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.component.html b/src/app/common/modals/confirmation-modal/confirmation-modal.component.html new file mode 100644 index 0000000000..3ec07fc729 --- /dev/null +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.component.html @@ -0,0 +1,20 @@ +
+

+
+ +
+
{{ title }}
+ Please confirm that you want to perform this action. +
+
+

+ + {{ message }} + + + + + +
diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.component.scss b/src/app/common/modals/confirmation-modal/confirmation-modal.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts b/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts new file mode 100644 index 0000000000..f9506e5b8b --- /dev/null +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.component.ts @@ -0,0 +1,47 @@ +import {Component, OnInit, Input, Inject} from '@angular/core'; +import {AlertService} from '../../services/alert.service'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; + +export interface ConfirmationModalData { + title: string; + message: string; + action?: any; +} + +@Component({ + selector: 'confirmation-modal', + templateUrl: './confirmation-modal.component.html', + styleUrls: ['./confirmation-modal.component.scss'], +}) +export class ConfirmationModalComponent implements OnInit { + @Input() title: string; + @Input() message: string; + @Input() action: () => void; + + constructor( + @Inject(AlertService) private alertService: AlertService, + @Inject(MAT_DIALOG_DATA) public data: ConfirmationModalData, + + public dialogRef: MatDialogRef, + ) {} + + ngOnInit(): void { + this.title = this.data.title; + this.message = this.data.message; + this.action = this.data.action; + } + + public confirmAction() { + if (typeof this.action === 'function') { + this.action(); + } else { + this.alertService.error(`${this.title} action failed.`); + } + this.dialogRef.close(); + } + + public cancelAction() { + this.alertService.success(`${this.title} action cancelled.`); + this.dialogRef.close(); + } +} diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.scss b/src/app/common/modals/confirmation-modal/confirmation-modal.scss deleted file mode 100644 index f30b0e345c..0000000000 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.scss +++ /dev/null @@ -1,3 +0,0 @@ -.confirmation-modal .modal-body { - font-size: 1.5em; -} diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts b/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts new file mode 100644 index 0000000000..6257277eff --- /dev/null +++ b/src/app/common/modals/confirmation-modal/confirmation-modal.service.ts @@ -0,0 +1,26 @@ +import {Injectable} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import {ConfirmationModalComponent, ConfirmationModalData} from './confirmation-modal.component'; + +@Injectable({ + providedIn: 'root', +}) +export class ConfirmationModalService { + constructor(public dialog: MatDialog) {} + + public show(title: string, message: string, action?: any) { + this.dialog.open( + ConfirmationModalComponent, + { + data: { + title, + message, + action, + }, + position: {top: '2.5%'}, + width: '100%', + maxWidth: '650px', + }, + ); + } +} diff --git a/src/app/common/modals/confirmation-modal/confirmation-modal.tpl.html b/src/app/common/modals/confirmation-modal/confirmation-modal.tpl.html deleted file mode 100644 index 4bd87f6868..0000000000 --- a/src/app/common/modals/confirmation-modal/confirmation-modal.tpl.html +++ /dev/null @@ -1,22 +0,0 @@ -
- - - -
diff --git a/src/app/common/modals/modals.coffee b/src/app/common/modals/modals.coffee index 16d2be1ec8..73aae8685b 100644 --- a/src/app/common/modals/modals.coffee +++ b/src/app/common/modals/modals.coffee @@ -1,5 +1,4 @@ angular.module("doubtfire.common.modals", [ 'doubtfire.common.modals.csv-result-modal' - 'doubtfire.common.modals.confirmation-modal' 'doubtfire.common.modals.comments-modal' ]) diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 338e6591f4..88c2206141 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -97,6 +97,7 @@ import {ExtensionCommentComponent} from './tasks/task-comments-viewer/extension- import {CampusListComponent} from './admin/institution-settings/campuses/campus-list/campus-list.component'; import {ExtensionModalComponent} from './common/modals/extension-modal/extension-modal.component'; import {CalendarModalComponent} from './common/modals/calendar-modal/calendar-modal.component'; +import { ConfirmationModalComponent } from './common/modals/confirmation-modal/confirmation-modal.component'; import {MatRadioModule} from '@angular/material/radio'; import {MatButtonToggleModule} from '@angular/material/button-toggle'; import { @@ -301,6 +302,7 @@ import {TaskStatusPieChartComponent} from './visualisations/task-status-pie-char OverseerImageListComponent, ExtensionModalComponent, CalendarModalComponent, + ConfirmationModalComponent, InstitutionSettingsComponent, HomeComponent, CommentBubbleActionComponent, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index bd638cbea8..76ae8c64c5 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -107,7 +107,6 @@ import 'build/src/app/units/states/students-list/students-list.js'; import 'build/src/app/units/states/analytics/analytics.js'; import 'build/src/app/common/filters/filters.js'; import 'build/src/app/common/content-editable/content-editable.js'; -import 'build/src/app/common/modals/confirmation-modal/confirmation-modal.js'; import 'build/src/app/common/modals/comments-modal/comments-modal.js'; import 'build/src/app/common/modals/csv-result-modal/csv-result-modal.js'; import 'build/src/app/common/modals/modals.js'; @@ -140,6 +139,7 @@ import {ExtensionCommentComponent} from './tasks/task-comments-viewer/extension- import {TaskAssessmentCommentComponent} from './tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component'; import {ExtensionModalService} from './common/modals/extension-modal/extension-modal.service'; import {CalendarModalService} from './common/modals/calendar-modal/calendar-modal.service'; +import { ConfirmationModalService } from './common/modals/confirmation-modal/confirmation-modal.service'; import {CampusListComponent} from './admin/institution-settings/campuses/campus-list/campus-list.component'; import {ActivityTypeListComponent} from './admin/institution-settings/activity-type-list/activity-type-list.component'; import {InstitutionSettingsComponent} from './admin/institution-settings/institution-settings.component'; @@ -241,6 +241,7 @@ DoubtfireAngularJSModule.factory('AboutDoubtfireModal', downgradeInjectable(Abou DoubtfireAngularJSModule.factory('DoubtfireConstants', downgradeInjectable(DoubtfireConstants)); DoubtfireAngularJSModule.factory('ExtensionModal', downgradeInjectable(ExtensionModalService)); DoubtfireAngularJSModule.factory('CalendarModal', downgradeInjectable(CalendarModalService)); +DoubtfireAngularJSModule.factory('ConfirmationModal', downgradeInjectable(ConfirmationModalService)); DoubtfireAngularJSModule.factory('TaskCommentService', downgradeInjectable(TaskCommentService)); DoubtfireAngularJSModule.factory('alertService', downgradeInjectable(AlertService)); DoubtfireAngularJSModule.factory('tutorialService', downgradeInjectable(TutorialService)); diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts index 72c309371d..bf4c3b626b 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts @@ -138,9 +138,7 @@ export class UnitTaskEditorComponent implements AfterViewInit { () => { this.unit.deleteTaskDefinition(taskDefinition); //TODO: reinstate ProgressModal.show "Deleting Task #{task.abbreviation}", 'Please wait while student projects are updated.', promise - - this.alerts.success('Task deleted'); - } + }, ); } From d9560c3b68df0a09f577cdab990d252b5cd24c58 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:45:33 +1000 Subject: [PATCH 10/20] fix: check for valid unit --- .../unit-staff-editor/unit-staff-editor.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html index 82938cde3f..9f3a65b727 100644 --- a/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html +++ b/src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.component.html @@ -45,13 +45,13 @@

Unit Staff

@if (unitRole?.role === 'Convenor') { } From 373cd1eef326a528e1dd4546777a939c0db24d22 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:55:31 +1000 Subject: [PATCH 11/20] refactor: migrate portfolio grade select step (#1014) * Frontend migration of portfolio-grade-select-step to Angular 17 component * Addressed review comments in portfolio-grade-select-step component * Implement OnInit in PortfolioGradeSelectStepComponent * replaced bootsrap UI component with Angular material * chore: bring back deleted lines * refactor: replace css with tailwind * chore: revert import order * chore: revert import order * chore: revert import order * chore: revert order * refactor: simplify grade changing --------- Co-authored-by: Pasindu Fernando <116358471+Pasindufdo98@users.noreply.github.com> --- src/app/doubtfire-angular.module.ts | 14 ++- src/app/doubtfire-angularjs.module.ts | 31 ++++-- .../portfolio/directives/directives.coffee | 1 - .../portfolio-grade-select-step.coffee | 22 ----- ...portfolio-grade-select-step.component.html | 96 +++++++++++++++++++ ...portfolio-grade-select-step.component.scss | 0 .../portfolio-grade-select-step.component.ts | 57 +++++++++++ .../portfolio-grade-select-step.scss | 10 -- .../portfolio-grade-select-step.tpl.html | 60 ------------ .../states/portfolio/portfolio.tpl.html | 6 +- 10 files changed, 189 insertions(+), 108 deletions(-) delete mode 100644 src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee create mode 100644 src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html create mode 100644 src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.scss create mode 100644 src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts delete mode 100644 src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss delete mode 100644 src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 88c2206141..e65faa1d11 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -97,7 +97,7 @@ import {ExtensionCommentComponent} from './tasks/task-comments-viewer/extension- import {CampusListComponent} from './admin/institution-settings/campuses/campus-list/campus-list.component'; import {ExtensionModalComponent} from './common/modals/extension-modal/extension-modal.component'; import {CalendarModalComponent} from './common/modals/calendar-modal/calendar-modal.component'; -import { ConfirmationModalComponent } from './common/modals/confirmation-modal/confirmation-modal.component'; +import {ConfirmationModalComponent} from './common/modals/confirmation-modal/confirmation-modal.component'; import {MatRadioModule} from '@angular/material/radio'; import {MatButtonToggleModule} from '@angular/material/button-toggle'; import { @@ -256,11 +256,14 @@ import {TaskScormCardComponent} from './projects/states/dashboard/directives/tas import {TestAttemptService} from './api/services/test-attempt.service'; import {ScormExtensionCommentComponent} from './tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component'; import {ScormExtensionModalComponent} from './common/modals/scorm-extension-modal/scorm-extension-modal.component'; -import { GradeIconComponent } from './common/grade-icon/grade-icon.component'; -import { GradeTaskModalComponent } from './tasks/modals/grade-task-modal/grade-task-modal.component'; -import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; -import { UnitStaffEditorComponent } from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; +import {GradeIconComponent} from './common/grade-icon/grade-icon.component'; +import {GradeTaskModalComponent} from './tasks/modals/grade-task-modal/grade-task-modal.component'; +import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; +// import {GradeTaskModalComponent} from './tasks/modals/grade-task-modal/grade-task-modal.component'; +// import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; +import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; +import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; // See https://stackoverflow.com/questions/55721254/how-to-change-mat-datepicker-date-format-to-dd-mm-yyyy-in-simplest-way/58189036#58189036 const MY_DATE_FORMAT = { @@ -403,6 +406,7 @@ import {TaskStatusPieChartComponent} from './visualisations/task-status-pie-char ScormExtensionModalComponent, UnitStaffEditorComponent, GroupSetSelectorComponent, + PortfolioGradeSelectStepComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 76ae8c64c5..0a48a0b2b7 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -71,7 +71,6 @@ import 'build/src/app/projects/states/outcomes/outcomes.js'; import 'build/src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.js'; import 'build/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.js'; import 'build/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.js'; -import 'build/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.js'; import 'build/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.js'; import 'build/src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.js'; import 'build/src/app/projects/states/portfolio/directives/directives.js'; @@ -139,7 +138,7 @@ import {ExtensionCommentComponent} from './tasks/task-comments-viewer/extension- import {TaskAssessmentCommentComponent} from './tasks/task-comments-viewer/task-assessment-comment/task-assessment-comment.component'; import {ExtensionModalService} from './common/modals/extension-modal/extension-modal.service'; import {CalendarModalService} from './common/modals/calendar-modal/calendar-modal.service'; -import { ConfirmationModalService } from './common/modals/confirmation-modal/confirmation-modal.service'; +import {ConfirmationModalService} from './common/modals/confirmation-modal/confirmation-modal.service'; import {CampusListComponent} from './admin/institution-settings/campuses/campus-list/campus-list.component'; import {ActivityTypeListComponent} from './admin/institution-settings/activity-type-list/activity-type-list.component'; import {InstitutionSettingsComponent} from './admin/institution-settings/institution-settings.component'; @@ -212,16 +211,19 @@ import {FTaskSheetViewComponent} from './units/task-viewer/directives/task-sheet import {ProgressBurndownChartComponent} from './visualisations/progress-burndown-chart/progressburndownchart.component'; import {TaskVisualisationComponent} from './visualisations/task-visualisation/taskvisualisation.component'; import {ProgressDashboardComponent} from './projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component'; - import {FUnitsComponent} from './admin/states/units/units.component'; import {AlertService} from './common/services/alert.service'; - import {GradeService} from './common/services/grade.service'; import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component'; -import { UnitStudentEnrolmentModalService } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; -import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; -import { UnitStaffEditorComponent } from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; + +// import { UnitStudentEnrolmentModalService } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; +// import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; +import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; +import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; + +import {UnitStudentEnrolmentModalService} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; +import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -241,7 +243,10 @@ DoubtfireAngularJSModule.factory('AboutDoubtfireModal', downgradeInjectable(Abou DoubtfireAngularJSModule.factory('DoubtfireConstants', downgradeInjectable(DoubtfireConstants)); DoubtfireAngularJSModule.factory('ExtensionModal', downgradeInjectable(ExtensionModalService)); DoubtfireAngularJSModule.factory('CalendarModal', downgradeInjectable(CalendarModalService)); -DoubtfireAngularJSModule.factory('ConfirmationModal', downgradeInjectable(ConfirmationModalService)); +DoubtfireAngularJSModule.factory( + 'ConfirmationModal', + downgradeInjectable(ConfirmationModalService), +); DoubtfireAngularJSModule.factory('TaskCommentService', downgradeInjectable(TaskCommentService)); DoubtfireAngularJSModule.factory('alertService', downgradeInjectable(AlertService)); DoubtfireAngularJSModule.factory('tutorialService', downgradeInjectable(TutorialService)); @@ -481,12 +486,20 @@ DoubtfireAngularJSModule.directive( ); DoubtfireAngularJSModule.directive('newFUnits', downgradeComponent({component: FUnitsComponent})); -DoubtfireAngularJSModule.directive('unitStaffEditor', downgradeComponent({ component: UnitStaffEditorComponent })); +DoubtfireAngularJSModule.directive( + 'unitStaffEditor', + downgradeComponent({component: UnitStaffEditorComponent}), +); DoubtfireAngularJSModule.directive( 'unauthorised', downgradeComponent({component: UnauthorisedComponent}), ); +DoubtfireAngularJSModule.directive( + 'fPortfolioGradeSelectStep', + downgradeComponent({component: PortfolioGradeSelectStepComponent}), +); + // Global configuration // If the user enters a URL that doesn't match any known URL (state), send them to `/home` diff --git a/src/app/projects/states/portfolio/directives/directives.coffee b/src/app/projects/states/portfolio/directives/directives.coffee index 8ce9be0be3..661acd16e7 100644 --- a/src/app/projects/states/portfolio/directives/directives.coffee +++ b/src/app/projects/states/portfolio/directives/directives.coffee @@ -1,6 +1,5 @@ angular.module('doubtfire.projects.states.portfolio.directives', [ 'doubtfire.projects.states.portfolio.directives.portfolio-add-extra-files-step' - 'doubtfire.projects.states.portfolio.directives.portfolio-grade-select-step' 'doubtfire.projects.states.portfolio.directives.portfolio-learning-summary-report-step' 'doubtfire.projects.states.portfolio.directives.portfolio-review-step' 'doubtfire.projects.states.portfolio.directives.portfolio-tasks-step' diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee deleted file mode 100644 index 3a7ee7f2a4..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee +++ /dev/null @@ -1,22 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-grade-select-step', []) - -# -# Allows students to select the target grade they are hoping -# to achieve with their portfolio -# -.directive('portfolioGradeSelectStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html' - controller: ($scope, newProjectService, gradeService) -> - if ! $scope.project.submittedGrade - $scope.project.submittedGrade = 0 - $scope.grades = gradeService.gradeValues - $scope.gradeName = (grade) -> gradeService.grades[grade] - $scope.agreedToAssessmentCriteria = $scope.projectHasLearningSummaryReport() - $scope.chooseGrade = (idx) -> - $scope.project.submittedGrade = idx - newProjectService.update($scope.project).subscribe((project) -> - $scope.project.refreshBurndownChartData() - ) -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html new file mode 100644 index 0000000000..ad2c4c4f0c --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.html @@ -0,0 +1,96 @@ +
+ + + + +

Select Grade

+
+
+ + +

+ In preparing your portfolio, you need to undertake a self-assessment. Use the unit's + assessment criteria to determine the grade your portfolio should be awarded. +

+ + + + + + warning + Read the assessment criteria + + + + +

+ Make sure that you have reviewed the Assessment Criteria for the grade you are applying + for. Each grade will have a list of criteria that you can use to determine if you meet + the requirements to achieve that grade. +

+
+ + + + I have read the Assessment Criteria for this unit + + +
+ + + + @if (agreedToAssessmentCriteria) { + + + + Grade Application + + + + +

+ Select the grade you are applying for {{ unit.code }} + {{ unit.name }} below. +

+
+ + + + @for (grade of gradeValues; track grade) { + + + + } + + +

+ Make sure your Learning Summary Report justifies how your portfolio + demonstrates you have + met all unit learning outcomes to a {{ targetGrade }} level +

+
+
+ } +
+ + + + + + +
+
diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts new file mode 100644 index 0000000000..cce8ee1748 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component.ts @@ -0,0 +1,57 @@ +import {Component, Injector, Input} from '@angular/core'; +import {Project, Unit} from 'src/app/api/models/doubtfire-model'; +import {ProjectService} from 'src/app/api/services/project.service'; +import {GradeService} from 'src/app/common/services/grade.service'; + +@Component({ + selector: 'f-portfolio-grade-select-step', + templateUrl: 'portfolio-grade-select-step.component.html', + styleUrls: ['portfolio-grade-select-step.component.scss'], +}) +export class PortfolioGradeSelectStepComponent { + @Input() project: Project; + @Input() unit: Unit; + + public agreedToAssessmentCriteria: boolean = false; + + constructor( + private gradeService: GradeService, + private injector: Injector, + private projectService: ProjectService, + ) { + this.$scope = this.injector.get('$scope'); + } + + public get gradeValues() { + return this.gradeService.gradeValues; + } + + updateSubmittedGrade(newGrade: number): void { + const previousSubmittedGrade = this.project.submittedGrade; + this.project.submittedGrade = newGrade; + + this.projectService.update(this.project).subscribe( + (project) => { + project.refreshBurndownChartData?.(); + }, + (error) => { + this.project.submittedGrade = previousSubmittedGrade; + console.error('Error updating target grade:', error); + }, + ); + } + + // TODO: remove this once parent component has been migrated + private $scope: any; + goToNextStep(): void { + if (typeof this.$scope?.advanceActiveTab === 'function') { + this.$scope.advanceActiveTab(1); + } + } + + goToPreviousStep(): void { + if (typeof this.$scope?.advanceActiveTab === 'function') { + this.$scope.advanceActiveTab(-1); + } + } +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss deleted file mode 100644 index bfc229c4b1..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.scss +++ /dev/null @@ -1,10 +0,0 @@ -.project-portfolio-wizard .portfolio-grade-select-step { - .confirm-read-assessment-criteria { - font-size: 1.2em; - } - .select-the-grade { - .btn { - padding: 1em; - } - } -} diff --git a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html deleted file mode 100644 index 097b685e35..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.tpl.html +++ /dev/null @@ -1,60 +0,0 @@ -
-
-

Select Grade

-
-
-

- In preparing your portfolio, you need to undertake a self assessment. Use the unit's assessment criteria to - determine the grade your portfolio should be awarded. -

-
-
-

Read the assessment criteria

- Make sure that you have reviewed the Assessment Criteria for the grade you are applying for. Each grade will - have a list of criteria that you can use to determine if you meet the requirements to achieve that grade. -
-
- - -
-
- -
-
-

Grade Application

- Select the grade you are applying for {{unit.name}} below. -
-
-
- -
-

- Make sure your Learning Summary Report justifies how your portfolio demonstrates you have - met all unit learning outcomes to a {{gradeName(project.submittedGrade)}} level -

-
-
- -
- - -
diff --git a/src/app/projects/states/portfolio/portfolio.tpl.html b/src/app/projects/states/portfolio/portfolio.tpl.html index 1c009b47ab..ae536e081f 100644 --- a/src/app/projects/states/portfolio/portfolio.tpl.html +++ b/src/app/projects/states/portfolio/portfolio.tpl.html @@ -7,7 +7,11 @@ - + + From 95402a6f6678d9bad066386b60f0ce5a3b3766aa Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Wed, 8 Oct 2025 09:31:06 +1100 Subject: [PATCH 12/20] refactor: migrate/tutorials (#934) (#1013) * refactor: migrate/tutorials (#934) * chore: migrate tutorials - unlink old component - link new component - delete old files - add UI-router state declaration - remove reference to old state * chore: migrate tutorials - delete old files * chore: migrate tutorials - declare and define `tutorials` Typescript class - define `tutorials` markup in Angular and Angular material - add stylesheet * chore: migrate tutorials - delete old template * chore: amend file name - change template file name to adhere to styling convention - add `todo` comment to Typescript file --------- Co-authored-by: Boink <40929320+b0ink@users.noreply.github.com> * fix: map unit staff - 9.x api currently exposes :staff instead of :unit_roles * fix: accurately check for tutorial enrolment * fix: ensure tutorial for current enrolment is displayed * refactor: use mat table * refactor: improve tutorials component and add sorting - fetch unit to ensure tutorials are loaded correctly - add table sorting * chore: format * chore: remove styling * chore: remove unused class * chore: reword description --------- Co-authored-by: Jason Vellucci --- src/app/api/models/project.ts | 2 +- src/app/api/services/unit.service.ts | 3 +- src/app/doubtfire-angular.module.ts | 2 + src/app/doubtfire-angularjs.module.ts | 11 +- src/app/doubtfire.states.ts | 20 +++ src/app/projects/states/states.coffee | 1 - .../states/tutorials/tutorials.coffee | 23 --- .../states/tutorials/tutorials.component.html | 104 ++++++++++++ .../states/tutorials/tutorials.component.scss | 0 .../states/tutorials/tutorials.component.ts | 149 ++++++++++++++++++ .../projects/states/tutorials/tutorials.scss | 10 -- .../states/tutorials/tutorials.tpl.html | 71 --------- 12 files changed, 285 insertions(+), 111 deletions(-) delete mode 100644 src/app/projects/states/tutorials/tutorials.coffee create mode 100644 src/app/projects/states/tutorials/tutorials.component.html create mode 100644 src/app/projects/states/tutorials/tutorials.component.scss create mode 100644 src/app/projects/states/tutorials/tutorials.component.ts delete mode 100644 src/app/projects/states/tutorials/tutorials.scss delete mode 100644 src/app/projects/states/tutorials/tutorials.tpl.html diff --git a/src/app/api/models/project.ts b/src/app/api/models/project.ts index 380c62d199..3c154842e2 100644 --- a/src/app/api/models/project.ts +++ b/src/app/api/models/project.ts @@ -347,7 +347,7 @@ export class Project extends Entity { } public isEnrolledIn(tutorial: Tutorial): boolean { - return this.tutorials.includes(tutorial); + return this.tutorials.some((t) => t.id === tutorial.id); } public updateUnitEnrolment(): void { diff --git a/src/app/api/services/unit.service.ts b/src/app/api/services/unit.service.ts index e04d3bec82..42c8211ab8 100644 --- a/src/app/api/services/unit.service.ts +++ b/src/app/api/services/unit.service.ts @@ -53,7 +53,8 @@ export class UnitService extends CachedEntityService { }, }, { - keys: 'unitRoles', + // keys: 'unitRoles', + keys: 'staff', toEntityOp: (data, key, entity) => { const unitRoleService = AppInjector.get(UnitRoleService); // Add staff diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index e65faa1d11..5396335ffe 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -277,6 +277,7 @@ const MY_DATE_FORMAT = { monthYearA11yLabel: 'MMMM yyyy', }, }; +import {TutorialsComponent} from './projects/states/tutorials/tutorials.component'; import {UnitStudentEnrolmentModalComponent} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component'; import {TaskStatusPieChartComponent} from './visualisations/task-status-pie-chart/taskstatuspiechart.component'; @@ -404,6 +405,7 @@ import {TaskStatusPieChartComponent} from './visualisations/task-status-pie-char TaskScormCardComponent, ScormExtensionCommentComponent, ScormExtensionModalComponent, + TutorialsComponent, UnitStaffEditorComponent, GroupSetSelectorComponent, PortfolioGradeSelectStepComponent, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 0a48a0b2b7..ade3d4e6d3 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -76,7 +76,6 @@ import 'build/src/app/projects/states/portfolio/directives/portfolio-tasks-step/ import 'build/src/app/projects/states/portfolio/directives/directives.js'; import 'build/src/app/projects/states/portfolio/portfolio.js'; import 'build/src/app/projects/states/index/index.js'; -import 'build/src/app/projects/states/tutorials/tutorials.js'; import 'build/src/app/projects/project-outcome-alignment/project-outcome-alignment.js'; import 'build/src/app/admin/modals/modals.js'; import 'build/src/app/groups/group-selector/group-selector.js'; @@ -210,11 +209,14 @@ import {FTaskDetailsViewComponent} from './units/task-viewer/directives/task-det import {FTaskSheetViewComponent} from './units/task-viewer/directives/task-sheet-view/task-sheet-view.component'; import {ProgressBurndownChartComponent} from './visualisations/progress-burndown-chart/progressburndownchart.component'; import {TaskVisualisationComponent} from './visualisations/task-visualisation/taskvisualisation.component'; +import {TutorialsComponent} from './projects/states/tutorials/tutorials.component'; import {ProgressDashboardComponent} from './projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component'; import {FUnitsComponent} from './admin/states/units/units.component'; import {AlertService} from './common/services/alert.service'; import {GradeService} from './common/services/grade.service'; import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component'; +import {UnitStudentEnrolmentModalService} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; +import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; // import { UnitStudentEnrolmentModalService } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; // import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; @@ -222,9 +224,6 @@ import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staf import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; -import {UnitStudentEnrolmentModalService} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; -import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; - export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', 'doubtfire.sessions', @@ -486,6 +485,10 @@ DoubtfireAngularJSModule.directive( ); DoubtfireAngularJSModule.directive('newFUnits', downgradeComponent({component: FUnitsComponent})); +DoubtfireAngularJSModule.directive( + 'fTutorials', + downgradeComponent({component: TutorialsComponent}), +); DoubtfireAngularJSModule.directive( 'unitStaffEditor', downgradeComponent({component: UnitStaffEditorComponent}), diff --git a/src/app/doubtfire.states.ts b/src/app/doubtfire.states.ts index 7baa910f57..d2a7226cb2 100644 --- a/src/app/doubtfire.states.ts +++ b/src/app/doubtfire.states.ts @@ -14,6 +14,7 @@ import {ProjectRootState} from './projects/states/project-root-state.component'; import { TaskViewerState } from './units/task-viewer/task-viewer-state.component'; import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; import { Ng2ViewDeclaration } from '@uirouter/angular'; +import { TutorialsComponent } from './projects/states/tutorials/tutorials.component'; /* * Use this file to store any states that are sourced by angular components. @@ -411,6 +412,24 @@ const ScormPlayerReviewState: NgHybridStateDeclaration = { }, }; +const TutorialState: NgHybridStateDeclaration = { + name: 'projects/tutorials', + url: '/tutorials/project/:projectId', + views: { + main: { + component: TutorialsComponent, // Link to the Angular component + }, + }, + resolve: { + projectId: ['$stateParams', ($stateParams) => $stateParams.projectId], // Resolve the project object + }, + data: { + task: 'Tutorial List', + pageTitle: '_Home_', + roleWhiteList: ['Tutor', 'Convenor', 'Admin', 'Student', 'Auditor'], // Roles allowed to access this state + }, +}; + /** * Export the list of states we have created in angular */ @@ -433,4 +452,5 @@ export const doubtfireStates = [ ScormPlayerNormalState, ScormPlayerReviewState, ScormPlayerStudentReviewState, + TutorialState, ]; diff --git a/src/app/projects/states/states.coffee b/src/app/projects/states/states.coffee index 01b9d42dd4..a2a24c4bc4 100644 --- a/src/app/projects/states/states.coffee +++ b/src/app/projects/states/states.coffee @@ -1,7 +1,6 @@ angular.module('doubtfire.projects.states', [ 'doubtfire.projects.states.index' 'doubtfire.projects.states.dashboard' - 'doubtfire.projects.states.tutorials' 'doubtfire.projects.states.portfolio' 'doubtfire.projects.states.groups' 'doubtfire.projects.states.outcomes' diff --git a/src/app/projects/states/tutorials/tutorials.coffee b/src/app/projects/states/tutorials/tutorials.coffee deleted file mode 100644 index 5c22b609e3..0000000000 --- a/src/app/projects/states/tutorials/tutorials.coffee +++ /dev/null @@ -1,23 +0,0 @@ -angular.module('doubtfire.projects.states.tutorials', []) - -# -# Tasks state for projects -# -.config(($stateProvider) -> - $stateProvider.state 'projects/tutorials', { - parent: 'projects/index' - url: '/tutorials' - controller: 'ProjectsTutorialsStateCtrl' - templateUrl: 'projects/states/tutorials/tutorials.tpl.html' - data: - task: "Tutorial List" - pageTitle: "_Home_" - } -) - -.controller("ProjectsTutorialsStateCtrl", ($scope) -> - if $scope.unit.tutorialStreamsCache.size > 0 - $scope.sortOrder = 'tutorialStream.name' - else - $scope.sortOrder = 'abbreviation' -) diff --git a/src/app/projects/states/tutorials/tutorials.component.html b/src/app/projects/states/tutorials/tutorials.component.html new file mode 100644 index 0000000000..39ec57fbf8 --- /dev/null +++ b/src/app/projects/states/tutorials/tutorials.component.html @@ -0,0 +1,104 @@ +
+
+

Tutorials

+

+ View available tutorials and manage your enrolment. Note that availability is subject to + capacity. If you are unable to enrol in a tutorial, please contact your unit coordinator. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Stream + @if (unit.tutorialStreamsCache.size > 0) { +
{{ tutorial.tutorialStream?.name || 'All' }}
+ } @else { +
N/A
+ } +
Campus + {{ tutorial.campus?.name || 'All' }} + Code + {{ tutorial.abbreviation }} + Day + {{ tutorial.meetingDay }} + Time + {{ shortTime(tutorial.meetingTime) }} + Room + {{ tutorial.meetingLocation }} + Tutor + {{ tutorial.tutorName }} + Actions + @if (project.isEnrolledIn(tutorial)) { + @if (unit.allowStudentChangeTutorial) { + + } @else { +
+ Enrolled +
+ } + } @else if (unit.allowStudentChangeTutorial) { + + } @else { +
+ + } +
+
diff --git a/src/app/projects/states/tutorials/tutorials.component.scss b/src/app/projects/states/tutorials/tutorials.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/tutorials/tutorials.component.ts b/src/app/projects/states/tutorials/tutorials.component.ts new file mode 100644 index 0000000000..6a28239240 --- /dev/null +++ b/src/app/projects/states/tutorials/tutorials.component.ts @@ -0,0 +1,149 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {Sort} from '@angular/material/sort'; +import {MatTableDataSource} from '@angular/material/table'; +import {Tutorial, UnitService} from 'src/app/api/models/doubtfire-model'; +import {Project} from 'src/app/api/models/project'; +import {Unit} from 'src/app/api/models/unit'; +import {ProjectService} from 'src/app/api/services/project.service'; + +@Component({ + selector: 'f-tutorials', + templateUrl: './tutorials.component.html', + styleUrls: ['./tutorials.component.scss'], +}) +export class TutorialsComponent implements OnInit { + @Input() projectId: number; + + filteredTutorials: Tutorial[] = []; + + project: Project; + unit: Unit; + + displayedColumns: string[] = [ + 'stream', + 'campus', + 'code', + 'day', + 'time', + 'room', + 'tutor', + 'actions', + ]; + + dataSource = new MatTableDataSource([]); + + constructor( + private projectService: ProjectService, + private unitService: UnitService, + ) {} + + ngOnInit(): void { + this.projectService.fetch(this.projectId).subscribe({ + next: (project) => { + this.unitService.get(project.unit.id).subscribe({ + next: (unit) => { + this.unit = unit; + this.project = project; + this.filteredTutorials = this.tutorialCampusFilter([...unit.tutorials], this.project); + this.dataSource.data = this.filteredTutorials; + }, + error: (error) => { + console.error('Error fetching unit:', error); + }, + }); + }, + error: (error) => { + console.error('Error fetching project:', error); + }, + }); + } + + /** + * Switches to the passed-in tutorial. + * + * @param tutorial + * + * @returns void + */ + switchToTutorial(tutorial: Tutorial): void { + this.project.switchToTutorial(tutorial); + } + + /** + * Filters a collection of passed-in tutorials based on the campus_id of the passed-in project. + * + * @param tutorials + * @param project + * + * @returns Tutorial[] + */ + tutorialCampusFilter(tutorials: Tutorial[], project: Project): Tutorial[] { + if (!project) { + return tutorials; + } + return tutorials.filter((tutorial) => { + return ( + !project.campus?.id || + !tutorial.campus || + tutorial.campus.id === project.campus.id || + project.isEnrolledIn(tutorial) + ); + }); + } + + /** + * Formats the passed-in time string to the format of: HH:mm + * Todo: Add date validation + * @param meetingTime + * + * @returns string + */ + shortTime(meetingTime: string): string { + const [hours, minutes] = meetingTime.split(':'); + const formattedHours = hours.padStart(2, '0'); + const formattedMinutes = minutes.padStart(2, '0'); + + return `${formattedHours}:${formattedMinutes}`; + } + + private sortCompare(aValue: number | string, bValue: number | string, isAsc: boolean) { + return (aValue < bValue ? -1 : 1) * (isAsc ? 1 : -1); + } + + sortTableData(sort: Sort) { + if (!sort.active || sort.direction === '') { + return; + } + this.dataSource.data = this.dataSource.data.sort((a, b) => { + switch (sort.active) { + case 'stream': + return this.sortCompare( + a.tutorialStream?.name, + b.tutorialStream?.name, + sort.direction === 'asc', + ); + case 'campus': + return this.sortCompare(a.campus?.name, b.campus?.name, sort.direction === 'asc'); + case 'code': + return this.sortCompare(a.abbreviation, b.abbreviation, sort.direction === 'asc'); + case 'day': { + return this.sortCompare(a.meetingDay, b.meetingDay, sort.direction === 'asc'); + } + case 'time': { + return this.sortCompare( + this.shortTime(a.meetingTime), + this.shortTime(b.meetingTime), + sort.direction === 'asc', + ); + } + case 'room': { + return this.sortCompare(a.meetingLocation, b.meetingLocation, sort.direction === 'asc'); + } + case 'tutor': + return this.sortCompare(a.tutorName, b.tutorName, sort.direction === 'asc'); + default: + return 0; + } + }); + } +} diff --git a/src/app/projects/states/tutorials/tutorials.scss b/src/app/projects/states/tutorials/tutorials.scss deleted file mode 100644 index d402eae6a6..0000000000 --- a/src/app/projects/states/tutorials/tutorials.scss +++ /dev/null @@ -1,10 +0,0 @@ -#tutorials-state table { - th.stream { width: 10%; } - th.campus { width: 20%; } - th.code { width: 10%; } - th.day { width: 10%; } - th.time { width: 10%; } - th.room { width: 10%; } - th.tutor { width: 15%; } - th.actions { width: 15%; } -} diff --git a/src/app/projects/states/tutorials/tutorials.tpl.html b/src/app/projects/states/tutorials/tutorials.tpl.html deleted file mode 100644 index fae91d6ae3..0000000000 --- a/src/app/projects/states/tutorials/tutorials.tpl.html +++ /dev/null @@ -1,71 +0,0 @@ -
-
-
-

Select a Tutorial

-
-
-

- Click the plus on the specific tutorial to enrol in that tutorial, or click the minus icon to withdraw from your - current tutorial. -

-
- - - - - - - - - - - - - - - - - - - - - - - - - -
- Stream - - Campus - - Code - - Day - - Time - - Room - - Tutor - Actions
{{tutorial.tutorialStream.name || 'All'}}{{tutorial.campus ? tutorial.campus.name : 'All'}}{{tutorial.abbreviation}}{{tutorial.meetingDay}}{{tutorial.meetingTime | date: 'shortTime'}}{{tutorial.meetingLocation}}{{tutorial.tutorName}} - - -
-
-
From 85518268d0720f88ed7844c9af4bf187a3c49a60 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:52:45 +1100 Subject: [PATCH 13/20] refactor: migrate group members list (#1017) * refactor: init group members list migration * refactor: migrate group members list * chore: update loading text * chore: remove old component files --- src/app/doubtfire-angular.module.ts | 2 + src/app/doubtfire-angularjs.module.ts | 7 +- .../group-member-list.coffee | 58 ---------------- .../group-member-list.component.html | 55 +++++++++++++++ .../group-member-list.component.scss | 0 .../group-member-list.component.ts | 68 +++++++++++++++++++ .../group-member-list/group-member-list.scss | 6 -- .../group-member-list.tpl.html | 55 --------------- .../group-set-manager.tpl.html | 14 ++-- src/app/groups/groups.coffee | 1 - 10 files changed, 138 insertions(+), 128 deletions(-) delete mode 100644 src/app/groups/group-member-list/group-member-list.coffee create mode 100644 src/app/groups/group-member-list/group-member-list.component.html create mode 100644 src/app/groups/group-member-list/group-member-list.component.scss create mode 100644 src/app/groups/group-member-list/group-member-list.component.ts delete mode 100644 src/app/groups/group-member-list/group-member-list.scss delete mode 100644 src/app/groups/group-member-list/group-member-list.tpl.html diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 5396335ffe..ff8b001f19 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -280,6 +280,7 @@ const MY_DATE_FORMAT = { import {TutorialsComponent} from './projects/states/tutorials/tutorials.component'; import {UnitStudentEnrolmentModalComponent} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component'; import {TaskStatusPieChartComponent} from './visualisations/task-status-pie-chart/taskstatuspiechart.component'; +import {GroupMemberListComponent} from './groups/group-member-list/group-member-list.component'; @NgModule({ // Components we declare @@ -409,6 +410,7 @@ import {TaskStatusPieChartComponent} from './visualisations/task-status-pie-char UnitStaffEditorComponent, GroupSetSelectorComponent, PortfolioGradeSelectStepComponent, + GroupMemberListComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index ade3d4e6d3..c3fe30e065 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -82,7 +82,6 @@ import 'build/src/app/groups/group-selector/group-selector.js'; import 'build/src/app/groups/group-set-manager/group-set-manager.js'; import 'build/src/app/groups/groups.js'; import 'build/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.js'; -import 'build/src/app/groups/group-member-list/group-member-list.js'; import 'build/src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.js'; import 'build/src/app/units/modals/modals.js'; import 'build/src/app/units/units.js'; @@ -223,6 +222,7 @@ import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; +import {GroupMemberListComponent} from './groups/group-member-list/group-member-list.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -530,3 +530,8 @@ DoubtfireAngularJSModule.directive( 'groupSetSelector', downgradeComponent({component: GroupSetSelectorComponent}), ); + +DoubtfireAngularJSModule.directive( + 'fGroupMemberList', + downgradeComponent({component: GroupMemberListComponent}), +); diff --git a/src/app/groups/group-member-list/group-member-list.coffee b/src/app/groups/group-member-list/group-member-list.coffee deleted file mode 100644 index ddb80ece67..0000000000 --- a/src/app/groups/group-member-list/group-member-list.coffee +++ /dev/null @@ -1,58 +0,0 @@ -angular.module('doubtfire.groups.group-member-list', []) - -# -# Lists members in a group -# -.directive('groupMemberList', -> - restrict: 'E' - templateUrl: 'groups/group-member-list/group-member-list.tpl.html' - scope: - unit: '=' - project: '=' - unitRole: '=' - selectedGroup: '=' - onMembersLoaded: '=?' - controller: ($scope, $timeout, gradeService, alertService, listenerService) -> - # Cleanup - listeners = listenerService.listenTo($scope) - - # Initial sort orders - $scope.tableSort = - order: 'student_name' - reverse: false - - # Table sorting - $scope.sortTableBy = (column) -> - $scope.tableSort.order = column - $scope.tableSort.reverse = !$scope.tableSort.reverse - - # Loading - startLoading = -> $scope.loaded = false - finishLoading = -> $timeout(-> - $scope.loaded = true - $scope.onMembersLoaded?() - , 500) - - # Initially not loaded - $scope.loaded = false - - # Remove group members - $scope.removeMember = (member) -> - $scope.selectedGroup.removeMember(member) - - # Listen for changes to group - listeners.push $scope.$watch "selectedGroup.id", (newGroupId) -> - return unless newGroupId? - startLoading() - $scope.canRemoveMembers = $scope.unitRole || ($scope.selectedGroup.groupSet.allowStudentsToManageGroups && !$scope.selectedGroup.locked) - - $scope.selectedGroup.getMembers().subscribe({ - next: (members) -> - finishLoading() - error: (failure) -> - $timeout((-> - alertService.error( "Unauthorised to view members in this group", 3000) - $scope.selectedGroup = null - ), 1000) - }) -) diff --git a/src/app/groups/group-member-list/group-member-list.component.html b/src/app/groups/group-member-list/group-member-list.component.html new file mode 100644 index 0000000000..8d334d5640 --- /dev/null +++ b/src/app/groups/group-member-list/group-member-list.component.html @@ -0,0 +1,55 @@ +@if (loading) { +
+ Loading members... +
+} @else if (selectedGroup.members.length === 0) { +
+ group_off +

There are no members in this group

+
+} @else { + + + + + + + + + + + + + + + + + + + + + + + +
{{ unitRole ? 'Student ID' : '' }} + @if (unitRole) { + {{ member.student.username || 'N/A' }} + } + Name + {{ member.student.name }} + {{ unitRole ? 'Target Grade' : '' }} + @if (unitRole) { + + } + {{ canRemoveMembers ? 'Actions' : '' }} + @if (canRemoveMembers) { + @if (!project && unitRole) { + + } @else if (project && project.id === member.id) { + + } + } +
+} diff --git a/src/app/groups/group-member-list/group-member-list.component.scss b/src/app/groups/group-member-list/group-member-list.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/groups/group-member-list/group-member-list.component.ts b/src/app/groups/group-member-list/group-member-list.component.ts new file mode 100644 index 0000000000..e7f0e9f312 --- /dev/null +++ b/src/app/groups/group-member-list/group-member-list.component.ts @@ -0,0 +1,68 @@ +import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core'; +import {MatTableDataSource} from '@angular/material/table'; +import {Subscription} from 'rxjs'; +import {Group, UnitRole} from 'src/app/api/models/doubtfire-model'; +import {Project} from 'src/app/api/models/project'; +import {Unit} from 'src/app/api/models/unit'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-group-member-list', + templateUrl: './group-member-list.component.html', + styleUrls: ['./group-member-list.component.scss'], +}) +export class GroupMemberListComponent implements OnInit, OnChanges { + @Input() unit: Unit; + @Input() unitRole: UnitRole; + @Input() project: Project; + @Input() selectedGroup: Group; + @Input() onMembersLoaded: () => void; + + loading = false; + + canRemoveMembers = false; + + displayedColumns: string[] = ['student_id', 'name', 'target_grade', 'actions']; + groupMembers: Project[] = []; + dataSource = new MatTableDataSource(); + + private groupMembersSub?: Subscription; + + constructor(private alertService: AlertService) {} + + ngOnInit() { + this.groupMembersSub = this.selectedGroup.projectsCache.values.subscribe((values) => { + this.dataSource.data = values; + }); + } + + public removeMember(member: Project) { + this.selectedGroup.removeMember(member); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['selectedGroup'] && this.selectedGroup) { + this.loading = true; + this.selectedGroup.getMembers().subscribe({ + next: (members) => { + this.loading = false; + this.onMembersLoaded(); + this.canRemoveMembers = + !!this.unitRole || + (this.selectedGroup.groupSet.allowStudentsToManageGroups && !this.selectedGroup.locked); + + this.dataSource.data = members; + + this.groupMembersSub?.unsubscribe(); + this.groupMembersSub = this.selectedGroup.projectsCache.values.subscribe((values) => { + this.dataSource.data = values; + }); + }, + error: (error) => { + this.alertService.error(`Failed to fetch group members: ${error}`, 6000); + this.selectedGroup = null; + }, + }); + } + } +} diff --git a/src/app/groups/group-member-list/group-member-list.scss b/src/app/groups/group-member-list/group-member-list.scss deleted file mode 100644 index 75fb259794..0000000000 --- a/src/app/groups/group-member-list/group-member-list.scss +++ /dev/null @@ -1,6 +0,0 @@ -group-member-list table { - th.student-id { width: 25%; } - th.student-name { width: 50%; } - th.actions { width: 25%; } - th.student-grade { width: 50%; } -} diff --git a/src/app/groups/group-member-list/group-member-list.tpl.html b/src/app/groups/group-member-list/group-member-list.tpl.html deleted file mode 100644 index 8c3b3c2dc5..0000000000 --- a/src/app/groups/group-member-list/group-member-list.tpl.html +++ /dev/null @@ -1,55 +0,0 @@ -
- Loading Members... -
-
-
-

No members in group

-

There are no members in this group

-
-
- - - - - - - - - - - - - - - - - -
- - Student ID - - - - - Name - - - - - Target Grade - - - - Actions -
{{member.student.username || "N/A"}}{{member.student.name}} - - - - -
diff --git a/src/app/groups/group-set-manager/group-set-manager.tpl.html b/src/app/groups/group-set-manager/group-set-manager.tpl.html index 09386b1daa..da022a0b37 100644 --- a/src/app/groups/group-set-manager/group-set-manager.tpl.html +++ b/src/app/groups/group-set-manager/group-set-manager.tpl.html @@ -56,13 +56,13 @@

- - + diff --git a/src/app/groups/groups.coffee b/src/app/groups/groups.coffee index 391a78d492..3d4534d754 100644 --- a/src/app/groups/groups.coffee +++ b/src/app/groups/groups.coffee @@ -1,6 +1,5 @@ angular.module('doubtfire.groups', [ 'doubtfire.groups.group-member-contribution-assigner' - 'doubtfire.groups.group-member-list' 'doubtfire.groups.group-selector' 'doubtfire.groups.group-set-manager' ]) From 26407d86276089fa4ff19ff28b25da9aba34d7e3 Mon Sep 17 00:00:00 2001 From: Jason Vellucci Date: Sat, 25 Oct 2025 17:28:20 +1100 Subject: [PATCH 14/20] docs: update frontend migration progress list (#982) * docs: update frontend migration progress list - Mark all components in the `thoth-tech/9.x` branch as `MIGRATED` if they exist as TS files. - Mark components with open PRs against the `doubtfire-lms` repo as `MIGRATED`. * docs: update frontend migration progress list - remove completed migrations from TODO section * docs: update frontend migration progress list - Amend progress list * docs: update frontend migration progress list - resolve discrepancies in progress list - add additional section for components `awaiting upstream review` - add instructions for PR author and upstream repo maintainer - update summary section * docs: fix typos - fix typos for components in progress list * docs: amend frontend migration list changes - remove `AWAITING UPSTREAM REVIEW` section as this wouldn't work for our workflow - move components from the `AWAITING UPSTREAM REVIEW` section back to the `TODO` section - leave comment to update `migration progress list` on all PRs for components that were previously in the `AWAITING UPSTREAM REVIEW` section. to verify this, search the PR section in `doubtfire-lms/doubtfire-web` * docs: further updates to README.MD - move migrated component to `MIGRATED` section of `Migration Progress List` * docs: amend migration progress list - mark components that have .ts files as being migrated * docs: amend frontend migration progress list - amend the `No longer in thoth-tech/9.x` section of the frontend migration progress list * docs: remove duplicates & update tally - remove duplicates from the MIGRATED section of the frontend migration progress list - amend tally * docs: fix formatting - fix formatting of frontend migration progress list in README.MD * chore: update repository reference --------- Co-authored-by: Boink <40929320+b0ink@users.noreply.github.com> --- README.md | 115 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 64 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 34fcf6a733..36dbc6e0ea 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,35 @@ A modern, lightweight learning management system. ## Migration Progress +Important: When completing a frontend migration, please update the below list regarding the component you have migrated. + SUMMARY: -74 / 132 components migrated +- `89 / 183` components migrated +- `19` components no longer in the doubtfire-lms/9.x branch + + +NO LONGER IN doubtfire-lms/9.x + +- [x] ./src/app/projects/states/all/directives/all-projects-list/all-projects-list.coffee +- [x] ./src/app/projects/states/all/all.coffee +- [x] ./src/app/groups/tutor-group-manager/tutor-group-manager.coffee +- [x] ./src/app/tasks/task-definition-selector/task-definition-selector.coffee +- [x] ./src/app/tasks/task-status-selector/task-status-selector.coffee +- [x] ./src/app/config/debug/debug.coffee +- [x] ./src/app/projects/states/all/directives/directives.coffee +- [x] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee +- [x] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/task-outcomes-card/task-outcomes-card.coffee +- [x] ./src/app/admin/states/states.coffee +- [x] ./src/app/admin/admin.coffee +- [x] ./src/app/units/states/tasks/viewer/directives/directives.coffee +- [x] ./src/app/units/states/tasks/viewer/viewer.coffee +- [x] ./src/app/units/states/all/directives/all-units-list/all-units-list.coffee +- [x] ./src/app/units/states/all/directives/directives.coffee +- [x] ./src/app/units/states/all/all.coffee +- [x] ./src/app/common/alert-list/alert-list.coffee +- [x] ./src/app/common/modals/progress-modal/progress-modal.coffee +- [x] ./src/app/errors/states/not-found/not-found.coffee MIGRATED: @@ -25,6 +51,7 @@ MIGRATED: - [x] ./src/app/tasks/task-comments-viewer/extension-comment/extension-comment.component.ts - [x] ./src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-player.component.ts - [x] ./src/app/tasks/task-comments-viewer/intelligent-discussion-player/intelligent-discussion-recorder/intelligent-discussion-recorder.component.ts +- [x] ./src/app/tasks/project-tasks-list/project-tasks-list.coffee - [x] ./src/app/tasks/task-comments-viewer/pdf-image-comment/pdf-image-comment.component.ts - [x] ./src/app/tasks/task-comments-viewer/comment-bubble-action/comment-bubble-action.component.ts - [x] ./src/app/tasks/task-comments-viewer/task-comments-viewer.component.ts @@ -46,10 +73,10 @@ MIGRATED: - [x] ./src/app/admin/tii-action-log/tii-action-log.component.ts - [x] ./src/app/admin/states/teaching-periods/teaching-period-list/teaching-period-list.component.ts - [x] ./src/app/admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog.ts +- [x] ./src/app/admin/modals/create-unit-modal/create-new-unit-modal.component.ts - [x] ./src/app/eula/accept-eula/accept-eula.component.ts - [x] ./src/app/welcome/welcome.component.ts - [x] ./src/app/units/states/tasks/inbox/directives/staff-task-list/staff-task-list.component.ts -- [x] ./src/app/units/states/tasks/inbox/inbox.component.ts - [x] ./src/app/units/states/edit/directives/unit-students-editor/student-tutorial-select/student-tutorial-select.component.ts - [x] ./src/app/units/states/edit/directives/unit-students-editor/unit-students-editor.component.ts - [x] ./src/app/units/states/edit/directives/unit-students-editor/student-campus-select/student-campus-select.component.ts @@ -92,9 +119,30 @@ MIGRATED: - [x] ./src/app/common/services/alert.service.ts - [x] ./src/app/sessions/states/sign-in/sign-in.component.ts - [x] ./src/app/account/edit-profile/edit-profile.component.ts +- [x] ./src/app/tasks/modals/grade-task-modal/grade-task-modal.component.ts +- [x] ./src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component.ts +- [x] ./src/app/visualisations/progress-burndown-chart/progressburndownchart.component.ts +- [x] ./src/app/config/privacy-policy/privacy-policy.coffee +- [x] ./src/app/units/states/tasks/viewer/directives/task-sheet-view/task-sheet-view.coffee +- [x] ./src/app/units/states/tasks/viewer/directives/task-details-view/task-details-view.coffee +- [x] ./src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.coffee +- [x] ./src/app/projects/states/dashboard/directives/student-task-list/student-task-list.coffee +- [x] ./src/app/units/states/tasks/inbox/inbox.coffee +- [x] ./src/app/admin/states/units/units.component.ts +- [x] ./src/app/admin/states/users/users.component.ts +- [x] ./src/app/common/grade-icon/grade-icon.component.ts +- [x] ./src/app/common/services/grade.service.ts +- [x] ./src/app/common/services/alert.service.ts +- [x] ./src/app/errors/states/unauthorised/unauthorised.component.ts - [x] ./src/app/groups/group-set-selector/group-set-selector.component.ts - [x] ./src/app/admin/modals/create-unit-modal/create-unit-modal.coffee - [x] ./src/app/common/services/date.service.ts +- [x] ./src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee (IN 10.0.x) +- [x] ./src/app/groups/group-member-list/group-member-list.coffee +- [x] ./src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee +- [x] ./src/app/common/modals/confirmation-modal/confirmation-modal.coffee +- [x] ./src/app/common/modals/comments-modal/comments-modal.coffee (IN 10.0.x) + TODO: @@ -104,46 +152,33 @@ TODO: - [ ] ./src/app/visualisations/achievement-custom-bar-chart.coffee - [ ] ./src/app/visualisations/student-task-status-pie-chart.coffee - [ ] ./src/app/visualisations/alignment-bullet-chart.coffee -- [ ] ./src/app/visualisations/progress-burndown-chart.coffee - [ ] ./src/app/visualisations/task-status-pie-chart.coffee - [ ] ./src/app/visualisations/achievement-box-plot.coffee - [ ] ./src/app/visualisations/task-completion-box-plot.coffee - [ ] ./src/app/visualisations/visualisations.coffee -- [ ] ./src/app/tasks/task-status-selector/task-status-selector.coffee - [ ] ./src/app/tasks/tasks.coffee -- [ ] ./src/app/tasks/modals/modals.coffee - [ ] ./src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee -- [ ] ./src/app/tasks/modals/grade-task-modal/grade-task-modal.coffee -- [ ] ./src/app/tasks/task-definition-selector/task-definition-selector.coffee -- [ ] ./src/app/tasks/project-tasks-list/project-tasks-list.coffee -- [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.coffee - [ ] ./src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment-modal/task-ilo-alignment-modal.coffee - [ ] ./src/app/tasks/task-ilo-alignment/modals/task-ilo-alignment.coffee - [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.coffee - [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment.coffee - [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-viewer/task-ilo-alignment-viewer.coffee -- [ ] ./src/app/config/privacy-policy/privacy-policy.coffee +- [ ] ./src/app/tasks/task-ilo-alignment/task-ilo-alignment-rater/task-ilo-alignment-rater.coffee +- [ ] ./src/app/tasks/modals/modals.coffee - [ ] ./src/app/config/config.coffee - [ ] ./src/app/config/runtime/runtime.coffee - [ ] ./src/app/config/root-controller/root-controller.coffee - [ ] ./src/app/config/local-storage/local-storage.coffee -- [ ] ./src/app/config/routing/routing.coffee - [ ] ./src/app/config/vendor-dependencies/vendor-dependencies.coffee +- [ ] ./src/app/config/routing/routing.coffee - [ ] ./src/app/config/analytics/analytics.coffee -- [ ] ./src/app/config/debug/debug.coffee - [ ] ./src/app/projects/projects.coffee - [ ] ./src/app/projects/project-progress-dashboard/project-progress-dashboard.coffee - [ ] ./src/app/projects/states/states.coffee -- [ ] ./src/app/projects/states/all/directives/directives.coffee -- [ ] ./src/app/projects/states/all/directives/all-projects-list/all-projects-list.coffee -- [ ] ./src/app/projects/states/all/all.coffee - [ ] ./src/app/projects/states/groups/groups.coffee - [ ] ./src/app/projects/states/feedback/feedback.coffee - [ ] ./src/app/projects/states/dashboard/directives/directives.coffee - [ ] ./src/app/projects/states/dashboard/directives/progress-dashboard/progress-dashboard.coffee -- [ ] ./src/app/projects/states/dashboard/directives/student-task-list/student-task-list.coffee -- [ ] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/directives.coffee -- [ ] ./src/app/projects/states/dashboard/directives/task-dashboard/directives/task-outcomes-card/task-outcomes-card.coffee - [ ] ./src/app/projects/states/dashboard/directives/task-dashboard/task-dashboard.coffee - [ ] ./src/app/projects/states/dashboard/dashboard.coffee - [ ] ./src/app/projects/states/outcomes/outcomes.coffee @@ -151,45 +186,31 @@ TODO: - [ ] ./src/app/projects/states/portfolio/directives/directives.coffee - [ ] ./src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.coffee - [ ] ./src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee +- [ ] ./src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.coffee - [ ] ./src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee - [ ] ./src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee -- [ ] ./src/app/projects/states/portfolio/directives/portfolio-tasks-step/portfolio-tasks-step.coffee - [ ] ./src/app/projects/states/portfolio/portfolio.coffee - [ ] ./src/app/projects/states/index/index.coffee -- [ ] ./src/app/projects/states/tutorials/tutorials.coffee - [ ] ./src/app/projects/project-outcome-alignment/project-outcome-alignment.coffee +- [ ] ./src/app/projects/states/tutorials/tutorials.coffee - [ ] ./src/app/admin/modals/modals.coffee -- [ ] ./src/app/admin/states/states.coffee -- [ ] ./src/app/admin/states/units/units.coffee -- [ ] ./src/app/admin/states/users/users.coffee -- [ ] ./src/app/admin/admin.coffee - [ ] ./src/app/groups/group-selector/group-selector.coffee - [ ] ./src/app/groups/group-set-manager/group-set-manager.coffee - [ ] ./src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.coffee -- [ ] ./src/app/groups/group-member-list/group-member-list.coffee +- [ ] ./src/app/groups/group-set-selector/group-set-selector.coffee - [ ] ./src/app/groups/tutor-group-manager/tutor-group-manager.coffee - [ ] ./src/app/groups/groups.coffee -- [ ] ./src/app/units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.coffee +- [ ] ./src/app/units/states/groups/groups.coffee +- [ ] ./src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.coffee - [ ] ./src/app/units/modals/modals.coffee - [ ] ./src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.coffee - [ ] ./src/app/units/units.coffee - [ ] ./src/app/units/states/states.coffee -- [ ] ./src/app/units/states/tasks/inbox/inbox.coffee - [ ] ./src/app/units/states/tasks/tasks.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/directives.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/task-sheet-view/task-sheet-view.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/task-details-view/task-details-view.coffee -- [ ] ./src/app/units/states/tasks/viewer/directives/unit-task-list/unit-task-list.coffee -- [ ] ./src/app/units/states/tasks/viewer/viewer.coffee - [ ] ./src/app/units/states/tasks/definition/definition.coffee - [ ] ./src/app/units/states/portfolios/portfolios.coffee -- [ ] ./src/app/units/states/all/directives/all-units-list/all-units-list.coffee -- [ ] ./src/app/units/states/all/directives/directives.coffee -- [ ] ./src/app/units/states/all/all.coffee -- [ ] ./src/app/units/states/groups/groups.coffee +- [ ] ./src/app/units/states/analytics/analytics.coffee - [ ] ./src/app/units/states/edit/directives/directives.coffee -- [ ] ./src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.coffee -- [ ] ./src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.coffee - [ ] ./src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.coffee - [ ] ./src/app/units/states/edit/edit.coffee - [ ] ./src/app/units/states/rollover/directives/directives.coffee @@ -197,33 +218,25 @@ TODO: - [ ] ./src/app/units/states/rollover/rollover.coffee - [ ] ./src/app/units/states/index/index.coffee - [ ] ./src/app/units/states/students-list/students-list.coffee -- [ ] ./src/app/units/states/analytics/analytics.coffee -- [ ] ./src/app/common/filters/filters.coffee -- [ ] ./src/app/common/content-editable/content-editable.coffee -- [ ] ./src/app/common/alert-list/alert-list.coffee -- [ ] ./src/app/common/modals/confirmation-modal/confirmation-modal.coffee -- [ ] ./src/app/common/modals/comments-modal/comments-modal.coffee - [ ] ./src/app/common/modals/modals.coffee - [ ] ./src/app/common/modals/csv-result-modal/csv-result-modal.coffee -- [ ] ./src/app/common/modals/progress-modal/progress-modal.coffee -- [ ] ./src/app/common/grade-icon/grade-icon.coffee - [ ] ./src/app/common/file-uploader/file-uploader.coffee - [ ] ./src/app/common/common.coffee -- [ ] ./src/app/common/services/grade-service.coffee -- [ ] ./src/app/common/services/alert-service.coffee +- [ ] ./src/app/common/content-editable/content-editable.coffee - [ ] ./src/app/common/services/media-service.coffee - [ ] ./src/app/common/services/recorder-service.coffee - [ ] ./src/app/common/services/outcome-service.coffee - [ ] ./src/app/common/services/listener-service.coffee -- [ ] ./src/app/common/services/analytics-service.coffee - [ ] ./src/app/common/services/services.coffee +- [ ] ./src/app/common/services/date-service.coffee +- [ ] ./src/app/common/services/analytics-service.coffee - [ ] ./src/app/sessions/auth/http-auth-injector.coffee - [ ] ./src/app/sessions/sessions.coffee - [ ] ./src/app/errors/errors.coffee - [ ] ./src/app/errors/states/states.coffee -- [ ] ./src/app/errors/states/unauthorised/unauthorised.coffee -- [ ] ./src/app/errors/states/not-found/not-found.coffee - [ ] ./src/app/errors/states/timeout/timeout.coffee +- [ ] ./src/app/common/filters/filters.coffee + ## Table of Contents From 95cb9ace6615b45dd75161fd02d6dd130420817b Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:09:41 +1100 Subject: [PATCH 15/20] fix: ensure selected group is valid --- .../groups/group-member-list/group-member-list.component.html | 2 +- .../groups/group-member-list/group-member-list.component.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/groups/group-member-list/group-member-list.component.html b/src/app/groups/group-member-list/group-member-list.component.html index 8d334d5640..aa9cc8ae8a 100644 --- a/src/app/groups/group-member-list/group-member-list.component.html +++ b/src/app/groups/group-member-list/group-member-list.component.html @@ -2,7 +2,7 @@
Loading members...
-} @else if (selectedGroup.members.length === 0) { +} @else if (!selectedGroup || selectedGroup.members.length === 0) {
group_off

There are no members in this group

diff --git a/src/app/groups/group-member-list/group-member-list.component.ts b/src/app/groups/group-member-list/group-member-list.component.ts index e7f0e9f312..3e71170de3 100644 --- a/src/app/groups/group-member-list/group-member-list.component.ts +++ b/src/app/groups/group-member-list/group-member-list.component.ts @@ -31,6 +31,10 @@ export class GroupMemberListComponent implements OnInit, OnChanges { constructor(private alertService: AlertService) {} ngOnInit() { + if (!this.selectedGroup) { + return; + } + this.groupMembersSub = this.selectedGroup.projectsCache.values.subscribe((values) => { this.dataSource.data = values; }); From 8a2286774caa06952d63dd71fb12610999acdb4e Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Tue, 28 Oct 2025 08:45:12 +1100 Subject: [PATCH 16/20] refactor: group selector migration (#1030) * refactor: init group selector migration * refactor: move group set selector * chore: change color of locked icon * refactor: ability to join group * refactor: conditional rendering for project view * chore: ensure group is selected * chore: add information if group doesnt exist * chore: disable click event while editing * refactor: add filters * chore: fix margins * chore: fix margins * chore: remove coffeescript files * chore: cleanup * refactor: add locked icon * refactor: remove group set selector --- README.md | 6 +- src/app/doubtfire-angular.module.ts | 4 +- src/app/doubtfire-angularjs.module.ts | 11 +- .../group-selector/group-selector.coffee | 210 -------------- .../group-selector.component.html | 196 +++++++++++++ .../group-selector.component.scss | 9 + .../group-selector.component.ts | 264 ++++++++++++++++++ .../groups/group-selector/group-selector.scss | 45 --- .../group-selector/group-selector.tpl.html | 220 --------------- .../group-set-manager.coffee | 3 +- .../group-set-manager.tpl.html | 17 +- .../group-set-selector.component.html | 10 - .../group-set-selector.component.scss | 7 - .../group-set-selector.component.ts | 31 -- src/app/groups/groups.coffee | 1 - 15 files changed, 487 insertions(+), 547 deletions(-) delete mode 100644 src/app/groups/group-selector/group-selector.coffee create mode 100644 src/app/groups/group-selector/group-selector.component.html create mode 100644 src/app/groups/group-selector/group-selector.component.scss create mode 100644 src/app/groups/group-selector/group-selector.component.ts delete mode 100644 src/app/groups/group-selector/group-selector.scss delete mode 100644 src/app/groups/group-selector/group-selector.tpl.html delete mode 100644 src/app/groups/group-set-selector/group-set-selector.component.html delete mode 100644 src/app/groups/group-set-selector/group-set-selector.component.scss delete mode 100644 src/app/groups/group-set-selector/group-set-selector.component.ts diff --git a/README.md b/README.md index 36dbc6e0ea..77fafe2a61 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,6 @@ SUMMARY: - `89 / 183` components migrated - `19` components no longer in the doubtfire-lms/9.x branch - NO LONGER IN doubtfire-lms/9.x - [x] ./src/app/projects/states/all/directives/all-projects-list/all-projects-list.coffee @@ -142,7 +141,7 @@ MIGRATED: - [x] ./src/app/units/states/edit/directives/unit-staff-editor/unit-staff-editor.coffee - [x] ./src/app/common/modals/confirmation-modal/confirmation-modal.coffee - [x] ./src/app/common/modals/comments-modal/comments-modal.coffee (IN 10.0.x) - +- [x] ./src/app/groups/group-selector/group-selector.coffee TODO: @@ -194,10 +193,8 @@ TODO: - [ ] ./src/app/projects/project-outcome-alignment/project-outcome-alignment.coffee - [ ] ./src/app/projects/states/tutorials/tutorials.coffee - [ ] ./src/app/admin/modals/modals.coffee -- [ ] ./src/app/groups/group-selector/group-selector.coffee - [ ] ./src/app/groups/group-set-manager/group-set-manager.coffee - [ ] ./src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.coffee -- [ ] ./src/app/groups/group-set-selector/group-set-selector.coffee - [ ] ./src/app/groups/tutor-group-manager/tutor-group-manager.coffee - [ ] ./src/app/groups/groups.coffee - [ ] ./src/app/units/states/groups/groups.coffee @@ -237,7 +234,6 @@ TODO: - [ ] ./src/app/errors/states/timeout/timeout.coffee - [ ] ./src/app/common/filters/filters.coffee - ## Table of Contents - [Doubtfire Web ![CI](https://github.com/doubtfire-lms/doubtfire-web/actions/workflows/nodejs-ci.yml)](#doubtfire-web-) diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index ff8b001f19..61b1586d71 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -262,7 +262,6 @@ import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; // import {GradeTaskModalComponent} from './tasks/modals/grade-task-modal/grade-task-modal.component'; // import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; -import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; // See https://stackoverflow.com/questions/55721254/how-to-change-mat-datepicker-date-format-to-dd-mm-yyyy-in-simplest-way/58189036#58189036 @@ -281,6 +280,7 @@ import {TutorialsComponent} from './projects/states/tutorials/tutorials.componen import {UnitStudentEnrolmentModalComponent} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component'; import {TaskStatusPieChartComponent} from './visualisations/task-status-pie-chart/taskstatuspiechart.component'; import {GroupMemberListComponent} from './groups/group-member-list/group-member-list.component'; +import {GroupSelectorComponent} from './groups/group-selector/group-selector.component'; @NgModule({ // Components we declare @@ -408,9 +408,9 @@ import {GroupMemberListComponent} from './groups/group-member-list/group-member- ScormExtensionModalComponent, TutorialsComponent, UnitStaffEditorComponent, - GroupSetSelectorComponent, PortfolioGradeSelectStepComponent, GroupMemberListComponent, + GroupSelectorComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index c3fe30e065..29ec49ed15 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -78,7 +78,6 @@ import 'build/src/app/projects/states/portfolio/portfolio.js'; import 'build/src/app/projects/states/index/index.js'; import 'build/src/app/projects/project-outcome-alignment/project-outcome-alignment.js'; import 'build/src/app/admin/modals/modals.js'; -import 'build/src/app/groups/group-selector/group-selector.js'; import 'build/src/app/groups/group-set-manager/group-set-manager.js'; import 'build/src/app/groups/groups.js'; import 'build/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.js'; @@ -220,9 +219,9 @@ import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; // import { UnitStudentEnrolmentModalService } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; // import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staff-editor/unit-staff-editor.component'; -import {GroupSetSelectorComponent} from './groups/group-set-selector/group-set-selector.component'; import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; import {GroupMemberListComponent} from './groups/group-member-list/group-member-list.component'; +import {GroupSelectorComponent} from './groups/group-selector/group-selector.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -527,11 +526,11 @@ DoubtfireAngularJSModule.directive( ); DoubtfireAngularJSModule.directive( - 'groupSetSelector', - downgradeComponent({component: GroupSetSelectorComponent}), + 'fGroupMemberList', + downgradeComponent({component: GroupMemberListComponent}), ); DoubtfireAngularJSModule.directive( - 'fGroupMemberList', - downgradeComponent({component: GroupMemberListComponent}), + 'fGroupSelector', + downgradeComponent({component: GroupSelectorComponent}), ); diff --git a/src/app/groups/group-selector/group-selector.coffee b/src/app/groups/group-selector/group-selector.coffee deleted file mode 100644 index f6e0a56745..0000000000 --- a/src/app/groups/group-selector/group-selector.coffee +++ /dev/null @@ -1,210 +0,0 @@ -angular.module('doubtfire.groups.group-selector', []) - -# -# Allows tutors and students to select (and create if applicable) -# new groups for teamwork -# -.directive('groupSelector', -> - restrict: 'E' - templateUrl: 'groups/group-selector/group-selector.tpl.html' - scope: - unit: "=" - # Use project for student context - project: "=?" - # Use unit role for tutor context - unitRole: "=?" - # Pass in a groupset to set the groupset context - selectedGroupSet: '=' - # Bind the selected group for switching - selectedGroup: '=?' - # Shows the groupset selector - showGroupSetSelector: '=?' - # On change of a group - onSelect: '=?' - controller: ($scope, $filter, $timeout, alertService, listenerService, newUserService, newGroupService) -> - # Cleanup - listeners = listenerService.listenTo($scope) - - # Unit role or project should be included in $scope - if !$scope.unitRole? && !$scope.project? || $scope.unitRole? && $scope.project? - throw Error "Group selector must have exactly one unit role or one project" - - # Filtering - applyFilters = -> - if $scope.unitRole? # apply staff filter - filteredGroups = $filter('groupsInTutorials')($scope.selectedGroupSet.groups, $scope.unitRole, $scope.staffFilter) - else # apply project filter - filteredGroups = $scope.selectedGroupSet.groups - # Apply remaining filters - $scope.filteredGroups = $filter('paginateAndSort')(filteredGroups, $scope.pagination, $scope.tableSort) - - $scope.setStaffFilter = (scope) -> - $scope.staffFilter = scope - applyFilters() - - # Pagination values - $scope.pagination = - currentPage: 1 - maxSize: 10 - pageSize: 10 - totalSize: null - show: false - onChange: applyFilters - - # Initial sort orders - $scope.tableSort = - order: 'name' - reverse: false - - # Table sorting - $scope.sortTableBy = (column) -> - $scope.tableSort.order = column - $scope.tableSort.reverse = !$scope.tableSort.reverse - applyFilters() - - # Loading - startLoading = -> $scope.loaded = false - finishLoading = -> $timeout((-> - $scope.loaded = true - if $scope.project? - $scope.selectGroup($scope.project.groupForGroupSet($scope.selectedGroupSet)) - ), 500) - - # Select group function - $scope.selectGroup = (group) -> - return if $scope.project? && ! $scope.project.inGroup(group) # its the student view - - $scope.selectedGroup = group - $scope.onSelect?(group) - - # Sets the placeholder text (useful to know named - # groups are technically optional) - resetNewGroupForm = () -> - $scope.newGroupName = "" - - # Group set selector - $scope.selectedGroupSet ?= _.first($scope.unit.groupSets) - $scope.showGroupSetSelector ?= $scope.unit.groupSets.length > 1 - $scope.selectGroupSet = (groupSet) -> - return unless groupSet? - startLoading() - $scope.selectGroup(null) - # Can only create groups if unitRole provided and selectedGroupSet - $scope.canCreateGroups = $scope.unitRole? || groupSet?.allowStudentsToCreateGroups - $scope.unit.getGroups(groupSet).subscribe({ - next: (groups) -> - $scope.selectedGroupSet = groupSet - finishLoading() - resetNewGroupForm() - applyFilters() - error: (message) -> - finishLoading() - alertService.error( "Unable to get groups #{message}", 6000) - }) - - $scope.selectGroupSet($scope.selectedGroupSet) - - # Load groups if not loaded - # $scope.unit.getGroups($scope.selectedGroupSet.id) if $scope.selectedGroupSet?.groups? - - # Staff filter options (convenor should see all) - $scope.staffFilter = { - Convenor: 'all', - Tutor: 'mine' - }[$scope.unitRole.role] if $scope.unitRole? - - # Changing staff filter reapplies filter - $scope.onChangeStaffFilter = applyFilters - - # Search text reapplies filter - $scope.searchTextChanged = applyFilters - - # Adds a group to the unit - $scope.addGroup = (name) -> - if $scope.unit.tutorials.length == 0 - alertService.error( "Please ensure there is at least one tutorial before groups are created", 6000) - # Student context - if $scope.project - #TODO: Need to add stream to group set - tutorialId = $scope.project.tutorials[0].id || $scope.unit.tutorials[0].id - else - # Convenor or Tutor - tutorName = $scope.unitRole?.name || newUserService.currentUser.name - tutorialId = _.find($scope.unit.tutorials, (tute) -> tute.tutor?.name == tutorName)?.id - # Default to first tutorial if can't find - tutorialId ?= _.first($scope.unit.tutorials).id - - newGroupService.create({ - unitId: $scope.unit.id, - groupSetId: $scope.selectedGroupSet.id, - }, { - cache: $scope.selectedGroupSet.groupsCache, - constructorParams: $scope.unit - body: { - group: { - name: name, - tutorial_id: tutorialId - } - } - }).subscribe({ - next: (group) -> - resetNewGroupForm() - applyFilters() - $scope.selectedGroup = group - error: (message) -> alertService.error( message, 6000) - }) - - # Join or leave group as project - $scope.projectInGroup = (group) -> - $scope.project?.inGroup(group) - - $scope.joinGroup = (group) -> - return unless $scope.project? - partOfGroup = $scope.projectInGroup(group) - return alertService.error( "You are already member of this group") if partOfGroup - group.addMember($scope.project, - () -> - $scope.selectedGroup = group - () -> - ) - - # Update group function - $scope.updateGroup = (data, group) -> - group.capacityAdjustment = data.capacityAdjustment - group.tutorial = data.tutorial - group.name = data.name - - newGroupService.update(group).subscribe({ - next: () -> - alertService.success( "Updated group", 2000) - applyFilters() - error: (message) -> alertService.error( "Failed to update group. #{message}", 6000) - }) - - # Remove group function - $scope.deleteGroup = (group) -> - newGroupService.delete(group, { cache: $scope.selectedGroupSet.groupsCache }).subscribe({ - next: () -> - alertService.success( "Deleted group", 2000) - $scope.selectedGroup = null if group.id == $scope.selectedGroup?.id - resetNewGroupForm() - applyFilters() - error: () -> alertService.error( "Failed to delete group. #{message}", 6000) - }) - - # Toggle lockable group - $scope.toggleLocked = (group) -> - group.locked = !group.locked - newGroupService.update(group).subscribe({ - next: (success) -> - group.locked = success.locked - alertService.success( "Group updated", 2000) - error: () -> alertService.error( "Failed to lock group. #{message}", 6000) - }) - - # Watch selected group set changes - listeners.push $scope.$on 'UnitGroupSetEditor/SelectedGroupSetChanged', (evt, args) -> - newGroupSet = $scope.unit.findGroupSet(args.id) - # return if newGroupSet == $scope.selectedGroupSet - $scope.selectGroupSet(newGroupSet) -) diff --git a/src/app/groups/group-selector/group-selector.component.html b/src/app/groups/group-selector/group-selector.component.html new file mode 100644 index 0000000000..857f840a64 --- /dev/null +++ b/src/app/groups/group-selector/group-selector.component.html @@ -0,0 +1,196 @@ + + +
+
+
+ + Groups for + @if (!showGroupSetSelector && selectedGroup) { + "{{ selectedGroupSet?.name }}" + } + + + @if (showGroupSetSelector) { + + + @for (gs of unit.groupSets; track gs.id) { + {{ gs.name }} + } + + + } +
+ @if (unitRole || selectedGroupSet?.allowStudentsToCreateGroups) { +
+ + + + +
+ } +
+ @if (unitRole) { +
+ + All Tutorials + My Tutorials + +
+ } +
+
+ + @if (selectedGroupSet && selectedGroupSet.groups.length === 0) { +
+ group_off +

There are no groups in this set

+
+ } @else { + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + @if (editing(group)) { + + + + } @else { + {{ group.name || 'Not set' }} + } + Tutorial + @if (editing(group)) { + + + @for (tutorial of unit.tutorials; track tutorial) { + {{ tutorial.abbreviation }} + } + + + } @else { + {{ group.tutorial.abbreviation }} + } + + @if (unitRole) { + Capacity Adjustment + } + + @if (unitRole) { + @if (editing(group)) { + + + + } @else { + {{ group.capacityAdjustment }} + } + } + Capacity + @if (group.hasSpace()) { + Available + } @else { + Full + } + + @if (unitRole || (project && selectedGroupSet.allowStudentsToManageGroups)) { + Actions + } + + @if (project && group.hasSpace() && selectedGroupSet.allowStudentsToManageGroups) { +
+ @if (!group.locked && !selectedGroupSet.locked) { + + } @else { + lock + } +
+ } + @if (unitRole) { +
+ @if (editing(group)) { +
+ + +
+ } @else { + + + + } +
+ } +
+ + } + + @if (selectedGroupSet.keepGroupsInSameClass && selectedGroupSet.groups.length > 0 && !unitRole) { +

+ Can't see the group you need to join? Groups shown are limited to those in your allocated + tutorials. Use the + Tutorial List to check and update + your tutorial enrolment if needed. +

+ } +
diff --git a/src/app/groups/group-selector/group-selector.component.scss b/src/app/groups/group-selector/group-selector.component.scss new file mode 100644 index 0000000000..200fdfce50 --- /dev/null +++ b/src/app/groups/group-selector/group-selector.component.scss @@ -0,0 +1,9 @@ +.mat-mdc-row .mat-mdc-cell { + border-bottom: 1px solid transparent; + border-top: 1px solid transparent; + cursor: pointer; +} + +.mat-mdc-row:hover { + background-color: #eee; +} diff --git a/src/app/groups/group-selector/group-selector.component.ts b/src/app/groups/group-selector/group-selector.component.ts new file mode 100644 index 0000000000..f0fc2b3615 --- /dev/null +++ b/src/app/groups/group-selector/group-selector.component.ts @@ -0,0 +1,264 @@ +import { + AfterViewInit, + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewChild, +} from '@angular/core'; +import {UntypedFormControl, Validators} from '@angular/forms'; +import {MatButtonToggleChange} from '@angular/material/button-toggle'; +import {MatPaginator} from '@angular/material/paginator'; +import {MatTableDataSource} from '@angular/material/table'; +import {Subscription} from 'rxjs'; +import {Group, GroupSet, UnitRole, UserService} from 'src/app/api/models/doubtfire-model'; +import {Project} from 'src/app/api/models/project'; +import {Unit} from 'src/app/api/models/unit'; +import {GroupService} from 'src/app/api/services/group.service'; +import {EntityFormComponent} from 'src/app/common/entity-form/entity-form.component'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-group-selector', + templateUrl: './group-selector.component.html', + styleUrls: ['./group-selector.component.scss'], +}) +export class GroupSelectorComponent + extends EntityFormComponent + implements OnInit, OnChanges, AfterViewInit +{ + @Input() unit: Unit; + @Input() unitRole: UnitRole; + @Input() project: Project; + @Input() selectedGroup: Group; + @Input() selectedGroupSet: GroupSet; + @Input() onSelect: (group: Group) => void; + + @ViewChild(MatPaginator) paginator!: MatPaginator; + displayedColumns: string[] = ['name', 'tutorial', 'capacity_adjustment', 'capacity', 'actions']; + public groups: Group[] = []; + + public newGroupName: string; + public staffTutorialFilter: 'all' | 'mine' = 'all'; + + private groupsSub?: Subscription; + + constructor( + private userService: UserService, + private groupService: GroupService, + private alertService: AlertService, + ) { + super( + { + name: new UntypedFormControl('', [Validators.required]), + tutorial: new UntypedFormControl(null, [Validators.required]), + capacityAdjustment: new UntypedFormControl('', [Validators.required]), + }, + 'Group', + ); + } + + public get showGroupSetSelector() { + return this.unit.groupSets.length > 1; + } + + ngOnInit(): void { + if (this.unit.groupSets.length > 0) { + this.selectedGroupSet = this.unit.groupSets[0]; + } + } + + selectGroupSet(groupSet: GroupSet) { + this.selectedGroupSet = groupSet; + this.refreshGroups(); + } + + ngAfterViewInit() { + this.dataSource = new MatTableDataSource(); + this.dataSource.paginator = this.paginator; + + if (this.unit.groupSets.length > 0) { + this.selectedGroupSet = this.unit.groupSets[0]; + } + + this.refreshGroups(); + } + + refreshGroups() { + this.groupsSub?.unsubscribe(); + this.groupsSub = this.selectedGroupSet.groupsCache.values.subscribe((values) => { + this.groups = [...values]; + }); + this.applyFilters(); + } + + onGroupNameChange() { + this.applyFilters(); + } + + applyFilters() { + const filteredGroups = this.groups + .filter( + (g) => + this.staffTutorialFilter === 'all' || + (this.unitRole && g.tutorial.tutor.id === this.unitRole.user.id), + ) + .filter( + (g) => !this.newGroupName || g.name.toLowerCase().includes(this.newGroupName.toLowerCase()), + ); + + this.dataSource.data = filteredGroups.sort((a, b) => a.name.localeCompare(b.name)); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['selectedGroupSet'] && this.selectedGroupSet) { + if (!this.dataSource) { + this.dataSource = new MatTableDataSource(); + } + this.refreshGroups(); + } + } + + onTutorialFilterChange(event: MatButtonToggleChange) { + this.staffTutorialFilter = event.value; + this.applyFilters(); + } + + addGroup(name: string) { + if (this.unit.tutorials.length == 0) { + this.alertService.error( + `Please ensure there is at least one tutorial before groups are created`, + 6000, + ); + return; + } + let tutorialId = -1; + if (this.project) { + tutorialId = this.project.tutorials[0].id || this.unit.tutorials[0].id; + } else { + const tutorName = this.unitRole?.user.name || this.userService.currentUser.name; + tutorialId = + this.unit.tutorials.find((t) => t.tutor?.name === tutorName)?.id ?? + this.unit.tutorials[0].id; + } + + this.groupService + .create( + { + unitId: this.unit.id, + groupSetId: this.selectedGroupSet.id, + }, + { + cache: this.selectedGroupSet.groupsCache, + constructorParams: this.unit, + body: { + group: { + name, + tutorial_id: tutorialId, + }, + }, + }, + ) + .subscribe({ + next: (group) => { + this.alertService.success('Successfully created group', 3000); + this.selectedGroup = group; + this.newGroupName = ''; + this.applyFilters(); + }, + error: (error) => { + this.alertService.error(`Failed to create group: ${error}`); + }, + }); + } + + isPartOfGroup(project: Project, group: Group) { + return project.inGroup(group); + } + + joinGroup(group: Group) { + if (!this.project) { + return; + } + + if (this.isPartOfGroup(this.project, group)) { + this.alertService.error('You are already member of this group'); + return; + } + + group.addMember(this.project, () => { + this.selectedGroup = group; + this.selectGroup(group); + }); + } + + selectGroup(group: Group) { + if (this.project && !this.project.inGroup(group)) { + // Return because we're in the student view + return; + } + + if (this.editing(group)) { + return; + } + + this.selectedGroup = group; + this.onSelect(group); + } + + deleteGroup(event: Event, group: Group) { + event.stopPropagation(); + + this.groupService.delete(group, {cache: this.selectedGroupSet.groupsCache}).subscribe({ + next: () => { + this.alertService.success('Deleted group', 3000); + if (group.id === this.selectedGroup?.id) { + this.selectedGroup = null; + this.selectGroup(null); + } + }, + error: (error) => { + this.alertService.error(`Failed to delete group: ${error}`, 6000); + }, + }); + } + + toggleLocked(event: Event, group: Group) { + event.stopPropagation(); + + const originalLockedState = group.locked; + group.locked = !group.locked; + + this.groupService.update(group).subscribe({ + next: (success) => { + group.locked = success.locked; + this.alertService.success(`Group has been ${!group.locked ? 'un' : ''}locked`, 3000); + }, + error: (error) => { + this.alertService.error(`Failed to ${!group.locked ? 'un' : ''}lock group: ${error}`, 6000); + group.locked = originalLockedState; + }, + }); + } + + startEditGroup(event: Event, group: Group) { + event.stopPropagation(); + this.flagEdit(group); + } + + cancelEditGroup(event: Event) { + event.stopPropagation(); + this.cancelEdit(); + } + + saveEdit(event: Event) { + event.stopPropagation(); + super.submit(this.groupService, this.alertService, this.onSuccess.bind(this)); + this.cancelEdit(); + } + + onSuccess(): void { + this.refreshGroups(); + } +} diff --git a/src/app/groups/group-selector/group-selector.scss b/src/app/groups/group-selector/group-selector.scss deleted file mode 100644 index c176bf951a..0000000000 --- a/src/app/groups/group-selector/group-selector.scss +++ /dev/null @@ -1,45 +0,0 @@ -group-selector { - display: block; -} -group-selector table { - th.name { - width: 25%; - } - th.tutorial { - width: 15%; - } - th.capacity_adjustment { - width: 15%; - } - th.capacity { - width: 15%; - } - th.actions { - width: 25%; - } -} -group-selector .panel-title > group-set-selector { - display: inline-block; - max-width: 50%; - padding-left: 1ex; -} -@media (max-width: $screen-md) { - group-selector .input-group.staff-filter { - margin-bottom: 1em; - &, - .btn-group { - width: 100%; - } - .btn { - width: 50%; - } - } -} - -.lockButton { - width: 70px; -} - -.joinButton { - width: 70px; -} diff --git a/src/app/groups/group-selector/group-selector.tpl.html b/src/app/groups/group-selector/group-selector.tpl.html deleted file mode 100644 index e0a9446a14..0000000000 --- a/src/app/groups/group-selector/group-selector.tpl.html +++ /dev/null @@ -1,220 +0,0 @@ -
-

- Groups for - {{selectedGroupSet.name}} - - -

-
- -
-
-
-
- - -
-
- -
- - - - -
- -
-
- -
Loading Groups...
- -
-
-

No Groups To Show

-

- There are no groups available for {{selectedGroupSet.name}}{{staffFilter == 'mine' || - selectedGroupSet.keepGroupsInSameClass ? " in your tutorials." : ""}}{{newGroupName.length > 0 ? " with name " + newGroupName + "." : "."}} -

-

- Please make sure that you are enrolled in the correct tutorial. You can only join a group that is running in your - allocated tutorial. Use the Tutorial List to - check and update your tutorial enrolment. -

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
- - Name - - - - Tutorial - - - - - Capacity Adjustment - - - - - Capacity - - - - Actions -
- - {{ group.name || 'Not Set' }} - - - - - {{group.tutorial.abbreviation}} - - - - - {{group.capacityAdjustment}} - - - Available - Full - -
- - - - -
-
-
- - -
- - -
-
- diff --git a/src/app/groups/group-set-manager/group-set-manager.coffee b/src/app/groups/group-set-manager/group-set-manager.coffee index 5e8506f7cb..91d1d81c1c 100644 --- a/src/app/groups/group-set-manager/group-set-manager.coffee +++ b/src/app/groups/group-set-manager/group-set-manager.coffee @@ -17,7 +17,8 @@ angular.module('doubtfire.groups.group-set-manager', []) if !$scope.unitRole? && !$scope.project? throw Error "Group set group manager must have exactly one unit role or project" # Reset member panel toolbar visibility - $scope.newGroupSelected = -> + $scope.newGroupSelected = (group) -> + $scope.selectedGroup = group $scope.showMemberPanelToolbar = false if $scope.unitRole? $scope.groupMembersLoaded = -> $scope.showMemberPanelToolbar = true if $scope.unitRole? diff --git a/src/app/groups/group-set-manager/group-set-manager.tpl.html b/src/app/groups/group-set-manager/group-set-manager.tpl.html index da022a0b37..8b99425cb0 100644 --- a/src/app/groups/group-set-manager/group-set-manager.tpl.html +++ b/src/app/groups/group-set-manager/group-set-manager.tpl.html @@ -1,14 +1,13 @@ - - +
diff --git a/src/app/groups/group-set-selector/group-set-selector.component.html b/src/app/groups/group-set-selector/group-set-selector.component.html deleted file mode 100644 index 46486cf3f8..0000000000 --- a/src/app/groups/group-set-selector/group-set-selector.component.html +++ /dev/null @@ -1,10 +0,0 @@ - - - @for (gs of unit.groupSets; track gs.id) { - {{gs.name}} - } - - diff --git a/src/app/groups/group-set-selector/group-set-selector.component.scss b/src/app/groups/group-set-selector/group-set-selector.component.scss deleted file mode 100644 index 6dee9e802c..0000000000 --- a/src/app/groups/group-set-selector/group-set-selector.component.scss +++ /dev/null @@ -1,7 +0,0 @@ -.groupset-selector .dropdown { - cursor: pointer; -} - -.lockButton { - width: 70px; -} diff --git a/src/app/groups/group-set-selector/group-set-selector.component.ts b/src/app/groups/group-set-selector/group-set-selector.component.ts deleted file mode 100644 index 60ece272be..0000000000 --- a/src/app/groups/group-set-selector/group-set-selector.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; -import { Unit, GroupSet } from 'src/app/api/models/doubtfire-model'; - -@Component({ - selector: 'group-set-selector', - templateUrl: './group-set-selector.component.html', - styleUrls: ['./group-set-selector.component.scss'] -}) -export class GroupSetSelectorComponent implements OnInit { - @Input() unit: Unit; - @Input() selectedGroupSet: GroupSet; - @Output() selectedGroupSetChange = new EventEmitter(); - - ngOnInit(): void { - if (!this.unit) { - throw new Error('Unit not supplied to group set selector'); - } - } - - /** - * Emits the selected group set and updates the parent component. - * - * Also updates the local state. - * - * @param {GroupSet} groupSet - */ - selectGroupSet(groupSet: GroupSet): void { - this.selectedGroupSet = groupSet; - this.selectedGroupSetChange.emit(this.selectedGroupSet); - } -} diff --git a/src/app/groups/groups.coffee b/src/app/groups/groups.coffee index 3d4534d754..5f95e962e9 100644 --- a/src/app/groups/groups.coffee +++ b/src/app/groups/groups.coffee @@ -1,5 +1,4 @@ angular.module('doubtfire.groups', [ 'doubtfire.groups.group-member-contribution-assigner' - 'doubtfire.groups.group-selector' 'doubtfire.groups.group-set-manager' ]) From a48c7d6f58d2367677aa6c1c5c5bd43195730ec2 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Fri, 31 Oct 2025 11:26:19 +1100 Subject: [PATCH 17/20] refactor: group set manager migration (#1036) * refactor: init group set manager migration * refactor: allow editable group name * chore: display locked icon * refactor: replace with new component * refactor: shorten create group button * refactor: remove old component * chore: cleanup * refactor: hide search bar for students * refactor: remove css --- README.md | 2 +- src/app/doubtfire-angular.module.ts | 2 + src/app/doubtfire-angularjs.module.ts | 7 +- .../group-selector.component.html | 2 +- .../group-set-manager.coffee | 45 ------- .../group-set-manager.component.html | 76 ++++++++++++ .../group-set-manager.component.scss | 0 .../group-set-manager.component.ts | 115 ++++++++++++++++++ .../group-set-manager/group-set-manager.scss | 6 - .../group-set-manager.tpl.html | 67 ---------- src/app/groups/groups.coffee | 1 - .../projects/states/groups/groups.tpl.html | 10 +- .../unit-group-set-editor.tpl.html | 25 ++-- src/app/units/states/groups/groups.tpl.html | 13 +- 14 files changed, 224 insertions(+), 147 deletions(-) delete mode 100644 src/app/groups/group-set-manager/group-set-manager.coffee create mode 100644 src/app/groups/group-set-manager/group-set-manager.component.html create mode 100644 src/app/groups/group-set-manager/group-set-manager.component.scss create mode 100644 src/app/groups/group-set-manager/group-set-manager.component.ts delete mode 100644 src/app/groups/group-set-manager/group-set-manager.scss delete mode 100644 src/app/groups/group-set-manager/group-set-manager.tpl.html diff --git a/README.md b/README.md index 77fafe2a61..7885b47a9b 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ MIGRATED: - [x] ./src/app/common/modals/confirmation-modal/confirmation-modal.coffee - [x] ./src/app/common/modals/comments-modal/comments-modal.coffee (IN 10.0.x) - [x] ./src/app/groups/group-selector/group-selector.coffee +- [x] ./src/app/groups/group-set-manager/group-set-manager.coffee TODO: @@ -193,7 +194,6 @@ TODO: - [ ] ./src/app/projects/project-outcome-alignment/project-outcome-alignment.coffee - [ ] ./src/app/projects/states/tutorials/tutorials.coffee - [ ] ./src/app/admin/modals/modals.coffee -- [ ] ./src/app/groups/group-set-manager/group-set-manager.coffee - [ ] ./src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.coffee - [ ] ./src/app/groups/tutor-group-manager/tutor-group-manager.coffee - [ ] ./src/app/groups/groups.coffee diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 61b1586d71..4bf79b47d1 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -281,6 +281,7 @@ import {UnitStudentEnrolmentModalComponent} from './units/modals/unit-student-en import {TaskStatusPieChartComponent} from './visualisations/task-status-pie-chart/taskstatuspiechart.component'; import {GroupMemberListComponent} from './groups/group-member-list/group-member-list.component'; import {GroupSelectorComponent} from './groups/group-selector/group-selector.component'; +import {GroupSetManagerComponent} from './groups/group-set-manager/group-set-manager.component'; @NgModule({ // Components we declare @@ -411,6 +412,7 @@ import {GroupSelectorComponent} from './groups/group-selector/group-selector.com PortfolioGradeSelectStepComponent, GroupMemberListComponent, GroupSelectorComponent, + GroupSetManagerComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 29ec49ed15..ba04e6285a 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -78,7 +78,6 @@ import 'build/src/app/projects/states/portfolio/portfolio.js'; import 'build/src/app/projects/states/index/index.js'; import 'build/src/app/projects/project-outcome-alignment/project-outcome-alignment.js'; import 'build/src/app/admin/modals/modals.js'; -import 'build/src/app/groups/group-set-manager/group-set-manager.js'; import 'build/src/app/groups/groups.js'; import 'build/src/app/groups/group-member-contribution-assigner/group-member-contribution-assigner.js'; import 'build/src/app/units/modals/unit-ilo-edit-modal/unit-ilo-edit-modal.js'; @@ -222,6 +221,7 @@ import {UnitStaffEditorComponent} from './units/states/edit/directives/unit-staf import {PortfolioGradeSelectStepComponent} from './projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.component'; import {GroupMemberListComponent} from './groups/group-member-list/group-member-list.component'; import {GroupSelectorComponent} from './groups/group-selector/group-selector.component'; +import {GroupSetManagerComponent} from './groups/group-set-manager/group-set-manager.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -534,3 +534,8 @@ DoubtfireAngularJSModule.directive( 'fGroupSelector', downgradeComponent({component: GroupSelectorComponent}), ); + +DoubtfireAngularJSModule.directive( + 'fGroupSetManager', + downgradeComponent({component: GroupSetManagerComponent}), +); diff --git a/src/app/groups/group-selector/group-selector.component.html b/src/app/groups/group-selector/group-selector.component.html index 857f840a64..0bf47af51e 100644 --- a/src/app/groups/group-selector/group-selector.component.html +++ b/src/app/groups/group-selector/group-selector.component.html @@ -31,7 +31,7 @@ />
} diff --git a/src/app/groups/group-set-manager/group-set-manager.coffee b/src/app/groups/group-set-manager/group-set-manager.coffee deleted file mode 100644 index 91d1d81c1c..0000000000 --- a/src/app/groups/group-set-manager/group-set-manager.coffee +++ /dev/null @@ -1,45 +0,0 @@ -angular.module('doubtfire.groups.group-set-manager', []) - -# -# Manager directive for tutors to add and remove group -# members from a group within a group set context -# -.directive('groupSetManager', -> - restrict: 'E' - templateUrl: 'groups/group-set-manager/group-set-manager.tpl.html' - scope: - unit: '=' - unitRole: '=' - project: '=' - selectedGroupSet: '=' - showGroupSetSelector: '=?' - controller: ($scope, newGroupService, gradeService, alertService) -> - if !$scope.unitRole? && !$scope.project? - throw Error "Group set group manager must have exactly one unit role or project" - # Reset member panel toolbar visibility - $scope.newGroupSelected = (group) -> - $scope.selectedGroup = group - $scope.showMemberPanelToolbar = false if $scope.unitRole? - $scope.groupMembersLoaded = -> - $scope.showMemberPanelToolbar = true if $scope.unitRole? - - # Add new member to the group - $scope.addMember = (member) -> - $scope.selectedGroup.addMember(member) - $scope.selectedStudent = null - - # Update name of group - $scope.updateGroup = (data) -> - newGroupService.update({ - unitId: $scope.unit.id, - groupSetId: $scope.selectedGroupSet.id, - id: $scope.selectedGroup.id, - }, { - entity: data - }).subscribe({ - next: (response) -> - alertService.success( "Group changed", 2000) - error: (response) -> - alertService.error( response, 6000) - }) -) diff --git a/src/app/groups/group-set-manager/group-set-manager.component.html b/src/app/groups/group-set-manager/group-set-manager.component.html new file mode 100644 index 0000000000..08b85bf29d --- /dev/null +++ b/src/app/groups/group-set-manager/group-set-manager.component.html @@ -0,0 +1,76 @@ +
+ + + + @if (selectedGroup) { + + + Members of + @if (!editingGroupName) { + {{ selectedGroup?.name }} + @if (unitRole || selectedGroup.groupSet?.allowStudentsToManageGroups) { + + } + } @else { + @if (unitRole || selectedGroup.groupSet?.allowStudentsToManageGroups) { + + + + + + } + } + + @if (selectedGroup.locked) { + lock + } + + + + + @if (unitRole) { + + + + + @for (project of filteredProjects | async; track project) { + {{ project.student.name }} + } + + + + } + + } +
diff --git a/src/app/groups/group-set-manager/group-set-manager.component.scss b/src/app/groups/group-set-manager/group-set-manager.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/groups/group-set-manager/group-set-manager.component.ts b/src/app/groups/group-set-manager/group-set-manager.component.ts new file mode 100644 index 0000000000..ae3873315b --- /dev/null +++ b/src/app/groups/group-set-manager/group-set-manager.component.ts @@ -0,0 +1,115 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {FormControl} from '@angular/forms'; +import {map, Observable, startWith} from 'rxjs'; +import {Group, GroupSet, Project, Unit, UnitRole} from 'src/app/api/models/doubtfire-model'; +import {GroupService} from 'src/app/api/services/group.service'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-group-set-manager', + templateUrl: './group-set-manager.component.html', + styleUrls: ['./group-set-manager.component.scss'], +}) +export class GroupSetManagerComponent implements OnInit { + @Input() project: Project; + @Input() unit: Unit; + @Input() selectedGroupSet: GroupSet; + @Input() showGroupSetSelector: boolean; + @Input() unitRole: UnitRole; + + public selectedGroup: Group; + + editingGroupName = false; + + control = new FormControl(''); + projects: Project[] = []; + filteredProjects: Observable; + + constructor( + private groupService: GroupService, + private alertService: AlertService, + ) {} + + ngOnInit(): void { + this.filteredProjects = this.control.valueChanges.pipe( + startWith(''), + map((value) => this._filter(value)), + ); + } + + get groupSelectHandler() { + return (group: Group) => this.newGroupSelected(group); + } + + displayFn(project: Project): string { + return project && project.student.name ? project.student.name : ''; + } + + newGroupSelected(group: Group) { + if (this.selectedGroup) { + this.selectedGroup.name = this.originalGroupName; + } + this.editingGroupName = false; + this.selectedGroup = group; + + const students = this.unit.studentsForGroupTypeAhead(group) || []; + this.projects = students.filter((project) => !group.projects.find((p) => project.id === p.id)); + + this.originalGroupName = group.name; + } + + private _filter(value: string | Project): Project[] { + if (typeof value !== 'string') { + return; + } + + const filterValue = value.toLowerCase(); + return this.projects.filter( + (project) => + project.student.name.toLowerCase().includes(filterValue.toLowerCase()) && // Find by name + !this.selectedGroup.projects.find((p) => project.id === p.id), // Not already assigned to the group + ); + } + + groupMembersLoaded() {} + + addMember(project: Project) { + this.selectedGroup.addMember(project); + this.control.setValue(''); + } + + private originalGroupName: string; + startEditingGroupName() { + this.originalGroupName = this.selectedGroup.name; + this.editingGroupName = true; + } + + stopEditinGroupName() { + this.selectedGroup.name = this.originalGroupName; + this.editingGroupName = false; + } + + updateGroup() { + this.editingGroupName = false; + this.groupService + .update( + { + unitId: this.unit.id, + groupSetId: this.selectedGroup.groupSet.id, + id: this.selectedGroup.id, + }, + { + entity: this.selectedGroup, + }, + ) + .subscribe({ + next: () => { + this.alertService.success('Successfully updated group', 3000); + }, + error: (error) => { + this.selectedGroup.name = this.originalGroupName; + this.alertService.error(`Failed to update gorup: ${error}`, 6000); + }, + }); + } +} diff --git a/src/app/groups/group-set-manager/group-set-manager.scss b/src/app/groups/group-set-manager/group-set-manager.scss deleted file mode 100644 index b65662abca..0000000000 --- a/src/app/groups/group-set-manager/group-set-manager.scss +++ /dev/null @@ -1,6 +0,0 @@ -@media (min-width: $screen-lg) { - group-set-manager { - display: block; - @include panel-row; - } -} diff --git a/src/app/groups/group-set-manager/group-set-manager.tpl.html b/src/app/groups/group-set-manager/group-set-manager.tpl.html deleted file mode 100644 index 8b99425cb0..0000000000 --- a/src/app/groups/group-set-manager/group-set-manager.tpl.html +++ /dev/null @@ -1,67 +0,0 @@ - - -
-
-
-
-

- Members of - {{selectedGroup.name}} -

-
-
- -
-
-
- -
-
- -
- -
-
-
- - - -
- diff --git a/src/app/groups/groups.coffee b/src/app/groups/groups.coffee index 5f95e962e9..feae90ec11 100644 --- a/src/app/groups/groups.coffee +++ b/src/app/groups/groups.coffee @@ -1,4 +1,3 @@ angular.module('doubtfire.groups', [ 'doubtfire.groups.group-member-contribution-assigner' - 'doubtfire.groups.group-set-manager' ]) diff --git a/src/app/projects/states/groups/groups.tpl.html b/src/app/projects/states/groups/groups.tpl.html index e5086ab3c6..804d268d35 100644 --- a/src/app/projects/states/groups/groups.tpl.html +++ b/src/app/projects/states/groups/groups.tpl.html @@ -1,10 +1,8 @@
- - -
+ + +
+
diff --git a/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html b/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html index db550ef42d..c83b2d5fd7 100644 --- a/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html +++ b/src/app/units/states/edit/directives/unit-group-set-editor/unit-group-set-editor.tpl.html @@ -123,18 +123,21 @@

No Group Sets Created

New Group Set -
-
-
-
- +
+ +
+ + + + +

diff --git a/src/app/units/states/groups/groups.tpl.html b/src/app/units/states/groups/groups.tpl.html index 319f46e08f..6d79bdb9a3 100644 --- a/src/app/units/states/groups/groups.tpl.html +++ b/src/app/units/states/groups/groups.tpl.html @@ -1,11 +1,8 @@ -
- - -
+
+ + +
+
From 560394c4705a5db03b10aeb50556e88086f6d144 Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Fri, 31 Oct 2025 11:31:10 +1100 Subject: [PATCH 18/20] chore: display joined status --- src/app/groups/group-selector/group-selector.component.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/groups/group-selector/group-selector.component.html b/src/app/groups/group-selector/group-selector.component.html index 0bf47af51e..b0984e69f3 100644 --- a/src/app/groups/group-selector/group-selector.component.html +++ b/src/app/groups/group-selector/group-selector.component.html @@ -129,7 +129,9 @@ } - @if (project && group.hasSpace() && selectedGroupSet.allowStudentsToManageGroups) { + @if (isPartOfGroup(project, group)) { +
Joined
+ } @else if (project && group.hasSpace() && selectedGroupSet.allowStudentsToManageGroups) {
@if (!group.locked && !selectedGroupSet.locked) {