From ac72ee0f468c27c3ddb242a31d2b455c927a376a Mon Sep 17 00:00:00 2001 From: Supun Shettiarachchi Date: Sat, 17 Jan 2026 13:49:56 +0400 Subject: [PATCH 1/3] feat(projects): restore Projects Index migration files --- src/app/projects/states/index/index.coffee | 7 +- .../states/index/index.component.html | 46 +++++++++ .../states/index/index.component.scss | 0 .../projects/states/index/index.component.ts | 99 +++++++++++++++++++ 4 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 src/app/projects/states/index/index.component.html create mode 100644 src/app/projects/states/index/index.component.scss create mode 100644 src/app/projects/states/index/index.component.ts diff --git a/src/app/projects/states/index/index.coffee b/src/app/projects/states/index/index.coffee index d77b2be900..8a0f4dfde8 100644 --- a/src/app/projects/states/index/index.coffee +++ b/src/app/projects/states/index/index.coffee @@ -13,15 +13,16 @@ angular.module('doubtfire.projects.states.index', []) templateUrl: "units/states/index/index.tpl.html" # We can re-use unit's index here data: pageTitle: "_Home_" + roleWhitelist: ['Student', 'Tutor', 'Convenor', 'Admin', 'Auditor'] } ) -.controller("ProjectsIndexStateCtrl", ($scope, $rootScope, $state, $stateParams, newProjectService, listenerService, globalStateService) -> +.controller("ProjectsIndexStateCtrl", ($scope, $rootScope, $state, $stateParams, newProjectService, listenerService, GlobalStateService) -> # Error - required projectId is missing! projectId = +$stateParams.projectId return $state.go('home') unless projectId - globalStateService.onLoad () -> + GlobalStateService.onLoad () -> # Load in project newProjectService.get(projectId, { # Ensure that we cache queries here... so that we get any projects we are in @@ -38,7 +39,7 @@ angular.module('doubtfire.projects.states.index', []) $scope.project = project $scope.unit = project.unit if project.unit.taskDefinitions.length > 0 && project.tasks.length == project.unit.taskDefinitions.length - globalStateService.setView('PROJECT', $scope.project) + GlobalStateService.setView('PROJECT', $scope.project) # Go home if no project was found return $state.go('home') unless project? diff --git a/src/app/projects/states/index/index.component.html b/src/app/projects/states/index/index.component.html new file mode 100644 index 0000000000..031daaa43a --- /dev/null +++ b/src/app/projects/states/index/index.component.html @@ -0,0 +1,46 @@ +
+ +

Projects Index

+ + +
+

+ Project: {{ project.name }} +

+

+ Unit: {{ unit?.name }} +

+ + +

Tasks

+ +
    +
  • + + {{ task.name }} – Status: + {{ task.status }} + + +
  • +
+ + + +

No tasks found for this project.

