From 257dd9bc29a62a93ee1063161d4a8bf2d5cd5e6a Mon Sep 17 00:00:00 2001 From: Sujay-Deakin Date: Mon, 11 May 2026 16:01:18 +1000 Subject: [PATCH 1/7] feat: create initial files for migration of units/states/tasks --- .../units/states/tasks/tasks.component.html | 1 + .../units/states/tasks/tasks.component.scss | 0 src/app/units/states/tasks/tasks.component.ts | 84 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 src/app/units/states/tasks/tasks.component.html create mode 100644 src/app/units/states/tasks/tasks.component.scss create mode 100644 src/app/units/states/tasks/tasks.component.ts diff --git a/src/app/units/states/tasks/tasks.component.html b/src/app/units/states/tasks/tasks.component.html new file mode 100644 index 0000000000..5105d51131 --- /dev/null +++ b/src/app/units/states/tasks/tasks.component.html @@ -0,0 +1 @@ + diff --git a/src/app/units/states/tasks/tasks.component.scss b/src/app/units/states/tasks/tasks.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/units/states/tasks/tasks.component.ts b/src/app/units/states/tasks/tasks.component.ts new file mode 100644 index 0000000000..e458b1a358 --- /dev/null +++ b/src/app/units/states/tasks/tasks.component.ts @@ -0,0 +1,84 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { StateService, TransitionService, Transition } from '@uirouter/angular'; +import { NewTaskService } from 'src/app/api/services/new-task.service'; + +@Component({ + selector: 'units-tasks-state', + templateUrl: 'tasks.component.html', + styleUrls: ['tasks.component.scss'], +}) +export class UnitsTasksStateComponent implements OnInit, OnDestroy { + taskData: { + taskKey: any; + source: any; + selectedTask: any; + taskDefMode: boolean; + onSelectedTaskChange: (task: any) => void; + }; + + private deregisterTransition: Function; + + constructor( + private stateService: StateService, + private transitionService: TransitionService, + private newTaskService: NewTaskService, + ) {} + + ngOnInit(): void { + this.taskData = { + taskKey: null, + source: null, + selectedTask: null, + taskDefMode: false, + onSelectedTaskChange: (task: any) => { + // Bug fix 4: cleaner taskKey assignment — only call taskKey() if task exists + this.taskData.taskKey = task?.taskKey() ?? null; + // Bug fix 3: avoid redundant $state.go — only navigate if task exists + if (task) { + this.setTaskKeyAsUrlParams(task); + } + }, + }; + + // Read initial taskKey from URL on load + // Bug fix 1: null-safe parsing — guard against missing params + const initialKey = this.stateService.params?.['taskKey']; + this.setTaskKeyFromUrlParams(initialKey); + + // Listen for state transitions to keep taskKey in sync + this.deregisterTransition = this.transitionService.onStart({}, (trans: Transition) => { + const toParams = trans.params('to'); + const fromState = trans.from(); + const toState = trans.to(); + const fromParams = trans.params('from'); + + // Bug fix 2: force taskKeyString to a string to avoid type mismatches + const taskKeyString = toParams['taskKey'] != null ? String(toParams['taskKey']) : null; + this.setTaskKeyFromUrlParams(taskKeyString); + + // Bug fix 5: safer preventDefault — also check that states are defined before comparing + if (fromState?.name && fromState.name === toState?.name && fromParams['unitId'] === toParams['unitId']) { + return false; + } + }); + } + + ngOnDestroy(): void { + if (this.deregisterTransition) { + this.deregisterTransition(); + } + } + + private setTaskKeyAsUrlParams(task: any): void { + this.stateService.go( + this.stateService.$current.name, + { taskKey: task?.taskKeyToUrlString() }, + { notify: false } + ); + } + + private setTaskKeyFromUrlParams(taskKeyString: string | null): void { + // Bug fix 1 & 2: null-safe + forced string before passing to service + this.taskData.taskKey = this.newTaskService.taskKeyFromString(taskKeyString ?? ''); + } +} From 0b73f0dc9de9335a498289a694c29bd839bad46b Mon Sep 17 00:00:00 2001 From: Sujay-Deakin Date: Mon, 11 May 2026 16:24:00 +1000 Subject: [PATCH 2/7] feat: wire UnitsTasksStateComponent into Angular and AngularJS modules and remove tasks.coffee --- src/app/doubtfire-angular.module.ts | 2 + src/app/doubtfire-angularjs.module.ts | 6 ++- src/app/units/states/tasks/tasks.coffee | 70 ------------------------- 3 files changed, 7 insertions(+), 71 deletions(-) delete mode 100644 src/app/units/states/tasks/tasks.coffee diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index ae030bcc3e..fa7073757e 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -213,6 +213,7 @@ import {TaskAssessmentCardComponent} from './projects/states/dashboard/directive import {TaskSubmissionCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component'; import {TaskDashboardComponent} from './projects/states/dashboard/directives/task-dashboard/task-dashboard.component'; import {InboxComponent} from './units/states/tasks/inbox/inbox.component'; +import {UnitsTasksStateComponent} from './units/states/tasks/tasks.component'; import {ProjectProgressBarComponent} from './common/project-progress-bar/project-progress-bar.component'; import {TeachingPeriodListComponent} from './admin/states/teaching-periods/teaching-period-list/teaching-period-list.component'; import {FChipComponent} from './common/f-chip/f-chip.component'; @@ -454,6 +455,7 @@ const GANTT_CHART_CONFIG = { TaskSubmissionCardComponent, TaskDashboardComponent, InboxComponent, + UnitsTasksStateComponent, ProjectProgressBarComponent, TeachingPeriodListComponent, CreateNewUnitModal, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index ca57426fd2..f257f355a4 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -91,7 +91,6 @@ 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'; import 'build/src/app/units/states/tasks/inbox/inbox.js'; -import 'build/src/app/units/states/tasks/tasks.js'; import 'build/src/app/units/states/tasks/viewer/directives/directives.js'; import 'build/src/app/units/states/tasks/viewer/viewer.js'; import 'build/src/app/units/states/tasks/definition/definition.js'; @@ -203,6 +202,7 @@ 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 {UnitsTasksStateComponent} from './units/states/tasks/tasks.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'; @@ -417,6 +417,10 @@ DoubtfireAngularJSModule.directive( downgradeComponent({component: TasksViewerComponent}), ); DoubtfireAngularJSModule.directive('fInbox', downgradeComponent({component: InboxComponent})); +DoubtfireAngularJSModule.directive( + 'unitsTasksState', + downgradeComponent({component: UnitsTasksStateComponent}), +); DoubtfireAngularJSModule.directive( 'fTaskDueCard', downgradeComponent({component: TaskDueCardComponent}), diff --git a/src/app/units/states/tasks/tasks.coffee b/src/app/units/states/tasks/tasks.coffee deleted file mode 100644 index d4bff19b2e..0000000000 --- a/src/app/units/states/tasks/tasks.coffee +++ /dev/null @@ -1,70 +0,0 @@ -angular.module('doubtfire.units.states.tasks', [ - 'doubtfire.units.states.tasks.inbox' - 'doubtfire.units.states.tasks.definition' - 'doubtfire.units.states.tasks.moderation' - 'doubtfire.units.states.tasks.overflow' - 'doubtfire.units.states.tasks.viewer' -]) - -# -# Teacher child state for units for task-related activites -# -.config(($stateProvider) -> - $stateProvider.state 'units/tasks', { - abstract: true - parent: 'units/index' - url: '/tasks' - controller: 'UnitsTasksStateCtrl' - template: '' - data: - pageTitle: "_Home_" - roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Auditor'] - } -) - -.controller('UnitsTasksStateCtrl', ($scope, $state, newTaskService, listenerService, $transition$) -> - # Cleanup - listeners = listenerService.listenTo($scope) - - # Task data wraps: - # * the URL task composite key (project username + task def abbreviation) sourced from the URL, - # * the task source used for the task inbox list, - # * the actual selectedTask reference - # * the callback for when a task is updated (accepts the new task) - $scope.taskData = { - taskKey: null - source: null - selectedTask: null - onSelectedTaskChange: (task) -> - taskKey = task?.taskKey() - $scope.taskData.taskKey = taskKey - setTaskKeyAsUrlParams(task) - } - - # Sets URL parameters for the task key - setTaskKeyAsUrlParams = (task) -> - # Change URL of new task without notify - $state.go($state.$current, {taskKey: task?.taskKeyToUrlString()}, {notify: false}) - - # Sets task key from URL parameters - setTaskKeyFromUrlParams = (taskKeyString) -> - # Propagate selected task change downward to search for actual task - # inside the task inbox list - $scope.taskData.taskKey = newTaskService.taskKeyFromString(taskKeyString) - - # Child states will use taskKey to notify what task has been - # selected by the child on first load. - taskKey = $transition$.params().taskKey - setTaskKeyFromUrlParams(taskKey) - - # Whenever the state is changed, we look at the taskKey in the URL params - # see if we can set it as an actual taskKey object - listeners.push $scope.$on '$stateChangeStart', ($event, toState, toParams, fromState, fromParams) -> - setTaskKeyFromUrlParams(toParams.taskKey) - # Use preventDefault to prevent destroying the child state's - # scope if they are the same states. Otherwise, if they are - # the same, we destroy the state's scope and recreate it again - # unnecessarily; doing so will cause a re-request in the task - # list which is not required. - $event.preventDefault() if fromState == toState && fromParams.unitId == toParams.unitId -) From 34c2550c3b055fd2dd069487313ecdd8e143859a Mon Sep 17 00:00:00 2001 From: Sujay-Deakin Date: Mon, 11 May 2026 17:27:45 +1000 Subject: [PATCH 3/7] fix: resolve module registration and correct TaskService import in tasks component --- src/app/doubtfire-angularjs.module.ts | 8 ++++++++ src/app/units/states/tasks/tasks.component.ts | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index f257f355a4..d9de6fab75 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -242,6 +242,14 @@ import {TaskPlannerCardComponent} from './projects/states/dashboard/directives/p import {TaskOverseerReportComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-overseer-report/task-overseer-report.component'; import {TutorNotesComponent} from './projects/states/tutor-notes/tutor-notes.component'; +angular.module('doubtfire.units.states.tasks', [ + 'doubtfire.units.states.tasks.inbox', + 'doubtfire.units.states.tasks.definition', + 'doubtfire.units.states.tasks.moderation', + 'doubtfire.units.states.tasks.overflow', + 'doubtfire.units.states.tasks.viewer', +]); + export const DoubtfireAngularJSModule = angular .module('doubtfire', [ 'doubtfire.config', diff --git a/src/app/units/states/tasks/tasks.component.ts b/src/app/units/states/tasks/tasks.component.ts index e458b1a358..676221440b 100644 --- a/src/app/units/states/tasks/tasks.component.ts +++ b/src/app/units/states/tasks/tasks.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { StateService, TransitionService, Transition } from '@uirouter/angular'; -import { NewTaskService } from 'src/app/api/services/new-task.service'; +import { TaskService } from 'src/app/api/services/task.service'; @Component({ selector: 'units-tasks-state', @@ -21,7 +21,7 @@ export class UnitsTasksStateComponent implements OnInit, OnDestroy { constructor( private stateService: StateService, private transitionService: TransitionService, - private newTaskService: NewTaskService, + private taskService: TaskService, ) {} ngOnInit(): void { @@ -79,6 +79,6 @@ export class UnitsTasksStateComponent implements OnInit, OnDestroy { private setTaskKeyFromUrlParams(taskKeyString: string | null): void { // Bug fix 1 & 2: null-safe + forced string before passing to service - this.taskData.taskKey = this.newTaskService.taskKeyFromString(taskKeyString ?? ''); + this.taskData.taskKey = this.taskService.taskKeyFromString(taskKeyString ?? ''); } } From 24eb2a1d35c70f1d02319bc4b6fc740f798a152b Mon Sep 17 00:00:00 2001 From: Sujay-Deakin Date: Fri, 15 May 2026 16:32:13 +1000 Subject: [PATCH 4/7] fix: restore units/tasks state config and controller for hybrid routing --- src/app/doubtfire-angularjs.module.ts | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index d9de6fab75..0475060d55 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -248,6 +248,51 @@ angular.module('doubtfire.units.states.tasks', [ 'doubtfire.units.states.tasks.moderation', 'doubtfire.units.states.tasks.overflow', 'doubtfire.units.states.tasks.viewer', +]) +.config(($stateProvider: any) => { + $stateProvider.state('units/tasks', { + abstract: true, + parent: 'units/index', + url: '/tasks', + controller: 'UnitsTasksStateCtrl', + template: '', + data: { + pageTitle: '_Home_', + roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Auditor'], + }, + }); +}) +.controller('UnitsTasksStateCtrl', ['$scope', '$state', 'newTaskService', 'listenerService', '$transition$', + function($scope: any, $state: any, newTaskService: any, listenerService: any, $transition$: any) { + const listeners = listenerService.listenTo($scope); + + $scope.taskData = { + taskKey: null, + source: null, + selectedTask: null, + taskDefMode: false, + onSelectedTaskChange: (task: any) => { + $scope.taskData.taskKey = task?.taskKey() ?? null; + if (task) { + $state.go($state.$current, {taskKey: task?.taskKeyToUrlString()}, {notify: false}); + } + }, + }; + + const taskKey = $transition$.params().taskKey; + if (taskKey) { + $scope.taskData.taskKey = newTaskService.taskKeyFromString(String(taskKey)); + } + + listeners.push($scope.$on('$stateChangeStart', ($event: any, toState: any, toParams: any, fromState: any, fromParams: any) => { + if (toParams.taskKey != null) { + $scope.taskData.taskKey = newTaskService.taskKeyFromString(String(toParams.taskKey)); + } + if (fromState?.name && fromState.name === toState?.name && fromParams.unitId === toParams.unitId) { + $event.preventDefault(); + } + })); + } ]); export const DoubtfireAngularJSModule = angular From d2178c08be38ade1475cf2c601d5684909d32aa4 Mon Sep 17 00:00:00 2001 From: Sujay-Deakin Date: Sun, 17 May 2026 11:46:37 +1000 Subject: [PATCH 5/7] refactor: address PR review feedback - fix routing, null guard and transition scope --- src/app/doubtfire-angularjs.module.ts | 37 +------------------ src/app/units/states/tasks/tasks.component.ts | 29 ++++++++------- 2 files changed, 17 insertions(+), 49 deletions(-) diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 0475060d55..1cca87e131 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -254,46 +254,13 @@ angular.module('doubtfire.units.states.tasks', [ abstract: true, parent: 'units/index', url: '/tasks', - controller: 'UnitsTasksStateCtrl', - template: '', + template: '', data: { pageTitle: '_Home_', roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Auditor'], }, }); -}) -.controller('UnitsTasksStateCtrl', ['$scope', '$state', 'newTaskService', 'listenerService', '$transition$', - function($scope: any, $state: any, newTaskService: any, listenerService: any, $transition$: any) { - const listeners = listenerService.listenTo($scope); - - $scope.taskData = { - taskKey: null, - source: null, - selectedTask: null, - taskDefMode: false, - onSelectedTaskChange: (task: any) => { - $scope.taskData.taskKey = task?.taskKey() ?? null; - if (task) { - $state.go($state.$current, {taskKey: task?.taskKeyToUrlString()}, {notify: false}); - } - }, - }; - - const taskKey = $transition$.params().taskKey; - if (taskKey) { - $scope.taskData.taskKey = newTaskService.taskKeyFromString(String(taskKey)); - } - - listeners.push($scope.$on('$stateChangeStart', ($event: any, toState: any, toParams: any, fromState: any, fromParams: any) => { - if (toParams.taskKey != null) { - $scope.taskData.taskKey = newTaskService.taskKeyFromString(String(toParams.taskKey)); - } - if (fromState?.name && fromState.name === toState?.name && fromParams.unitId === toParams.unitId) { - $event.preventDefault(); - } - })); - } -]); +}); export const DoubtfireAngularJSModule = angular .module('doubtfire', [ diff --git a/src/app/units/states/tasks/tasks.component.ts b/src/app/units/states/tasks/tasks.component.ts index 676221440b..2469bd36a6 100644 --- a/src/app/units/states/tasks/tasks.component.ts +++ b/src/app/units/states/tasks/tasks.component.ts @@ -3,20 +3,20 @@ import { StateService, TransitionService, Transition } from '@uirouter/angular'; import { TaskService } from 'src/app/api/services/task.service'; @Component({ - selector: 'units-tasks-state', + selector: 'f-units-tasks-state', templateUrl: 'tasks.component.html', styleUrls: ['tasks.component.scss'], }) export class UnitsTasksStateComponent implements OnInit, OnDestroy { taskData: { - taskKey: any; - source: any; - selectedTask: any; + taskKey: unknown; + source: unknown; + selectedTask: unknown; taskDefMode: boolean; - onSelectedTaskChange: (task: any) => void; + onSelectedTaskChange: (task: unknown) => void; }; - private deregisterTransition: Function; + private deregisterTransition: () => void; constructor( private stateService: StateService, @@ -30,9 +30,9 @@ export class UnitsTasksStateComponent implements OnInit, OnDestroy { source: null, selectedTask: null, taskDefMode: false, - onSelectedTaskChange: (task: any) => { + onSelectedTaskChange: (task: unknown) => { // Bug fix 4: cleaner taskKey assignment — only call taskKey() if task exists - this.taskData.taskKey = task?.taskKey() ?? null; + this.taskData.taskKey = (task as {taskKey: () => unknown})?.taskKey() ?? null; // Bug fix 3: avoid redundant $state.go — only navigate if task exists if (task) { this.setTaskKeyAsUrlParams(task); @@ -46,7 +46,7 @@ export class UnitsTasksStateComponent implements OnInit, OnDestroy { this.setTaskKeyFromUrlParams(initialKey); // Listen for state transitions to keep taskKey in sync - this.deregisterTransition = this.transitionService.onStart({}, (trans: Transition) => { + this.deregisterTransition = this.transitionService.onStart({to: 'units/tasks.**'}, (trans: Transition) => { const toParams = trans.params('to'); const fromState = trans.from(); const toState = trans.to(); @@ -69,16 +69,17 @@ export class UnitsTasksStateComponent implements OnInit, OnDestroy { } } - private setTaskKeyAsUrlParams(task: any): void { + private setTaskKeyAsUrlParams(task: unknown): void { this.stateService.go( this.stateService.$current.name, - { taskKey: task?.taskKeyToUrlString() }, - { notify: false } + {taskKey: (task as {taskKeyToUrlString: () => string})?.taskKeyToUrlString()}, + {notify: false }, ); } private setTaskKeyFromUrlParams(taskKeyString: string | null): void { - // Bug fix 1 & 2: null-safe + forced string before passing to service - this.taskData.taskKey = this.taskService.taskKeyFromString(taskKeyString ?? ''); + if (taskKeyString) { + this.taskData.taskKey = this.taskService.taskKeyFromString(taskKeyString); + } } } From fc035c644c2f358c119e4a52cd61aed6b1994d25 Mon Sep 17 00:00:00 2001 From: Sujay-Deakin Date: Sun, 17 May 2026 11:58:47 +1000 Subject: [PATCH 6/7] refactor: address PR review feedback --- src/app/doubtfire-angularjs.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 1cca87e131..90a415b29d 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -254,7 +254,7 @@ angular.module('doubtfire.units.states.tasks', [ abstract: true, parent: 'units/index', url: '/tasks', - template: '', + template: '', data: { pageTitle: '_Home_', roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Auditor'], From 009455dfa2330b1f4e64135bd56daaebf41ef369 Mon Sep 17 00:00:00 2001 From: Sujay-Deakin Date: Sun, 17 May 2026 17:12:08 +1000 Subject: [PATCH 7/7] fix: correct deregisterTransition type to Function for transitionService compatibility --- src/app/units/states/tasks/tasks.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/units/states/tasks/tasks.component.ts b/src/app/units/states/tasks/tasks.component.ts index 2469bd36a6..5d8490546e 100644 --- a/src/app/units/states/tasks/tasks.component.ts +++ b/src/app/units/states/tasks/tasks.component.ts @@ -16,7 +16,7 @@ export class UnitsTasksStateComponent implements OnInit, OnDestroy { onSelectedTaskChange: (task: unknown) => void; }; - private deregisterTransition: () => void; + private deregisterTransition: Function; constructor( private stateService: StateService,