mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-21 17:12:43 +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 {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: [],
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -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()"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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]"
|
||||||
|
|||||||
@@ -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%;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user