+
+
+ + + +
Loading project...
+
Error loading project.
+
+
+ +
Loading project...
+
Error loading project.
+
\ No newline at end of file diff --git a/src/app/projects/states/index/index.component.scss b/src/app/projects/states/index/index.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/projects/states/index/index.component.ts b/src/app/projects/states/index/index.component.ts new file mode 100644 index 0000000000..35b58b8988 --- /dev/null +++ b/src/app/projects/states/index/index.component.ts @@ -0,0 +1,99 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute, Router, ParamMap } from '@angular/router'; +import { Subject } from 'rxjs'; +import { switchMap, takeUntil } from 'rxjs/operators'; +import { GlobalStateService } from './global-state.service'; +import { ProjectService } from '../../services/project.service'; +import { ListenerService } from '../../services/listener.service'; +import { ViewType } from '../../common/types/view-type'; + +@Component({ + selector: 'app-projects-index', + templateUrl: './index.component.html', + styleUrls: ['./index.component.scss'] +}) +export class ProjectsIndexComponent implements OnInit, OnDestroy { + projectId!: number; + project: any; + unit: any; + isLoading = true; + hasError = false; + + private destroy$ = new Subject(); + + constructor( + private route: ActivatedRoute, + private router: Router, + private projectService: ProjectService, + private listenerService: ListenerService, + private globalStateService: GlobalStateService + ) { } + + ngOnInit(): void { + // Wait for global state to be ready, then react to route changes + this.globalStateService.onLoad(() => { + this.route.paramMap + .pipe( + takeUntil(this.destroy$), + switchMap((params: ParamMap) => { + const idParam = params.get('projectId'); + this.projectId = Number(idParam); + + if (!this.projectId || Number.isNaN(this.projectId)) { + this.hasError = true; + this.isLoading = false; + this.router.navigate(['/home']); + // Return an empty observable when navigation happens + return new Subject(); + } + + this.isLoading = true; + this.hasError = false; + + return this.projectService.get(this.projectId, { + cacheBehaviourOnGet: 'cacheQuery', + mappingCompleteCallback: (project: any) => { + this.unit = project?.unit; + } + }); + }) + ) + .subscribe({ + next: (project: any) => { + this.project = project; + + // Guard against null/undefined project + if (!project) { + this.hasError = true; + this.isLoading = false; + this.router.navigate(['/home']); + return; + } + + // Ensure unit is set when tasks match task definitions + if ( + project.unit?.taskDefinitions?.length > 0 && + project.tasks?.length === project.unit.taskDefinitions.length + ) { + this.unit = project.unit; + } + + // Use enum for view type + this.globalStateService.setView(ViewType.PROJECT, this.project); + + this.isLoading = false; + }, + error: () => { + this.isLoading = false; + this.hasError = true; + this.router.navigate(['/home']); + } + }); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} \ No newline at end of file From 838c78332a6be50ec52875699b1dcc6d46c3aa12 Mon Sep 17 00:00:00 2001 From: Supun Shettiarachchi Date: Tue, 20 Jan 2026 08:41:53 +0400 Subject: [PATCH 2/3] fix(projects): address review feedback (template, types, rxjs, cleanup) --- .../states/index/index.component.html | 11 +----- .../projects/states/index/index.component.ts | 34 +++++++++++++------ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/app/projects/states/index/index.component.html b/src/app/projects/states/index/index.component.html index 031daaa43a..0a6492e0fa 100644 --- a/src/app/projects/states/index/index.component.html +++ b/src/app/projects/states/index/index.component.html @@ -21,10 +21,6 @@

Tasks

{{ task.name }} – Status: {{ task.status }} - @@ -38,9 +34,4 @@

Tasks

Loading project...
Error loading project.
-
- - -
Loading project...
-
Error loading project.
-
\ No newline at end of file + \ No newline at end of file diff --git a/src/app/projects/states/index/index.component.ts b/src/app/projects/states/index/index.component.ts index 35b58b8988..0a077f73c9 100644 --- a/src/app/projects/states/index/index.component.ts +++ b/src/app/projects/states/index/index.component.ts @@ -1,12 +1,27 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute, Router, ParamMap } from '@angular/router'; -import { Subject } from 'rxjs'; +import { Subject, EMPTY } from 'rxjs'; import { switchMap, takeUntil } from 'rxjs/operators'; import { GlobalStateService } from './global-state.service'; import { ProjectService } from '../../services/project.service'; -import { ListenerService } from '../../services/listener.service'; import { ViewType } from '../../common/types/view-type'; +// ---------------------- +// Added Interfaces +// ---------------------- +interface Unit { + id: number; + name: string; + taskDefinitions?: any[]; +} + +interface Project { + id: number; + name: string; + tasks?: any[]; + unit?: Unit; +} + @Component({ selector: 'app-projects-index', templateUrl: './index.component.html', @@ -14,8 +29,8 @@ import { ViewType } from '../../common/types/view-type'; }) export class ProjectsIndexComponent implements OnInit, OnDestroy { projectId!: number; - project: any; - unit: any; + project: Project | null = null; + unit: Unit | null = null; isLoading = true; hasError = false; @@ -25,7 +40,6 @@ export class ProjectsIndexComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private router: Router, private projectService: ProjectService, - private listenerService: ListenerService, private globalStateService: GlobalStateService ) { } @@ -43,8 +57,8 @@ export class ProjectsIndexComponent implements OnInit, OnDestroy { this.hasError = true; this.isLoading = false; this.router.navigate(['/home']); - // Return an empty observable when navigation happens - return new Subject(); + // Use EMPTY instead of new Subject() + return EMPTY; } this.isLoading = true; @@ -52,14 +66,14 @@ export class ProjectsIndexComponent implements OnInit, OnDestroy { return this.projectService.get(this.projectId, { cacheBehaviourOnGet: 'cacheQuery', - mappingCompleteCallback: (project: any) => { - this.unit = project?.unit; + mappingCompleteCallback: (project: Project) => { + this.unit = project?.unit ?? null; } }); }) ) .subscribe({ - next: (project: any) => { + next: (project: Project) => { this.project = project; // Guard against null/undefined project From 53d99ec757498b57801ce9ecf97163d61dc1f397 Mon Sep 17 00:00:00 2001 From: Supun Shettiarachchi Date: Tue, 20 Jan 2026 08:56:45 +0400 Subject: [PATCH 3/3] chore(projects): remove unused empty scss file --- src/app/projects/states/index/index.component.scss | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/app/projects/states/index/index.component.scss diff --git a/src/app/projects/states/index/index.component.scss b/src/app/projects/states/index/index.component.scss deleted file mode 100644 index e69de29bb2..0000000000