Skip to content

Commit 2155790

Browse files
committed
refactor: add skeleton ui unit and inbox loading
1 parent aeca829 commit 2155790

13 files changed

Lines changed: 416 additions & 136 deletions

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"ngx-entity-service": "^0.0.43",
7878
"ngx-lottie": "^11.0.2",
7979
"ngx-monaco-editor-v2": "^21",
80+
"ngx-skeleton-loader": "^12.0.0",
8081
"npm": "^10.4.0",
8182
"nvd3": "1.8.6",
8283
"qrcode": "^1.5.4",

src/app/doubtfire-angular.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {MatTooltipModule} from '@angular/material/tooltip';
4141
import player from 'lottie-web';
4242
import {PdfViewerModule} from 'ng2-pdf-viewer';
4343
import {LottieComponent, provideLottieOptions} from 'ngx-lottie';
44+
import {NgxSkeletonLoaderModule} from 'ngx-skeleton-loader';
4445
import {AlertComponent, AlertService} from 'src/app/common/services/alert.service';
4546
import {ProgressDashboardComponent} from './projects/states/dashboard/directives/progress-dashboard/progress-dashboard.component';
4647

@@ -751,6 +752,7 @@ const GANTT_CHART_CONFIG = {
751752
NgxGanttModule,
752753
MonacoEditorModule.forRoot(),
753754
MatChipListbox,
755+
NgxSkeletonLoaderModule,
754756
],
755757
bootstrap: [AppComponent],
756758
})
Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,42 @@
11
import {ResolveFn} from '@angular/router';
2-
import {AsyncSubject} from 'rxjs';
2+
import {Observable} from 'rxjs';
33
import {Project, ProjectService} from 'src/app/api/models/doubtfire-model';
44
import {GlobalStateService, ViewType} from './states/index/global-state.service';
55
import {inject} from '@angular/core';
66

