mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-20 00:23:03 +00:00
feat: assessment tree view
This commit is contained in:
@@ -36,6 +36,7 @@ import {AssessmentsSimpleDataListComponent} from './list/assessments-simple-data
|
||||
import {ProtectedRoutes} from '../auth/protected.routes';
|
||||
import {AssessmentsTreeListComponent} from './list/assessments-tree-list.component';
|
||||
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
|
||||
import {UtilModule} from '../../util/util.module';
|
||||
|
||||
const routes: ProtectedRoutes = [
|
||||
{
|
||||
@@ -75,6 +76,7 @@ const routes: ProtectedRoutes = [
|
||||
DataModule,
|
||||
ThingTranslateModule,
|
||||
MomentModule,
|
||||
UtilModule,
|
||||
],
|
||||
providers: [AssessmentsProvider],
|
||||
exports: [],
|
||||
|
||||
@@ -15,9 +15,43 @@
|
||||
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {SCAssessment} from '@openstapps/core';
|
||||
import {SCAssessment, SCUuid} from '@openstapps/core';
|
||||
import {DefaultAuthService} from '../auth/default-auth.service';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {uniqBy} from '../../_helpers/collections/uniq';
|
||||
import {keyBy} from '../../_helpers/collections/key-by';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function toAssessmentMap(
|
||||
data: SCAssessment[],
|
||||
): Record<SCUuid, SCAssessment> {
|
||||
return keyBy(
|
||||
uniqBy(
|
||||
[
|
||||
...data,
|
||||
...data.flatMap<SCAssessment>(
|
||||
assessment =>
|
||||
[...(assessment.superAssessments ?? [])]
|
||||
.reverse()
|
||||
.map<SCAssessment>((superAssessment, index, array) => {
|
||||
const superAssessmentCopy = {
|
||||
...superAssessment,
|
||||
} as SCAssessment;
|
||||
superAssessmentCopy.origin = assessment.origin;
|
||||
superAssessmentCopy.superAssessments = array
|
||||
.slice(index + 1)
|
||||
.reverse();
|
||||
return superAssessmentCopy;
|
||||
}) ?? [],
|
||||
),
|
||||
] as SCAssessment[],
|
||||
it => it.uid,
|
||||
),
|
||||
it => it.uid,
|
||||
);
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -29,6 +63,8 @@ export class AssessmentsProvider {
|
||||
// is very aggressive about too many requests being made to the server
|
||||
cache?: Promise<SCAssessment[]>;
|
||||
|
||||
assessments: Promise<Record<SCUuid, SCAssessment>>;
|
||||
|
||||
cacheTimestamp = 0;
|
||||
|
||||
// 15 minutes
|
||||
@@ -40,6 +76,16 @@ export class AssessmentsProvider {
|
||||
readonly http: HttpClient,
|
||||
) {}
|
||||
|
||||
async getAssessment(
|
||||
uid: SCUuid,
|
||||
accessToken?: string | null,
|
||||
forceFetch = false,
|
||||
): Promise<SCAssessment> {
|
||||
await this.getAssessments(accessToken, forceFetch);
|
||||
|
||||
return (await this.assessments)[uid];
|
||||
}
|
||||
|
||||
async getAssessments(
|
||||
accessToken?: string | null,
|
||||
forceFetch = false,
|
||||
@@ -53,6 +99,7 @@ export class AssessmentsProvider {
|
||||
this.cache = import('./assessment-mock-data.json').then(
|
||||
it => it.data as SCAssessment[],
|
||||
);
|
||||
this.assessments = this.cache.then(toAssessmentMap);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -60,7 +107,7 @@ export class AssessmentsProvider {
|
||||
!forceFetch &&
|
||||
Date.now() - this.cacheTimestamp < this.cacheMaxAge
|
||||
) {
|
||||
return await this.cache;
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
const url = this.configProvider.config.app.features.extern?.hisometry.url;
|
||||
@@ -77,8 +124,10 @@ export class AssessmentsProvider {
|
||||
.toPromise()
|
||||
.then(it => {
|
||||
this.cacheTimestamp = Date.now();
|
||||
|
||||
return it?.data ?? [];
|
||||
});
|
||||
this.assessments = this.cache.then(toAssessmentMap);
|
||||
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import {NavController, ViewWillEnter} from '@ionic/angular';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {DataRoutingService} from '../../data/data-routing.service';
|
||||
import {SCAssessment} from '@openstapps/core';
|
||||
|
||||
@Component({
|
||||
selector: 'assessments-detail',
|
||||
@@ -47,6 +48,8 @@ export class AssessmentsDetailComponent
|
||||
@ViewChild(DataDetailComponent)
|
||||
detailComponent: DataDetailComponent;
|
||||
|
||||
item: SCAssessment;
|
||||
|
||||
ngOnInit() {
|
||||
if (!this.dataPathAutoRouting) return;
|
||||
this.subscriptions.push(
|
||||
@@ -69,25 +72,14 @@ export class AssessmentsDetailComponent
|
||||
|
||||
getItem(event: ExternalDataLoadEvent) {
|
||||
this.assessmentsProvider
|
||||
.getAssessments(
|
||||
.getAssessment(
|
||||
event.uid,
|
||||
this.route.snapshot.queryParamMap.get('token'),
|
||||
event.forceReload,
|
||||
)
|
||||
.then(assessments => {
|
||||
const assessment = assessments.find(it => it.uid === event.uid);
|
||||
event.resolve(
|
||||
assessment ??
|
||||
assessments
|
||||
.flatMap(it =>
|
||||
Array.isArray(it.superAssessments)
|
||||
? it.superAssessments.map(superAssessment => ({
|
||||
...superAssessment,
|
||||
origin: it.origin,
|
||||
}))
|
||||
: [],
|
||||
)
|
||||
.find(it => it?.uid === event.uid),
|
||||
);
|
||||
.then(assessment => {
|
||||
this.item = assessment;
|
||||
event.resolve(this.item);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<stapps-data-list-item
|
||||
[item]="item"
|
||||
[hideThumbnail]="hideThumbnail"
|
||||
[lines]="'none'"
|
||||
[favoriteButton]="false"
|
||||
>
|
||||
<ng-template let-data>
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
<!--
|
||||
~ Copyright (C) 2022 StApps
|
||||
~ This program is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU General Public License as published by the Free
|
||||
~ Software Foundation, version 3.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
~ more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along with
|
||||
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary" mode="ios">
|
||||
<ion-buttons slot="start">
|
||||
@@ -7,30 +22,31 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-segment
|
||||
#segment
|
||||
(ionChange)="sharedAxisChoreographer.changeViewForState(segment.value)"
|
||||
value=""
|
||||
>
|
||||
<ion-segment-button *ngFor="let key of assessmentKeys" [value]="key">
|
||||
<div *ngIf="assessments | async as assessments">
|
||||
<ion-label
|
||||
*ngIf="
|
||||
assessments[key].courseOfStudy | async as course;
|
||||
else defaultLabel
|
||||
"
|
||||
>
|
||||
{{ 'name' | thingTranslate: course }} ({{
|
||||
'academicDegree' | thingTranslate: course
|
||||
}})
|
||||
</ion-label>
|
||||
</div>
|
||||
<ng-template #defaultLabel>
|
||||
<ion-label>{{ key }}</ion-label>
|
||||
</ng-template>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<ion-content>
|
||||
<ion-segment
|
||||
#segment
|
||||
(ionChange)="sharedAxisChoreographer.changeViewForState(segment.value)"
|
||||
value=""
|
||||
>
|
||||
<ion-segment-button *ngFor="let key of assessmentKeys" [value]="key">
|
||||
<div *ngIf="assessments | async as assessments">
|
||||
<ion-label
|
||||
class="ion-text-wrap"
|
||||
*ngIf="
|
||||
assessments[key].courseOfStudy | async as course;
|
||||
else defaultLabel
|
||||
"
|
||||
>
|
||||
{{ 'name' | thingTranslate: course }} ({{
|
||||
'academicDegree' | thingTranslate: course
|
||||
}})
|
||||
</ion-label>
|
||||
</div>
|
||||
<ng-template #defaultLabel>
|
||||
<ion-label>{{ key }}</ion-label>
|
||||
</ng-template>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<div
|
||||
[ngSwitch]="sharedAxisChoreographer.currentValue"
|
||||
[@materialSharedAxisX]="sharedAxisChoreographer.animationState"
|
||||
|
||||
@@ -38,6 +38,8 @@ export class DataListItemComponent {
|
||||
|
||||
@Input() favoriteButton = true;
|
||||
|
||||
@Input() lines = 'inset';
|
||||
|
||||
@ContentChild(TemplateRef) contentTemplateRef: TemplateRef<
|
||||
DataListContext<SCThings>
|
||||
>;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<ion-item
|
||||
class="ion-text-wrap ion-margin"
|
||||
button="true"
|
||||
lines="inset"
|
||||
lines="lines"
|
||||
detail="false"
|
||||
(click)="notifySelect()"
|
||||
>
|
||||
|
||||
@@ -13,20 +13,34 @@
|
||||
~ this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<ion-accordion-group *ngIf="entries as entries" [value]="entries[0]?.[0]">
|
||||
<ion-accordion *ngFor="let entry of entries" [value]="entry[0]">
|
||||
<ion-item *ngIf="groupMap[entry[0]] as header" slot="header">
|
||||
<ion-label>{{ header.name }}</ion-label>
|
||||
</ion-item>
|
||||
<ion-list slot="content">
|
||||
<ng-container *ngFor="let item of entry[1]._ || []">
|
||||
<ion-accordion-group
|
||||
*ngIf="entries as entries"
|
||||
[readonly]="true"
|
||||
[value]="entries"
|
||||
[multiple]="true"
|
||||
>
|
||||
<ion-accordion *ngFor="let entry of entries" [value]="entry">
|
||||
<div
|
||||
*ngIf="groupMap[entry[0]] as header"
|
||||
slot="header"
|
||||
class="tree-indicator"
|
||||
>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
listItemTemplateRef || defaultListItem;
|
||||
context: {$implicit: header}
|
||||
"
|
||||
></ng-container>
|
||||
</div>
|
||||
<ion-list slot="content" lines="none">
|
||||
<div *ngFor="let item of entry[1]._ || []" class="tree-indicator">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
listItemTemplateRef || defaultListItem;
|
||||
context: {$implicit: item}
|
||||
"
|
||||
></ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<tree-list-fragment
|
||||
[groupMap]="groupMap"
|
||||
[items]="entry[1]"
|
||||
|
||||
@@ -16,3 +16,52 @@
|
||||
ion-list {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
:host ::ng-deep ion-item {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.tree-indicator {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ion-accordion::before,
|
||||
.tree-indicator::before,
|
||||
.tree-indicator::after {
|
||||
content: "";
|
||||
display: block;
|
||||
|
||||
background-color: grey;
|
||||
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
ion-accordion::before,
|
||||
.tree-indicator::before {
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.tree-indicator::after {
|
||||
width: 40px;
|
||||
height: 1px;
|
||||
|
||||
transform: translateX(calc(-50% - 8px));
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
ion-accordion::after {
|
||||
top: 24px;
|
||||
}
|
||||
|
||||
ion-accordion:last-of-type::before {
|
||||
height: 24px;
|
||||
}
|
||||
.tree-indicator:last-of-type::before {
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user