mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-22 09:32:41 +00:00
feat: show menu for multiple days for canteens and cafes
Closes #19, #79
This commit is contained in:
committed by
Jovan Krunić
parent
66b8720da0
commit
3c079cd189
1000
package-lock.json
generated
1000
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,7 @@
|
||||
"@ionic/storage": "2.2.0",
|
||||
"@ngx-translate/core": "11.0.1",
|
||||
"@ngx-translate/http-loader": "4.0.0",
|
||||
"@openstapps/api": "0.22.0",
|
||||
"@openstapps/api": "0.25.0",
|
||||
"@openstapps/configuration": "0.25.0",
|
||||
"@openstapps/core": "0.36.0",
|
||||
"cordova-android": "8.0.0",
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/* tslint:disable */
|
||||
|
||||
import moment from 'moment'
|
||||
|
||||
export const sampleResources = [{
|
||||
'errorNames': [],
|
||||
"instance": {
|
||||
@@ -981,8 +984,8 @@ export const sampleResources = [{
|
||||
'offers': [
|
||||
{
|
||||
'availability': 'in stock',
|
||||
'availabilityStarts': '2017-01-30T00:00:00.000Z',
|
||||
'availabilityEnds': '2017-01-30T23:59:59.999Z',
|
||||
'availabilityStarts': moment().startOf('day').add(2,'days').toISOString(),
|
||||
'availabilityEnds': moment().endOf('day').add(2,'days').toISOString(),
|
||||
'prices': {
|
||||
'default': 6.5,
|
||||
'student': 5,
|
||||
@@ -1101,8 +1104,8 @@ export const sampleResources = [{
|
||||
'offers': [
|
||||
{
|
||||
'availability': 'in stock',
|
||||
'availabilityStarts': '2017-01-30T00:00:00.000Z',
|
||||
'availabilityEnds': '2017-01-30T23:59:59.999Z',
|
||||
'availabilityStarts': moment().startOf('day').toISOString(),
|
||||
'availabilityEnds': moment().endOf('day').add(2,'days').toISOString(),
|
||||
'prices': {
|
||||
'default': 4.85,
|
||||
'student': 2.85,
|
||||
@@ -1117,7 +1120,7 @@ export const sampleResources = [{
|
||||
'type': 'remote'
|
||||
},
|
||||
'type': 'organization',
|
||||
'uid': '3b9b3df6-3a7a-58cc-922f-c7335c002634'
|
||||
'uid': 'b7206fb5-bd77-5572-928f-16aa70910f64'
|
||||
},
|
||||
'inPlace': {
|
||||
'geo': {
|
||||
@@ -1138,7 +1141,7 @@ export const sampleResources = [{
|
||||
'alternateNames': [
|
||||
'MensaHardenberg'
|
||||
],
|
||||
'uid': '72fbc8a3-ebd1-58f9-9526-ad65cba2e402',
|
||||
'uid': 'b7206fb5-bd77-5572-928f-16aa70910f64',
|
||||
'address': {
|
||||
'addressCountry': 'Germany',
|
||||
'addressLocality': 'Berlin',
|
||||
@@ -1191,8 +1194,8 @@ export const sampleResources = [{
|
||||
'uid': '3b9b3df6-3a7a-58cc-922f-c7335c002634'
|
||||
},
|
||||
'availability': 'in stock',
|
||||
'availabilityStarts': '2017-01-30T00:00:00.000Z',
|
||||
'availabilityEnds': '2017-01-30T23:59:59.999Z',
|
||||
'availabilityStarts': moment().startOf('day').add(2,'days').toISOString(),
|
||||
'availabilityEnds': moment().endOf('day').add(2,'days').toISOString(),
|
||||
'inPlace': {
|
||||
'geo': {
|
||||
'point': {
|
||||
@@ -1264,8 +1267,8 @@ export const sampleResources = [{
|
||||
],
|
||||
'offers': [
|
||||
{
|
||||
'availabilityEnds': '2017-03-27T23:59:59.000Z',
|
||||
'availabilityStarts': '2017-03-27T00:00:00.000Z',
|
||||
'availabilityEnds': moment().endOf('day').toISOString(),
|
||||
'availabilityStarts': moment().startOf('day').toISOString(),
|
||||
'availability': 'in stock',
|
||||
'inPlace': {
|
||||
'type': 'room',
|
||||
@@ -1273,7 +1276,7 @@ export const sampleResources = [{
|
||||
'categories': [
|
||||
'cafe'
|
||||
],
|
||||
'uid': 'e5492c9c-064e-547c-8633-c8fc8955cfcf',
|
||||
'uid': 'b7206fb5-bd77-5572-928f-16aa70910f64',
|
||||
'alternateNames': [
|
||||
'Cafeteria LEVEL'
|
||||
],
|
||||
@@ -1306,7 +1309,7 @@ export const sampleResources = [{
|
||||
'type': 'remote'
|
||||
},
|
||||
'type': 'organization',
|
||||
'uid': '3b9b3df6-3a7a-58cc-922f-c7335c002634'
|
||||
'uid': 'b7206fb5-bd77-5572-928f-16aa70910f64'
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -14,10 +14,13 @@
|
||||
*/
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {SCAcademicEvent, SCArticle, SCBook, SCBuilding, SCCatalog,
|
||||
SCDateSeries, SCDish, SCFavorite, SCMessage, SCPerson, SCRoom,
|
||||
SCThing, SCThingOriginType, SCThingType, SCToDo, SCToDoPriority} from '@openstapps/core';
|
||||
import {
|
||||
SCAcademicEvent, SCArticle, SCBook, SCBuilding, SCCatalog,
|
||||
SCDateSeries, SCDish, SCFavorite, SCMessage, SCPerson, SCRoom, SCSearchFilter,
|
||||
SCThing, SCThingOriginType, SCThingType, SCToDo, SCToDoPriority,
|
||||
} from '@openstapps/core';
|
||||
import {Observable, of} from 'rxjs';
|
||||
import {checkFilter} from '../fakesearch/filters';
|
||||
import {sampleResources} from './resources/test-resources';
|
||||
|
||||
// tslint:disable:no-magic-numbers
|
||||
@@ -339,7 +342,7 @@ const sampleDateSeries: SCDateSeries[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const sampleThingsMap: {[key in SCThingType | string]: SCThing[]} = {
|
||||
export const sampleThingsMap: { [key in SCThingType | string]: SCThing[] } = {
|
||||
'academic event': sampleAcademicEvents,
|
||||
article: sampleArticles,
|
||||
book: sampleBooks,
|
||||
@@ -364,35 +367,6 @@ export const sampleThingsMap: {[key in SCThingType | string]: SCThing[]} = {
|
||||
tour: [],
|
||||
video: [],
|
||||
};
|
||||
// tslint:enable:no-magic-numbers
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* provides all or certain sample things to be used in tests or backendless development
|
||||
*/
|
||||
export function getSampleThings(...uids: string[]): SCThing[] {
|
||||
const sampleThings: SCThing[] = [];
|
||||
if (typeof uids !== 'undefined' && uids.length > 0) {
|
||||
uids.forEach((uid) => {
|
||||
Object.keys(sampleThingsMap)
|
||||
.forEach((key) => {
|
||||
sampleThingsMap[key].forEach((thing) => {
|
||||
if (thing.uid === uid) {
|
||||
sampleThings.push(thing);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Object.keys(sampleThingsMap)
|
||||
.forEach((key) => {
|
||||
sampleThings.push(...sampleThingsMap[key]);
|
||||
});
|
||||
}
|
||||
|
||||
return sampleThings;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
@@ -407,17 +381,13 @@ export class SampleThings {
|
||||
constructor(http: HttpClient) {
|
||||
this.http = http;
|
||||
}
|
||||
// getSampleThings(): Observable<any> {
|
||||
// return this.http.get('http://localhost:8100/assets/json/joined.json');
|
||||
// // return of(sampleDishes[0]);
|
||||
// }
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
// tslint:disable-next-line:prefer-function-over-method
|
||||
// tslint:disable-next-line:prefer-function-over-method no-any
|
||||
getSampleThing(uid: string): Observable<any[]> {
|
||||
// const jsonContent: any[] = await this.http.get('http://localhost:8100/assets/json/joined.json').toPromise();
|
||||
// tslint:disable-next-line:no-any
|
||||
const sampleThings: any[] = [];
|
||||
for (const resource of sampleResources) {
|
||||
if (resource.instance.uid as SCThingType === uid) {
|
||||
@@ -433,16 +403,18 @@ export class SampleThings {
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
// tslint:disable-next-line:prefer-function-over-method
|
||||
getSampleThings(): Observable<any[]> {
|
||||
// const jsonContent: any[] = await this.http.get('http://localhost:8100/assets/json/joined.json').toPromise();
|
||||
// tslint:disable-next-line:prefer-function-over-method no-any
|
||||
getSampleThings(filter?: SCSearchFilter): Observable<any[]> {
|
||||
// tslint:disable-next-line:no-any
|
||||
const sampleThings: any[] = [];
|
||||
sampleResources.forEach((resource) => {
|
||||
for (const resource of sampleResources) {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
// if ([SCThingType.Video].includes(resource.instance.type as SCThingType)) {
|
||||
if (typeof filter === 'undefined' || checkFilter(resource.instance as SCThing, filter)) {
|
||||
sampleThings.push(resource.instance);
|
||||
}
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
return of(sampleThings);
|
||||
}
|
||||
|
||||
@@ -382,13 +382,13 @@ export class FakeBackendInterceptor implements HttpInterceptor {
|
||||
if (request.body.filter.arguments.field === 'uid') {
|
||||
return this.sampleFetcher.getSampleThing(request.body.filter.arguments.value)
|
||||
// tslint:disable-next-line:no-any
|
||||
.pipe(map((sampleData: any) => {
|
||||
return new HttpResponse({status: 200, body: {data: sampleData}});
|
||||
}), delay(this.RESPONSE_DELAY)); // add delay for skeleton screens to be seen (see !16)
|
||||
.pipe(map((sampleData: any) => {
|
||||
return new HttpResponse({status: 200, body: {data: sampleData}});
|
||||
}), delay(this.RESPONSE_DELAY)); // add delay for skeleton screens to be seen (see !16)
|
||||
}
|
||||
}
|
||||
|
||||
return this.sampleFetcher.getSampleThings()
|
||||
return this.sampleFetcher.getSampleThings(request.body.filter)
|
||||
// tslint:disable-next-line:no-any
|
||||
.pipe(map((sampleData: any) => {
|
||||
return new HttpResponse({status: 200, body: {data: sampleData, facets: facetsMock}});
|
||||
|
||||
170
src/app/_helpers/fakesearch/filters.ts
Normal file
170
src/app/_helpers/fakesearch/filters.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 {SCSearchBooleanFilter, SCSearchFilter, SCSearchValueFilter, SCThing} from '@openstapps/core';
|
||||
import {logger} from '../ts-logger';
|
||||
|
||||
/**
|
||||
* Checks if any filter applies to an SCThing
|
||||
*/
|
||||
export function checkFilter(thing: SCThing, filter: SCSearchFilter): boolean {
|
||||
switch (filter.type) {
|
||||
case 'availability': /*TODO*/
|
||||
break;
|
||||
case 'boolean':
|
||||
return applyBooleanFilter(thing, filter);
|
||||
case 'distance': /*TODO*/
|
||||
break;
|
||||
case 'value':
|
||||
return applyValueFilter(thing, filter);
|
||||
}
|
||||
|
||||
void logger.error(`Not implemented filter method "${filter.type}" in fake backend!`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value filter applies to an SCThing
|
||||
*/
|
||||
function applyValueFilter(thing: SCThing, filter: SCSearchValueFilter): boolean {
|
||||
const path = filter.arguments.field.split('.');
|
||||
const thingFieldValue = traverseToFieldPath(thing, path, filter.arguments.value);
|
||||
|
||||
if (!(thingFieldValue.found)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return thingFieldValue.result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Object that can be accessed using foo[bar]
|
||||
*/
|
||||
interface IndexableObject {
|
||||
// tslint:disable-next-line:no-any
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a search for a field and comparison to a desired value
|
||||
*/
|
||||
type FieldSearchResult = {
|
||||
/**
|
||||
* Weather the field was found
|
||||
*/
|
||||
found: true;
|
||||
|
||||
/**
|
||||
* The result of the comparison
|
||||
*/
|
||||
result: boolean;
|
||||
} | {
|
||||
/**
|
||||
* Weather the field was found
|
||||
*/
|
||||
found: false;
|
||||
};
|
||||
|
||||
// tslint:disable-next-line:completed-docs
|
||||
function traverseToFieldPath(value: IndexableObject, path: string[], desiredFieldValue: unknown): FieldSearchResult {
|
||||
if (path.length === 0) {
|
||||
void logger.error(`Value filter provided with zero length path`);
|
||||
|
||||
return {found: false};
|
||||
}
|
||||
|
||||
if (value.hasOwnProperty(path[0])) {
|
||||
const nestedProperty = value[path[0]];
|
||||
|
||||
if (path.length === 1) {
|
||||
return esStyleFieldHandler(nestedProperty, (nestedValue) => {
|
||||
return {
|
||||
found: true,
|
||||
result: nestedValue === desiredFieldValue,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return esStyleFieldHandler(nestedProperty, (nestedValue) => {
|
||||
if (typeof nestedValue === 'object') {
|
||||
return traverseToFieldPath(
|
||||
nestedValue as IndexableObject,
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
path.slice(1),
|
||||
desiredFieldValue);
|
||||
}
|
||||
|
||||
return {found: false};
|
||||
});
|
||||
}
|
||||
|
||||
return {found: false};
|
||||
}
|
||||
|
||||
/**
|
||||
* ES treats arrays like normal fields
|
||||
*/
|
||||
function esStyleFieldHandler<T>(field: T | T[],
|
||||
handler: (value: T) => FieldSearchResult): FieldSearchResult {
|
||||
if (Array.isArray(field)) {
|
||||
for (const nestedField of field) {
|
||||
const result = handler(nestedField);
|
||||
|
||||
if (result.found && result.result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: found is not accurate
|
||||
return {found: false};
|
||||
}
|
||||
|
||||
return handler(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a boolean filter applies to an SCThing
|
||||
*/
|
||||
function applyBooleanFilter(thing: SCThing, filter: SCSearchBooleanFilter): boolean {
|
||||
let out = false;
|
||||
|
||||
switch (filter.arguments.operation) {
|
||||
case 'and':
|
||||
out = true;
|
||||
for (const nesterFilter of filter.arguments.filters) {
|
||||
out = out && checkFilter(thing, nesterFilter);
|
||||
}
|
||||
|
||||
return out;
|
||||
case 'or':
|
||||
for (const nesterFilter of filter.arguments.filters) {
|
||||
out = out || checkFilter(thing, nesterFilter);
|
||||
}
|
||||
|
||||
return out;
|
||||
case 'not':
|
||||
if (filter.arguments.filters.length === 1) {
|
||||
return !checkFilter(thing, filter.arguments.filters[0]);
|
||||
}
|
||||
void logger.error(`Too many filters for "not" boolean operation`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void logger.error(`Not implemented boolean filter "${filter.arguments.operation}"`);
|
||||
|
||||
return false;
|
||||
}
|
||||
19
src/app/_helpers/ts-logger.ts
Normal file
19
src/app/_helpers/ts-logger.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 {NGXLogger} from 'ngx-logger';
|
||||
|
||||
export let logger: NGXLogger;
|
||||
|
||||
export const initLogger = (newLogger: NGXLogger) => logger = newLogger;
|
||||
@@ -21,6 +21,7 @@ import {NGXLogger} from 'ngx-logger';
|
||||
import {ConfigProvider} from './modules/config/config.provider';
|
||||
import {SettingsProvider} from './modules/settings/settings.provider';
|
||||
|
||||
import {initLogger} from './_helpers/ts-logger';
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@@ -60,6 +61,7 @@ export class AppComponent {
|
||||
private readonly settingsProvider: SettingsProvider,
|
||||
private readonly configProvider: ConfigProvider,
|
||||
private readonly logger: NGXLogger) {
|
||||
initLogger(logger);
|
||||
this.initializeApp();
|
||||
|
||||
// this language will be used as a fallback when a translation isn't found in the current language
|
||||
|
||||
@@ -16,10 +16,12 @@ import {NgModule} from '@angular/core';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {DataDetailComponent} from './detail/data-detail.component';
|
||||
import {DataListComponent} from './list/data-list.component';
|
||||
import {FoodDataListComponent} from './list/food-data-list.component';
|
||||
|
||||
const dataRoutes: Routes = [
|
||||
{path: 'search', component: DataListComponent},
|
||||
{path: 'data-detail/:uid', component: DataDetailComponent},
|
||||
{path: 'canteen', component: FoodDataListComponent},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,9 +35,11 @@ import {OriginDetailComponent} from './elements/origin-detail.component';
|
||||
import {OriginInListComponent} from './elements/origin-in-list.component';
|
||||
import {SimpleCardComponent} from './elements/simple-card.component';
|
||||
import {SkeletonListItem} from './elements/skeleton-list-item.component';
|
||||
import {SkeletonSegment} from './elements/skeleton-segment-button.component';
|
||||
import {SkeletonSimpleCard} from './elements/skeleton-simple-card.component';
|
||||
import {DataListItem} from './list/data-list-item.component';
|
||||
import {DataListComponent} from './list/data-list.component';
|
||||
import {FoodDataListComponent} from './list/food-data-list.component';
|
||||
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
||||
import {ArticleDetailContentComponent} from './types/article/article-detail-content.component';
|
||||
import {ArticleListItem} from './types/article/article-list-item.component';
|
||||
@@ -59,6 +61,7 @@ import {PersonDetailContentComponent} from './types/person/person-detail-content
|
||||
import {PersonListItem} from './types/person/person-list-item.component';
|
||||
import {PlaceDetailContentComponent} from './types/place/place-detail-content.component';
|
||||
import {PlaceListItem} from './types/place/place-list-item.component';
|
||||
import {PlaceMensaDetailComponent} from './types/place/special/mensa/place-mensa-detail.component';
|
||||
import {SemesterDetailContentComponent} from './types/semester/semester-detail-content.component';
|
||||
import {SemesterListItem} from './types/semester/semester-list-item.component';
|
||||
import {VideoDetailContentComponent} from './types/video/video-detail-content.component';
|
||||
@@ -80,6 +83,7 @@ import {VideoListItem} from './types/video/video-list-item.component';
|
||||
CatalogListItem,
|
||||
DataDetailComponent,
|
||||
DataDetailContentComponent,
|
||||
FoodDataListComponent,
|
||||
DataListComponent,
|
||||
DataListItem,
|
||||
DateSeriesDetailContentComponent,
|
||||
@@ -101,9 +105,11 @@ import {VideoListItem} from './types/video/video-list-item.component';
|
||||
PersonListItem,
|
||||
PlaceDetailContentComponent,
|
||||
PlaceListItem,
|
||||
PlaceMensaDetailComponent,
|
||||
SemesterDetailContentComponent,
|
||||
SemesterListItem,
|
||||
SkeletonListItem,
|
||||
SkeletonSegment,
|
||||
VideoDetailContentComponent,
|
||||
VideoListItem,
|
||||
],
|
||||
|
||||
@@ -16,9 +16,9 @@ import {Injectable} from '@angular/core';
|
||||
import {Client} from '@openstapps/api/lib/client';
|
||||
import {SCSearchQuery, SCSearchResponse, SCThingOriginType, SCThings, SCThingType} from '@openstapps/core';
|
||||
import {SCSaveableThing} from '@openstapps/core';
|
||||
import {environment} from '../../../environments/environment';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
||||
import {environment} from './../../../environments/environment';
|
||||
|
||||
export enum DataScope {
|
||||
Local = 'local',
|
||||
|
||||
@@ -27,4 +27,5 @@ export class DataDetailContentComponent {
|
||||
* TODO
|
||||
*/
|
||||
@Input() item: SCThings;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* 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} from '@angular/core';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-skeleton-segment-button',
|
||||
templateUrl: 'skeleton-segment-button.html',
|
||||
})
|
||||
export class SkeletonSegment {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<ion-segment-button>
|
||||
<ion-skeleton-text animated style="width: 85%;"></ion-skeleton-text>
|
||||
</ion-segment-button>
|
||||
|
||||
@@ -22,9 +22,9 @@ import {
|
||||
SCThing,
|
||||
} from '@openstapps/core';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
import {Subject} from 'rxjs';
|
||||
import {Subject, Subscription} from 'rxjs';
|
||||
import {debounceTime, distinctUntilChanged} from 'rxjs/operators';
|
||||
import {MenuService} from '../../menu/menu.service';
|
||||
import {ContextMenuService} from '../../menu/context/context-menu.service';
|
||||
import {SettingsProvider} from '../../settings/settings.provider';
|
||||
import {DataProvider} from '../data.provider';
|
||||
|
||||
@@ -34,6 +34,7 @@ import {DataProvider} from '../data.provider';
|
||||
@Component({
|
||||
selector: 'stapps-data-list',
|
||||
templateUrl: 'data-list.html',
|
||||
providers: [ContextMenuService],
|
||||
})
|
||||
export class DataListComponent implements OnInit {
|
||||
|
||||
@@ -50,7 +51,7 @@ export class DataListComponent implements OnInit {
|
||||
/**
|
||||
* Container for queried things
|
||||
*/
|
||||
items: SCThing[];
|
||||
items: Promise<SCThing[]>;
|
||||
|
||||
/**
|
||||
* Page size of queries
|
||||
@@ -77,22 +78,27 @@ export class DataListComponent implements OnInit {
|
||||
*/
|
||||
sortQuery: SCSearchSort | undefined;
|
||||
|
||||
/**
|
||||
* Array of all subscriptions to Observables
|
||||
*/
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
/**
|
||||
*
|
||||
* @param alertController AlertController
|
||||
* @param dataProvider DataProvider
|
||||
* @param menuService MenuService
|
||||
* @param contextMenuService ContextMenuService
|
||||
* @param settingsProvider SettingsProvider
|
||||
* @param logger An angular logger
|
||||
*/
|
||||
constructor(
|
||||
private readonly alertController: AlertController,
|
||||
private readonly dataProvider: DataProvider,
|
||||
private readonly menuService: MenuService,
|
||||
private readonly settingsProvider: SettingsProvider,
|
||||
private readonly logger: NGXLogger,
|
||||
protected readonly alertController: AlertController,
|
||||
protected dataProvider: DataProvider,
|
||||
protected readonly contextMenuService: ContextMenuService,
|
||||
protected readonly settingsProvider: SettingsProvider,
|
||||
protected readonly logger: NGXLogger,
|
||||
) {
|
||||
this.queryTextChanged
|
||||
this.subscriptions.push(this.queryTextChanged
|
||||
.pipe(
|
||||
debounceTime(this.searchQueryDueTime),
|
||||
distinctUntilChanged())
|
||||
@@ -100,30 +106,32 @@ export class DataListComponent implements OnInit {
|
||||
this.from = 0;
|
||||
this.queryText = model;
|
||||
this.fetchAndUpdateItems();
|
||||
});
|
||||
}));
|
||||
|
||||
this.menuService.filterQueryChanged$.subscribe((query) => {
|
||||
this.subscriptions.push(this.contextMenuService.filterQueryChanged$.subscribe((query) => {
|
||||
this.filterQuery = query;
|
||||
this.from = 0;
|
||||
this.fetchAndUpdateItems();
|
||||
});
|
||||
this.menuService.sortQueryChanged$.subscribe((query) => {
|
||||
}));
|
||||
this.subscriptions.push(this.contextMenuService.sortQueryChanged$.subscribe((query) => {
|
||||
this.sortQuery = query;
|
||||
this.from = 0;
|
||||
this.fetchAndUpdateItems();
|
||||
});
|
||||
}));
|
||||
|
||||
this.fetchAndUpdateItems();
|
||||
|
||||
/**
|
||||
* Subscribe to 'settings.changed' events
|
||||
*/
|
||||
this.settingsProvider.settingsActionChanged$.subscribe(({type, payload}) => {
|
||||
this.subscriptions.push(this.settingsProvider.settingsActionChanged$.subscribe(({type, payload}) => {
|
||||
if (type === 'stapps.settings.changed') {
|
||||
const {category, name, value} = payload!;
|
||||
this.logger.log(`received event "settings.changed" with category:
|
||||
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,7 +139,7 @@ export class DataListComponent implements OnInit {
|
||||
*
|
||||
* @param append If true fetched data gets appended to existing, override otherwise (default false)
|
||||
*/
|
||||
private async fetchAndUpdateItems(append = false): Promise<void> {
|
||||
protected async fetchAndUpdateItems(append = false): Promise<void> {
|
||||
// build query search options
|
||||
const searchOptions: SCSearchQuery = {
|
||||
from: this.from,
|
||||
@@ -154,17 +162,17 @@ export class DataListComponent implements OnInit {
|
||||
}
|
||||
|
||||
return this.dataProvider.search(searchOptions)
|
||||
.then((result) => {
|
||||
.then(async (result) => {
|
||||
if (append) {
|
||||
let items = await this.items;
|
||||
// append results
|
||||
this.items = this.items.concat(result.data);
|
||||
items = items.concat(result.data);
|
||||
this.items = (async () => items)();
|
||||
} else {
|
||||
// override items with results
|
||||
this.items = result.data;
|
||||
}
|
||||
// update filter options if result contains facets
|
||||
if (typeof result.facets !== 'undefined') {
|
||||
this.updateContextFilter(result.facets);
|
||||
this.items = (async () => {
|
||||
this.updateContextFilter(result.facets);
|
||||
return result.data} )();
|
||||
}
|
||||
}, async (err) => {
|
||||
const alert: HTMLIonAlertElement = await this.alertController.create({
|
||||
@@ -188,11 +196,10 @@ export class DataListComponent implements OnInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises the sort context of menuService
|
||||
* Initialises the possible sort options in ContextMenuService
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// initialise sort option for context menu
|
||||
this.menuService.setContextSort({
|
||||
this.contextMenuService.setContextSort({
|
||||
name: 'sort',
|
||||
reversed: false,
|
||||
value: 'relevance',
|
||||
@@ -213,6 +220,15 @@ export class DataListComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from Observables
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
for (let sub of this.subscriptions) {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search event of search bar
|
||||
*/
|
||||
@@ -221,9 +237,9 @@ export class DataListComponent implements OnInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates context filter in menuService with facets
|
||||
* Updates the possible filter options in ContextMenuService with facets
|
||||
*/
|
||||
updateContextFilter(facets: SCFacet[]) {
|
||||
this.menuService.updateContextFilter(facets);
|
||||
this.contextMenuService.updateContextFilter(facets);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<ion-menu-button></ion-menu-button>
|
||||
</ion-buttons>
|
||||
<ion-buttons slot="end">
|
||||
<ion-menu-button menu="context">
|
||||
<ion-menu-button menu="context" auto-hide="false">
|
||||
<ion-icon name="options"></ion-icon>
|
||||
</ion-menu-button>
|
||||
</ion-buttons>
|
||||
@@ -17,13 +17,15 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content id="data-list">
|
||||
<ion-list *ngIf="items">
|
||||
<stapps-data-list-item [item]="item" *ngFor="let item of items"></stapps-data-list-item>
|
||||
</ion-list>
|
||||
<ion-list *ngIf="!items">
|
||||
<ion-content id="data-list">
|
||||
<div *ngIf="items | async as items; else loading">
|
||||
<ion-list>
|
||||
<stapps-data-list-item [item]="item" *ngFor="let item of items"></stapps-data-list-item>
|
||||
</ion-list>
|
||||
</div>
|
||||
<ng-template #loading>
|
||||
<stapps-skeleton-list-item *ngFor="let skeleton of [1, 2, 3, 4, 5]"></stapps-skeleton-list-item>
|
||||
</ion-list>
|
||||
</ng-template>
|
||||
<ion-infinite-scroll (ionInfinite)="loadMore($event)">
|
||||
<ion-infinite-scroll-content></ion-infinite-scroll-content>
|
||||
</ion-infinite-scroll>
|
||||
|
||||
126
src/app/modules/data/list/food-data-list.component.ts
Normal file
126
src/app/modules/data/list/food-data-list.component.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (C) 2018-2020 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} from '@angular/core';
|
||||
import {
|
||||
SCSearchQuery, SCThing,
|
||||
} from '@openstapps/core';
|
||||
import {DataListComponent} from './data-list.component';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-data-list',
|
||||
templateUrl: 'data-list.html',
|
||||
})
|
||||
export class FoodDataListComponent extends DataListComponent {
|
||||
/**
|
||||
* Fetches items with set query configuration
|
||||
*
|
||||
* @param append If true fetched data gets appended to existing, override otherwise (default false)
|
||||
*/
|
||||
protected async fetchAndUpdateItems(append = false): Promise<void> {
|
||||
try {
|
||||
// build query search options
|
||||
const searchOptions: SCSearchQuery = {
|
||||
from: this.from,
|
||||
size: this.pageSize,
|
||||
};
|
||||
|
||||
if (this.queryText && this.queryText.length > 0) {
|
||||
// add query string
|
||||
searchOptions.query = this.queryText;
|
||||
}
|
||||
|
||||
if (this.sortQuery) {
|
||||
// add query sorting
|
||||
searchOptions.sort = [this.sortQuery];
|
||||
}
|
||||
|
||||
searchOptions.filter = {
|
||||
arguments: {
|
||||
filters: [
|
||||
{
|
||||
arguments: {
|
||||
field: 'categories',
|
||||
value: 'canteen',
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
arguments: {
|
||||
field: 'categories',
|
||||
value: 'student canteen',
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
arguments: {
|
||||
field: 'categories',
|
||||
value: 'cafe',
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
arguments: {
|
||||
field: 'categories',
|
||||
value: 'restaurant',
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
operation: 'or',
|
||||
},
|
||||
type: 'boolean',
|
||||
};
|
||||
|
||||
if (this.filterQuery !== undefined) {
|
||||
searchOptions.filter = {
|
||||
arguments: {
|
||||
filters: [
|
||||
searchOptions.filter,
|
||||
this.filterQuery,
|
||||
],
|
||||
operation: 'and',
|
||||
},
|
||||
type: 'boolean',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await this.dataProvider.search(searchOptions);
|
||||
|
||||
this.items = (async () => {
|
||||
this.updateContextFilter(result.facets);
|
||||
let items: SCThing[];
|
||||
if(append) {
|
||||
items = (await this.items).concat(result.data);
|
||||
} else {
|
||||
items = result.data;
|
||||
}
|
||||
|
||||
return items;
|
||||
})();
|
||||
} catch (err) {
|
||||
const alert: HTMLIonAlertElement = await this.alertController.create({
|
||||
buttons: ['Dismiss'],
|
||||
header: 'Error',
|
||||
subHeader: err.message,
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,12 +14,14 @@
|
||||
*/
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {SCBuilding, SCFloor, SCPointOfInterest, SCRoom, SCThing, SCTranslations} from '@openstapps/core';
|
||||
import {SCThingTranslator} from '@openstapps/core';
|
||||
import {SCThings, SCThingTranslator} from '@openstapps/core';
|
||||
import {DataProvider} from '../../data.provider';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@Component({
|
||||
providers: [ DataProvider ],
|
||||
selector: 'stapps-place-detail-content',
|
||||
templateUrl: 'place-detail-content.html',
|
||||
})
|
||||
@@ -47,4 +49,26 @@ export class PlaceDetailContentComponent {
|
||||
constructor() {
|
||||
this.translator = new SCThingTranslator(this.language);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param item TODO
|
||||
*/
|
||||
// tslint:disable-next-line:completed-docs prefer-function-over-method
|
||||
hasCategories(item: SCThings): item is SCThings & { categories: string[]; } {
|
||||
// tslint:disable-next-line:completed-docs
|
||||
return typeof (item as { categories: string[]; }).categories !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function as 'typeof' is not accessible in HTML
|
||||
*
|
||||
* @param item TODO
|
||||
*/
|
||||
isMensaThing(item: SCThings): boolean {
|
||||
return this.hasCategories(item) &&
|
||||
((item.categories as string[]).includes('canteen') || (item.categories as string[]).includes('cafe')
|
||||
|| (item.categories as string[]).includes('student canteen') || (item.categories as string[]).includes('restaurant'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<stapps-place-mensa-detail-content [item]="item" [language]="language" *ngIf="isMensaThing(item)"></stapps-place-mensa-detail-content>
|
||||
<ng-container *ngIf="item.type !== 'floor'">
|
||||
<stapps-simple-card *ngIf="item.type !== 'floor' && item.categories" [title]="'Categories'" [content]="translator.translate(item).categories()"></stapps-simple-card>
|
||||
<stapps-address-detail *ngIf="item.type !== 'floor' && item.address" [address]="item.address"></stapps-address-detail>
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* 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 moment, {Moment} from 'moment';
|
||||
|
||||
import {AfterViewInit, ChangeDetectorRef, Component, Input} from '@angular/core';
|
||||
import {SCDish, SCPlace, SCThing, SCThingTranslator, SCTranslations} from '@openstapps/core';
|
||||
import {PlaceMensaService} from './place-mensa-service';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@Component({
|
||||
providers: [PlaceMensaService],
|
||||
selector: 'stapps-place-mensa-detail-content',
|
||||
templateUrl: 'place-mensa.html',
|
||||
})
|
||||
export class PlaceMensaDetailComponent implements AfterViewInit {
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
dishes: Promise<Record<string, SCDish[]>> | null = null;
|
||||
|
||||
/**
|
||||
* number of days to display mensa menus for
|
||||
*/
|
||||
displayRange = 5;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@Input() item: SCPlace;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@Input() language: keyof SCTranslations<SCThing>;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
selectedDay: string;
|
||||
|
||||
/**
|
||||
* First day to display menu items for
|
||||
*/
|
||||
startingDay: Moment;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
translator: SCThingTranslator;
|
||||
|
||||
|
||||
constructor(private mensaService: PlaceMensaService, private changeDectectorRef: ChangeDetectorRef) {
|
||||
// TODO: translation
|
||||
this.translator = new SCThingTranslator(this.language);
|
||||
this.startingDay = moment();
|
||||
this.startingDay.hour(0);
|
||||
this.startingDay.minute(0);
|
||||
this.startingDay.millisecond(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
ngAfterViewInit() {
|
||||
this.dishes = this.mensaService.getAllDishes(this.item)
|
||||
.then((dishesResult) => {
|
||||
const out = this.splitDishes(dishesResult, this.item, this.startingDay, this.displayRange);
|
||||
for (const key in out) {
|
||||
if (out.hasOwnProperty(key)) {
|
||||
this.selectedDay = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.changeDectectorRef.detectChanges();
|
||||
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a list of dishes with availability start and end into multiple lists by day
|
||||
*/
|
||||
// tslint:disable-next-line:prefer-function-over-method
|
||||
splitDishes(dishes: SCDish[], place: SCPlace, startingDay: Moment, displayRange: number): Record<string, SCDish[]> {
|
||||
const out: Record<string, SCDish[]> = {};
|
||||
|
||||
for (let i = 0; i < displayRange; i++) {
|
||||
const nextDay: Moment = moment(startingDay)
|
||||
.add(1, 'days');
|
||||
const selectedDishes: SCDish[] = [];
|
||||
for (const dish of dishes) {
|
||||
// go through all offers
|
||||
if (dish.offers === undefined) { continue; }
|
||||
for (const offer of dish.offers) {
|
||||
if (offer.inPlace === undefined || offer.inPlace.uid !== place.uid) { continue; }
|
||||
// get availabilities
|
||||
const availabilityStarts = offer.availabilityStarts === undefined ? undefined : moment(offer.availabilityStarts);
|
||||
const availabilityEnds = offer.availabilityEnds === undefined ? undefined : moment(offer.availabilityEnds);
|
||||
|
||||
if ((availabilityStarts === undefined || availabilityStarts.isBefore(nextDay))
|
||||
&& (availabilityEnds === undefined || availabilityEnds.isAfter(startingDay))) {
|
||||
selectedDishes.push(dish);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectedDishes.length) {
|
||||
out[startingDay.format('YYYY-MM-DD')] = selectedDishes;
|
||||
}
|
||||
// tslint:disable-next-line:no-parameter-reassignment
|
||||
startingDay = nextDay;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* 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 {SCDish, SCPlace, SCThingType} from '@openstapps/core';
|
||||
import {DataProvider} from '../../../../data.provider';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PlaceMensaService {
|
||||
constructor(private dataProvider: DataProvider) { }
|
||||
|
||||
/**
|
||||
* Fetches all dishes for this building
|
||||
*/
|
||||
async getAllDishes(place: SCPlace): Promise<SCDish[]> {
|
||||
// use filter to get all dishes with the building's uid in one of the offer's inPlace field
|
||||
// TODO: make sure this actually works with ES
|
||||
const result = await this.dataProvider.search({
|
||||
filter: {
|
||||
arguments: {
|
||||
filters: [
|
||||
{
|
||||
arguments: {
|
||||
field: 'offers.inPlace.uid',
|
||||
value: place.uid,
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
{
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: SCThingType.Dish,
|
||||
},
|
||||
type: 'value',
|
||||
},
|
||||
],
|
||||
operation: 'and',
|
||||
},
|
||||
type: 'boolean',
|
||||
},
|
||||
size: 1000,
|
||||
});
|
||||
|
||||
return result.data as SCDish[];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<ng-container>
|
||||
<div *ngIf="dishes | async as dishes; else loading">
|
||||
<ion-segment [(ngModel)]="selectedDay">
|
||||
<ion-segment-button *ngFor="let day of dishes | keyvalue" [value]="day.key">
|
||||
<ion-label>{{day.key | amParse:'YYYY-MM-DD' | amDateFormat:'dddd, L'}}</ion-label>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<div [ngSwitch]="selectedDay">
|
||||
<div *ngFor="let day of dishes | keyvalue">
|
||||
<ion-list *ngSwitchCase="day.key">
|
||||
<stapps-data-list-item [item]="dish" *ngFor="let dish of day.value"></stapps-data-list-item>
|
||||
</ion-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #loading>
|
||||
<ion-segment>
|
||||
<stapps-skeleton-segment-button *ngFor="let skeleton of [1, 2, 3]"></stapps-skeleton-segment-button>
|
||||
</ion-segment>
|
||||
<ion-list>
|
||||
<stapps-skeleton-list-item *ngFor="let skeleton of [1, 2]"></stapps-skeleton-list-item>
|
||||
</ion-list>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@@ -21,7 +21,7 @@ import {TranslateModule,} from '@ngx-translate/core';
|
||||
import {SCFacet, SCThingType} from '@openstapps/core';
|
||||
import {ContextMenuComponent} from './context-menu.component';
|
||||
import {SettingsModule} from '../../settings/settings.module';
|
||||
import {MenuService} from '../menu.service';
|
||||
import {ContextMenuService} from '../context/context-menu.service';
|
||||
import {FilterContext, SortContext} from './context-type';
|
||||
|
||||
describe('ContextMenuComponent', async () => {
|
||||
@@ -36,7 +36,7 @@ describe('ContextMenuComponent', async () => {
|
||||
ChildrenOutletContexts,
|
||||
Location,
|
||||
UrlSerializer,
|
||||
MenuService,
|
||||
ContextMenuService,
|
||||
{provide: LocationStrategy, useClass: PathLocationStrategy},
|
||||
{provide: APP_BASE_HREF, useValue: '/'},
|
||||
],
|
||||
|
||||
@@ -20,8 +20,9 @@ import {
|
||||
SCThingType,
|
||||
SCTranslations,
|
||||
} from '@openstapps/core';
|
||||
import {MenuService} from '../menu.service';
|
||||
import {ContextMenuService} from './context-menu.service';
|
||||
import {FilterContext, SortContext, SortContextOption} from './context-type';
|
||||
import {Subscription} from 'rxjs';
|
||||
|
||||
/**
|
||||
* The context menu
|
||||
@@ -68,30 +69,45 @@ export class ContextMenuComponent {
|
||||
*/
|
||||
translator: SCThingTranslator;
|
||||
|
||||
/**
|
||||
* Array of all Subscriptions
|
||||
*/
|
||||
subscriptions: Subscription[] = [];
|
||||
|
||||
|
||||
constructor(private translateService: TranslateService,
|
||||
private readonly menuService: MenuService) {
|
||||
private readonly contextMenuService: ContextMenuService) {
|
||||
this.language = this.translateService.currentLang as keyof SCTranslations<SCLanguage>;
|
||||
this.translator = new SCThingTranslator(this.language);
|
||||
|
||||
this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
|
||||
this.subscriptions.push(this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
|
||||
this.language = event.lang as keyof SCTranslations<SCLanguage>;
|
||||
this.translator = new SCThingTranslator(this.language);
|
||||
});
|
||||
}));
|
||||
|
||||
this.menuService.filterContextChanged$.subscribe((filterContext) => {
|
||||
this.subscriptions.push(this.contextMenuService.filterContextChanged$.subscribe((filterContext) => {
|
||||
this.filterOption = filterContext;
|
||||
});
|
||||
}));
|
||||
|
||||
this.menuService.sortOptions.subscribe((sortContext) => {
|
||||
this.subscriptions.push(this.contextMenuService.sortOptions.subscribe((sortContext) => {
|
||||
this.sortOption = sortContext;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from Observables
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
for (let sub of this.subscriptions) {
|
||||
sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets selected filter options and updates listener
|
||||
*/
|
||||
filterChanged = () => {
|
||||
this.menuService.contextFilterChanged(this.filterOption);
|
||||
this.contextMenuService.contextFilterChanged(this.filterOption);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +133,7 @@ export class ContextMenuComponent {
|
||||
option.options.forEach((filterFacet) => filterFacet.buckets.forEach((filterBucket) => {
|
||||
filterBucket.checked = false;
|
||||
}));
|
||||
this.menuService.contextFilterChanged(this.filterOption);
|
||||
this.contextMenuService.contextFilterChanged(this.filterOption);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,6 +150,6 @@ export class ContextMenuComponent {
|
||||
option.reversed = false;
|
||||
}
|
||||
}
|
||||
this.menuService.contextSortChanged(option);
|
||||
this.contextMenuService.contextSortChanged(option);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {MenuService} from './menu.service';
|
||||
import {ContextMenuService} from './context-menu.service';
|
||||
import {SCFacet} from '@openstapps/core';
|
||||
import {FilterContext, SortContext} from './context/context-type';
|
||||
import {FilterContext, SortContext} from './context-type';
|
||||
|
||||
describe('MenuService', () => {
|
||||
let service: MenuService;
|
||||
describe('ContextMenuService', () => {
|
||||
let service: ContextMenuService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [MenuService]
|
||||
providers: [ContextMenuService]
|
||||
});
|
||||
service = TestBed.get(MenuService);
|
||||
service = TestBed.get(ContextMenuService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
@@ -15,13 +15,13 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {SCFacet, SCFacetBucket, SCSearchFilter, SCSearchSort} from '@openstapps/core';
|
||||
import {Subject} from 'rxjs';
|
||||
import {FilterBucket, FilterContext, FilterFacet, SortContext} from './context/context-type';
|
||||
import {FilterBucket, FilterContext, FilterFacet, SortContext} from './context-type';
|
||||
|
||||
/**
|
||||
* MenuService provides bidirectional communication of context menu options and search queries
|
||||
* ContextMenuService provides bidirectional communication of context menu options and search queries
|
||||
*/
|
||||
@Injectable()
|
||||
export class MenuService {
|
||||
export class ContextMenuService {
|
||||
|
||||
/**
|
||||
* Local filter context object
|
||||
@@ -82,9 +82,10 @@ export class MenuService {
|
||||
const filters: SCSearchFilter[] = [];
|
||||
|
||||
filterContext.options.forEach((filterFacet) => {
|
||||
const optionFilters: SCSearchFilter[] = [];
|
||||
filterFacet.buckets.forEach((filterBucket) => {
|
||||
if (filterBucket.checked) {
|
||||
filters.push(
|
||||
optionFilters.push(
|
||||
{
|
||||
arguments: {
|
||||
field: filterFacet.field,
|
||||
@@ -94,13 +95,22 @@ export class MenuService {
|
||||
});
|
||||
}
|
||||
});
|
||||
if (optionFilters.length > 0) {
|
||||
filters.push({
|
||||
arguments: {
|
||||
filters: optionFilters,
|
||||
operation: 'or',
|
||||
},
|
||||
type: 'boolean',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (filters.length > 0) {
|
||||
return {
|
||||
arguments: {
|
||||
filters: filters,
|
||||
operation: 'or',
|
||||
operation: 'and',
|
||||
},
|
||||
type: 'boolean',
|
||||
};
|
||||
@@ -20,7 +20,7 @@ import {IonicModule} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {SettingsModule} from '../settings/settings.module';
|
||||
import {ContextMenuComponent} from './context/context-menu.component';
|
||||
import {MenuService} from './menu.service';
|
||||
import {ContextMenuService} from './context/context-menu.service';
|
||||
import {NavigationComponent} from './navigation/navigation.component';
|
||||
|
||||
/**
|
||||
@@ -44,7 +44,7 @@ import {NavigationComponent} from './navigation/navigation.component';
|
||||
SettingsModule,
|
||||
],
|
||||
providers: [
|
||||
MenuService,
|
||||
ContextMenuService,
|
||||
],
|
||||
})
|
||||
export class MenuModule {}
|
||||
|
||||
Reference in New Issue
Block a user