7-
export const resolveProject: ResolveFn<Project> = (route) => {
7+
export const resolveProject: ResolveFn<Project> = (route, state) => {
88
const projectService = inject(ProjectService);
99
const globalState = inject(GlobalStateService);
1010
const projectId = Number(route.paramMap.get('projectId'));
11-
const result = new AsyncSubject<Project>();
11+
const resolveProgressively = state.url.split('?')[0].includes('/dashboard');
1212

13-
const mappingCompleteCallback = (project: Project) => {
14-
globalState.setView(ViewType.PROJECT, project);
15-
result.next(project);
16-
result.complete();
17-
};
13+
return new Observable<Project>((observer) => {
14+
const mappingCompleteCallback = (project: Project) => {
15+
globalState.setView(ViewType.PROJECT, project);
16+
if (!resolveProgressively) {
17+
observer.next(project);
18+
observer.complete();
19+
}
20+
};
1821

19-
globalState.onLoad(() => {
20-
projectService
21-
.get(
22-
{id: projectId},
23-
{
24-
cacheBehaviourOnGet: 'cacheQuery',
25-
mappingCompleteCallback,
26-
},
27-
)
28-
.subscribe();
29-
});
22+
globalState.onLoad(() => {
23+
if (resolveProgressively) {
24+
observer.next(projectService.cache.getOrCreate(projectId, projectService, {id: projectId}));
25+
observer.complete();
26+
return;
27+
}
3028

31-
return result;
29+
projectService
30+
.get(
31+
{id: projectId},
32+
{
33+
cacheBehaviourOnGet: 'cacheQuery',
34+
mappingCompleteCallback,
35+
},
36+
)
37+
.subscribe({
38+
error: (error) => observer.error(error),
39+
});
40+
});
41+
});
3242
};

src/app/projects/states/dashboard/project-dashboard/project-dashboard.component.html

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,60 @@
1+
<ng-template #TaskListSkeleton>
2+
<div class="task-list-skeleton flex h-full flex-col p-2">
3+
<div class="mb-3 rounded-2xl bg-white p-2">
4+
<div class="flex items-center gap-3">
5+
<ngx-skeleton-loader
6+
appearance="circle"
7+
[theme]="{width: '32px', height: '32px', margin: '0'}"
8+
></ngx-skeleton-loader>
9+
<ngx-skeleton-loader
10+
class="min-w-0 flex-1"
11+
[theme]="{height: '16px', width: '100%', 'border-radius': '999px', margin: '0'}"
12+
></ngx-skeleton-loader>
13+
</div>
14+
</div>
15+
16+
<div class="min-h-0 flex-1 overflow-hidden rounded-lg bg-white">
17+
@for (row of skeletonRows; track row) {
18+
<div
19+
class="skeleton-task-row flex h-[65px] items-center gap-3 border-b border-gray-200 px-3"
20+
>
21+
<div class="min-w-0 flex-1">
22+
<ngx-skeleton-loader
23+
[theme]="{height: '12px', width: '82%', 'border-radius': '999px', margin: '0 0 8px'}"
24+
></ngx-skeleton-loader>
25+
<ngx-skeleton-loader
26+
[theme]="{height: '10px', width: '58%', 'border-radius': '999px', margin: '0'}"
27+
></ngx-skeleton-loader>
28+
</div>
29+
<ngx-skeleton-loader
30+
appearance="circle"
31+
[theme]="{width: '34px', height: '34px', margin: '0'}"
32+
></ngx-skeleton-loader>
33+
</div>
34+
}
35+
</div>
36+
</div>
37+
</ng-template>
38+
139
@if (project$ | async; as project) {
240
<div class="w-full h-[calc(100vh-100px)] overflow-hidden">
341
<div class="flex flex-row h-full min-h-0" #boundary>
442
@if (subs$ | async) {
5-
@if (project.unit.taskDefinitions.length > 0) {
43+
@if (isProjectTaskListReady(project)) {
644
<f-unit-task-list
745
class="flex-none p-2 h-full min-h-0"
846
[style.width.px]="leftWidth"
947
#leftPanel
1048
[mode]="'project'"
1149
[project]="project"
12-
[taskDefinitions]="project.unit.taskDefinitionsForGrade(project.targetGrade)"
50+
[taskDefinitions]="taskDefinitionsForProject(project)"
1351
[tasks]="project.tasks"
1452
[selectedTaskDefinition$]="selectedTaskDefinition$"
1553
></f-unit-task-list>
54+
} @else {
55+
<div class="flex-none h-full min-h-0" [style.width.px]="leftWidth">
56+
<ng-container *ngTemplateOutlet="TaskListSkeleton"></ng-container>
57+
</div>
1658
}
1759
<div class="relative flex-none" [style.width.px]="10">
1860
<div
@@ -37,6 +79,25 @@
3779
<task-comments-viewer [task]="project.findTaskForDefinition(selectedTaskDefinition.id)">
3880
</task-comments-viewer>
3981
</div>
82+
} @else if (!isProjectTaskListReady(project)) {
83+
<div class="flex-1 p-2 min-h-0 overflow-y-auto">
84+
<div class="dashboard-skeleton h-full rounded-lg bg-white p-4">
85+
<ngx-skeleton-loader
86+
[theme]="{
87+
height: '28px',
88+
width: '220px',
89+
'border-radius': '999px',
90+
margin: '0 0 20px',
91+
}"
92+
></ngx-skeleton-loader>
93+
<ngx-skeleton-loader
94+
[theme]="{height: '140px', width: '100%', 'border-radius': '8px', margin: '0 0 16px'}"
95+
></ngx-skeleton-loader>
96+
<ngx-skeleton-loader
97+
[theme]="{height: '220px', width: '100%', 'border-radius': '8px', margin: '0'}"
98+
></ngx-skeleton-loader>
99+
</div>
100+
</div>
40101
} @else {
41102
<div class="flex-1 p-2 min-h-0 overflow-y-auto">
42103
<f-progress-dashboard class="w-full" [project]="project"></f-progress-dashboard>

src/app/projects/states/dashboard/project-dashboard/project-dashboard.component.ts

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,23 @@ import {
77
Observable,
88
Subject,
99
auditTime,
10+
first,
1011
merge,
1112
of,
1213
tap,
1314
withLatestFrom,
1415
} from 'rxjs';
1516
import {ProjectService} from 'src/app/api/services/project.service';
17+
import {UnitService} from 'src/app/api/services/unit.service';
1618
import {GlobalStateService, ViewType} from '../../index/global-state.service';
1719
import {UserService} from 'src/app/api/services/user.service';
1820
import {Project, TaskDefinition} from 'src/app/api/models/doubtfire-model';
1921

2022
@Component({
21-
selector: 'f-project-dashboard',
22-
templateUrl: './project-dashboard.component.html',
23-
styleUrl: './project-dashboard.component.scss',
24-
standalone: false
23+
selector: 'f-project-dashboard',
24+
templateUrl: './project-dashboard.component.html',
25+
styleUrl: './project-dashboard.component.scss',
26+
standalone: false,
2527
})
2628
export class ProjectDashboardComponent implements OnInit {
2729
@Input() public project$: Observable<Project>;
@@ -34,16 +36,20 @@ export class ProjectDashboardComponent implements OnInit {
3436
new BehaviorSubject<TaskDefinition>(null);
3537

3638
subs$: Observable<unknown>;
39+
readonly skeletonRows = Array.from({length: 10}, (_, index) => index);
40+
private readonly projectSubject = new BehaviorSubject<Project>(null);
3741

3842
private leftComponentStartSize$ = new Subject<number>();
3943
private dragMove$ = new Subject<{event: CdkDragMove; div: HTMLDivElement}>();
4044
private dragMoveAudited$;
45+
private projectReady = false;
4146

4247
projectTasks = [];
4348

4449
constructor(
4550
private currentUser: UserService,
4651
private projectService: ProjectService,
52+
private unitService: UnitService,
4753
private globalStateService: GlobalStateService,
4854
private route: ActivatedRoute,
4955
) {}
@@ -54,6 +60,24 @@ export class ProjectDashboardComponent implements OnInit {
5460

5561
public startLeftX = 0;
5662

63+
public isProjectTaskListReady(project: Project): boolean {
64+
return (
65+
this.projectReady &&
66+
!!project?.id &&
67+
!!project.unit?.id &&
68+
project.targetGrade !== undefined &&
69+
project.targetGrade !== null
70+
);
71+
}
72+
73+
public taskDefinitionsForProject(project: Project): TaskDefinition[] {
74+
if (!this.isProjectTaskListReady(project)) {
75+
return [];
76+
}
77+
78+
return project.unit.taskDefinitionsForGrade(project.targetGrade);
79+
}
80+
5781
startedDragging(event: CdkDragStart, boundary: HTMLElement) {
5882
const rect = boundary.getBoundingClientRect();
5983
// x relative to the container
@@ -80,7 +104,15 @@ export class ProjectDashboardComponent implements OnInit {
80104
}
81105

82106
ngOnInit(): void {
83-
this.project$ = this.project$ ?? of(this.route.parent?.snapshot.data.project as Project);
107+
const initialProject$ =
108+
this.project$ ?? of(this.route.parent?.snapshot.data.project as Project);
109+
this.project$ = this.projectSubject.asObservable();
110+
initialProject$.pipe(first()).subscribe((project) => {
111+
this.projectSubject.next(project);
112+
this.loadProject(
113+
project?.id ?? Number(this.route.parent?.snapshot.paramMap.get('projectId')),
114+
);
115+
});
84116

85117
this.dragMoveAudited$ = this.dragMove$.pipe(
86118
withLatestFrom(this.leftComponentStartSize$),
@@ -112,4 +144,45 @@ export class ProjectDashboardComponent implements OnInit {
112144
this.subs$ = merge(this.dragMoveAudited$, of(true));
113145
window.dispatchEvent(new Event('resize'));
114146
}
147+
148+
private loadProject(projectId: number): void {
149+
if (!projectId) {
150+
return;
151+
}
152+
153+
this.projectService
154+
.get(
155+
{id: projectId},
156+
{
157+
cacheBehaviourOnGet: 'cacheQuery',
158+
mappingCompleteCallback: (project: Project) => this.loadUnit(project),
159+
},
160+
)
161+
.subscribe();
162+
}
163+
164+
private loadUnit(project: Project): void {
165+
const unitId = project.unit?.id;
166+
if (!unitId) {
167+
this.showLoadedProject(project);
168+
return;
169+
}
170+
171+
this.unitService.get(unitId).subscribe({
172+
next: (unit) => {
173+
project.unit = unit;
174+
unit.studentCache.add(project);
175+
this.showLoadedProject(project);
176+
},
177+
error: () => {
178+
this.showLoadedProject(project);
179+
},
180+
});
181+
}
182+
183+
private showLoadedProject(project: Project): void {
184+
this.projectReady = true;
185+
this.globalStateService.setView(ViewType.PROJECT, project);
186+
this.projectSubject.next(project);
187+
}
115188
}

0 commit comments

Comments
 (0)