From cd66566df2025c8c379c3d6627cdbc912f65a403 Mon Sep 17 00:00:00 2001 From: Husainuddin Mohammed Date: Fri, 16 Jan 2026 16:12:40 +1100 Subject: [PATCH 1/4] refactor: migrate units/index parent state to Angular Replace AngularJS units/index with Angular implementation by adapting UnitRootState. This provides unit and unitRole data to child states while maintaining compatibility with existing AngularJS children. Changes: - Rename unit-root-state to units/index - Update URL from /units2/:unitId to /units/:unitId - Split unit and unitRole into separate resolvers - Add loading state to template - Use default ui-view instead of named unitView - Remove old AngularJS files (index.coffee, index.tpl.html) - Update module dependencies in states.coffee - Fix TaskViewerState parent reference Part of inbox migration (PR 1 of 3) --- src/app/units/states/index/index.coffee | 50 ---------- src/app/units/states/index/index.tpl.html | 7 -- src/app/units/states/states.coffee | 1 - .../task-viewer-state.component.ts | 2 +- src/app/units/unit-root-state.component.html | 9 +- src/app/units/unit-root-state.component.ts | 93 +++++++++---------- 6 files changed, 55 insertions(+), 107 deletions(-) delete mode 100644 src/app/units/states/index/index.coffee delete mode 100644 src/app/units/states/index/index.tpl.html diff --git a/src/app/units/states/index/index.coffee b/src/app/units/states/index/index.coffee deleted file mode 100644 index 69b9888ea5..0000000000 --- a/src/app/units/states/index/index.coffee +++ /dev/null @@ -1,50 +0,0 @@ -angular.module('doubtfire.units.states.index', []) - -# -# Root state for units -# -.config(($stateProvider) -> - $stateProvider.state 'units/index', { - url: "/units/:unitId" - abstract: true - views: - main: - controller: "UnitsIndexStateCtrl" - templateUrl: "units/states/index/index.tpl.html" - data: - pageTitle: "_Home_" - roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Auditor'] - } -) - -.controller("UnitsIndexStateCtrl", ($scope, $rootScope, $state, $stateParams, newUnitService, newProjectService, listenerService, globalStateService, newUserService, alertService) -> - # Error - required unitId is missing! - unitId = +$stateParams.unitId - return $state.go('home') unless unitId - - globalStateService.onLoad () -> - # Load assessing unit role - $scope.unitRole = globalStateService.loadedUnitRoles.currentValues.find((unitRole) -> unitRole.unit.id == unitId) - - if (! $scope.unitRole?) && ( newUserService.currentUser.role == "Admin" || newUserService.currentUser.role == "Auditor" ) - $scope.unitRole = newUserService.adminOrAuditorRoleFor(newUserService.currentUser.role, unitId, newUserService.currentUser) - - # Go home if no unit role was found - return $state.go('home') unless $scope.unitRole? - - globalStateService.setView("UNIT", $scope.unitRole) - - newUnitService.get(unitId).subscribe({ - next: (unit)-> - newProjectService.loadStudents(unit).subscribe({ - next: (students)-> - $scope.unit = unit - error: (err)-> - alertService.error( "Error loading students: " + err, 8000) - setTimeout((()-> $state.go('home')), 5000) - }) - error: (err)-> - alertService.error( "Error loading unit: " + err, 8000) - setTimeout((()-> $state.go('home')), 5000) - }) -) diff --git a/src/app/units/states/index/index.tpl.html b/src/app/units/states/index/index.tpl.html deleted file mode 100644 index 795574f613..0000000000 --- a/src/app/units/states/index/index.tpl.html +++ /dev/null @@ -1,7 +0,0 @@ -
- -

Loading unit details...

