feat: add catalog module

This commit is contained in:
Rainer Killinger
2021-07-02 10:27:31 +02:00
committed by Jovan Krunić
parent e628f396e2
commit 03084b1c96
24 changed files with 744 additions and 45 deletions

View File

@@ -39,6 +39,7 @@ import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
import {environment} from '../environments/environment';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {CatalogModule} from './modules/catalog/catalog.module';
import {ConfigModule} from './modules/config/config.module';
import {ConfigProvider} from './modules/config/config.provider';
import {DataModule} from './modules/data/data.module';
@@ -137,6 +138,7 @@ const providers: Provider[] = [
AppRoutingModule,
BrowserModule,
BrowserAnimationsModule,
CatalogModule,
CommonModule,
ConfigModule,
DataModule,

View File

@@ -0,0 +1,53 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
<ion-menu-button></ion-menu-button>
</ion-buttons>
<!--TODO: read this from the config (menu item title)-->
<ion-title>{{ 'catalog.title' | translate | titlecase }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-segment
(ionChange)="segmentChanged($event)"
[value]="selectedSemesterUID"
mode="md"
>
<ion-segment-button
*ngFor="let semester of availableSemesters"
[value]="semester.uid"
>
<ion-label>{{ semester.acronym }}</ion-label>
</ion-segment-button>
</ion-segment>
<ion-list *ngIf="catalogs && catalogs.length > 0">
<ion-item
*ngFor="let catalog of catalogs"
button="true"
lines="inset"
(click)="notifySelect(catalog)"
>
<ion-label>
<h2>{{ catalog.name }}</h2>
<h3>{{ catalog.acronym }}</h3>
</ion-label>
</ion-item>
</ion-list>
<ion-list *ngIf="!catalogs">
<stapps-skeleton-list-item *ngFor="let skeleton of [].constructor(10)">
</stapps-skeleton-list-item>
</ion-list>
<ion-grid *ngIf="catalogs && catalogs.length === 0">
<ion-row>
<ion-col>
<div class="ion-text-center margin-top">
<ion-label>
{{ 'catalog.detail.EMPTY_SEMESTER' | translate }}
</ion-label>
</div>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -0,0 +1,9 @@
ion-segment-button {
max-width: 100%;
text-transform: none;
}
.margin-top {
margin-top: 20vh;
}

View File

@@ -0,0 +1,159 @@
/*
* 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.
*
* 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/>.
*/
import {Component, OnInit, OnDestroy} from '@angular/core';
import {Router, ActivatedRoute} from '@angular/router';
import {SCCatalog, SCSemester} from '@openstapps/core';
import moment from 'moment';
import {Subscription} from 'rxjs';
import {CatalogProvider} from './catalog.provider';
import {NGXLogger} from 'ngx-logger';
import {Location} from '@angular/common';
import {DataRoutingService} from '../data/data-routing.service';
@Component({
selector: 'app-catalog',
templateUrl: './catalog.component.html',
styleUrls: ['./catalog.component.scss'],
})
export class CatalogComponent implements OnInit, OnDestroy {
/**
* SCSemester to show
*/
activeSemester?: SCSemester;
/**
* UID of the selected SCSemester
*/
selectedSemesterUID = '';
/**
* Available SCSemesters
*/
availableSemesters: SCSemester[] = [];
/**
* Catalogs (SCCatalog) to show
*/
catalogs: SCCatalog[] | undefined;
/**
* Array of all subscriptions to Observables
*/
subscriptions: Subscription[] = [];
/**
* Supercatalog (SCCatalog) to refer to
*/
superCatalog: SCCatalog;
constructor(
private readonly route: ActivatedRoute,
private readonly catalogProvider: CatalogProvider,
private readonly dataRoutingService: DataRoutingService,
private readonly logger: NGXLogger,
protected router: Router,
public location: Location,
) {
this.subscriptions.push(
this.dataRoutingService.itemSelectListener().subscribe(item => {
void this.router.navigate(['data-detail', item.uid]);
}),
);
}
ngOnInit() {
this.selectedSemesterUID = this.route.snapshot.paramMap.get('uid') ?? '';
void this.fetchCatalog();
}
/**
* Remove subscriptions when the component is removed
*/
ngOnDestroy() {
for (const sub of this.subscriptions) {
sub.unsubscribe();
}
}
async fetchCatalog() {
try {
if (this.availableSemesters.length === 0) {
await this.fetchSemesters();
}
const response = await this.catalogProvider.getCatalogsWith(
0,
this.superCatalog,
this.activeSemester?.uid,
);
this.catalogs = (response.data as SCCatalog[]).sort((a, b) => {
return new Intl.Collator('en', {
numeric: true,
sensitivity: 'accent',
}).compare(a.name, b.name);
});
} catch (error) {
this.logger.error(error.message);
return;
}
}
async fetchSemesters(): Promise<void> {
const semesters = await this.catalogProvider.getRelevantSemesters();
this.availableSemesters = semesters.slice(0, 3).reverse();
if (typeof this.activeSemester !== 'undefined') {
return;
}
const today = moment().startOf('day').toISOString();
this.activeSemester = this.availableSemesters[0];
this.activeSemester =
this.selectedSemesterUID === ''
? this.availableSemesters.find(
semester => semester.startDate <= today && semester.endDate > today,
)
: this.availableSemesters.find(
semester => semester.uid === this.selectedSemesterUID,
);
if (this.activeSemester && this.selectedSemesterUID === '') {
this.selectedSemesterUID = this.activeSemester.uid;
this.updateLocation(this.selectedSemesterUID);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
segmentChanged(event: any) {
this.updateLocation(event.detail.value as string);
this.activeSemester = this.availableSemesters.find(
semester => semester.uid === (event.detail.value as string),
);
delete this.catalogs;
void this.fetchCatalog();
}
updateLocation(semesterUID: string) {
const url = this.router
.createUrlTree(['/catalog/', semesterUID])
.toString();
this.location.go(url);
}
/**
* Emit event that a catalog item was selected
*/
notifySelect(catalog: SCCatalog) {
this.dataRoutingService.emitChildEvent(catalog);
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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.
*
* 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/>.
*/
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {RouterModule, Routes} from '@angular/router';
import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core';
import {MomentModule} from 'ngx-moment';
import {DataModule} from '../data/data.module';
import {SettingsProvider} from '../settings/settings.provider';
import {CatalogComponent} from './catalog.component';
const catalogRoutes: Routes = [
{path: 'catalog', component: CatalogComponent},
{path: 'catalog/:uid', component: CatalogComponent},
];
/**
* Catalog Module
*/
@NgModule({
declarations: [CatalogComponent],
imports: [
IonicModule.forRoot(),
FormsModule,
TranslateModule.forChild(),
RouterModule.forChild(catalogRoutes),
CommonModule,
MomentModule,
DataModule,
],
providers: [SettingsProvider],
})
export class CatalogModule {}

View File

@@ -0,0 +1,122 @@
/*
* 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.
*
* 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/>.
*/
import {Injectable} from '@angular/core';
import {
SCCatalogWithoutReferences,
SCSearchFilter,
SCSearchResponse,
SCSemester,
SCThingType,
} from '@openstapps/core';
import {DataProvider} from '../data/data.provider';
/**
* Service for providing catalog and semester data
*/
@Injectable({
providedIn: 'root',
})
export class CatalogProvider {
constructor(private readonly dataProvider: DataProvider) {}
/**
* Get news messages
* TODO: make dates sortable on the backend side and then adjust this method
*
* @param offset TODO
* @param superCatalog TODO
* @param semesterUID TODO
*/
async getCatalogsWith(
offset?: number,
superCatalog?: SCCatalogWithoutReferences,
semesterUID?: string,
): Promise<SCSearchResponse> {
const filters: SCSearchFilter[] = [
{
type: 'value',
arguments: {
field: 'type',
value: 'catalog',
},
},
];
if (typeof semesterUID === 'string') {
filters.push({
type: 'value',
arguments: {
field: 'academicTerm.uid',
value: semesterUID,
},
});
}
if (typeof superCatalog?.uid === 'string') {
filters.push({
type: 'value',
arguments: {
field: 'superCatalog.uid',
value: superCatalog.uid,
},
});
} else {
filters.push({
type: 'value',
arguments: {
field: 'level',
value: '0',
},
});
}
return this.dataProvider.search({
filter: {
arguments: {
operation: 'and',
filters: filters,
},
type: 'boolean',
},
size: 40,
from: offset,
sort: [
{
type: 'ducet',
order: 'asc',
arguments: {
field: 'name',
},
},
],
});
}
async getRelevantSemesters(): Promise<SCSemester[]> {
const response = await this.dataProvider.search({
filter: {
arguments: {
field: 'type',
value: SCThingType.Semester,
},
type: 'value',
},
size: 10,
});
return (response.data as SCSemester[]).sort((a, b) =>
b.startDate.localeCompare(a.startDate, 'en'),
);
}
}

View File

@@ -81,6 +81,7 @@ import {OriginInListComponent} from './elements/origin-in-list.component';
import {CoordinatedSearchProvider} from './coordinated-search.provider';
import {Geolocation} from '@ionic-native/geolocation/ngx';
import {FavoriteButtonComponent} from './elements/favorite-button.component';
import {SimpleDataListComponent} from './list/simple-data-list.component';
/**
* Module for handling data
@@ -135,8 +136,9 @@ import {FavoriteButtonComponent} from './elements/favorite-button.component';
SkeletonSimpleCardComponent,
VideoDetailContentComponent,
VideoListItemComponent,
SimpleDataListComponent,
],
entryComponents: [DataListComponent],
entryComponents: [DataListComponent, SimpleDataListComponent],
imports: [
CommonModule,
DataRoutingModule,
@@ -177,6 +179,7 @@ import {FavoriteButtonComponent} from './elements/favorite-button.component';
SkeletonListItemComponent,
SkeletonSimpleCardComponent,
SearchPageComponent,
SimpleDataListComponent,
],
})
export class DataModule {}

View File

@@ -1,3 +1,14 @@
<span
>{{ text | slice: 0:size }}<span *ngIf="text.length > size">...</span></span
>
<span class="ion-hide-sm-up">
{{ text | slice: 0:size }}
<span *ngIf="text.length > size" class="ion-hide-sm-up"></span>
</span>
<span class="ion-hide-sm-down ion-hide-md-up">
{{ text | slice: 0:size * 2 }}
<span *ngIf="text.length > size * 2" class="ion-hide-sm-down ion-hide-md-up">
</span>
</span>
<span class="ion-hide-md-down">
{{ text | slice: 0:size * 3 }}
<span *ngIf="text.length > size * 3" class="ion-hide-md-down"></span>
</span>

View File

@@ -12,7 +12,7 @@
* 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, Input} from '@angular/core';
/**
* A placeholder to show when a list item is being loaded
@@ -22,4 +22,6 @@ import {Component} from '@angular/core';
templateUrl: 'skeleton-list-item.html',
styleUrls: ['skeleton-list-item.scss'],
})
export class SkeletonListItemComponent {}
export class SkeletonListItemComponent {
@Input() hideThumbnail = false;
}

View File

@@ -1,5 +1,5 @@
<ion-item>
<ion-thumbnail slot="start" class="ion-margin-end">
<ion-thumbnail *ngIf="!hideThumbnail" slot="start" class="ion-margin-end">
<ion-skeleton-text animated></ion-skeleton-text>
</ion-thumbnail>
<ion-grid>

View File

@@ -26,7 +26,6 @@ import {
ViewChild,
} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {ceil} from 'lodash-es';
import {BehaviorSubject, Observable, Subscription} from 'rxjs';
/**
@@ -72,7 +71,7 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
/**
* Items that display the skeleton list
*/
skeletonItems: number[];
skeletonItems: number;
/**
* Array of all subscriptions to Observables
@@ -84,12 +83,10 @@ export class DataListComponent implements OnChanges, OnInit, OnDestroy {
/**
* Calculate how many items would fill the screen
*/
@HostListener('window.resize', ['$event'])
@HostListener('window:resize', ['$event'])
calcSkeletonItems() {
const itemHeight = 122;
this.skeletonItems = Array.from({
length: ceil(window.innerHeight / itemHeight),
});
const itemHeight = 40;
this.skeletonItems = Math.ceil(window.innerHeight / itemHeight);
}
/**

View File

@@ -22,6 +22,7 @@
</div>
<ion-list [style.display]="items ? 'none' : 'block'">
<stapps-skeleton-list-item
*ngFor="let skeleton of skeletonItems"
[hideThumbnail]="singleType"
*ngFor="let skeleton of [].constructor(skeletonItems)"
></stapps-skeleton-list-item>
</ion-list>

View File

@@ -1,5 +1,6 @@
cdk-virtual-scroll-viewport {
min-height: 100%;
height: 100%;
width: 100%;
}
@@ -8,3 +9,7 @@ cdk-virtual-scroll-viewport {
width: 100%;
}
}
.virtual-scroll-expander {
clear: both;
}

View File

@@ -12,7 +12,7 @@
* 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, OnInit, OnDestroy} from '@angular/core';
import {Router} from '@angular/router';
import {AlertController} from '@ionic/angular';
import {
@@ -39,7 +39,7 @@ import {PositionService} from '../../map/position.service';
templateUrl: 'search-page.html',
providers: [ContextMenuService],
})
export class SearchPageComponent {
export class SearchPageComponent implements OnInit, OnDestroy {
/**
* Api query filter
*/
@@ -126,9 +126,7 @@ export class SearchPageComponent {
protected dataRoutingService: DataRoutingService,
protected router: Router,
protected positionService: PositionService,
) {
this.initialize();
}
) {}
/**
* Fetches items with set query configuration
@@ -230,7 +228,8 @@ export class SearchPageComponent {
this.contextMenuService.updateContextFilter(facets);
}
ionViewWillEnter() {
ngOnInit() {
this.initialize();
this.contextMenuService.setContextSort({
name: 'sort',
reversed: false,
@@ -290,7 +289,7 @@ export class SearchPageComponent {
);
}
ionViewWillLeave() {
ngOnDestroy() {
for (const subscription of this.subscriptions) {
subscription.unsubscribe();
}

View File

@@ -0,0 +1,76 @@
/*
* 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.
*
* 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/>.
*/
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {Subscription} from 'rxjs';
import {Router} from '@angular/router';
import {DataRoutingService} from '../data-routing.service';
/**
* Shows the list of items
*/
@Component({
selector: 'stapps-simple-data-list',
templateUrl: 'simple-data-list.html',
styleUrls: ['simple-data-list.scss'],
})
export class SimpleDataListComponent implements OnInit, OnDestroy {
/**
* All SCThings to display
*/
@Input() items?: SCThings[];
/**
* Indicates whether or not the list is to display SCThings of a single type
*/
@Input() singleType = false;
/**
* List header
*/
@Input() listHeader?: string;
/**
* Items that display the skeleton list
*/
skeletonItems = 6;
/**
* Array of all subscriptions to Observables
*/
subscriptions: Subscription[] = [];
constructor(
protected router: Router,
private readonly dataRoutingService: DataRoutingService,
) {}
ngOnInit(): void {
this.subscriptions.push(
this.dataRoutingService.itemSelectListener().subscribe(item => {
void this.router.navigate(['data-detail', item.uid]);
}),
);
}
/**
* Remove subscriptions when the component is removed
*/
ngOnDestroy() {
for (const sub of this.subscriptions) {
sub.unsubscribe();
}
}
}

View File

@@ -0,0 +1,28 @@
<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>
</ion-list>
</ng-container>
<ng-template #loading>
<ion-list>
<stapps-skeleton-list-item
[hideThumbnail]="singleType"
*ngFor="let skeleton of [].constructor(skeletonItems)"
>
</stapps-skeleton-list-item>
</ion-list>
</ng-template>
<ng-template #header>
<ion-list-header lines="inset">
<ion-text color="dark">
<h3>
{{ listHeader }}
</h3>
</ion-text>
</ion-list-header>
</ng-template>

View File

@@ -12,19 +12,159 @@
* 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 {SCCatalog} from '@openstapps/core';
import {
Component,
Input,
OnInit,
ViewChild,
ElementRef,
HostListener,
OnDestroy,
OnChanges,
} from '@angular/core';
import {SCCatalog, SCSearchBooleanFilter} from '@openstapps/core';
import {SearchPageComponent} from '../../list/search-page.component';
enum AccordionButtonState {
collapsed = 'chevron-down',
expanded = 'chevron-up',
}
/**
* TODO
*/
@Component({
selector: 'stapps-catalog-detail-content',
templateUrl: 'catalog-detail-content.html',
styleUrls: ['catalog-detail-content.scss'],
})
export class CatalogDetailContentComponent {
export class CatalogDetailContentComponent
extends SearchPageComponent
implements OnInit, OnDestroy, OnChanges
{
/**
* TODO
* SCCatalog to display
*/
@Input() item: SCCatalog;
@ViewChild('accordionTextArea') accordionTextArea: ElementRef;
buttonState = AccordionButtonState.collapsed;
buttonShown = true;
descriptionLinesShown: number;
descriptionLinesTotal: number;
descriptionPreviewLines = 3;
descriptionLinesToDisplay = 0;
ngOnInit() {
super.ngOnInit();
if (this.item.description) {
this.descriptionLinesToDisplay = this.descriptionPreviewLines;
setTimeout(() => this.checkTextElipsis(), 0);
}
}
ngOnChanges() {
this.checkTextElipsis();
}
initialize() {
this.pageSize = 100;
this.sortQuery = {
arguments: {field: 'name'},
order: 'asc',
type: 'ducet',
};
const subCatalogFilter: SCSearchBooleanFilter = {
arguments: {
operation: 'and',
filters: [
{
type: 'value',
arguments: {
field: 'type',
value: 'catalog',
},
},
{
type: 'value',
arguments: {
field: 'superCatalog.uid',
value: this.item.uid,
},
},
],
},
type: 'boolean',
};
const subEventsFilter: SCSearchBooleanFilter = {
arguments: {
operation: 'and',
filters: [
{
type: 'value',
arguments: {
field: 'type',
value: 'academic event',
},
},
{
type: 'value',
arguments: {
field: 'catalogs.uid',
value: this.item.uid,
},
},
],
},
type: 'boolean',
};
this.forcedFilter = {
arguments: {
filters: [subCatalogFilter, subEventsFilter],
operation: 'or',
},
type: 'boolean',
};
}
@HostListener('window:resize', ['$event'])
checkTextElipsis() {
if (typeof this.accordionTextArea === 'undefined') {
return;
}
const element = this.accordionTextArea.nativeElement as HTMLElement;
const lineHeight = Number.parseInt(
getComputedStyle(element).getPropertyValue('line-height'),
);
this.descriptionLinesTotal = element?.scrollHeight / lineHeight;
this.descriptionLinesShown = element?.offsetHeight / lineHeight;
if (this.buttonState === AccordionButtonState.expanded) {
this.descriptionLinesToDisplay = this.descriptionLinesTotal;
}
const isElipsed = element?.offsetHeight < element?.scrollHeight;
this.buttonShown =
(isElipsed && this.buttonState === AccordionButtonState.collapsed) ||
(!isElipsed && this.buttonState === AccordionButtonState.expanded);
}
toggleDescriptionAccordion() {
if (this.descriptionLinesToDisplay > 0) {
this.descriptionLinesToDisplay =
this.descriptionLinesToDisplay === this.descriptionPreviewLines
? this.descriptionLinesTotal
: this.descriptionPreviewLines;
}
this.buttonState =
this.buttonState === AccordionButtonState.collapsed
? AccordionButtonState.expanded
: AccordionButtonState.collapsed;
setTimeout(() => this.checkTextElipsis(), 0);
}
}

View File

@@ -1,5 +1,33 @@
<stapps-simple-card
[title]="'categories' | propertyNameTranslate: item | titlecase"
[content]="'categories' | thingTranslate: item"
<ion-card>
<ion-card-content>
<ion-text color="dark">
<h1>
{{ 'name' | thingTranslate: item }}
</h1>
</ion-text>
<div *ngIf="item.description">
<br />
<div
class="text-accordion"
[style.-webkit-line-clamp]="descriptionLinesToDisplay"
#accordionTextArea
>
{{ 'description' | thingTranslate: item }}
</div>
</div>
</ion-card-content>
</ion-card>
<ion-button
expand="full"
fill="clear"
*ngIf="item.description && buttonShown"
(click)="toggleDescriptionAccordion()"
>
</stapps-simple-card>
<ion-icon [name]="buttonState" size="large"></ion-icon>
</ion-button>
<stapps-simple-data-list
id="simple-data-list"
[items]="items"
[singleType]="true"
[listHeader]="'type' | thingTranslate: item | titlecase"
></stapps-simple-data-list>

View File

@@ -0,0 +1,6 @@
.text-accordion {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
}

View File

@@ -2,14 +2,14 @@
<ion-row>
<ion-col>
<div class="ion-text-wrap">
<h2 class="name">{{ 'name' | thingTranslate: item }}</h2>
<p *ngIf="item.description">
<stapps-long-inline-text
[text]="'description' | thingTranslate: item"
[size]="80"
></stapps-long-inline-text>
</p>
<p *ngIf="item.academicTerm">{{ item.academicTerm.name }}</p>
<ion-label>
<h2>
{{ 'name' | thingTranslate: item }}
</h2>
</ion-label>
<ion-note *ngIf="item.academicTerm">{{
item.academicTerm.name
}}</ion-note>
</div>
</ion-col>
</ion-row>

View File

@@ -58,7 +58,6 @@ export class FavoritesPageComponent extends SearchPageComponent {
}
ionViewWillEnter() {
super.ionViewWillEnter();
this.subscriptions.push(
this.favoritesService.favoritesChanged$.subscribe(_favoritesMap => {
this.fetchAndUpdateItems();