feat: assessments module

This commit is contained in:
Thea Schöbl
2022-03-17 09:59:52 +00:00
parent eea8d6d339
commit e68d1b73f9
51 changed files with 3372 additions and 222 deletions

View File

@@ -63,6 +63,7 @@ import {AuthModule} from './modules/auth/auth.module';
import {BackgroundModule} from './modules/background/background.module';
import {LibraryModule} from './modules/library/library.module';
import {StorageProvider} from './modules/storage/storage.provider';
import {AssessmentsModule} from './modules/assessments/assessments.module';
registerLocaleData(localeDe);
@@ -129,6 +130,7 @@ export function createTranslateLoader(http: HttpClient) {
AboutModule,
AppRoutingModule,
AuthModule,
AssessmentsModule,
BackgroundModule,
BrowserModule,
BrowserAnimationsModule,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,78 @@
/*
* 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 Licens 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/>.
*/
import {NgModule} from '@angular/core';
import {AssessmentListItemComponent} from './types/assessment/assessment-list-item.component';
import {AssessmentBaseInfoComponent} from './types/assessment/assessment-base-info.component';
import {AssessmentDetailComponent} from './types/assessment/assessment-detail.component';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';
import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core';
import {DataModule} from '../data/data.module';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {CourseOfStudyAssessmentComponent} from './types/course-of-study/course-of-study-assessment.component';
import {AssessmentsPageComponent} from './page/assessments-page.component';
import {RouterModule} from '@angular/router';
import {AuthGuardService} from '../auth/auth-guard.service';
import {MomentModule} from 'ngx-moment';
import {AssessmentsListItemComponent} from './list/assessments-list-item.component';
import {AssessmentsDataListComponent} from './list/assessments-data-list.component';
import {AssessmentsDetailComponent} from './detail/assessments-detail.component';
import {AssessmentsProvider} from './assessments.provider';
import {AssessmentsSimpleDataListComponent} from './list/assessments-simple-data-list.component';
import {ProtectedRoutes} from '../auth/protected.routes';
const routes: ProtectedRoutes = [
{
path: 'assessments',
component: AssessmentsPageComponent,
data: {authProvider: 'default'},
canActivate: [AuthGuardService],
},
{
path: 'assessments/detail/:uid',
component: AssessmentsDetailComponent,
data: {authProvider: 'default'},
canActivate: [AuthGuardService],
},
];
@NgModule({
declarations: [
AssessmentListItemComponent,
AssessmentBaseInfoComponent,
AssessmentDetailComponent,
AssessmentsListItemComponent,
CourseOfStudyAssessmentComponent,
AssessmentsPageComponent,
AssessmentsDataListComponent,
AssessmentsDetailComponent,
AssessmentsSimpleDataListComponent,
],
imports: [
CommonModule,
FormsModule,
IonicModule,
RouterModule.forChild(routes),
TranslateModule,
DataModule,
ThingTranslateModule,
MomentModule,
],
providers: [AssessmentsProvider],
exports: [],
})
export class AssessmentsModule {}

View File

@@ -0,0 +1,85 @@
/*
* 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 Licens 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/>.
*/
import {Injectable} from '@angular/core';
import {ConfigProvider} from '../config/config.provider';
import {SCAssessment} from '@openstapps/core';
import {DefaultAuthService} from '../auth/default-auth.service';
import {HttpClient} from '@angular/common/http';
@Injectable({
providedIn: 'root',
})
export class AssessmentsProvider {
assessmentPath = 'assessments';
// usually this wouldn't be necessary, but the assessment service
// is very aggressive about too many requests being made to the server
cache?: Promise<SCAssessment[]>;
cacheTimestamp = 0;
// 15 minutes
cacheMaxAge = 15 * 60 * 1000;
constructor(
readonly configProvider: ConfigProvider,
readonly defaultAuth: DefaultAuthService,
readonly http: HttpClient,
) {}
async getAssessments(
accessToken?: string | null,
forceFetch = false,
): Promise<SCAssessment[]> {
// again, this is a hack to get around the fact that the assessment service
// is very aggressive how many requests you can make, so it can happen
// during development that simply by reloading pages over and over again
// the assessment service will block you
if (accessToken === 'mock' && !this.cache) {
this.cacheTimestamp = Date.now();
this.cache = import('./assessment-mock-data.json').then(
it => it.data as SCAssessment[],
);
}
if (
this.cache &&
!forceFetch &&
Date.now() - this.cacheTimestamp < this.cacheMaxAge
) {
return await this.cache;
}
const url = this.configProvider.config.app.features.extern?.hisometry.url;
if (!url) throw new Error('Config lacks url for hisometry');
this.cache = this.http
.get<{data: SCAssessment[]}>(`${url}/${this.assessmentPath}`, {
headers: {
Authorization: `Bearer ${
accessToken ?? (await this.defaultAuth.getValidToken()).accessToken
}`,
},
})
.toPromise()
.then(it => {
this.cacheTimestamp = Date.now();
return it?.data ?? [];
});
return this.cache;
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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 Licens 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/>.
*/
import {Component, ViewChild} from '@angular/core';
import {flatMap} from 'lodash-es';
import {ActivatedRoute} from '@angular/router';
import {AssessmentsProvider} from '../assessments.provider';
import {
DataDetailComponent,
ExternalDataLoadEvent,
} from '../../data/detail/data-detail.component';
import {ViewWillEnter} from '@ionic/angular';
@Component({
selector: 'assessments-detail',
templateUrl: 'assessments-detail.html',
styleUrls: ['assessments-detail.scss'],
})
export class AssessmentsDetailComponent implements ViewWillEnter {
constructor(
readonly route: ActivatedRoute,
readonly assessmentsProvider: AssessmentsProvider,
) {}
@ViewChild(DataDetailComponent)
detailComponent: DataDetailComponent;
getItem(event: ExternalDataLoadEvent) {
this.assessmentsProvider
.getAssessments(
this.route.snapshot.queryParamMap.get('token'),
event.forceReload,
)
.then(assessments => {
const assessment = assessments.find(it => it.uid === event.uid);
event.resolve(
assessment
? assessment
: flatMap(assessments, it =>
Array.isArray(it.superAssessments)
? it.superAssessments.map(superAssessment => ({
...superAssessment,
origin: it.origin,
}))
: [],
).find(it => it?.uid === event.uid),
);
});
}
async ionViewWillEnter() {
await this.detailComponent.ionViewWillEnter();
}
}

View File

@@ -0,0 +1,32 @@
<!--
~ 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 Licens 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">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>{{ 'data.detail.TITLE' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<stapps-data-detail
[externalData]="true"
(loadItem)="getItem($event)"
[defaultHeader]="false"
>
<ng-template let-item>
<assessment-detail [item]="item"></assessment-detail>
</ng-template>
</stapps-data-detail>

View File

@@ -0,0 +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 Licens 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/>.
*/
stapps-data-detail {
height: 100%;
}

View File

@@ -0,0 +1,51 @@
/*
* 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 Licens 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/>.
*/
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {Observable} from 'rxjs';
@Component({
selector: 'assessments-data-list',
templateUrl: './assessments-data-list.html',
styleUrls: ['./assessments-data-list.scss'],
})
export class AssessmentsDataListComponent {
/**
* All SCThings to display
*/
@Input() items?: SCThings[];
/**
* Output binding to trigger pagination fetch
*/
// eslint-disable-next-line @angular-eslint/no-output-rename
@Output('loadmore') loadMore = new EventEmitter<void>();
/**
* Emits when scroll view should reset to top
*/
@Input() resetToTop?: Observable<void>;
/**
* Indicates whether the list is to display SCThings of a single type
*/
@Input() singleType = false;
/**
* Signalizes that the data is being loaded
*/
@Input() loading = true;
}

View File

@@ -0,0 +1,32 @@
<!--
~ 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 Licens 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/>.
-->
<stapps-data-list
[items]="items"
[loading]="loading"
[singleType]="singleType"
[resetToTop]="resetToTop"
(loadmore)="loadMore.emit($event)"
>
<ng-template let-item>
<assessments-list-item
[item]="item"
[hideThumbnail]="singleType"
></assessments-list-item>
</ng-template>
<ng-container header>
<ng-content select="[header]"></ng-content>
</ng-container>
</stapps-data-list>

View File

@@ -0,0 +1,34 @@
/*
* 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 Licens 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/>.
*/
import {Component, Input} from '@angular/core';
import {SCThings} from '@openstapps/core';
@Component({
selector: 'assessments-list-item',
templateUrl: 'assessments-list-item.html',
styleUrls: ['assessments-list-item.scss'],
})
export class AssessmentsListItemComponent {
/**
* Whether the list item should show a thumbnail
*/
@Input() hideThumbnail = false;
/**
* An item to show
*/
@Input() item: SCThings;
}

View File

@@ -0,0 +1,24 @@
<!--
~ 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 Licens 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/>.
-->
<stapps-data-list-item
[item]="item"
[hideThumbnail]="hideThumbnail"
[favoriteButton]="false"
>
<ng-template let-data>
<stapps-assessment-list-item [item]="data"></stapps-assessment-list-item>
</ng-template>
</stapps-data-list-item>

View File

@@ -0,0 +1,70 @@
/*
* 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 Licens 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/>.
*/
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {Subscription} from 'rxjs';
import {DataRoutingService} from '../../data/data-routing.service';
import {ActivatedRoute, Router} from '@angular/router';
@Component({
selector: 'assessments-simple-data-list',
templateUrl: 'assessments-simple-data-list.html',
styleUrls: ['assessments-simple-data-list.scss'],
})
export class AssessmentsSimpleDataListComponent implements OnInit, OnDestroy {
/**
* All SCThings to display
*/
_items?: Promise<SCThings[] | undefined>;
/**
* Indicates whether or not the list is to display SCThings of a single type
*/
@Input() singleType = false;
/**
* List header
*/
@Input() listHeader?: string;
@Input() set items(items: SCThings[] | undefined) {
this._items = new Promise(resolve => resolve(items));
}
subscriptions: Subscription[] = [];
constructor(
readonly dataRoutingService: DataRoutingService,
readonly router: Router,
readonly activatedRoute: ActivatedRoute,
) {}
ngOnInit() {
this.subscriptions.push(
this.dataRoutingService.itemSelectListener().subscribe(thing => {
void this.router.navigate(['assessments', 'detail', thing.uid], {
queryParams: {
token: this.activatedRoute.snapshot.queryParamMap.get('token'),
},
});
}),
);
}
ngOnDestroy() {
for (const subscription of this.subscriptions) subscription.unsubscribe();
}
}

View File

@@ -0,0 +1,28 @@
<!--
~ 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 Licens 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/>.
-->
<stapps-simple-data-list
[singleType]="singleType"
[items]="_items"
[listHeader]="listHeader"
[autoRouting]="false"
>
<ng-template let-item>
<assessments-list-item
[item]="item"
[hideThumbnail]="singleType"
></assessments-list-item>
</ng-template>
</stapps-simple-data-list>

View File

@@ -0,0 +1,125 @@
/*
* 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 Licens 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/>.
*/
import {
AfterViewInit,
Component,
OnDestroy,
OnInit,
ViewChild,
} from '@angular/core';
import {AssessmentsProvider} from '../assessments.provider';
import {SCAssessment, SCCourseOfStudy} from '@openstapps/core';
import {groupBy, mapValues} from 'lodash-es';
import {ActivatedRoute, Router} from '@angular/router';
import {Subscription} from 'rxjs';
import {NGXLogger} from 'ngx-logger';
import {materialSharedAxisX} from '../../../animation/material-motion';
import {SharedAxisChoreographer} from '../../../animation/animation-choreographer';
import {DataProvider, DataScope} from '../../data/data.provider';
import {DataRoutingService} from '../../data/data-routing.service';
@Component({
selector: 'app-assessments-page',
templateUrl: 'assessments-page.html',
styleUrls: ['assessments-page.scss'],
animations: [materialSharedAxisX],
})
export class AssessmentsPageComponent
implements OnInit, AfterViewInit, OnDestroy
{
assessments: Promise<
Record<
string,
{
assessments: SCAssessment[];
courseOfStudy: Promise<SCCourseOfStudy | undefined>;
}
>
>;
assessmentKeys: string[] = [];
routingSubscription: Subscription;
@ViewChild('segment') segmentView!: HTMLIonSegmentElement;
sharedAxisChoreographer: SharedAxisChoreographer<string> =
new SharedAxisChoreographer<string>('', []);
constructor(
readonly logger: NGXLogger,
readonly assessmentsProvider: AssessmentsProvider,
readonly dataProvider: DataProvider,
readonly activatedRoute: ActivatedRoute,
readonly dataRoutingService: DataRoutingService,
readonly router: Router,
) {}
ngAfterViewInit() {
this.segmentView.value = this.sharedAxisChoreographer.currentValue;
}
ngOnDestroy() {
this.routingSubscription.unsubscribe();
}
ngOnInit() {
this.routingSubscription = this.dataRoutingService
.itemSelectListener()
.subscribe(thing => {
void this.router.navigate(['assessments', 'detail', thing.uid], {
queryParams: {
token: this.activatedRoute.snapshot.queryParamMap.get('token'),
},
});
});
this.activatedRoute.queryParams.subscribe(parameters => {
try {
this.assessments = this.assessmentsProvider
.getAssessments(parameters.token)
.then(assessments =>
groupBy(assessments, it => it.courseOfStudy?.uid ?? 'unknown'),
)
.then(it => {
this.assessmentKeys = Object.keys(it);
this.sharedAxisChoreographer = new SharedAxisChoreographer(
this.assessmentKeys[0],
this.assessmentKeys,
);
if (this.segmentView) {
this.segmentView.value =
this.sharedAxisChoreographer.currentValue;
}
return it;
})
.then(groups =>
mapValues(groups, (group, uid) => ({
assessments: group,
courseOfStudy: this.dataProvider
.get(uid, DataScope.Remote)
.catch(
() => group[0].courseOfStudy,
) as Promise<SCCourseOfStudy>,
})),
);
} catch (error) {
this.logger.error(error);
this.assessments = Promise.resolve({});
}
});
}
}

View File

@@ -0,0 +1,49 @@
<ion-header>
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>{{ 'assessments.TITLE' | translate }}</ion-title>
</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>
<div
[ngSwitch]="sharedAxisChoreographer.currentValue"
[@materialSharedAxisX]="sharedAxisChoreographer.animationState"
(@materialSharedAxisX.done)="sharedAxisChoreographer.animationDone()"
*ngIf="assessments | async as items"
class="content"
>
<course-of-study-assessment
[assessments]="items[sharedAxisChoreographer.currentValue].assessments"
[courseOfStudy]="
items[sharedAxisChoreographer.currentValue].courseOfStudy | async
"
></course-of-study-assessment>
</div>
</ion-content>

View File

@@ -0,0 +1,3 @@
.content {
height: 100%;
}

View File

@@ -0,0 +1,33 @@
/*
* 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 Licens 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/>.
*/
import {Component, Input} from '@angular/core';
import {SCAssessment} from '@openstapps/core';
@Component({
selector: 'assessment-base-info',
templateUrl: 'assessment-base-info.html',
styleUrls: ['assessment-base-info.scss'],
})
export class AssessmentBaseInfoComponent {
_item: SCAssessment;
passed = false;
@Input() set item(item: SCAssessment) {
this._item = item;
this.passed = !/^(5[,.]0)|FX?$/i.test(item.grade);
}
}

View File

@@ -0,0 +1,30 @@
<!--
~ 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 Licens 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-label [color]="passed ? undefined : 'danger'"
>{{
(_item.grade | isNumeric)
? (_item.grade
| numberLocalized: 'minimumFractionDigits:1,maximumFractionDigits:1')
: ''
}}
{{ 'status' | thingTranslate: _item | titlecase }},
{{ 'attempt' | propertyNameTranslate: _item }}
{{ _item.attempt }}
</ion-label>
<ion-note>
{{ _item.ects }}
{{ 'ects' | propertyNameTranslate: _item }}</ion-note
>

View File

@@ -0,0 +1,26 @@
/*
* 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 Licens 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/>.
*/
import {Component, Input} from '@angular/core';
import {SCAssessment} from '@openstapps/core';
@Component({
selector: 'assessment-detail',
templateUrl: 'assessment-detail.html',
styleUrls: ['assessment-detail.scss'],
})
export class AssessmentDetailComponent {
@Input() item: SCAssessment;
}

View File

@@ -0,0 +1,18 @@
<ion-note *ngIf="item.courseOfStudy as courseOfStudy">
{{ $any('courseOfStudy' | propertyNameTranslate: item) | titlecase }}:
{{ 'name' | thingTranslate: $any(courseOfStudy) }}
({{ 'academicDegree' | thingTranslate: $any(courseOfStudy) }})
</ion-note>
<ion-list class="container">
<ion-item lines="none">
<assessment-base-info [item]="item"></assessment-base-info>
</ion-item>
<h2 *ngIf="item.superAssessments">
{{ $any('superAssessments' | propertyNameTranslate: item) | titlecase }}
</h2>
<assessments-simple-data-list
*ngIf="item.superAssessments"
[items]="$any(item.superAssessments)"
[singleType]="true"
></assessments-simple-data-list>
</ion-list>

View File

@@ -0,0 +1,4 @@
stapps-data-list {
height: 100px;
width: 100%;
}

View File

@@ -0,0 +1,26 @@
/*
* 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 Licens 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/>.
*/
import {Component, Input} from '@angular/core';
import {SCAssessment} from '@openstapps/core';
@Component({
selector: 'stapps-assessment-list-item',
templateUrl: './assessment-list-item.html',
styleUrls: ['./assessment-list-item.scss'],
})
export class AssessmentListItemComponent {
@Input() item: SCAssessment;
}

View File

@@ -0,0 +1,5 @@
<h2 class="name">
{{ 'name' | thingTranslate: item }}
{{ item.date ? (item.date | amDateFormat) : '' }}
</h2>
<assessment-base-info [item]="item"></assessment-base-info>

View File

@@ -0,0 +1,45 @@
/*
* 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 Licens 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/>.
*/
import {Component, Input} from '@angular/core';
import {SCAssessment, SCCourseOfStudyWithoutReferences} from '@openstapps/core';
import {sum, sumBy} from 'lodash-es';
@Component({
selector: 'course-of-study-assessment',
templateUrl: 'course-of-study-assessment.html',
styleUrls: ['course-of-study-assessment.scss'],
})
export class CourseOfStudyAssessmentComponent {
@Input() courseOfStudy: SCCourseOfStudyWithoutReferences | null;
_assessments: SCAssessment[];
grade = 0;
ects = 0;
@Input() set assessments(value: SCAssessment[]) {
this._assessments = value;
const grades = this._assessments
// TODO: find out if this is correct
.filter(assessment => assessment.status === 'bestanden')
.map(assessment => Number(assessment.grade))
.filter(grade => !Number.isNaN(grade));
this.grade = grades.length > 0 ? sum(grades) / grades.length : 0;
this.ects = sumBy(this._assessments, 'ects');
}
}

View File

@@ -0,0 +1,37 @@
<!--
~ 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 Licens 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/>.
-->
<assessments-data-list
[singleType]="true"
[items]="_assessments"
[loading]="false"
>
<div header>
<section>
<h3>{{ 'assessments.courseOfStudyAssessments.PROGRESS' | translate }}</h3>
<p>
{{ $any('grade' | propertyNameTranslate: 'assessment') | titlecase }}:
{{
grade
| numberLocalized: 'minimumFractionDigits:1,maximumFractionDigits:1'
}}
</p>
<p>{{ 'ects' | propertyNameTranslate: 'assessment' }}: {{ ects }}</p>
</section>
<h3>
{{ 'assessments.courseOfStudyAssessments.ASSESSMENTS' | translate }}
</h3>
</div>
</assessments-data-list>

View File

@@ -0,0 +1,14 @@
/*!
* 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 Licens 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/>.
*/

View File

@@ -18,6 +18,10 @@ export class AuthGuardService implements CanActivate {
route: ActivatedProtectedRouteSnapshot,
_state: RouterStateSnapshot,
) {
if (route.queryParamMap.get('token')) {
return true;
}
try {
await this.authHelper
.getProvider(route.data.authProvider)

View File

@@ -1,19 +1,20 @@
/*
* Copyright (C) 2019 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.
* 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.
* 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 Licens 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/>.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {Component, Input, TemplateRef} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {DataListContext} from '../list/data-list.component';
/**
* TODO
@@ -28,4 +29,6 @@ export class DataDetailContentComponent {
* TODO
*/
@Input() item: SCThings;
@Input() contentTemplateRef?: TemplateRef<DataListContext<SCThings>>;
}

View File

@@ -1,86 +1,99 @@
<stapps-title-card [item]="item"> </stapps-title-card>
<div [ngSwitch]="item.type">
<stapps-article-detail-content
[item]="item"
*ngSwitchCase="'article'"
></stapps-article-detail-content>
<stapps-catalog-detail-content
[item]="item"
*ngSwitchCase="'catalog'"
></stapps-catalog-detail-content>
<stapps-date-series-detail-content
[item]="item"
*ngSwitchCase="'date series'"
></stapps-date-series-detail-content>
<stapps-dish-detail-content
[item]="item"
*ngSwitchCase="'dish'"
></stapps-dish-detail-content>
<stapps-event-detail-content
[item]="item"
*ngSwitchCase="'academic event'"
></stapps-event-detail-content>
<stapps-event-detail-content
[item]="item"
*ngSwitchCase="'sport course'"
></stapps-event-detail-content>
<stapps-favorite-detail-content
[item]="item"
*ngSwitchCase="'favorite'"
></stapps-favorite-detail-content>
<stapps-message-detail-content
[item]="item"
*ngSwitchCase="'message'"
></stapps-message-detail-content>
<stapps-person-detail-content
[item]="item"
*ngSwitchCase="'person'"
></stapps-person-detail-content>
<stapps-place-detail-content
[item]="item"
*ngSwitchCase="'building'"
></stapps-place-detail-content>
<stapps-place-detail-content
[item]="item"
*ngSwitchCase="'floor'"
></stapps-place-detail-content>
<stapps-place-detail-content
[item]="item"
*ngSwitchCase="'point of interest'"
></stapps-place-detail-content>
<stapps-place-detail-content
[item]="item"
*ngSwitchCase="'room'"
></stapps-place-detail-content>
<stapps-semester-detail-content
[item]="item"
*ngSwitchCase="'semester'"
></stapps-semester-detail-content>
<stapps-video-detail-content
[item]="item"
*ngSwitchCase="'video'"
></stapps-video-detail-content>
<ng-container *ngSwitchDefault>
<ion-item class="ion-text-wrap" lines="inset">
<ion-thumbnail slot="start" class="ion-margin-end">
<ion-icon color="medium" [attr.name]="item.type | dataIcon"></ion-icon>
</ion-thumbnail>
<ion-grid>
<ion-row>
<ion-col>
<div class="ion-text-wrap">
<h2 class="name">{{ item.name }}</h2>
<ion-note>{{ item.type }}</ion-note>
</div>
</ion-col>
</ion-row>
</ion-grid>
</ion-item>
<stapps-simple-card
*ngIf="item.description"
[title]="'description' | propertyNameTranslate: item | titlecase"
[content]="'description' | thingTranslate: item"
></stapps-simple-card>
</ng-container>
</div>
<ng-container
*ngTemplateOutlet="
contentTemplateRef || defaultContent;
context: {$implicit: item}
"
>
</ng-container>
<stapps-origin-detail [origin]="item.origin"></stapps-origin-detail>
<ng-template #defaultContent>
<div [ngSwitch]="item.type">
<stapps-article-detail-content
[item]="$any(item)"
*ngSwitchCase="'article'"
></stapps-article-detail-content>
<stapps-catalog-detail-content
[item]="$any(item)"
*ngSwitchCase="'catalog'"
></stapps-catalog-detail-content>
<stapps-date-series-detail-content
[item]="$any(item)"
*ngSwitchCase="'date series'"
></stapps-date-series-detail-content>
<stapps-dish-detail-content
[item]="$any(item)"
*ngSwitchCase="'dish'"
></stapps-dish-detail-content>
<stapps-event-detail-content
[item]="$any(item)"
*ngSwitchCase="'academic event'"
></stapps-event-detail-content>
<stapps-event-detail-content
[item]="$any(item)"
*ngSwitchCase="'sport course'"
></stapps-event-detail-content>
<stapps-favorite-detail-content
[item]="$any(item)"
*ngSwitchCase="'favorite'"
></stapps-favorite-detail-content>
<stapps-message-detail-content
[item]="$any(item)"
*ngSwitchCase="'message'"
></stapps-message-detail-content>
<stapps-person-detail-content
[item]="$any(item)"
*ngSwitchCase="'person'"
></stapps-person-detail-content>
<stapps-place-detail-content
[item]="$any(item)"
*ngSwitchCase="'building'"
></stapps-place-detail-content>
<stapps-place-detail-content
[item]="$any(item)"
*ngSwitchCase="'floor'"
></stapps-place-detail-content>
<stapps-place-detail-content
[item]="$any(item)"
*ngSwitchCase="'point of interest'"
></stapps-place-detail-content>
<stapps-place-detail-content
[item]="$any(item)"
*ngSwitchCase="'room'"
></stapps-place-detail-content>
<stapps-semester-detail-content
[item]="$any(item)"
*ngSwitchCase="'semester'"
></stapps-semester-detail-content>
<stapps-video-detail-content
[item]="$any(item)"
*ngSwitchCase="'video'"
></stapps-video-detail-content>
<ng-container *ngSwitchDefault>
<ion-item class="ion-text-wrap" lines="inset">
<ion-thumbnail slot="start" class="ion-margin-end">
<ion-icon
color="medium"
[attr.name]="item.type | dataIcon"
></ion-icon>
</ion-thumbnail>
<ion-grid>
<ion-row>
<ion-col>
<div class="ion-text-wrap">
<h2 class="name">{{ item.name }}</h2>
<ion-note>{{ item.type }}</ion-note>
</div>
</ion-col>
</ion-row>
</ion-grid>
</ion-item>
<stapps-simple-card
*ngIf="item.description"
[title]="$any('description' | propertyNameTranslate: item) | titlecase"
[content]="'description' | thingTranslate: item"
></stapps-simple-card>
</ng-container>
</div>
</ng-template>

View File

@@ -1,18 +1,19 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion,@typescript-eslint/no-explicit-any */
/*
* Copyright (C) 2018, 2019 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.
* 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.
* 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 Licens 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/>.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* eslint-disable @typescript-eslint/no-non-null-assertion,@typescript-eslint/no-explicit-any */
import {CUSTOM_ELEMENTS_SCHEMA, DebugElement} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ActivatedRoute, RouterModule} from '@angular/router';
@@ -118,9 +119,10 @@ describe('DataDetailComponent', () => {
});
it('should get a data item', () => {
comp.getItem(sampleThing.uid);
comp.getItem(sampleThing.uid, false);
expect(DataDetailComponent.prototype.getItem).toHaveBeenCalledWith(
sampleThing.uid,
false,
);
});
@@ -128,6 +130,7 @@ describe('DataDetailComponent', () => {
comp.ionViewWillEnter();
expect(DataDetailComponent.prototype.getItem).toHaveBeenCalledWith(
sampleThing.uid,
false,
);
});
@@ -135,6 +138,7 @@ describe('DataDetailComponent', () => {
await comp.refresh(refresher);
expect(DataDetailComponent.prototype.getItem).toHaveBeenCalledWith(
sampleThing.uid,
true,
);
expect(refresher.complete).toHaveBeenCalled();
});

View File

@@ -1,20 +1,27 @@
/*
* Copyright (C) 2018, 2019 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.
* 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.
* 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 Licens 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/>.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component} from '@angular/core';
import {
Component,
ContentChild,
EventEmitter,
Input,
Output,
TemplateRef,
} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {IonRefresher} from '@ionic/angular';
import {IonRefresher, ViewWillEnter} from '@ionic/angular';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
import {
SCLanguageCode,
@@ -26,6 +33,13 @@ import {DataProvider, DataScope} from '../data.provider';
import {FavoritesService} from '../../favorites/favorites.service';
import {take} from 'rxjs/operators';
import {Network} from '@capacitor/network';
import {DataListContext} from '../list/data-list.component';
export interface ExternalDataLoadEvent {
uid: SCUuid;
forceReload: boolean;
resolve: (item: SCThings | null | undefined) => void;
}
/**
* A Component to display an SCThing detailed
@@ -35,7 +49,7 @@ import {Network} from '@capacitor/network';
styleUrls: ['data-detail.scss'],
templateUrl: 'data-detail.html',
})
export class DataDetailComponent {
export class DataDetailComponent implements ViewWillEnter {
/**
* The associated item
*
@@ -53,6 +67,25 @@ export class DataDetailComponent {
*/
isDisconnected: Promise<boolean>;
@ContentChild(TemplateRef) contentTemplateRef: TemplateRef<
DataListContext<SCThings>
>;
@Input() externalData = false;
/**
* This is kind of a stupid situation where we would
* like to use the default header in overriding elements
* such as the assessment detail page, however the ionic
* back button will not work if the header is in a subcomponent
* which then means we have to copy and paste the header from
* here into the overriding component.
*/
@Input() defaultHeader = true;
@Output() loadItem: EventEmitter<ExternalDataLoadEvent> =
new EventEmitter<ExternalDataLoadEvent>();
/**
* Type guard for SCSavableThing
*/
@@ -90,11 +123,22 @@ export class DataDetailComponent {
* Provides data item with given UID
*
* @param uid Unique identifier of a thing
* @param forceReload Indicating whether cached data should be ignored
*/
async getItem(uid: SCUuid) {
async getItem(uid: SCUuid, forceReload: boolean) {
try {
const item = await this.dataProvider.get(uid, DataScope.Remote);
this.item = DataDetailComponent.isSCSavableThing(item) ? item.data : item;
const item = await (this.externalData
? new Promise<SCThings | null | undefined>(resolve =>
this.loadItem.emit({uid, forceReload, resolve}),
)
: this.dataProvider.get(uid, DataScope.Remote));
this.item = !item
? // eslint-disable-next-line unicorn/no-null
null
: DataDetailComponent.isSCSavableThing(item)
? item.data
: item;
} catch {
// eslint-disable-next-line unicorn/no-null
this.item = null;
@@ -106,7 +150,7 @@ export class DataDetailComponent {
*/
async ionViewWillEnter() {
const uid = this.route.snapshot.paramMap.get('uid') || '';
await this.getItem(uid ?? '');
await this.getItem(uid ?? '', false);
// fallback to the saved item (from favorites)
if (this.item === null) {
this.favoritesService
@@ -126,7 +170,7 @@ export class DataDetailComponent {
* @param refresher Refresher component that triggers the update
*/
async refresh(refresher: IonRefresher) {
await this.getItem(this.route.snapshot.paramMap.get('uid') ?? '');
await this.getItem(this.route.snapshot.paramMap.get('uid') ?? '', true);
await refresher.complete();
}
}

View File

@@ -1,4 +1,4 @@
<ion-header>
<ion-header *ngIf="defaultHeader">
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
@@ -8,11 +8,12 @@
<ion-buttons slot="primary">
<stapps-favorite-button
*ngIf="item"
[item]="item"
[item]="$any(item)"
></stapps-favorite-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ng-content select="[header]"></ng-content>
<ion-content class="ion-no-padding">
<ion-refresher slot="fixed" (ionRefresh)="refresh($event.target)">
<ion-refresher-content
@@ -44,7 +45,10 @@
<stapps-skeleton-simple-card></stapps-skeleton-simple-card>
</ng-container>
<ng-container *ngSwitchDefault>
<stapps-data-detail-content [item]="item"></stapps-data-detail-content>
<stapps-data-detail-content
[item]="item"
[contentTemplateRef]="contentTemplateRef"
></stapps-data-detail-content>
</ng-container>
</div>
</ion-content>

View File

@@ -1,20 +1,21 @@
/*
* Copyright (C) 2018-2021 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.
* 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.
* 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 Licens 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/>.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {Component, ContentChild, Input, TemplateRef} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {DataRoutingService} from '../data-routing.service';
import {DataListContext} from './data-list.component';
/**
* Shows data items in lists such es search result
@@ -35,6 +36,12 @@ export class DataListItemComponent {
*/
@Input() item: SCThings;
@Input() favoriteButton = true;
@ContentChild(TemplateRef) contentTemplateRef: TemplateRef<
DataListContext<SCThings>
>;
constructor(private readonly dataRoutingService: DataRoutingService) {}
/**

View File

@@ -8,66 +8,83 @@
<ion-thumbnail slot="start" *ngIf="!hideThumbnail" class="ion-margin-end">
<ion-icon color="medium" [attr.name]="item.type | dataIcon"></ion-icon>
</ion-thumbnail>
<ng-container *ngIf="contentTemplateRef; else defaultContent">
<ion-label class="ion-text-wrap" [ngSwitch]="true">
<div>
<ng-container
*ngTemplateOutlet="contentTemplateRef; context: {$implicit: item}"
></ng-container>
</div>
</ion-label>
</ng-container>
<stapps-favorite-button
*ngIf="favoriteButton"
[item]="$any(item)"
></stapps-favorite-button>
</ion-item>
<ng-template #defaultContent>
<ion-label class="ion-text-wrap" [ngSwitch]="true">
<div>
<stapps-catalog-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'catalog'"
></stapps-catalog-list-item>
<stapps-date-series-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'date series'"
></stapps-date-series-list-item>
<stapps-dish-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'dish'"
></stapps-dish-list-item>
<stapps-event-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'academic event'"
></stapps-event-list-item>
<stapps-event-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'sport course'"
></stapps-event-list-item>
<stapps-favorite-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'favorite'"
></stapps-favorite-list-item>
<stapps-message-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'message'"
></stapps-message-list-item>
<stapps-organization-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'organization'"
></stapps-organization-list-item>
<stapps-person-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'person'"
></stapps-person-list-item>
<stapps-place-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'building'"
></stapps-place-list-item>
<stapps-place-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'floor'"
></stapps-place-list-item>
<stapps-place-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'point of interest'"
></stapps-place-list-item>
<stapps-place-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'room'"
></stapps-place-list-item>
<stapps-semester-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'semester'"
></stapps-semester-list-item>
<stapps-video-list-item
[item]="item"
[item]="$any(item)"
*ngSwitchCase="item.type === 'video'"
></stapps-video-list-item>
<div *ngSwitchDefault>
@@ -87,5 +104,4 @@
></stapps-action-chip-list>
</div>
</ion-label>
<stapps-favorite-button [item]="item"></stapps-favorite-button>
</ion-item>
</ng-template>

View File

@@ -1,20 +1,21 @@
/*
* Copyright (C) 2021 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.
* 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.
* 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 Licens 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/>.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
import {
Component,
ContentChild,
EventEmitter,
HostListener,
Input,
@@ -23,11 +24,16 @@ import {
OnInit,
Output,
SimpleChanges,
TemplateRef,
ViewChild,
} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {BehaviorSubject, Observable, Subscription} from 'rxjs';
export interface DataListContext<T> {
$implicit: T;
}
/**
* Shows the list of items
*/
@@ -47,6 +53,10 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
*/
@Input() items?: SCThings[];
@ContentChild(TemplateRef) listItemTemplateRef: TemplateRef<
DataListContext<SCThings>
>;
/**
* Stream of SCThings for virtual scroll to consume
*/

View File

@@ -6,12 +6,16 @@
(scrolledIndexChange)="scrolled($event)"
[style.display]="items && items.length ? 'block' : 'none'"
>
<ng-content select="[header]"></ng-content>
<ion-list>
<stapps-data-list-item
*cdkVirtualFor="let item of items; trackBy: identifyItem"
[item]="item"
[hideThumbnail]="singleType"
></stapps-data-list-item>
<ng-container *cdkVirtualFor="let item of items; trackBy: identifyItem">
<ng-container
*ngTemplateOutlet="
listItemTemplateRef || defaultListItem;
context: {$implicit: item}
"
></ng-container>
</ng-container>
</ion-list>
</cdk-virtual-scroll-viewport>
</ng-container>
@@ -28,3 +32,10 @@
*ngFor="let skeleton of [].constructor(skeletonItems)"
></stapps-skeleton-list-item>
</ion-list>
<ng-template let-item #defaultListItem>
<stapps-data-list-item
[item]="item"
[hideThumbnail]="singleType"
></stapps-data-list-item>
</ng-template>

View File

@@ -1,22 +1,30 @@
/*
* Copyright (C) 2021 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.
* 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.
* 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 Licens 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/>.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {
Component,
ContentChild,
Input,
OnDestroy,
OnInit,
TemplateRef,
} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {Subscription} from 'rxjs';
import {Router} from '@angular/router';
import {DataRoutingService} from '../data-routing.service';
import {DataListContext} from './data-list.component';
/**
* Shows the list of items
@@ -30,18 +38,24 @@ export class SimpleDataListComponent implements OnInit, OnDestroy {
/**
* All SCThings to display
*/
@Input() items?: SCThings[];
@Input() items?: Promise<SCThings[] | undefined>;
/**
* Indicates whether or not the list is to display SCThings of a single type
*/
@Input() singleType = false;
@Input() autoRouting = true;
/**
* List header
*/
@Input() listHeader?: string;
@ContentChild(TemplateRef) listItemTemplateRef: TemplateRef<
DataListContext<SCThings>
>;
/**
* Items that display the skeleton list
*/
@@ -58,6 +72,7 @@ export class SimpleDataListComponent implements OnInit, OnDestroy {
) {}
ngOnInit(): void {
if (!this.autoRouting) return;
this.subscriptions.push(
this.dataRoutingService.itemSelectListener().subscribe(item => {
void this.router.navigate(['data-detail', item.uid]);

View File

@@ -1,11 +1,14 @@
<ng-container *ngIf="items | async as items; else loading">
<ion-list>
<ng-container *ngIf="!listHeader; else header"></ng-container>
<stapps-data-list-item
*ngFor="let item of items"
[item]="item"
[hideThumbnail]="singleType"
></stapps-data-list-item>
<ng-container *ngFor="let item of items">
<ng-container
*ngTemplateOutlet="
listItemTemplateRef || defaultListItem;
context: {$implicit: item}
"
></ng-container>
</ng-container>
</ion-list>
</ng-container>
<ng-template #loading>
@@ -26,3 +29,9 @@
</ion-text>
</ion-list-header>
</ng-template>
<ng-template let-item #defaultListItem>
<stapps-data-list-item
[item]="item"
[hideThumbnail]="singleType"
></stapps-data-list-item>
</ng-template>

View File

@@ -1,18 +1,19 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion,@typescript-eslint/no-explicit-any */
/*
* Copyright (C) 2018, 2019 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.
* 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.
* 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 Licens 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/>.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* eslint-disable @typescript-eslint/no-non-null-assertion,@typescript-eslint/no-explicit-any */
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ActivatedRoute, RouterModule} from '@angular/router';

View File

@@ -1,17 +1,19 @@
/*
* Copyright (C) 2018, 2019 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.
* 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.
* 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 Licens 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/>.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* eslint-disable @typescript-eslint/no-non-null-assertion,@typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-non-null-assertion,@typescript-eslint/no-explicit-any */
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
@@ -110,9 +112,10 @@ describe('HebisDetailComponent', () => {
it('should create component', () => expect(comp).toBeDefined());
it('should get a data item', () => {
comp.getItem(sampleThing.uid);
comp.getItem(sampleThing.uid, false);
expect(HebisDetailComponent.prototype.getItem).toHaveBeenCalledWith(
sampleThing.uid,
false,
);
});
@@ -120,6 +123,7 @@ describe('HebisDetailComponent', () => {
comp.ionViewWillEnter();
expect(HebisDetailComponent.prototype.getItem).toHaveBeenCalledWith(
sampleThing.uid,
false,
);
});
@@ -127,6 +131,7 @@ describe('HebisDetailComponent', () => {
await comp.refresh(refresher);
expect(HebisDetailComponent.prototype.getItem).toHaveBeenCalledWith(
sampleThing.uid,
true,
);
expect(refresher.complete).toHaveBeenCalled();
});

View File

@@ -1,16 +1,16 @@
/*
* Copyright (C) 2018-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.
* 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.
* 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 Licens 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/>.
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
@@ -56,15 +56,16 @@ export class HebisDetailComponent extends DataDetailComponent {
*/
async ionViewWillEnter() {
const uid = this.route.snapshot.paramMap.get('uid') || '';
await this.getItem(uid ?? '');
await this.getItem(uid ?? '', false);
}
/**
* Provides data item with given UID
*
* @param uid Unique identifier of a thing
* @param _forceReload Ignore any cached data
*/
async getItem(uid: SCUuid) {
async getItem(uid: SCUuid, _forceReload: boolean) {
this.hebisDataProvider.hebisSearch({query: uid, page: 0}).then(result => {
// eslint-disable-next-line unicorn/no-null
this.item = (result.data && result.data[0]) || null;

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 Licens 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/>.
*/
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms';

View File

@@ -356,6 +356,34 @@ export class MetersLocalizedPipe implements PipeTransform, OnDestroy {
}
}
@Injectable()
@Pipe({
name: 'isNaN',
pure: true,
})
export class IsNaNPipe implements PipeTransform {
transform(value: unknown): boolean {
return Number.isNaN(value);
}
}
@Injectable()
@Pipe({
name: 'isNumeric',
pure: true,
})
export class IsNumericPipe implements PipeTransform {
transform(value: unknown): boolean {
return !Number.isNaN(
typeof value === 'number'
? value
: typeof value === 'string'
? Number.parseFloat(value)
: Number.NaN,
);
}
}
@Injectable()
@Pipe({
name: 'numberLocalized',

View File

@@ -25,6 +25,8 @@ import {
DurationLocalizedPipe,
ToUnixPipe,
EntriesPipe,
IsNaNPipe,
IsNumericPipe,
} from './common-string-pipes';
import {
ThingTranslateDefaultParser,
@@ -56,6 +58,8 @@ export interface ThingTranslateModuleConfig {
SentenceCasePipe,
ToUnixPipe,
EntriesPipe,
IsNaNPipe,
IsNumericPipe,
],
exports: [
ArrayJoinPipe,
@@ -71,6 +75,8 @@ export interface ThingTranslateModuleConfig {
SentenceCasePipe,
ToUnixPipe,
EntriesPipe,
IsNaNPipe,
IsNumericPipe,
],
})
export class ThingTranslateModule {