From d4d294971028a8ae41ab590e467c3f362336b0b7 Mon Sep 17 00:00:00 2001 From: zara1504 Date: Thu, 27 Mar 2025 14:21:08 +1100 Subject: [PATCH 01/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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) { + } + +
+ @if (showUploader && uploadingInfo === null && shownUploadZones.length) { +
+ @for (upload of shownUploadZones; track upload) { + @if (!singleDropZone && showName) { +
+ {{ uploadZones.length === 1 ? '' : $index + 1 + ' - ' }} + {{ upload.display.name }} +
+ } + + @if (singleDropZone && showName) { +
Select {{ upload.display.name }}
+ } + +
+ + +
+ @if (!upload.display.error) { + {{ upload.display.icon }} + @if (dropSupported) { +

+ Drop {{ upload.display.type }} file here
or click to select +

+ } @else { +

Click to select {{ upload.display.type }} file

+ } + } @else { +
+ block +

Invalid file provided

+ Accepted: {{ upload.accept.split(',').join(', ') }} +
+ } +
+
+ + Browse for file +
+
+ + @if (!singleDropZone && upload.model?.length > 0) { +
+ {{ upload.display.icon }} + {{ upload.model[0].name }} + +
+ } + } +
+ } + + @if (showUploader && singleDropZone && uploadingInfo === null) { +
+
Upload Summary
+ + @for (upload of uploadZones; track upload) { +
+
+ {{ upload.display.icon }} + {{ upload.display.name }} +
+ + @if (upload.model?.length > 0) { + {{ upload.model[0].name }} + + } @else { + File Pending + } +
+ } +
+ } +
+ + @if (showUploader && !isUploading) { +
+ @if (showUploadButton && readyToUpload() && uploadingInfo === null) { + + } + + @if (asButton) { + + } +
+ } + + @if (showUploader && readyToUpload() && isUploading) { + @if (!uploadingInfo?.complete) { +
+
+ @for (upload of uploadZones; track upload) { + {{ upload.display.icon }} + } + arrow_right_alt + +
+ + + +
+ } + + @if (uploadingInfo?.complete) { +
+
+ + {{ uploadingInfo.success ? 'check_circle' : 'cancel' }} + + + + File Upload {{ uploadingInfo.success ? 'Successful' : 'Failed' }} + +
+ + @if (!uploadingInfo.success) { +
+
+

Error Message: {{ uploadingInfo.error }}

+ +
+ + +
+
+ } +
+ } + } + + diff --git a/src/app/common/file-uploader/file-uploader.component.scss b/src/app/common/file-uploader/file-uploader.component.scss new file mode 100644 index 0000000000..6809e06815 --- /dev/null +++ b/src/app/common/file-uploader/file-uploader.component.scss @@ -0,0 +1,16 @@ +file-upload .mat-icon, +.complete .mat-icon { + font-size: 50px; + height: 50px; + width: 50px; +} + +.uploading .mat-icon { + font-size: 75px; + height: 75px; + width: 75px; +} + +::ng-deep file-upload .upload-input { + width: 100%; +} diff --git a/src/app/common/file-uploader/file-uploader.component.ts b/src/app/common/file-uploader/file-uploader.component.ts new file mode 100644 index 0000000000..877eddc9cd --- /dev/null +++ b/src/app/common/file-uploader/file-uploader.component.ts @@ -0,0 +1,325 @@ +import { + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, +} from '@angular/core'; +import {FileUploadControl} from '@iplab/ngx-file-upload'; +import {UserService} from 'src/app/api/services/user.service'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; + +interface File { + name: string; + type: string; +} + +interface UploadDisplay { + name: string; + icon: string; + type: string; + error: boolean; +} +interface UploadZone { + name: string; + model: any; + accept: string; + accepts: string[]; + rejects: string[]; + display: UploadDisplay; +} + +interface UploadingInfo { + progress: number; + success: boolean; + error: string; + complete: boolean; +} + +export const ACCEPTED_TYPES = { + document: { + extensions: ['pdf', 'ps'], + // icon: 'fa-file-pdf-o', + icon: 'article_outlined', + name: 'PDF', + }, + csv: { + extensions: ['csv', 'xls', 'xlsx'], + icon: 'insert_chart_outlined', + name: 'CSV', + }, + code: { + // prettier-ignore + extensions: [ + 'pas', 'cpp', 'c', 'cs', 'csv', 'h', 'hpp', 'java', 'py', 'js', 'html', 'coffee', 'rb', 'css', + 'scss', 'yaml', 'yml', 'xml', 'json', 'ts', 'r', 'rmd', 'rnw', 'rhtml', 'rpres', 'tex', + 'vb', 'sql', 'txt', 'md', 'jack', 'hack', 'asm', 'hdl', 'tst', 'out', 'cmp', 'vm', 'sh', 'bat', + 'dat', 'ipynb', 'pml', 'vue' + ], + // icon: 'fa-file-code-o', + // icon: 'code', + icon: 'integration_instructions_outlined', + name: 'code', + }, + image: { + extensions: ['png', 'bmp', 'tiff', 'tif', 'jpeg', 'jpg', 'gif'], + // icon: 'fa-file-image-o', + icon: 'image_outlined', + name: 'image', + }, + zip: { + extensions: ['zip', 'tar.gz', 'tar'], + // icon: 'fa-file-zip-o', + icon: 'zip_outlined', + name: 'archive', + }, +} as const; + +@Component({ + selector: 'f-file-uploader', + templateUrl: './file-uploader.component.html', + styleUrls: ['./file-uploader.component.scss'], +}) +export class FileUploaderComponent implements OnInit, OnChanges { + @Input() files: File[]; + @Input() url: string; + @Input() method = 'POST'; + @Input() payload?: any; + + @Input() onBeforeUpload?: () => void; + @Input() onSuccess?: (response: any) => void; + @Input() onFailure?: (response: any) => void; + @Input() onComplete?: () => void; + @Input() onClickFailureCancel?: () => void; + + @Input() isUploading: boolean; + @Input() isReady: boolean; + @Input() showName: boolean = true; + @Input() asButton: boolean = false; + @Input() singleDropZone: boolean = false; + @Input() showUploadButton: boolean = true; + @Input() resetAfterUpload: boolean = true; + + @Input() initiateUpload?: () => void; + + // HACK: workaround for TypeScript -> Coffeescript communication + // Once all parent components such as upload-submission-modal are migrated.. + // .. these *wont* be necessary anymore + // Parent components should declare the file-uploader using @ViewChild() and directly call initiateUpload() + @Output() isReadyChange = new EventEmitter(); + @Output() uploadReady = new EventEmitter<() => void>(); + + public readonly ACCEPTED_TYPES = ACCEPTED_TYPES; + + public showUploader: boolean = false; + public uploadingInfo: UploadingInfo = null; + + public fileUploadControl = new FileUploadControl({listVisible: false, discardInvalid: true}); + public shownUploadZones: UploadZone[] = []; + public uploadZones: UploadZone[] = []; + public dropSupported: boolean = true; + + constructor( + private userService: UserService, + private constants: DoubtfireConstants, + ) {} + + private externalName: string = 'OnTrack'; + + ngOnInit(): void { + this.showUploader = !this.asButton; + this.createUploadZones(this.files); + + this.fileUploadControl.valueChanges.subscribe(() => { + setTimeout(() => { + this.validateFiles(); + }); + }); + + this.uploadReady.emit(this.initiateUploadInternal.bind(this)); + + if (!this.onClickFailureCancel) { + this.onClickFailureCancel = this.resetUploader; + } + + this.resetUploader(); + + this.constants.ExternalName.subscribe((name) => { + this.externalName = name; + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['files']) { + this.createUploadZones(changes.files.currentValue); + } + } + + public backToUpload() { + this.isUploading = false; + this.uploadingInfo = null; + } + + validateFiles() { + for (const upload of this.shownUploadZones) { + if (upload.model?.length) { + const name: string = upload.model[0].name.toLowerCase(); + const accepts: string[] = upload.accepts.map((ext: string) => ext.toLowerCase()); + const valid = accepts.some((ext) => name.endsWith(ext)); + if (!valid) { + upload.model = null; + upload.display.error = true; + setTimeout(() => { + upload.display.error = null; + }, 5000); + } + } + } + this.refreshShownUploadZones(); + } + + clearEnqueuedUpload(upload: UploadZone) { + upload.model = null; + this.refreshShownUploadZones(); + } + + readyToUpload(): boolean { + const allSelected = this.uploadZones.every((zone) => zone.model?.length); + this.updateReadyState(allSelected); + return allSelected; + } + + updateReadyState(ready: boolean) { + this.isReady = ready; + this.isReadyChange.emit(ready); + } + + resetUploader() { + this.uploadingInfo = null; + this.isUploading = false; + this.showUploader = !this.asButton; + for (const upload of this.uploadZones) { + this.clearEnqueuedUpload(upload); + } + } + + initiateUploadInternal() { + if (!this.readyToUpload()) { + return; + } + if (this.onBeforeUpload) { + this.onBeforeUpload(); + } + + this.uploadingInfo = { + progress: 5, + success: null, + error: null, + complete: false, + }; + + this.isUploading = true; + + const xhr = new XMLHttpRequest(); + const form = new FormData(); + + // Append files + for (const zone of this.uploadZones) { + if (zone.model?.length) { + form.append(zone.name, zone.model[0]); + } + } + + // Append payload + if (this.payload) { + for (const [key, value] of Object.entries(this.payload)) { + let v = value; + if (typeof v === 'object') v = JSON.stringify(v); + form.append(key, v as any); + } + } + + xhr.upload.onprogress = (event) => { + if (event.total) { + this.uploadingInfo.progress = Math.floor((event.loaded / event.total) * 100); + } + }; + + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + setTimeout(() => { + this.uploadingInfo.complete = true; + let response: any = null; + try { + response = JSON.parse(xhr.responseText); + } catch (_e) { + if (xhr.status === 0) { + response = {error: `Could not connect to ${this.externalName} the server`}; + } else { + response = xhr.responseText; + } + } + + if (xhr.status >= 200 && xhr.status < 300) { + this.onSuccess?.(response); + this.uploadingInfo.success = true; + setTimeout(() => { + this.onComplete?.(); + if (this.resetAfterUpload) { + this.resetUploader(); + } + }, 2500); + } else { + this.onFailure?.(response); + this.uploadingInfo.success = false; + this.uploadingInfo.error = (response?.error ?? 'Unknown error') as string; + } + }, 2000); + } + }; + const method = this.method ?? 'POST'; + xhr.open(method, this.url, true); + + xhr.setRequestHeader('Auth-Token', this.userService.currentUser.authenticationToken); + xhr.setRequestHeader('Username', this.userService.currentUser.username); + + xhr.send(form); + } + + // onClickFailureCancelInternal() { + // console.log('onClickFailureCancelInternal'); + // } + + refreshShownUploadZones = () => { + if (this.singleDropZone) { + const firstEmpty = this.uploadZones.find((z) => !z.model || z.model.length === 0); + this.shownUploadZones = firstEmpty ? [firstEmpty] : []; + } + }; + + createUploadZones(files: File[]) { + const zones = Object.entries(files).map(([uploadName, uploadData]) => { + const typeData = ACCEPTED_TYPES[uploadData.type]; + if (!typeData) throw new Error(`Invalid type provided to File Uploader ${uploadData.type}`); + + return { + name: uploadName, + model: null, + accept: `.${typeData.extensions.join(',.')}`, + accepts: typeData.extensions, + rejects: null, + display: { + name: uploadData.name, + icon: typeData.icon, + type: typeData.name, + error: false, + }, + }; + }); + + this.shownUploadZones = this.singleDropZone ? [zones[0]] : zones; + this.uploadZones = zones; + } +} diff --git a/src/app/common/modals/csv-result-modal/csv-upload-modal.tpl.html b/src/app/common/modals/csv-result-modal/csv-upload-modal.tpl.html index 6112f50c7e..5f94b642fe 100644 --- a/src/app/common/modals/csv-result-modal/csv-upload-modal.tpl.html +++ b/src/app/common/modals/csv-result-modal/csv-upload-modal.tpl.html @@ -7,7 +7,7 @@
- - + +
diff --git a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html index 960c0014ab..b190d6f35c 100644 --- a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html +++ b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html @@ -55,11 +55,11 @@

you have achieved a {{targetGrade}} in {{unit.name}}.
- +
diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee index b9c9ebd2b9..7646c70ff4 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.coffee @@ -55,6 +55,12 @@ angular.module('doubtfire.tasks.modals.upload-submission-modal', []) $scope.submissionTypes = submissionTypes + $scope.isReadyChange = (ready) -> + $scope.uploader.isReady = ready + + $scope.uploadIsReady = (callback) -> + $scope.uploader.start = callback + # Upload files $scope.uploader = { # url: Task.generateSubmissionUrl($scope.task.project, $scope.task) diff --git a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html index 92070ac30f..9b96abcd0b 100644 --- a/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html +++ b/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.tpl.html @@ -60,20 +60,21 @@

- +
diff --git a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.tpl.html b/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.tpl.html index e95245bd46..20f3664c5c 100644 --- a/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.tpl.html +++ b/src/app/tasks/task-ilo-alignment/task-ilo-alignment-editor/task-ilo-alignment-editor.tpl.html @@ -105,12 +105,13 @@

Import Task Outcome Alignments

Import links between tasks and outcomes from a CSV containing: unit_code, learning_outcome, task_abbr, rating.
- +

Export Task Outcome Alignments

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 c83b2d5fd7..90313b86b6 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 @@ -162,14 +162,14 @@

Import Groups for {{selectedGroupSet.name}} Download CSV
- - + +

@@ -195,15 +195,17 @@

Download CSV
- - -

+ + + + diff --git a/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.tpl.html b/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.tpl.html index 3ce30ff314..84bd7ef0ab 100644 --- a/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.tpl.html +++ b/src/app/units/states/edit/directives/unit-ilo-editor/unit-ilo-editor.tpl.html @@ -54,7 +54,11 @@

Batch Upload Outcome Definitions

description.
- +
From db32f072fa912ee648c636f64e4a26ed10e5e35b Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:19:48 +1100 Subject: [PATCH 23/29] chore: remove any types --- .../file-uploader/file-uploader.component.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/app/common/file-uploader/file-uploader.component.ts b/src/app/common/file-uploader/file-uploader.component.ts index 877eddc9cd..85eb85e792 100644 --- a/src/app/common/file-uploader/file-uploader.component.ts +++ b/src/app/common/file-uploader/file-uploader.component.ts @@ -11,7 +11,7 @@ import {FileUploadControl} from '@iplab/ngx-file-upload'; import {UserService} from 'src/app/api/services/user.service'; import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; -interface File { +interface FileData { name: string; type: string; } @@ -24,7 +24,7 @@ interface UploadDisplay { } interface UploadZone { name: string; - model: any; + model: File[]; accept: string; accepts: string[]; rejects: string[]; @@ -83,14 +83,14 @@ export const ACCEPTED_TYPES = { styleUrls: ['./file-uploader.component.scss'], }) export class FileUploaderComponent implements OnInit, OnChanges { - @Input() files: File[]; + @Input() files: FileData[]; @Input() url: string; @Input() method = 'POST'; - @Input() payload?: any; + @Input() payload?: unknown; @Input() onBeforeUpload?: () => void; - @Input() onSuccess?: (response: any) => void; - @Input() onFailure?: (response: any) => void; + @Input() onSuccess?: (response) => void; + @Input() onFailure?: (response) => void; @Input() onComplete?: () => void; @Input() onClickFailureCancel?: () => void; @@ -237,7 +237,7 @@ export class FileUploaderComponent implements OnInit, OnChanges { for (const [key, value] of Object.entries(this.payload)) { let v = value; if (typeof v === 'object') v = JSON.stringify(v); - form.append(key, v as any); + form.append(key, v); } } @@ -251,10 +251,11 @@ export class FileUploaderComponent implements OnInit, OnChanges { if (xhr.readyState === 4) { setTimeout(() => { this.uploadingInfo.complete = true; - let response: any = null; + let response = null; try { response = JSON.parse(xhr.responseText); - } catch (_e) { + } catch (e) { + console.error(e); if (xhr.status === 0) { response = {error: `Could not connect to ${this.externalName} the server`}; } else { @@ -299,7 +300,7 @@ export class FileUploaderComponent implements OnInit, OnChanges { } }; - createUploadZones(files: File[]) { + createUploadZones(files: FileData[]) { const zones = Object.entries(files).map(([uploadName, uploadData]) => { const typeData = ACCEPTED_TYPES[uploadData.type]; if (!typeData) throw new Error(`Invalid type provided to File Uploader ${uploadData.type}`); From 37e109a9fffa0e2d71143afabb9423b5511cf2fd Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:29:14 +1100 Subject: [PATCH 24/29] chore: remove old file uploader component --- .../common/file-uploader/file-uploader.coffee | 298 ------------------ .../common/file-uploader/file-uploader.scss | 139 -------- .../file-uploader/file-uploader.tpl.html | 104 ------ 3 files changed, 541 deletions(-) delete mode 100644 src/app/common/file-uploader/file-uploader.coffee delete mode 100644 src/app/common/file-uploader/file-uploader.scss delete mode 100644 src/app/common/file-uploader/file-uploader.tpl.html diff --git a/src/app/common/file-uploader/file-uploader.coffee b/src/app/common/file-uploader/file-uploader.coffee deleted file mode 100644 index c492dac115..0000000000 --- a/src/app/common/file-uploader/file-uploader.coffee +++ /dev/null @@ -1,298 +0,0 @@ -angular.module('doubtfire.common.file-uploader', ["ngFileUpload"]) - -.directive 'fileUploader', -> - restrict: 'E' - replace: true - templateUrl: 'common/file-uploader/file-uploader.tpl.html' - scope: - # Files map a key (file name to be uploaded) to a value (containing a - # a display name, and the type of file that is to be accepted, where - # type is one of [document, csv, archive, code, image] - # E.g.: - # { file0: { name: 'Silly Name Code', type: 'code' }, - # fileX: { name: 'Silly name Shot', type: 'image' } ... } - files: '=' - # URL to where image is to be uploaded - url: '=' - # Optional HTTP method used to post data (defaults to POST) - method: '@' - # Other payload data to pass in the upload - # E.g.: - # { unit_id: 10, other: { key: data, with: [array, of, stuff] } ... } - payload: '=?' - # Optional function to notify just prior to upload, enables injection of payload for example - onBeforeUpload: '=?' - # Optional function to perform on success (with one response parameter) - onSuccess: '=?' - # Optional function to perform on failure (with one response parameter) - onFailure: '=?' - # Optional function to perform when the upload is successful and about - # to go back into its default state - onComplete: '=?' - # This value is bound to whether or not the uploader is currently uploading - isUploading: '=?' - # This value is bound to whether or not the uploader is ready to upload - isReady: '=?' - # Shows the names of files to be uploaded (defaults to true) - showName: '=?' - # Shows initially as button - asButton: '=?' - # Exposed files that are in the zone - filesSelected: '=?' - # Whether we have one or many drop zones (default is false) - singleDropZone: '=?' - # Whether or not we show the upload button or do we hide it allowing an - # external trigger to upload (default is true) - showUploadButton: '=?' - # Sets this scope variable to a function that can then be triggered externally - # from outside the scope - initiateUpload: '=?' - # What happens when we click cancel on failure - onClickFailureCancel: '=?' - # Whether we should reset after upload - resetAfterUpload: '=?' - controller: ($scope, $timeout, newUserService) -> - # - # Accepted upload types with associated data - # - ACCEPTED_TYPES = - document: - extensions: ['pdf', 'ps'] - icon: 'fa-file-pdf-o' - name: 'PDF' - csv: - extensions: ['csv','xls','xlsx'] - icon: 'fa-file-excel-o' - name: 'CSV' - code: - extensions: ['pas', 'cpp', 'c', 'cs', 'csv', 'h', 'hpp', 'java', 'py', 'js', 'html', 'coffee', 'rb', 'css', - 'scss', 'yaml', 'yml', 'xml', 'json', 'ts', 'r', 'rmd', 'rnw', 'rhtml', 'rpres', 'tex', - 'vb', 'sql', 'txt', 'md', 'jack', 'hack', 'asm', 'hdl', 'tst', 'out', 'cmp', 'vm', 'sh', 'bat', - 'dat', 'ipynb', 'pml', 'vue'] - icon: 'fa-file-code-o' - name: 'code' - image: - extensions: ['png', 'bmp', 'tiff', 'tif', 'jpeg', 'jpg', 'gif'] - name: 'image' - icon: 'fa-file-image-o' - zip: - extensions: ['zip', 'tar.gz', 'tar'] - name: 'archive' - icon: 'fa-file-zip-o' - - # - # Error handling; check if empty files - # - throw Error "No files provided to uploader" if $scope.files?.length is 0 - - # - # Whether or not clearEnqueuedFiles is enabled - # - $scope.clearEnqueuedUpload = (upload) -> - upload.model = null - refreshShownUploadZones() - - # - # Default showName - # - $scope.showName ?= true - - # - # Default singleDropZone - # - $scope.singleDropZone ?= false - - # - # Default asButton - # - $scope.asButton ?= false - - # - # Only initially show uploader if not presenting as button - # - $scope.showUploader = !$scope.asButton - - # - # Default show upload button - # - $scope.showUploadButton ?= true - - # - # Default resetAfterUpload to true - # - $scope.resetAfterUpload ?= true - - # - # When a file is dropped, if there has been rejected files - # warn the user that that file is not okay - # - checkForError = (upload) -> - if upload.rejects?.length > 0 - upload.display.error = yes - upload.rejects = null - $timeout (-> upload.display.error = no), 4000 - return true - false - - # Called when the model has changed - $scope.modelChanged = (newFiles, upload) -> - return unless newFiles.length > 0 || upload.rejects.length > 0 - gotError = checkForError(upload) - unless gotError - $scope.filesSelected = _.flatten(_.map($scope.uploadZones, 'model')) - if $scope.singleDropZone - $scope.selectedFiles = $scope.uploadZones - refreshShownUploadZones() - - # - # Will refresh which shown drop zones are shown - # Only changes if showing one drop zone - # - refreshShownUploadZones = -> - if $scope.singleDropZone - # Find the first-most empty model in each zone - firstEmptyZone = _.find($scope.uploadZones, (zone) -> !zone.model? || zone.model.length == 0) - if firstEmptyZone? - $scope.shownUploadZones = [firstEmptyZone] - else - $scope.shownUploadZones = [] - - # - # Whether or not drop is supported by this browser - assume - # true initially, but the drop zone will alter this - # - $scope.dropSupported = true - - # - # Data required for each upload zone - # - createUploadZones = (files) -> - zones = _.map(files, (uploadData, uploadName) -> - type = uploadData.type - typeData = ACCEPTED_TYPES[type] - # No typeData found? - unless typeData? - throw Error "Invalid type provided to File Uploader #{type}" - zone = - name: uploadName - model: null - accept: "'." + typeData.extensions.join(',.') + "'" - # Rejected files - rejects: null - display: - name: uploadData.name - # Font awesome supports PDF (from Document), - # CSV, Code and Image icons - icon: typeData.icon - type: typeData.name - # Whether or not a reject error is shown - error: false - zone - ) - # Remove all but the active drop zone - if $scope.singleDropZone - $scope.shownUploadZones = [_.first(zones)] - else - $scope.shownUploadZones = zones - $scope.uploadZones = zones - createUploadZones($scope.files) - - # - # Watch for changes in the files, and recreate the zones when - # they do change - # - $scope.$watch 'files', (files, oldFiles) -> - createUploadZones(files) - - # - # Checks if okay to upload (i.e., file models exist for each drop zone) - # - $scope.readyToUpload = -> - $scope.isReady = _.compact(_.flatten (upload.model for upload in $scope.uploadZones)).length is _.keys($scope.files).length - - # - # Resets the uploader and call it - # - $scope.resetUploader = -> - # No upload info and we're not uploading - $scope.uploadingInfo = null - $scope.isUploading = false - $scope.showUploader = !$scope.asButton - for upload in $scope.uploadZones - $scope.clearEnqueuedUpload(upload) - $scope.resetUploader() - - # - # Override on click failure cancel if not set to just reset uploader - # - $scope.onClickFailureCancel ?= $scope.resetUploader - - - # - # Initiates the upload - # - $scope.initiateUpload = -> - return unless $scope.readyToUpload() - $scope.onBeforeUpload?() - - xhr = new XMLHttpRequest() - form = new FormData() - # Append data - files = ({ name: zone.name; data: zone.model[0] } for zone in $scope.uploadZones) - form.append file.name, file.data for file in files - # Append payload - payload = ({ key: k; value: v } for k, v of $scope.payload) - for payloadItem in payload - payloadItem.value = JSON.stringify(payloadItem.value) if _.isObject payloadItem.value - form.append payloadItem.key, payloadItem.value - # Set the percent - $scope.uploadingInfo = - progress: 5 - success: null - error: null - complete: false - $scope.isUploading = true - # Callbacks - xhr.onreadystatechange = -> - if xhr.readyState is 4 - $timeout (-> - # Upload is now complete - $scope.uploadingInfo.complete = true - response = null - try - response = JSON.parse xhr.responseText - catch e - if xhr.status is 0 - response = { error: 'Could not connect to the Doubtfire server' } - else - response = xhr.responseText - # Success (20x success range) - if xhr.status >= 200 and xhr.status < 300 - $scope.onSuccess?(response) - $scope.uploadingInfo.success = true - $timeout((-> - $scope.onComplete?() - if $scope.resetAfterUpload - $scope.resetUploader() - ), 2500) - # Fail - else - $scope.onFailure?(response) - $scope.uploadingInfo.success = false - $scope.uploadingInfo.error = response.error or "Unknown error" - $scope.$apply() - ), 2000 - xhr.upload.onprogress = (event) -> - $scope.uploadingInfo.progress = parseInt(100.0 * event.position / event.totalSize) - $scope.$apply() - # Default the method to POST if it was not defined - $scope.method = 'POST' unless $scope.method? - - # Send it - xhr.open $scope.method, $scope.url, true - - # Add auth details - xhr.setRequestHeader('Auth-Token', newUserService.currentUser.authenticationToken) - xhr.setRequestHeader('Username', newUserService.currentUser.username) - - xhr.send form diff --git a/src/app/common/file-uploader/file-uploader.scss b/src/app/common/file-uploader/file-uploader.scss deleted file mode 100644 index 4ca5cb6988..0000000000 --- a/src/app/common/file-uploader/file-uploader.scss +++ /dev/null @@ -1,139 +0,0 @@ -.file-uploader { - display: block; - // Add some margin like a

- margin: 2.5em 0; - - // Colors to make it easy to understand - $hover-color: $brand-primary; - $accept-color: $brand-success; - $reject-color: $brand-danger; - - // Extra additional icons - $ban-icon: $fa-var-ban; - $download-icon: $fa-var-download; - - .upload-commit-actions { - margin-top: 1em; - .btn-upload { - margin-right: 1.5ex; - } - } - - .well.drop { - border: 2px #bbb dotted; - font-size: larger; - font-weight: bold; - color: #aaa; - text-align: center; - &, i { - @include transition(all 0.25s ease); - } - p small { - display: block; - } - &:hover { - cursor: pointer; - border-color: $hover-color; - color: $hover-color; - p small { - text-decoration: underline; - } - } - } - - // Wells which have file over - .well.drop.file-over { - cursor: copy; - border-color: $accept-color; - color: $accept-color; - // Switch the icon over - p.fa::before { - content: $download-icon; - } - } - // Rejected file over - .well.drop.file-rejected { - border-color: $reject-color; - color: $reject-color; - // Switch the icon over - p.fa::before { - content: $ban-icon; - } - } - - // File header - .selected-files { - &:not(.list-group) { - display: inline-block; - } - .selected-file { - display: block; - font-size: 1.2em; - i.file-type { - margin-right: 1ex; - font-size: 1.2em; - } - &.highlight { - animation: highlight-selected-file-animation; - animation-duration: 0.75s; - @keyframes highlight-selected-file-animation { - 0% { background: rgba(33, 150, 243, 0.4); } - 0% { box-shadow: 0 0 6px rgba(33, 150, 243, 1); } - 100% { box-shadow: 0 0 0px rgba(255, 255, 255, 0); } - } - } - } - } - a.clear-upload { - margin-left: 1ex; - &:hover i { - font-size: 1.15em; - color: $reject-color; - } - display: inline-block; - } - // Upload area/result - .upload-area { - .progress-area { - .progress { - margin-bottom: 0; - } - .icons { - width: 100%; - display: flex; - justify-content: center; - } - i.fa-arrow-right { - @include animation-wobble(); - } - i { - flex-basis: auto !important; - margin-right: 1ex; - font-size: 2em; - margin-bottom: 0.5em; - } - } - .result-area { - .result-text { - margin-bottom: 0; - display: flex; - justify-content: center; - align-items: center; - min-height: 34px; - } - i { - font-size: 2em; - @include animation-grow; - margin-right: 1ex; - } - .retry-options { - font-weight: bolder; - font-size: 1.2em; - a:first-child { - display: inline-block; - margin-right: 2ex; - } - } - } - } -} diff --git a/src/app/common/file-uploader/file-uploader.tpl.html b/src/app/common/file-uploader/file-uploader.tpl.html deleted file mode 100644 index 368f53380c..0000000000 --- a/src/app/common/file-uploader/file-uploader.tpl.html +++ /dev/null @@ -1,104 +0,0 @@ -

-
- -
-
-
-
- {{uploadZones.length == 1 ? '' : $index + 1 + ' -'}} {{upload.display.name}} -
-
- Select {{upload.display.name}} -
-
-

-

- Invalid file provided - Accepted files: {{upload.accept.split(',').join(', ')}} -

-

- Drop {{upload.display.type}} file here - or click to select one -

-

- Click to select {{upload.display.type}} file -

-
-
- - - {{upload.model[0].name}} - - - - -
-
-
-
Upload Summary
-
-
-
- - {{upload.display.name}} -
-
- {{upload.model[0].name}} - File Pending - - - -
-
-
-
-
-
- - -
-
-
-
- - - -
- -
-
-

- - File Upload {{uploadingInfo.success === true ? 'Successful' : 'Failed'}} -

-
-
-

- Error Message: - {{uploadingInfo.error}} -

-

- Retry Upload - Cancel -

-
-
-
-
From c7bcc89d799e1827096ca37e937ac1468070bbdd Mon Sep 17 00:00:00 2001 From: b0ink <40929320+b0ink@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:32:45 +1100 Subject: [PATCH 25/29] chore: update migration progress --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7885b47a9b..a754982870 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,10 @@ MIGRATED: - [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 +- [x] ./src/app/common/file-uploader/file-uploader.coffee +- [x] ./src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee +- [x] ./src/app/sessions/auth/http-auth-injector.coffee +- [x] ./src/app/sessions/sessions.coffee TODO: @@ -187,7 +191,6 @@ TODO: - [ ] ./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/portfolio.coffee - [ ] ./src/app/projects/states/index/index.coffee @@ -217,7 +220,6 @@ TODO: - [ ] ./src/app/units/states/students-list/students-list.coffee - [ ] ./src/app/common/modals/modals.coffee - [ ] ./src/app/common/modals/csv-result-modal/csv-result-modal.coffee -- [ ] ./src/app/common/file-uploader/file-uploader.coffee - [ ] ./src/app/common/common.coffee - [ ] ./src/app/common/content-editable/content-editable.coffee - [ ] ./src/app/common/services/media-service.coffee @@ -227,8 +229,6 @@ TODO: - [ ] ./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/timeout/timeout.coffee From ec6c2ee621f95e23036fcf94ae92908761be5e22 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:37:59 +1100 Subject: [PATCH 26/29] chore: remove unused files (#1044) --- src/app/doubtfire-angularjs.module.ts | 3 -- .../sessions/auth/http-auth-injector.coffee | 37 ------------------- src/app/sessions/sessions.coffee | 3 -- src/app/sessions/states/states.coffee | 0 4 files changed, 43 deletions(-) delete mode 100644 src/app/sessions/auth/http-auth-injector.coffee delete mode 100644 src/app/sessions/sessions.coffee delete mode 100644 src/app/sessions/states/states.coffee diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index c7c44c76e3..78bf107b5b 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -111,8 +111,6 @@ 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/analytics-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'; import 'build/src/app/errors/states/timeout/timeout.js'; import 'build/src/app/errors/states/states.js'; @@ -225,7 +223,6 @@ import {FileUploaderComponent} from './common/file-uploader/file-uploader.compon export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', - 'doubtfire.sessions', 'doubtfire.common', 'doubtfire.errors', 'doubtfire.units', diff --git a/src/app/sessions/auth/http-auth-injector.coffee b/src/app/sessions/auth/http-auth-injector.coffee deleted file mode 100644 index 56ef98961e..0000000000 --- a/src/app/sessions/auth/http-auth-injector.coffee +++ /dev/null @@ -1,37 +0,0 @@ -angular.module("doubtfire.sessions.auth.http-auth-injector", []) -# -# This module is responsible for injecting the auth credentials to -# all -# -.config(($httpProvider) -> - $httpProvider.interceptors.push ($q, $rootScope, DoubtfireConstants, newUserService) -> - # - # Inject authentication token for requests - # - injectAuthForRequest = (request) -> - # Intercept API requests and inject the auth token. - if _.startsWith(request.url, DoubtfireConstants.API_URL) and newUserService.currentUser.authenticationToken? - request.headers = {} unless _.has(request, "headers") - request.headers.Auth_Token = newUserService.currentUser.authenticationToken - request.headers.Username = newUserService.currentUser.username - request or $q.when request - - # - # Inject handlers for 419 and 401 response errors - # - injectAuthForResponseWithError = (response) -> - # Intercept unauthorised API responses and fire an event. - if response.config && response.config.url and _.startsWith(response.config.url, DoubtfireConstants.API_URL) - # Timeout? - if response.status is 419 - $rootScope.$broadcast "tokenTimeout" - # Unauthorised? - else if response.status is 401 - $rootScope.$broadcast "unauthorisedRequestIntercepted" - $q.reject response - - { - request: injectAuthForRequest - responseError: injectAuthForResponseWithError - } -) diff --git a/src/app/sessions/sessions.coffee b/src/app/sessions/sessions.coffee deleted file mode 100644 index ccf6e6b5f1..0000000000 --- a/src/app/sessions/sessions.coffee +++ /dev/null @@ -1,3 +0,0 @@ -angular.module('doubtfire.sessions', [ - "doubtfire.sessions.auth.http-auth-injector" -]) diff --git a/src/app/sessions/states/states.coffee b/src/app/sessions/states/states.coffee deleted file mode 100644 index e69de29bb2..0000000000 From a8a29fae3f5bb0a47ba2d13e225f339fe2756dd0 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Mon, 17 Nov 2025 18:20:33 +1100 Subject: [PATCH 27/29] refactor: portfolio welcome step migration (#1045) * refactor: init portfolio welcome step migration * chore: remove old portfolio welcome step component * chore: remove whitespace * chore: add todo * chore: increase padding --- src/app/doubtfire-angular.module.ts | 2 ++ src/app/doubtfire-angularjs.module.ts | 7 ++++- .../portfolio/directives/directives.coffee | 1 - .../portfolio-welcome-step.coffee | 10 ------- .../portfolio-welcome-step.component.html | 24 +++++++++++++++++ .../portfolio-welcome-step.component.scss | 0 .../portfolio-welcome-step.component.ts | 27 +++++++++++++++++++ .../portfolio-welcome-step.tpl.html | 21 --------------- .../states/portfolio/portfolio.tpl.html | 2 +- 9 files changed, 60 insertions(+), 34 deletions(-) delete mode 100644 src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee create mode 100644 src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.html create mode 100644 src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.scss create mode 100644 src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.ts delete mode 100644 src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 2456aef045..9f40010a1b 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -284,6 +284,7 @@ import {GroupMemberListComponent} from './groups/group-member-list/group-member- import {GroupSelectorComponent} from './groups/group-selector/group-selector.component'; import {GroupSetManagerComponent} from './groups/group-set-manager/group-set-manager.component'; import {FileUploaderComponent} from './common/file-uploader/file-uploader.component'; +import {PortfolioWelcomeStepComponent} from './projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component'; @NgModule({ // Components we declare @@ -416,6 +417,7 @@ import {FileUploaderComponent} from './common/file-uploader/file-uploader.compon GroupSelectorComponent, GroupSetManagerComponent, FileUploaderComponent, + PortfolioWelcomeStepComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 78bf107b5b..70c45bfc8e 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-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'; import 'build/src/app/projects/states/portfolio/portfolio.js'; @@ -220,6 +219,7 @@ import {GroupMemberListComponent} from './groups/group-member-list/group-member- import {GroupSelectorComponent} from './groups/group-selector/group-selector.component'; import {GroupSetManagerComponent} from './groups/group-set-manager/group-set-manager.component'; import {FileUploaderComponent} from './common/file-uploader/file-uploader.component'; +import {PortfolioWelcomeStepComponent} from './projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -541,3 +541,8 @@ DoubtfireAngularJSModule.directive( 'fFileUploader', downgradeComponent({component: FileUploaderComponent}), ); + +DoubtfireAngularJSModule.directive( + 'fPortfolioWelcomeStep', + downgradeComponent({component: PortfolioWelcomeStepComponent}), +); diff --git a/src/app/projects/states/portfolio/directives/directives.coffee b/src/app/projects/states/portfolio/directives/directives.coffee index 661acd16e7..d8ee720f68 100644 --- a/src/app/projects/states/portfolio/directives/directives.coffee +++ b/src/app/projects/states/portfolio/directives/directives.coffee @@ -3,5 +3,4 @@ angular.module('doubtfire.projects.states.portfolio.directives', [ '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' - 'doubtfire.projects.states.portfolio.directives.portfolio-welcome-step' ]) diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee deleted file mode 100644 index c34f31b354..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.coffee +++ /dev/null @@ -1,10 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-welcome-step', []) - -# -# Welcome introductory step -# -.directive('portfolioWelcomeStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html' -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.html new file mode 100644 index 0000000000..c5fd1a5cf8 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.html @@ -0,0 +1,24 @@ + + + Portfolio Preparation + + +

Preparing your portfolio involves 5 steps:

+
    +
  1. Select your Grade you are applying for
  2. +
  3. Upload your Learning Summary Report
  4. +
  5. Select the Tasks you want included
  6. +
  7. Upload any Other Resources you want to add
  8. +
  9. Compile your resources into your portfolio and review
  10. +
+

+ Once you have completed all of these steps, your portfolio will be prepared by + {{ externalName }} and you will be notified when it is ready. You can then check your work, + and if you want to make any corrections repeat these steps to create a new version of your + portfolio. +

+
+ + + +
diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.ts new file mode 100644 index 0000000000..41f953ea73 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component.ts @@ -0,0 +1,27 @@ +import {Component, OnInit, Injector} from '@angular/core'; +import {DoubtfireConstants} from 'src/app/config/constants/doubtfire-constants'; + +@Component({ + selector: 'f-portfolio-welcome-step', + templateUrl: 'portfolio-welcome-step.component.html', + styleUrls: ['portfolio-welcome-step.component.scss'], +}) +export class PortfolioWelcomeStepComponent implements OnInit { + public externalName: string = 'OnTrack'; + + constructor( + private constants: DoubtfireConstants, + private injector: Injector, + ) {} + + ngOnInit(): void { + this.constants.ExternalName.subscribe((name) => { + this.externalName = name; + }); + } + + goNextStep() { + // TODO: remove this once parent component is migrated + this.injector.get('$scope').advanceActiveTab(1); + } +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html deleted file mode 100644 index 524101329b..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.tpl.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
-

Portfolio Preparation

-
-
-

Preparing your portfolio involves 5 steps:

-
    -
  1. Select your Grade you are applying for
  2. -
  3. Upload your Learning Summary Report
  4. -
  5. Select the Tasks you want included
  6. -
  7. Upload any Other Resources you want to add
  8. -
  9. Compile your resources into your portfolio and review
  10. -
-

- Once you have completed all of these steps, your portfolio will be prepared by {{externalName.value}} and you will be notified when it is ready. You can then check your work, and if you want to make any corrections repeat these steps to create a new version of your portfolio. -

-
- -
diff --git a/src/app/projects/states/portfolio/portfolio.tpl.html b/src/app/projects/states/portfolio/portfolio.tpl.html index ae536e081f..e6a7bee0da 100644 --- a/src/app/projects/states/portfolio/portfolio.tpl.html +++ b/src/app/projects/states/portfolio/portfolio.tpl.html @@ -6,7 +6,7 @@ - + Date: Tue, 18 Nov 2025 11:46:05 +1100 Subject: [PATCH 28/29] refactor: migrate portfolio learning summary report step (#1046) * refactor: migrate portfolio learning summary report step * fix: typo * refactor: remove unused code * chore: update placeholder * refactor: check if draft task definition was submitted * chore: add type * refactor: add caption * refactor: add warning to ensure justification * chore: remove old learning summary report step component * chore: unlink old learning summary report step component * docs: update migration progress * chore: add typing --- README.md | 2 +- src/app/doubtfire-angular.module.ts | 2 + src/app/doubtfire-angularjs.module.ts | 7 +- .../portfolio/directives/directives.coffee | 1 - ...tfolio-learning-summary-report-step.coffee | 29 ------ ...earning-summary-report-step.component.html | 91 +++++++++++++++++++ ...earning-summary-report-step.component.scss | 5 + ...-learning-summary-report-step.component.ts | 67 ++++++++++++++ ...olio-learning-summary-report-step.tpl.html | 73 --------------- .../states/portfolio/portfolio.tpl.html | 2 +- 10 files changed, 173 insertions(+), 106 deletions(-) delete mode 100644 src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.coffee create mode 100644 src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.html create mode 100644 src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.scss create mode 100644 src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.ts delete mode 100644 src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html diff --git a/README.md b/README.md index a754982870..c7e9ed92e3 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ MIGRATED: - [x] ./src/app/projects/states/portfolio/directives/portfolio-grade-select-step/portfolio-grade-select-step.coffee - [x] ./src/app/sessions/auth/http-auth-injector.coffee - [x] ./src/app/sessions/sessions.coffee +- [x] ./src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.coffee TODO: @@ -188,7 +189,6 @@ TODO: - [ ] ./src/app/projects/states/outcomes/outcomes.coffee - [ ] ./src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.coffee - [ ] ./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-welcome-step/portfolio-welcome-step.coffee diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 9f40010a1b..470dc35f8c 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -285,6 +285,7 @@ import {GroupSelectorComponent} from './groups/group-selector/group-selector.com import {GroupSetManagerComponent} from './groups/group-set-manager/group-set-manager.component'; import {FileUploaderComponent} from './common/file-uploader/file-uploader.component'; import {PortfolioWelcomeStepComponent} from './projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component'; +import {PortfolioLearningSummaryReportStepComponent} from './projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component'; @NgModule({ // Components we declare @@ -418,6 +419,7 @@ import {PortfolioWelcomeStepComponent} from './projects/states/portfolio/directi GroupSetManagerComponent, FileUploaderComponent, PortfolioWelcomeStepComponent, + PortfolioLearningSummaryReportStepComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 70c45bfc8e..f7335be1fa 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -69,7 +69,6 @@ import 'build/src/app/projects/states/dashboard/directives/task-dashboard/task-d import 'build/src/app/projects/states/dashboard/dashboard.js'; 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-tasks-step/portfolio-tasks-step.js'; import 'build/src/app/projects/states/portfolio/directives/directives.js'; @@ -220,6 +219,7 @@ import {GroupSelectorComponent} from './groups/group-selector/group-selector.com import {GroupSetManagerComponent} from './groups/group-set-manager/group-set-manager.component'; import {FileUploaderComponent} from './common/file-uploader/file-uploader.component'; import {PortfolioWelcomeStepComponent} from './projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component'; +import {PortfolioLearningSummaryReportStepComponent} from './projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -546,3 +546,8 @@ DoubtfireAngularJSModule.directive( 'fPortfolioWelcomeStep', downgradeComponent({component: PortfolioWelcomeStepComponent}), ); + +DoubtfireAngularJSModule.directive( + 'fPortfolioLearningSummaryReportStep', + downgradeComponent({component: PortfolioLearningSummaryReportStepComponent}), +); diff --git a/src/app/projects/states/portfolio/directives/directives.coffee b/src/app/projects/states/portfolio/directives/directives.coffee index d8ee720f68..a20b4f1dde 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-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-learning-summary-report-step/portfolio-learning-summary-report-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.coffee deleted file mode 100644 index f2f27caaf7..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.coffee +++ /dev/null @@ -1,29 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-learning-summary-report-step', []) - -# -# Step to justify the portfolio with a Learning Summary Report -# -.directive('portfolioLearningSummaryReportStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html' - controller: ($scope) -> - $scope.forceLSRSubmit = false - $scope.acceptUploadNewLearningSummary = false - - $scope.learningSummaryReportFileUploadData = { - type: { - file0: { name: "Learning Summary Report", type: "document" } - }, - payload: { - name: "LearningSummaryReport" # DO NOT MODIFY - case senstitive on API - kind: "document" - } - } - - $scope.addNewFile = (newFile) -> - $scope.addNewFilesToPortfolio(newFile) - $scope.projectHasDraftLearningSummaryReport = false - $scope.acceptUploadNewLearningSummary = false - $scope.forceLSRSubmit = false -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.html new file mode 100644 index 0000000000..3ef553e787 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.html @@ -0,0 +1,91 @@ + + + Learning Summary Report + + +

+ Upload the Learning Summary Report, the primary porfolio document which justifies your desired + grade. +

+

+ Your Learning Summary Report is a + summary of what you have learnt in this unit. It consists of two sections: +

+ +
    +
  1. a self-assessment, and
  2. +
  3. your reflections on the unit.
  4. +
+ +

+ The self-assessment indicates how your portfolio aligns with the assessment + criteria, and which grade you are applying for. +

+ +

+ Your reflections are a personal comment on what you have learnt in the unit, + and how your knowledge and skills have developed. +

+ + @if ( + projectHasDraftLearningSummaryReport && !forceLSRSubmit && !acceptUploadNewLearningSummary + ) { +
+ + @if (draftTaskDefinitionWasUsed()) { +
+ Your learning summary report was automatically submitted from your + {{ unit.draftTaskDefinition.abbreviation }} {{ unit.draftTaskDefinition.name }} + submission. +
+ } + + + +
+ } +
+
+ warning + + Remember to provide a justification for why you believe you have achieved a + {{ targetGradeLabel }} in {{ unit.code }} {{ unit.name }}. +
+ +
+
+ + +
+ @if (!projectHasDraftLearningSummaryReport) { +
+ You're missing a Learning Summary Report. Upload one to continue. +
+ } + +
+
+
diff --git a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.scss new file mode 100644 index 0000000000..fde6acc603 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.scss @@ -0,0 +1,5 @@ +.submitted .mat-icon { + font-size: 50px; + width: 50px; + height: 50px; +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.ts new file mode 100644 index 0000000000..2f7744af06 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component.ts @@ -0,0 +1,67 @@ +import {Component, Injector, Input} from '@angular/core'; +import {Project} from 'src/app/api/models/project'; +import {Unit} from 'src/app/api/models/unit'; +import {GradeService} from 'src/app/common/services/grade.service'; + +@Component({ + selector: 'f-portfolio-learning-summary-report-step', + templateUrl: 'portfolio-learning-summary-report-step.component.html', + styleUrls: ['portfolio-learning-summary-report-step.component.scss'], +}) +export class PortfolioLearningSummaryReportStepComponent { + @Input() unit: Unit; + @Input() project: Project; + + public learningSummaryReportFileUploadData = { + type: { + file0: {name: 'Learning Summary Report', type: 'document'}, + }, + payload: { + name: 'LearningSummaryReport', // DO NOT MODIFY - case sensitive on API + kind: 'document', + }, + }; + + public forceLSRSubmit: boolean = false; + public acceptUploadNewLearningSummary: boolean = false; + + constructor( + private injector: Injector, + private gradeService: GradeService, + ) {} + + public get projectHasDraftLearningSummaryReport() { + return ( + this.project?.usesDraftLearningSummary || + this.project?.portfolioFiles.find((f) => f.idx === 0) + ); + } + + public get targetGradeLabel(): string { + return this.gradeService.grades[this.project.targetGrade]; + } + + // TODO: remove this once parent component is migrated + advanceActiveTab(index: 1 | -1) { + this.injector.get('$scope').advanceActiveTab(index); + } + + addNewFile(newFile: {kind: string; name: string; idx: number}) { + this.project.portfolioFiles.push(newFile); + this.acceptUploadNewLearningSummary = false; + this.forceLSRSubmit = false; + } + + draftTaskDefinitionWasUsed(): boolean { + const draftTaskDef = this.unit.draftTaskDefinition; + if (draftTaskDef) { + const task = this.project.findTaskForDefinition(draftTaskDef.id); + if (task && task.inSubmittedState()) { + return true; + } + } + return false; + } + + // downloadLearningSummaryReport(){} +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html deleted file mode 100644 index b190d6f35c..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.tpl.html +++ /dev/null @@ -1,73 +0,0 @@ -
-
-

- {{activeTab.title}} -

-
-
-

- Upload the Learning Summary Report, the primary porfolio document which - justifies your desired grade. -

-

- Your Learning Summary Report is a summary of what you have learnt in this unit. - It consists of two sections: -

    -
  1. a self-assessment, and
  2. -
  3. your reflections on the unit.
  4. -
-

-

- The self-assessment indicates how your portfolio aligns - with the assessment criteria, and which grade you are applying for. -

-

- Your reflections are a personal comment on what you have - learnt in the unit, and how your knowledge and skills have developed. -

-
-
-

- Before you submit your portfolio... -

- Your draft learning summary has already been copied over, - it is advised you upload a revised copy. -
-
- - -
-
- -
-
-

- Learning Summary Report Submitted -

- Click here to re-upload a new Learning Summary Report -
- -
-

- Before you submit the Learning Summary Report... -

- Remember to provide a justification for why you believe - you have achieved a {{targetGrade}} in {{unit.name}}. -
-
- -
-
-
- -
diff --git a/src/app/projects/states/portfolio/portfolio.tpl.html b/src/app/projects/states/portfolio/portfolio.tpl.html index e6a7bee0da..d60898e344 100644 --- a/src/app/projects/states/portfolio/portfolio.tpl.html +++ b/src/app/projects/states/portfolio/portfolio.tpl.html @@ -12,7 +12,7 @@ [unit]="unit" ng-if="activeTab == tabs.gradeStep">
- + From a0f3a73e130de7615a3de399a0951ca588ae00b6 Mon Sep 17 00:00:00 2001 From: Boink <40929320+b0ink@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:27:56 +1100 Subject: [PATCH 29/29] refactor: migrate portfolio add extra files step component (#1047) * refactor: migrate portfolio add extra files step component * chore: cleanup whitespace * chore: remove unused class * chore: remove debug * chore: remove old portfolio add extra files step component * chore: update migration progress --- README.md | 2 +- src/app/doubtfire-angular.module.ts | 2 + src/app/doubtfire-angularjs.module.ts | 7 +- .../portfolio/directives/directives.coffee | 1 - .../portfolio-add-extra-files-step.coffee | 25 ----- ...tfolio-add-extra-files-step.component.html | 51 ++++++++++ ...tfolio-add-extra-files-step.component.scss | 0 ...ortfolio-add-extra-files-step.component.ts | 96 +++++++++++++++++++ .../portfolio-add-extra-files-step.scss | 10 -- .../portfolio-add-extra-files-step.tpl.html | 58 ----------- .../states/portfolio/portfolio.tpl.html | 2 +- 11 files changed, 157 insertions(+), 97 deletions(-) delete mode 100644 src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee create mode 100644 src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.html create mode 100644 src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.scss create mode 100644 src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.ts delete mode 100644 src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.scss delete mode 100644 src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.tpl.html diff --git a/README.md b/README.md index c7e9ed92e3..04c76a2b2e 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ MIGRATED: - [x] ./src/app/sessions/auth/http-auth-injector.coffee - [x] ./src/app/sessions/sessions.coffee - [x] ./src/app/projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.coffee +- [x] ./src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee TODO: @@ -189,7 +190,6 @@ TODO: - [ ] ./src/app/projects/states/outcomes/outcomes.coffee - [ ] ./src/app/projects/states/portfolio/directives/portfolio-review-step/portfolio-review-step.coffee - [ ] ./src/app/projects/states/portfolio/directives/directives.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-welcome-step/portfolio-welcome-step.coffee - [ ] ./src/app/projects/states/portfolio/portfolio.coffee diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 470dc35f8c..cd3886f895 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -286,6 +286,7 @@ import {GroupSetManagerComponent} from './groups/group-set-manager/group-set-man import {FileUploaderComponent} from './common/file-uploader/file-uploader.component'; import {PortfolioWelcomeStepComponent} from './projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component'; import {PortfolioLearningSummaryReportStepComponent} from './projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component'; +import {PortfolioAddExtraFilesStepComponent} from './projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component'; @NgModule({ // Components we declare @@ -420,6 +421,7 @@ import {PortfolioLearningSummaryReportStepComponent} from './projects/states/por FileUploaderComponent, PortfolioWelcomeStepComponent, PortfolioLearningSummaryReportStepComponent, + PortfolioAddExtraFilesStepComponent, ], // Services we provide providers: [ diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index f7335be1fa..9bac403698 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -69,7 +69,6 @@ import 'build/src/app/projects/states/dashboard/directives/task-dashboard/task-d import 'build/src/app/projects/states/dashboard/dashboard.js'; 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-add-extra-files-step/portfolio-add-extra-files-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'; import 'build/src/app/projects/states/portfolio/portfolio.js'; @@ -220,6 +219,7 @@ import {GroupSetManagerComponent} from './groups/group-set-manager/group-set-man import {FileUploaderComponent} from './common/file-uploader/file-uploader.component'; import {PortfolioWelcomeStepComponent} from './projects/states/portfolio/directives/portfolio-welcome-step/portfolio-welcome-step.component'; import {PortfolioLearningSummaryReportStepComponent} from './projects/states/portfolio/directives/portfolio-learning-summary-report-step/portfolio-learning-summary-report-step.component'; +import {PortfolioAddExtraFilesStepComponent} from './projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -551,3 +551,8 @@ DoubtfireAngularJSModule.directive( 'fPortfolioLearningSummaryReportStep', downgradeComponent({component: PortfolioLearningSummaryReportStepComponent}), ); + +DoubtfireAngularJSModule.directive( + 'fPortfolioAddExtraFilesStep', + downgradeComponent({component: PortfolioAddExtraFilesStepComponent}), +); diff --git a/src/app/projects/states/portfolio/directives/directives.coffee b/src/app/projects/states/portfolio/directives/directives.coffee index a20b4f1dde..239280567d 100644 --- a/src/app/projects/states/portfolio/directives/directives.coffee +++ b/src/app/projects/states/portfolio/directives/directives.coffee @@ -1,5 +1,4 @@ angular.module('doubtfire.projects.states.portfolio.directives', [ - 'doubtfire.projects.states.portfolio.directives.portfolio-add-extra-files-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-add-extra-files-step/portfolio-add-extra-files-step.coffee b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee deleted file mode 100644 index f62984ff63..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.coffee +++ /dev/null @@ -1,25 +0,0 @@ -angular.module('doubtfire.projects.states.portfolio.directives.portfolio-add-extra-files-step', []) - -# -# Allow students to add additional files to the end of their portfolio -# They can choose any file they want to upload -# -.directive('portfolioAddExtraFilesStep', -> - restrict: 'E' - replace: true - templateUrl: 'projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.tpl.html' - controller: ($scope) -> - otherFileFileUploadData = (type) -> - type: { - file0: { name: "Other", type: type } - }, - payload: { - name: "Other" - kind: type - } - - $scope.uploadType = 'document' - $scope.$watch 'uploadType', (newType) -> - return unless newType? - $scope.uploadFileData = otherFileFileUploadData newType -) diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.html b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.html new file mode 100644 index 0000000000..53707b6891 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.html @@ -0,0 +1,51 @@ + + + Upload Other Files + + +

+ Now is your chance to upload any extra files to include in your portfolio. They'll appear at + the very top of your portfolio, before your tasks. +

+
+
    + @for (file of extraFiles; track file) { +
  1. +
    + {{ icons[file.kind] }} + {{ file.name }} +
    + +
  2. + } @empty { +

    If you do not have any files to add, you can skip this step.

    + } +
+
+
+ + Select type of file: + + Document File + Code File + Image File + + +
+ + +
+ + + + +
diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.scss b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.ts b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.ts new file mode 100644 index 0000000000..93b49f47a1 --- /dev/null +++ b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.component.ts @@ -0,0 +1,96 @@ +import {Component, Injector, Input, OnInit} from '@angular/core'; +import {MatSelectChange} from '@angular/material/select'; +import {Project} from 'src/app/api/models/project'; +import {AlertService} from 'src/app/common/services/alert.service'; + +@Component({ + selector: 'f-portfolio-add-extra-files-step', + templateUrl: 'portfolio-add-extra-files-step.component.html', + styleUrls: ['portfolio-add-extra-files-step.component.scss'], +}) +export class PortfolioAddExtraFilesStepComponent implements OnInit { + @Input() project: Project; + + public uploadType: 'document' | 'code' | 'image' = 'document'; + + public isUploading: boolean; + + public uploadFileType = { + file0: { + name: 'Other', + type: 'document', + }, + }; + + public uploadFilePayload = { + name: 'Other', + kind: 'document', + }; + + constructor( + private injector: Injector, + private alertService: AlertService, + ) {} + + public readonly icons = { + document: 'article_outlined', + code: 'integration_instructions_outlined', + image: 'image_outlined', + zip: 'zip_outlined', + }; + + ngOnInit(): void { + this.uploadType = 'document'; + + this.uploadFileType = { + file0: { + name: 'Other', + type: 'document', + }, + }; + + this.uploadFilePayload = { + name: 'Other', + kind: 'document', + }; + } + onTypeChange(event: MatSelectChange) { + console.log('on type change', event); + this.uploadFileType = { + file0: { + name: 'Other', + type: event.value, + }, + }; + + this.uploadFilePayload = { + name: 'Other', + kind: event.value, + }; + } + + public get extraFiles() { + // If file.idx === 0, then it's the Learning Summary Report, so we ignore it here + return this.project?.portfolioFiles.filter((file) => file.idx !== 0); + } + + deleteFileFromPortfolio(file: {idx: number; kind: string; name: string}) { + this.project.deleteFileFromPortfolio(file).subscribe({ + next: () => { + this.alertService.success('Succesfully delete file', 3000); + }, + error: (error) => { + this.alertService.error(`Failed to delete file: ${error}`, 6000); + }, + }); + } + + // TODO: remove this once parent component is migrated + advanceActiveTab(index: 1 | -1) { + this.injector.get('$scope').advanceActiveTab(index); + } + + addNewFilesToPortfolio(newFile: {kind: string; name: string; idx: number}) { + this.project.portfolioFiles.push(newFile); + } +} diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.scss b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.scss deleted file mode 100644 index 47c481a372..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.scss +++ /dev/null @@ -1,10 +0,0 @@ -.portfolio-add-extra-files-step { - a.clear-upload { - margin-left: 1ex; - &:hover i { - font-size: 1.15em; - color: $brand-danger; - } - display: inline-block; - } -} diff --git a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.tpl.html b/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.tpl.html deleted file mode 100644 index 136b5f5330..0000000000 --- a/src/app/projects/states/portfolio/directives/portfolio-add-extra-files-step/portfolio-add-extra-files-step.tpl.html +++ /dev/null @@ -1,58 +0,0 @@ -
-
-

{{activeTab.title}}

-
-
-

- Now is your chance to upload extra files to include in your portfolio. - These files will be added at to your portfolio before your selected tasks - from the previous step. -

-
-
-

No files to add?

-

If you do not have any files to add you can skip this step.

-
-
-

Extra file{{extraFiles().length > 1 ? 's' : ''}} added

-

- {{extraFiles().length > 1 ? 'The files you add will appear in the portfolio in the order shown below.' : ''}} - If you want to delete a file, click the cross beside the file's name. -

-
    -
  1. - {{file.name}} - - - -
  2. -
-
-
-
- -
- -
-
- - -
-
-
- -
diff --git a/src/app/projects/states/portfolio/portfolio.tpl.html b/src/app/projects/states/portfolio/portfolio.tpl.html index d60898e344..2cc7b69080 100644 --- a/src/app/projects/states/portfolio/portfolio.tpl.html +++ b/src/app/projects/states/portfolio/portfolio.tpl.html @@ -14,6 +14,6 @@ - +