-
-
- -
diff --git a/src/app/units/states/states.coffee b/src/app/units/states/states.coffee index 6860c0f2a0..4e41814d1a 100644 --- a/src/app/units/states/states.coffee +++ b/src/app/units/states/states.coffee @@ -1,6 +1,5 @@ angular.module('doubtfire.units.states', [ # 'doubtfire.units.states.all' - 'doubtfire.units.states.index' 'doubtfire.units.states.edit' 'doubtfire.units.states.tasks' 'doubtfire.units.states.groups' diff --git a/src/app/units/task-viewer/task-viewer-state.component.ts b/src/app/units/task-viewer/task-viewer-state.component.ts index 08fdcf38a6..9d857b01e5 100644 --- a/src/app/units/task-viewer/task-viewer-state.component.ts +++ b/src/app/units/task-viewer/task-viewer-state.component.ts @@ -36,7 +36,7 @@ export class TaskViewerStateComponent { export const TaskViewerState: NgHybridStateDeclaration = { name: 'units2/tasks', url: '/tasks/:taskDefId', - parent: 'unit-root-state', + parent: 'units/index', data: { pageTitle: 'Unit Tasks', roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Auditor'], diff --git a/src/app/units/unit-root-state.component.html b/src/app/units/unit-root-state.component.html index 7261f3fdc6..d90f3bdbd1 100644 --- a/src/app/units/unit-root-state.component.html +++ b/src/app/units/unit-root-state.component.html @@ -1 +1,8 @@ -
+
+ +

Loading unit details...

+
+ +
+ +
diff --git a/src/app/units/unit-root-state.component.ts b/src/app/units/unit-root-state.component.ts index cd239a61a5..c29a651b2f 100644 --- a/src/app/units/unit-root-state.component.ts +++ b/src/app/units/unit-root-state.component.ts @@ -1,23 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import {CdkDragEnd, CdkDragMove, CdkDragStart} from '@angular/cdk/drag-drop'; -import {Component, Input, OnInit} from '@angular/core'; -import { - BehaviorSubject, - Observable, - Subject, - auditTime, - first, - merge, - of, - tap, - withLatestFrom, -} from 'rxjs'; -import {Unit, UnitService, UserService} from 'src/app/api/models/doubtfire-model'; -import { AppInjector } from '../app-injector'; -import { NgHybridStateDeclaration } from '@uirouter/angular-hybrid'; -import { GlobalStateService, ViewType } from '../projects/states/index/global-state.service'; -import { StateService } from '@uirouter/core'; -import { AlertService } from '../common/services/alert.service'; +import {Component, Input} from '@angular/core'; +import {Observable} from 'rxjs'; +import {Unit, UnitRole} from 'src/app/api/models/doubtfire-model'; +import {AppInjector} from '../app-injector'; +import {NgHybridStateDeclaration} from '@uirouter/angular-hybrid'; +import {GlobalStateService, ViewType} from '../projects/states/index/global-state.service'; +import {StateService} from '@uirouter/core'; +import {AlertService} from '../common/services/alert.service'; +import {UnitService} from '../api/services/unit.service'; +import {UserService} from '../api/services/user.service'; +import {first} from 'rxjs/operators'; @Component({ selector: 'f-unit-root-state', @@ -26,14 +18,15 @@ import { AlertService } from '../common/services/alert.service'; }) export class UnitRootStateComponent { @Input() public unit$: Observable; + @Input() public unitRole$: Observable; } export const UnitRootState: NgHybridStateDeclaration = { - name: 'unit-root-state', - url: '/units2/:unitId', + name: 'units/index', + url: '/units/:unitId', abstract: true, data: { - pageTitle: 'Unit Root State', + pageTitle: '_Home_', roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Auditor'], }, views: { @@ -44,45 +37,51 @@ export const UnitRootState: NgHybridStateDeclaration = { resolve: { unit$: function ($stateParams) { const unitService = AppInjector.get(UnitService); + const stateService = AppInjector.get(StateService); + const alertService = AppInjector.get(AlertService); + + const unitId = parseInt($stateParams.unitId); + if (!unitId) { + stateService.go('home'); + return; + } + + return unitService.get(unitId).pipe( + first() + ); + }, + unitRole$: function ($stateParams, unit$) { const globalState = AppInjector.get(GlobalStateService); const userService = AppInjector.get(UserService); const stateService = AppInjector.get(StateService); + const alertService = AppInjector.get(AlertService); - return new Observable((observer) => { + return new Observable((observer) => { globalState.onLoad(() => { - const unitId: number = parseInt($stateParams.unitId); - let unitRole = globalState.loadedUnitRoles.currentValues.find( - (unitRole) => unitRole.unit.id === unitId, + const unitId = parseInt($stateParams.unitId); + + let role = globalState.loadedUnitRoles.currentValues.find( + (ur) => ur.unit.id === unitId ); - if ( - !unitRole && - (userService.currentUser.role == 'Admin' || userService.currentUser.role == 'Auditor') - ) { - unitRole = userService.adminOrAuditorRoleFor( + if (!role && (userService.currentUser.role === 'Admin' || userService.currentUser.role === 'Auditor')) { + role = userService.adminOrAuditorRoleFor( userService.currentUser.role, unitId, - userService.currentUser, + userService.currentUser ); } - // Go home if no unit role was found - if (!unitRole) { - console.log('No unit role found for unit', unitId); - return stateService.go('home'); + if (!role) { + alertService.error('You do not have access to this unit', 6000); + stateService.go('home'); + observer.complete(); + return; } - unitService.get(unitId).subscribe({ - next: (unit: Unit) => { - observer.next(unit); - globalState.setView(ViewType.UNIT, unitRole); - observer.complete(); - }, - error: (err) => { - AppInjector.get(AlertService).error('Error loading unit: ' + err, 8000); - setTimeout(() => stateService.go('home'), 5000); - } - }); + globalState.setView(ViewType.UNIT, role); + observer.next(role); + observer.complete(); }); }).pipe(first()); }, From e4706bcbc1801e7d1fad830ef1956f815645c624 Mon Sep 17 00:00:00 2001 From: Husainuddin Mohammed Date: Sun, 18 Jan 2026 13:06:26 +1100 Subject: [PATCH 2/4] fix: remove stale import for deleted units/index module Remove import reference to build/src/app/units/states/index/index.js which no longer exists after migration to Angular. This was causing the dev server to fail during build. --- src/app/doubtfire-angularjs.module.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 4855d04ad6..99e59f22e8 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -80,7 +80,6 @@ import 'build/src/app/units/states/edit/directives/unit-details-editor/unit-deta 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'; -import 'build/src/app/units/states/index/index.js'; 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'; From 222b28565caaae1932d31a8182773c07fb27e6a5 Mon Sep 17 00:00:00 2001 From: Mohammed Husainuddin Date: Wed, 21 Jan 2026 09:45:09 +1100 Subject: [PATCH 3/4] fix: bridge Angular observables to AngularJS scope for child state compatibility - Subscribe to unit$ and unitRole$ observables in ngOnInit - Store values as component properties for Angular template - Set values on for AngularJS child states to access - Use () to trigger AngularJS digest cycle - Add defensive checks for Angular availability - Clean up subscriptions in ngOnDestroy Fixes scope inheritance issue where AngularJS child states (units/tasks, units/edit, etc.) couldn't access unit data from the Angular parent component. --- src/app/units/unit-root-state.component.ts | 56 +++++++++++++++++++--- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/src/app/units/unit-root-state.component.ts b/src/app/units/unit-root-state.component.ts index c29a651b2f..659152a9da 100644 --- a/src/app/units/unit-root-state.component.ts +++ b/src/app/units/unit-root-state.component.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import {Component, Input} from '@angular/core'; -import {Observable} from 'rxjs'; +import {Component, Input, OnInit, OnDestroy} from '@angular/core'; +import {Observable, Subscription} from 'rxjs'; import {Unit, UnitRole} from 'src/app/api/models/doubtfire-model'; import {AppInjector} from '../app-injector'; import {NgHybridStateDeclaration} from '@uirouter/angular-hybrid'; @@ -16,9 +16,53 @@ import {first} from 'rxjs/operators'; templateUrl: './unit-root-state.component.html', styleUrl: './unit-root-state.component.css', }) -export class UnitRootStateComponent { +export class UnitRootStateComponent implements OnInit, OnDestroy { @Input() public unit$: Observable; @Input() public unitRole$: Observable; + + public unit: Unit; + public unitRole: UnitRole; + + private subscriptions: Subscription[] = []; + + ngOnInit(): void { + // Subscribe to observables and store values + // This makes them available to both Angular template and AngularJS child states + if (this.unit$) { + const unitSub = this.unit$.subscribe((unit) => { + this.unit = unit; + // Also set on window for AngularJS child state access + if ((window as any).angular) { + const $rootScope = (window as any).angular.element(document.body).injector()?.get('$rootScope'); + if ($rootScope) { + $rootScope.unit = unit; + $rootScope.$applyAsync(); + } + } + }); + this.subscriptions.push(unitSub); + } + + if (this.unitRole$) { + const roleSub = this.unitRole$.subscribe((unitRole) => { + this.unitRole = unitRole; + // Also set on window for AngularJS child state access + if ((window as any).angular) { + const $rootScope = (window as any).angular.element(document.body).injector()?.get('$rootScope'); + if ($rootScope) { + $rootScope.unitRole = unitRole; + $rootScope.$applyAsync(); + } + } + }); + this.subscriptions.push(roleSub); + } + } + + ngOnDestroy(): void { + // Clean up subscriptions + this.subscriptions.forEach(sub => sub.unsubscribe()); + } } export const UnitRootState: NgHybridStateDeclaration = { @@ -39,7 +83,7 @@ export const UnitRootState: NgHybridStateDeclaration = { const unitService = AppInjector.get(UnitService); const stateService = AppInjector.get(StateService); const alertService = AppInjector.get(AlertService); - + const unitId = parseInt($stateParams.unitId); if (!unitId) { stateService.go('home'); @@ -59,7 +103,7 @@ export const UnitRootState: NgHybridStateDeclaration = { return new Observable((observer) => { globalState.onLoad(() => { const unitId = parseInt($stateParams.unitId); - + let role = globalState.loadedUnitRoles.currentValues.find( (ur) => ur.unit.id === unitId ); @@ -86,4 +130,4 @@ export const UnitRootState: NgHybridStateDeclaration = { }).pipe(first()); }, }, -}; +}; \ No newline at end of file From eb09affd04180e291ad9e801799e371c5bf98afb Mon Sep 17 00:00:00 2001 From: Mohammed Husainuddin Date: Fri, 30 Jan 2026 20:06:15 +1100 Subject: [PATCH 4/4] fix: bridge Angular observables to AngularJS scope for child state compatibility Use hybrid pattern with controller + template to bridge data: - Controller sets .unit and .unitRole for AngularJS children - Template passes observables to Angular component via property binding - Fixes blank page issue where child states couldn't access unit data Resolves feedback from JeffySam and mannat2634 regarding blank pages. --- src/app/units/unit-root-state.component.ts | 38 ++++++++++------------ 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/app/units/unit-root-state.component.ts b/src/app/units/unit-root-state.component.ts index 659152a9da..2082878446 100644 --- a/src/app/units/unit-root-state.component.ts +++ b/src/app/units/unit-root-state.component.ts @@ -4,6 +4,7 @@ import {Observable, Subscription} from 'rxjs'; import {Unit, UnitRole} from 'src/app/api/models/doubtfire-model'; import {AppInjector} from '../app-injector'; import {NgHybridStateDeclaration} from '@uirouter/angular-hybrid'; +import {Ng2ViewDeclaration} from '@uirouter/angular'; import {GlobalStateService, ViewType} from '../projects/states/index/global-state.service'; import {StateService} from '@uirouter/core'; import {AlertService} from '../common/services/alert.service'; @@ -26,19 +27,10 @@ export class UnitRootStateComponent implements OnInit, OnDestroy { private subscriptions: Subscription[] = []; ngOnInit(): void { - // Subscribe to observables and store values - // This makes them available to both Angular template and AngularJS child states + // Subscribe to observables and store values for Angular template if (this.unit$) { const unitSub = this.unit$.subscribe((unit) => { this.unit = unit; - // Also set on window for AngularJS child state access - if ((window as any).angular) { - const $rootScope = (window as any).angular.element(document.body).injector()?.get('$rootScope'); - if ($rootScope) { - $rootScope.unit = unit; - $rootScope.$applyAsync(); - } - } }); this.subscriptions.push(unitSub); } @@ -46,14 +38,6 @@ export class UnitRootStateComponent implements OnInit, OnDestroy { if (this.unitRole$) { const roleSub = this.unitRole$.subscribe((unitRole) => { this.unitRole = unitRole; - // Also set on window for AngularJS child state access - if ((window as any).angular) { - const $rootScope = (window as any).angular.element(document.body).injector()?.get('$rootScope'); - if ($rootScope) { - $rootScope.unitRole = unitRole; - $rootScope.$applyAsync(); - } - } }); this.subscriptions.push(roleSub); } @@ -75,9 +59,23 @@ export const UnitRootState: NgHybridStateDeclaration = { }, views: { main: { - component: UnitRootStateComponent, - }, + controller: function($scope, unit$, unitRole$) { + // Set observables on $scope for template binding to Angular component + $scope.unit$ = unit$; + $scope.unitRole$ = unitRole$; + + // Subscribe and set values on $scope for AngularJS child states + unit$.subscribe((unit) => { + $scope.unit = unit; + }); + unitRole$.subscribe((unitRole) => { + $scope.unitRole = unitRole; + }); + }, + template: '', + } as unknown as Ng2ViewDeclaration, }, + resolve: { unit$: function ($stateParams) { const unitService = AppInjector.get(UnitService);