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..90a415b29d 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';
@@ -242,6 +242,26 @@ 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',
+])
+.config(($stateProvider: any) => {
+ $stateProvider.state('units/tasks', {
+ abstract: true,
+ parent: 'units/index',
+ url: '/tasks',
+ template: '',
+ data: {
+ pageTitle: '_Home_',
+ roleWhitelist: ['Tutor', 'Convenor', 'Admin', 'Auditor'],
+ },
+ });
+});
+
export const DoubtfireAngularJSModule = angular
.module('doubtfire', [
'doubtfire.config',
@@ -417,6 +437,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
-)
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..5d8490546e
--- /dev/null
+++ b/src/app/units/states/tasks/tasks.component.ts
@@ -0,0 +1,85 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { StateService, TransitionService, Transition } from '@uirouter/angular';
+import { TaskService } from 'src/app/api/services/task.service';
+
+@Component({
+ selector: 'f-units-tasks-state',
+ templateUrl: 'tasks.component.html',
+ styleUrls: ['tasks.component.scss'],
+})
+export class UnitsTasksStateComponent implements OnInit, OnDestroy {
+ taskData: {
+ taskKey: unknown;
+ source: unknown;
+ selectedTask: unknown;
+ taskDefMode: boolean;
+ onSelectedTaskChange: (task: unknown) => void;
+ };
+
+ private deregisterTransition: Function;
+
+ constructor(
+ private stateService: StateService,
+ private transitionService: TransitionService,
+ private taskService: TaskService,
+ ) {}
+
+ ngOnInit(): void {
+ this.taskData = {
+ taskKey: null,
+ source: null,
+ selectedTask: null,
+ taskDefMode: false,
+ onSelectedTaskChange: (task: unknown) => {
+ // Bug fix 4: cleaner taskKey assignment — only call taskKey() if task exists
+ 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);
+ }
+ },
+ };
+
+ // 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({to: 'units/tasks.**'}, (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: unknown): void {
+ this.stateService.go(
+ this.stateService.$current.name,
+ {taskKey: (task as {taskKeyToUrlString: () => string})?.taskKeyToUrlString()},
+ {notify: false },
+ );
+ }
+
+ private setTaskKeyFromUrlParams(taskKeyString: string | null): void {
+ if (taskKeyString) {
+ this.taskData.taskKey = this.taskService.taskKeyFromString(taskKeyString);
+ }
+ }
+}