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

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