feat: assessment tree view

This commit is contained in:
Thea Schöbl
2022-09-05 13:16:00 +00:00
parent 9bc3642990
commit 0b037f96e6
9 changed files with 175 additions and 50 deletions

View File

@@ -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: [],

View File

@@ -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;
}

View File

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

View File

@@ -16,6 +16,7 @@
<stapps-data-list-item
[item]="item"
[hideThumbnail]="hideThumbnail"
[lines]="'none'"
[favoriteButton]="false"
>
<ng-template let-data>

View File

@@ -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"

View File

@@ -38,6 +38,8 @@ export class DataListItemComponent {
@Input() favoriteButton = true;
@Input() lines = 'inset';
@ContentChild(TemplateRef) contentTemplateRef: TemplateRef<
DataListContext<SCThings>
>;

View File

@@ -16,7 +16,7 @@
<ion-item
class="ion-text-wrap ion-margin"
button="true"
lines="inset"
lines="lines"
detail="false"
(click)="notifySelect()"
>

View File

@@ -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]"

View File

@@ -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%;
}