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); + } + } +}