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 {ProtectedRoutes} from '../auth/protected.routes';
import {AssessmentsTreeListComponent} from './list/assessments-tree-list.component'; import {AssessmentsTreeListComponent} from './list/assessments-tree-list.component';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {UtilModule} from '../../util/util.module';
const routes: ProtectedRoutes = [ const routes: ProtectedRoutes = [
{ {
@@ -75,6 +76,7 @@ const routes: ProtectedRoutes = [
DataModule, DataModule,
ThingTranslateModule, ThingTranslateModule,
MomentModule, MomentModule,
UtilModule,
], ],
providers: [AssessmentsProvider], providers: [AssessmentsProvider],
exports: [], exports: [],

View File

@@ -15,9 +15,43 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {ConfigProvider} from '../config/config.provider'; 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 {DefaultAuthService} from '../auth/default-auth.service';
import {HttpClient} from '@angular/common/http'; 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({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -29,6 +63,8 @@ export class AssessmentsProvider {
// is very aggressive about too many requests being made to the server // is very aggressive about too many requests being made to the server
cache?: Promise<SCAssessment[]>; cache?: Promise<SCAssessment[]>;
assessments: Promise<Record<SCUuid, SCAssessment>>;
cacheTimestamp = 0; cacheTimestamp = 0;
// 15 minutes // 15 minutes
@@ -40,6 +76,16 @@ export class AssessmentsProvider {
readonly http: HttpClient, 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( async getAssessments(
accessToken?: string | null, accessToken?: string | null,
forceFetch = false, forceFetch = false,
@@ -53,6 +99,7 @@ export class AssessmentsProvider {
this.cache = import('./assessment-mock-data.json').then( this.cache = import('./assessment-mock-data.json').then(
it => it.data as SCAssessment[], it => it.data as SCAssessment[],
); );
this.assessments = this.cache.then(toAssessmentMap);
} }
if ( if (
@@ -60,7 +107,7 @@ export class AssessmentsProvider {
!forceFetch && !forceFetch &&
Date.now() - this.cacheTimestamp < this.cacheMaxAge Date.now() - this.cacheTimestamp < this.cacheMaxAge
) { ) {
return await this.cache; return this.cache;
} }
const url = this.configProvider.config.app.features.extern?.hisometry.url; const url = this.configProvider.config.app.features.extern?.hisometry.url;
@@ -77,8 +124,10 @@ export class AssessmentsProvider {
.toPromise() .toPromise()
.then(it => { .then(it => {
this.cacheTimestamp = Date.now(); this.cacheTimestamp = Date.now();
return it?.data ?? []; return it?.data ?? [];
}); });
this.assessments = this.cache.then(toAssessmentMap);
return this.cache; return this.cache;
} }

View File

@@ -23,6 +23,7 @@ import {
import {NavController, ViewWillEnter} from '@ionic/angular'; import {NavController, ViewWillEnter} from '@ionic/angular';
import {Subscription} from 'rxjs'; import {Subscription} from 'rxjs';
import {DataRoutingService} from '../../data/data-routing.service'; import {DataRoutingService} from '../../data/data-routing.service';
import {SCAssessment} from '@openstapps/core';
@Component({ @Component({
selector: 'assessments-detail', selector: 'assessments-detail',
@@ -47,6 +48,8 @@ export class AssessmentsDetailComponent
@ViewChild(DataDetailComponent) @ViewChild(DataDetailComponent)
detailComponent: DataDetailComponent; detailComponent: DataDetailComponent;
item: SCAssessment;
ngOnInit() { ngOnInit() {
if (!this.dataPathAutoRouting) return; if (!this.dataPathAutoRouting) return;
this.subscriptions.push( this.subscriptions.push(
@@ -69,25 +72,14 @@ export class AssessmentsDetailComponent
getItem(event: ExternalDataLoadEvent) { getItem(event: ExternalDataLoadEvent) {
this.assessmentsProvider this.assessmentsProvider
.getAssessments( .getAssessment(
event.uid,
this.route.snapshot.queryParamMap.get('token'), this.route.snapshot.queryParamMap.get('token'),
event.forceReload, event.forceReload,
) )
.then(assessments => { .then(assessment => {
const assessment = assessments.find(it => it.uid === event.uid); this.item = assessment;
event.resolve( event.resolve(this.item);
assessment ??
assessments
.flatMap(it =>
Array.isArray(it.superAssessments)
? it.superAssessments.map(superAssessment => ({
...superAssessment,
origin: it.origin,
}))
: [],
)
.find(it => it?.uid === event.uid),
);
}); });
} }

View File

@@ -16,6 +16,7 @@
<stapps-data-list-item <stapps-data-list-item
[item]="item" [item]="item"
[hideThumbnail]="hideThumbnail" [hideThumbnail]="hideThumbnail"
[lines]="'none'"
[favoriteButton]="false" [favoriteButton]="false"
> >
<ng-template let-data> <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-header>
<ion-toolbar color="primary" mode="ios"> <ion-toolbar color="primary" mode="ios">
<ion-buttons slot="start"> <ion-buttons slot="start">
@@ -7,14 +22,16 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-segment <ion-content>
<ion-segment
#segment #segment
(ionChange)="sharedAxisChoreographer.changeViewForState(segment.value)" (ionChange)="sharedAxisChoreographer.changeViewForState(segment.value)"
value="" value=""
> >
<ion-segment-button *ngFor="let key of assessmentKeys" [value]="key"> <ion-segment-button *ngFor="let key of assessmentKeys" [value]="key">
<div *ngIf="assessments | async as assessments"> <div *ngIf="assessments | async as assessments">
<ion-label <ion-label
class="ion-text-wrap"
*ngIf=" *ngIf="
assessments[key].courseOfStudy | async as course; assessments[key].courseOfStudy | async as course;
else defaultLabel else defaultLabel
@@ -29,8 +46,7 @@
<ion-label>{{ key }}</ion-label> <ion-label>{{ key }}</ion-label>
</ng-template> </ng-template>
</ion-segment-button> </ion-segment-button>
</ion-segment> </ion-segment>
<ion-content>
<div <div
[ngSwitch]="sharedAxisChoreographer.currentValue" [ngSwitch]="sharedAxisChoreographer.currentValue"
[@materialSharedAxisX]="sharedAxisChoreographer.animationState" [@materialSharedAxisX]="sharedAxisChoreographer.animationState"

View File

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

View File

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

View File

@@ -13,20 +13,34 @@
~ this program. If not, see <https://www.gnu.org/licenses/>. ~ this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<ion-accordion-group *ngIf="entries as entries" [value]="entries[0]?.[0]"> <ion-accordion-group
<ion-accordion *ngFor="let entry of entries" [value]="entry[0]"> *ngIf="entries as entries"
<ion-item *ngIf="groupMap[entry[0]] as header" slot="header"> [readonly]="true"
<ion-label>{{ header.name }}</ion-label> [value]="entries"
</ion-item> [multiple]="true"
<ion-list slot="content"> >
<ng-container *ngFor="let item of entry[1]._ || []"> <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 <ng-container
*ngTemplateOutlet=" *ngTemplateOutlet="
listItemTemplateRef || defaultListItem; listItemTemplateRef || defaultListItem;
context: {$implicit: item} context: {$implicit: item}
" "
></ng-container> ></ng-container>
</ng-container> </div>
<tree-list-fragment <tree-list-fragment
[groupMap]="groupMap" [groupMap]="groupMap"
[items]="entry[1]" [items]="entry[1]"

View File

@@ -16,3 +16,52 @@
ion-list { ion-list {
margin-left: 16px; 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%;
}