refactor: replace TSLint with ESLint

This commit is contained in:
Wieland Schöbl
2021-06-30 13:53:44 +02:00
committed by Jovan Krunić
parent 67fb4a43c9
commit d696215d08
147 changed files with 5471 additions and 2704 deletions

View File

@@ -21,12 +21,7 @@ import {ConfigProvider} from './config.provider';
* TODO
*/
@NgModule({
imports: [
StorageModule,
DataModule,
],
providers: [
ConfigProvider,
],
imports: [StorageModule, DataModule],
providers: [ConfigProvider],
})
export class ConfigModule {}

View File

@@ -13,12 +13,22 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {TestBed} from '@angular/core/testing';
import {SCIndexResponse, SCThingOriginType, SCThingType, SCSettingInputType} from '@openstapps/core';
import {
SCIndexResponse,
SCThingOriginType,
SCThingType,
SCSettingInputType,
} from '@openstapps/core';
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
import {StorageProvider} from '../storage/storage.provider';
import {ConfigProvider, STORAGE_KEY_CONFIG} from './config.provider';
import {ConfigFetchError, ConfigInitError, SavedConfigNotAvailable, WrongConfigVersionInStorage,} from './errors';
import {NGXLogger} from "ngx-logger";
import {
ConfigFetchError,
ConfigInitError,
SavedConfigNotAvailable,
WrongConfigVersionInStorage,
} from './errors';
import {NGXLogger} from 'ngx-logger';
import {dependencies} from '../../../../package.json';
describe('ConfigProvider', () => {
@@ -26,22 +36,35 @@ describe('ConfigProvider', () => {
let storageProviderSpy: jasmine.SpyObj<StorageProvider>;
beforeEach(() => {
const storageProviderMethodSpy = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put']);
const webHttpClientMethodSpy = jasmine.createSpyObj('StAppsWebHttpClient', ['request']);
const ngxLogger: jasmine.SpyObj<NGXLogger> = jasmine.createSpyObj('NGXLogger', ['log', 'error', 'warn']);
const storageProviderMethodSpy = jasmine.createSpyObj('StorageProvider', [
'init',
'get',
'has',
'put',
]);
const webHttpClientMethodSpy = jasmine.createSpyObj('StAppsWebHttpClient', [
'request',
]);
const ngxLogger: jasmine.SpyObj<NGXLogger> = jasmine.createSpyObj(
'NGXLogger',
['log', 'error', 'warn'],
);
TestBed.configureTestingModule({
imports: [],
providers: [
ConfigProvider,
{
provide: StorageProvider, useValue: storageProviderMethodSpy,
provide: StorageProvider,
useValue: storageProviderMethodSpy,
},
{
provide: StAppsWebHttpClient, useValue: webHttpClientMethodSpy,
provide: StAppsWebHttpClient,
useValue: webHttpClientMethodSpy,
},
{
provide: NGXLogger, useValue: ngxLogger,
provide: NGXLogger,
useValue: ngxLogger,
},
],
});
@@ -51,25 +74,30 @@ describe('ConfigProvider', () => {
});
it('should fetch app configuration', async () => {
spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse));
spyOn(configProvider.client, 'handshake').and.returnValue(
Promise.resolve(sampleIndexResponse),
);
const result = await configProvider.fetch();
expect(result).toEqual(sampleIndexResponse);
});
it('should throw error on fetch with error response', async () => {
spyOn(configProvider.client, 'handshake').and.throwError('');
// eslint-disable-next-line unicorn/error-message
let error = new Error('');
try {
await configProvider.fetch();
} catch (err) {
error = err;
} catch (error_) {
error = error_;
}
expect(error).toEqual(new ConfigFetchError());
});
it('should init from remote and saved config not available', async () => {
storageProviderSpy.has.and.returnValue(Promise.resolve(false));
spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse));
spyOn(configProvider.client, 'handshake').and.returnValue(
Promise.resolve(sampleIndexResponse),
);
try {
await configProvider.init();
} catch (error) {
@@ -79,34 +107,42 @@ describe('ConfigProvider', () => {
expect(storageProviderSpy.get).toHaveBeenCalledTimes(0);
expect(configProvider.client.handshake).toHaveBeenCalled();
expect(configProvider.initialised).toBe(true);
expect(await configProvider.getValue('name')).toEqual(sampleIndexResponse.app.name);
expect(await configProvider.getValue('name')).toEqual(
sampleIndexResponse.app.name,
);
});
it('should init from storage when remote fails', async () => {
storageProviderSpy.has.and.returnValue(Promise.resolve(true));
storageProviderSpy.get.and.returnValue(Promise.resolve(sampleIndexResponse));
storageProviderSpy.get.and.returnValue(
Promise.resolve(sampleIndexResponse),
);
spyOn(configProvider.client, 'handshake').and.throwError('');
// eslint-disable-next-line unicorn/error-message
let error = new Error('');
try {
await configProvider.init();
} catch (err) {
error = err;
} catch (error_) {
error = error_;
}
expect(error).toEqual(new ConfigFetchError());
expect(storageProviderSpy.has).toHaveBeenCalled();
expect(storageProviderSpy.get).toHaveBeenCalled();
expect(configProvider.initialised).toBe(true);
expect(await configProvider.getValue('name')).toEqual(sampleIndexResponse.app.name);
expect(await configProvider.getValue('name')).toEqual(
sampleIndexResponse.app.name,
);
});
it('should throw error on failed initialisation', async () => {
storageProviderSpy.has.and.returnValue(Promise.resolve(false));
spyOn(configProvider.client, 'handshake').and.throwError('');
// eslint-disable-next-line unicorn/no-null
let error = null;
try {
await configProvider.init();
} catch (err) {
error = err;
} catch (error_) {
error = error_;
}
expect(error).toEqual(new ConfigInitError());
});
@@ -116,30 +152,37 @@ describe('ConfigProvider', () => {
const wrongConfig = JSON.parse(JSON.stringify(sampleIndexResponse));
wrongConfig.backend.SCVersion = '0.1.0';
storageProviderSpy.get.and.returnValue(wrongConfig);
spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse));
spyOn(configProvider.client, 'handshake').and.returnValue(
Promise.resolve(sampleIndexResponse),
);
// eslint-disable-next-line unicorn/no-null
let error = null;
try {
await configProvider.init();
} catch (err) {
error = err;
} catch (error_) {
error = error_;
}
expect(error).toEqual(new WrongConfigVersionInStorage(scVersion, '0.1.0'));
});
it('should throw error on saved app configuration not available', async () => {
storageProviderSpy.has.and.returnValue(Promise.resolve(false));
// eslint-disable-next-line unicorn/error-message
let error = new Error('');
try {
await configProvider.loadLocal();
} catch (err) {
error = err;
} catch (error_) {
error = error_;
}
expect(error).toEqual(new SavedConfigNotAvailable());
});
it('should save app configuration', async () => {
await configProvider.save(sampleIndexResponse);
expect(storageProviderSpy.put).toHaveBeenCalledWith(STORAGE_KEY_CONFIG, sampleIndexResponse);
expect(storageProviderSpy.put).toHaveBeenCalledWith(
STORAGE_KEY_CONFIG,
sampleIndexResponse,
);
});
it('should set app configuration', async () => {
@@ -149,21 +192,31 @@ describe('ConfigProvider', () => {
it('should return app configuration value', async () => {
storageProviderSpy.has.and.returnValue(Promise.resolve(true));
storageProviderSpy.get.and.returnValue(Promise.resolve(sampleIndexResponse));
spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse));
storageProviderSpy.get.and.returnValue(
Promise.resolve(sampleIndexResponse),
);
spyOn(configProvider.client, 'handshake').and.returnValue(
Promise.resolve(sampleIndexResponse),
);
await configProvider.init();
expect(await configProvider.getValue('name')).toEqual(sampleIndexResponse.app.name);
expect(await configProvider.getValue('name')).toEqual(
sampleIndexResponse.app.name,
);
});
it('should return app configuration value if only saved config is available and fetch fails', async () => {
storageProviderSpy.has.and.returnValue(Promise.resolve(true));
storageProviderSpy.get.and.returnValue(Promise.resolve(sampleIndexResponse));
storageProviderSpy.get.and.returnValue(
Promise.resolve(sampleIndexResponse),
);
spyOn(configProvider.client, 'handshake').and.throwError('');
expect(await configProvider.getValue('name')).toEqual(sampleIndexResponse.app.name);
expect(await configProvider.getValue('name')).toEqual(
sampleIndexResponse.app.name,
);
});
});
const scVersion = dependencies["@openstapps/core"];
const scVersion = dependencies['@openstapps/core'];
const sampleIndexResponse: SCIndexResponse = {
app: {
@@ -192,7 +245,6 @@ const sampleIndexResponse: SCIndexResponse = {
},
},
},
],
name: 'main',
translations: {
@@ -235,11 +287,7 @@ const sampleIndexResponse: SCIndexResponse = {
backend: {
SCVersion: scVersion,
externalRequestTimeout: 5000,
hiddenTypes: [
SCThingType.DateSeries,
SCThingType.Diff,
SCThingType.Floor,
],
hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
mappingIgnoredTags: [],
maxMultiSearchRouteQueries: 5,
maxRequestBodySize: 512 * 1024,
@@ -299,9 +347,7 @@ const sampleIndexResponse: SCIndexResponse = {
},
{
fieldName: 'offers',
onlyOnTypes: [
SCThingType.Dish,
],
onlyOnTypes: [SCThingType.Dish],
sortTypes: ['price'],
},
],

View File

@@ -31,7 +31,7 @@ import {
/**
* Key to store config in storage module
*
* @TODO: Issue #41 centralise storage keys
* TODO: Issue #41 centralise storage keys
*/
export const STORAGE_KEY_CONFIG = 'stapps.config';
@@ -44,18 +44,22 @@ export class ConfigProvider {
* Api client
*/
client: Client;
/**
* App configuration as IndexResponse
*/
config: SCIndexResponse;
/**
* First session indicator
*/
firstSession = true;
/**
* Initialised status flag of config provider
*/
initialised = false;
/**
* Version of the @openstapps/core package that app is using
*/
@@ -68,10 +72,16 @@ export class ConfigProvider {
* @param swHttpClient Api client
* @param logger An angular logger
*/
constructor(private readonly storageProvider: StorageProvider,
swHttpClient: StAppsWebHttpClient,
private readonly logger: NGXLogger) {
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
constructor(
private readonly storageProvider: StorageProvider,
swHttpClient: StAppsWebHttpClient,
private readonly logger: NGXLogger,
) {
this.client = new Client(
swHttpClient,
environment.backend_url,
environment.backend_version,
);
}
/**
@@ -80,7 +90,7 @@ export class ConfigProvider {
async fetch(): Promise<SCIndexResponse> {
try {
return await this.client.handshake(this.scVersion);
} catch (error) {
} catch {
throw new ConfigFetchError();
}
}
@@ -124,7 +134,10 @@ export class ConfigProvider {
this.initialised = true;
this.logger.log(`initialised configuration from storage`);
if (this.config.backend.SCVersion !== this.scVersion) {
loadError = new WrongConfigVersionInStorage(this.scVersion, this.config.backend.SCVersion);
loadError = new WrongConfigVersionInStorage(
this.scVersion,
this.config.backend.SCVersion,
);
this.logger.warn(loadError);
}
} catch (error) {

View File

@@ -13,7 +13,7 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {AppError} from './../../_helpers/errors';
import {AppError} from '../../_helpers/errors';
/**
* Error that is thrown when fetching from backend fails
@@ -38,7 +38,10 @@ export class ConfigInitError extends AppError {
*/
export class ConfigValueNotAvailable extends AppError {
constructor(valueKey: string) {
super('ConfigValueNotAvailable', `No attribute "${valueKey}" in config available!`);
super(
'ConfigValueNotAvailable',
`No attribute "${valueKey}" in config available!`,
);
}
}
@@ -56,7 +59,10 @@ export class SavedConfigNotAvailable extends AppError {
*/
export class WrongConfigVersionInStorage extends AppError {
constructor(correctVersion: string, savedVersion: string) {
super('WrongConfigVersionInStorage', `The saved configs backend version ${savedVersion} ` +
`does not equal the configured backend version ${correctVersion} of the app.`);
super(
'WrongConfigVersionInStorage',
`The saved configs backend version ${savedVersion} ` +
`does not equal the configured backend version ${correctVersion} of the app.`,
);
}
}

View File

@@ -28,9 +28,11 @@ export class ActionChipListComponent {
* If chips are applicable
*/
applicable: Record<string, () => boolean> = {
'locate': () => this.item.hasOwnProperty('inPlace'),
'event': () => this.item.type === SCThingType.AcademicEvent ||
(this.item.type === SCThingType.DateSeries && (this.item as SCDateSeries).dates.length !== 0),
locate: () => this.item.hasOwnProperty('inPlace'),
event: () =>
this.item.type === SCThingType.AcademicEvent ||
(this.item.type === SCThingType.DateSeries &&
(this.item as SCDateSeries).dates.length > 0),
};
/**

View File

@@ -1,4 +1,10 @@
<div>
<stapps-locate-action-chip *ngIf='applicable["locate"]()' [item]='item'></stapps-locate-action-chip>
<stapps-add-event-action-chip *ngIf='applicable["event"]()' [item]='item'></stapps-add-event-action-chip>
<stapps-locate-action-chip
*ngIf="applicable['locate']()"
[item]="item"
></stapps-locate-action-chip>
<stapps-add-event-action-chip
*ngIf="applicable['event']()"
[item]="item"
></stapps-add-event-action-chip>
</div>

View File

@@ -28,9 +28,8 @@ enum Selection {
*
* The generic is to preserve type safety of how deep the tree goes.
*/
// tslint:disable-next-line:no-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
class TreeNode<T extends TreeNode<any> | SelectionValue> {
/**
* Value of this node
*/
@@ -55,13 +54,25 @@ class TreeNode<T extends TreeNode<any> | SelectionValue> {
* Accumulate values of children to set current value
*/
private accumulateApplyValues() {
const selections: number[] =
this.children.map(it => it instanceof TreeNode ?
(it.checked ? Selection.ON : (it.indeterminate ? Selection.PARTIAL : Selection.OFF)) :
(it as SelectionValue).selected ? Selection.ON : Selection.OFF);
const selections: number[] = this.children.map(
it =>
/* eslint-disable unicorn/no-nested-ternary */
it instanceof TreeNode
? it.checked
? Selection.ON
: it.indeterminate
? Selection.PARTIAL
: Selection.OFF
: (it as SelectionValue).selected
? Selection.ON
: Selection.OFF,
/* eslint-enable unicorn/no-nested-ternary */
);
this.checked = every(selections, it => it === Selection.ON);
this.indeterminate = this.checked ? false : some(selections, it => it > Selection.OFF);
this.indeterminate = this.checked
? false
: some(selections, it => it > Selection.OFF);
}
/**
@@ -72,7 +83,7 @@ class TreeNode<T extends TreeNode<any> | SelectionValue> {
if (child instanceof TreeNode) {
child.checked = this.checked;
child.indeterminate = false;
// tslint:disable-next-line:no-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(child as TreeNode<any>).applyValueDownwards();
} else {
(child as SelectionValue).selected = this.checked;
@@ -159,18 +170,23 @@ export class AddEventPopoverComponent implements OnInit {
*/
selection: TreeNode<TreeNode<SelectionValue>>;
constructor(readonly ref: ChangeDetectorRef) {
}
constructor(readonly ref: ChangeDetectorRef) {}
/**
* Init
*/
ngOnInit() {
this.selection =
new TreeNode(values(groupBy(sortBy(this.items.map(item => ({
selected: false,
item: item,
})), it => it.item.frequency), it => it.item.frequency))
.map(item => new TreeNode(item, this.ref)), this.ref);
this.selection = new TreeNode(
values(
groupBy(
sortBy(
this.items.map(item => ({selected: false, item: item})),
it => it.item.frequency,
),
it => it.item.frequency,
),
).map(item => new TreeNode(item, this.ref)),
this.ref,
);
}
}

View File

@@ -1,39 +1,47 @@
<ion-card-content>
<ion-item-group>
<ion-item-divider (click)='selection.click()'>
<ion-label>{{'data.chips.add_events.popover.ALL' | translate}}</ion-label>
<ion-checkbox slot='start'
[checked]='selection.checked'
[indeterminate]='selection.indeterminate'>
<ion-item-divider (click)="selection.click()">
<ion-label>{{
'data.chips.add_events.popover.ALL' | translate
}}</ion-label>
<ion-checkbox
slot="start"
[checked]="selection.checked"
[indeterminate]="selection.indeterminate"
>
</ion-checkbox>
</ion-item-divider>
<ion-item-group *ngFor='let frequency of selection.children'>
<ion-item-divider (click)='frequency.click()'>
<ion-label>{{('frequency' | thingTranslate: frequency.children[0].item) | titlecase}}</ion-label>
<ion-checkbox slot='start'
[checked]='frequency.checked'
[indeterminate]='frequency.indeterminate'>
<ion-item-group *ngFor="let frequency of selection.children">
<ion-item-divider (click)="frequency.click()">
<ion-label>{{
'frequency' | thingTranslate: frequency.children[0].item | titlecase
}}</ion-label>
<ion-checkbox
slot="start"
[checked]="frequency.checked"
[indeterminate]="frequency.indeterminate"
>
</ion-checkbox>
</ion-item-divider>
<ion-item *ngFor='let date of frequency.children'
(click)='date.selected = !date.selected; frequency.notifyChildChanged()'>
<ion-label *ngIf='date.item.dates.length > 1; else single_event'>
{{date.item.duration | amDuration: 'hours'}}
{{'data.chips.add_events.popover.AT' | translate}}
{{date.item.dates[0] | amDateFormat: 'HH:mm ddd'}}
{{'data.chips.add_events.popover.UNTIL' | translate}}
{{last(date.item.dates) | amDateFormat: 'll'}}
<ion-item
*ngFor="let date of frequency.children"
(click)="date.selected = !date.selected; frequency.notifyChildChanged()"
>
<ion-label *ngIf="date.item.dates.length > 1; else single_event">
{{ date.item.duration | amDuration: 'hours' }}
{{ 'data.chips.add_events.popover.AT' | translate }}
{{ date.item.dates[0] | amDateFormat: 'HH:mm ddd' }}
{{ 'data.chips.add_events.popover.UNTIL' | translate }}
{{ last(date.item.dates) | amDateFormat: 'll' }}
</ion-label>
<ng-template #single_event>
<ion-label>
{{date.item.duration | amDuration: 'hours'}}
{{'data.chips.add_events.popover.AT' | translate}}
{{last(date.item.dates) | amDateFormat: 'll, HH:mm'}}
{{ date.item.duration | amDuration: 'hours' }}
{{ 'data.chips.add_events.popover.AT' | translate }}
{{ last(date.item.dates) | amDateFormat: 'll, HH:mm' }}
</ion-label>
</ng-template>
<ion-checkbox slot='start'
[checked]='date.selected'>
</ion-checkbox>
<ion-checkbox slot="start" [checked]="date.selected"> </ion-checkbox>
</ion-item>
</ion-item-group>
</ion-item-group>

View File

@@ -1,4 +1,4 @@
/* tslint:disable:prefer-function-over-method */
/* eslint-disable class-methods-use-this */
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
@@ -91,9 +91,10 @@ export class AddEventActionChipComponent implements OnInit {
},
};
constructor(readonly popoverController: PopoverController,
readonly dataProvider: DataProvider) {
}
constructor(
readonly popoverController: PopoverController,
readonly dataProvider: DataProvider,
) {}
/**
* Apply state
@@ -110,35 +111,42 @@ export class AddEventActionChipComponent implements OnInit {
* Init
*/
ngOnInit() {
this.associatedDateSeries = this.item.type === SCThingType.DateSeries ?
Promise.resolve([this.item as SCDateSeries]) :
this.dataProvider.search({
filter: {
arguments: {
filters: [
{
this.associatedDateSeries =
this.item.type === SCThingType.DateSeries
? Promise.resolve([this.item as SCDateSeries])
: this.dataProvider
.search({
filter: {
arguments: {
field: 'type',
value: SCThingType.DateSeries,
filters: [
{
arguments: {
field: 'type',
value: SCThingType.DateSeries,
},
type: 'value',
},
{
arguments: {
field: 'event.uid',
value: this.item.uid,
},
type: 'value',
},
],
operation: 'and',
},
type: 'value',
type: 'boolean',
},
{
arguments: {
field: 'event.uid',
value: this.item.uid,
},
type: 'value',
},
],
operation: 'and',
},
type: 'boolean',
},
})
.then((it) => it.data as SCDateSeries[]);
this.associatedDateSeries.then((it) => this.applyState(
it.length < 1 ? AddEventStates.UNAVAILABLE : AddEventStates.REMOVED_ALL));
})
.then(it => it.data as SCDateSeries[]);
this.associatedDateSeries.then(it =>
this.applyState(
it.length === 0
? AddEventStates.UNAVAILABLE
: AddEventStates.REMOVED_ALL,
),
);
}
/**
@@ -159,7 +167,10 @@ export class AddEventActionChipComponent implements OnInit {
await popover.present();
// TODO: replace dummy implementation
await popover.onDidDismiss();
this.applyState(this.state === AddEventStates.ADDED_ALL ?
AddEventStates.REMOVED_ALL : AddEventStates.ADDED_ALL);
this.applyState(
this.state === AddEventStates.ADDED_ALL
? AddEventStates.REMOVED_ALL
: AddEventStates.ADDED_ALL,
);
}
}

View File

@@ -1,11 +1,14 @@
<div *ngIf='(associatedDateSeries | async) as associatedDateSeries; else loading'>
<ion-chip [disabled]='disabled' (click)='$event.stopPropagation(); onClick($event)'>
<ion-icon [name]='icon'></ion-icon>
<ion-label>{{label | translate}}</ion-label>
<div *ngIf="associatedDateSeries | async as associatedDateSeries; else loading">
<ion-chip
[disabled]="disabled"
(click)="$event.stopPropagation(); onClick($event)"
>
<ion-icon [name]="icon"></ion-icon>
<ion-label>{{ label | translate }}</ion-label>
</ion-chip>
</div>
<ng-template #loading>
<ion-chip>
<ion-skeleton-text animated='true' ></ion-skeleton-text>
<ion-skeleton-text animated="true"></ion-skeleton-text>
</ion-chip>
</ng-template>

View File

@@ -1,4 +1,4 @@
/* tslint:disable:prefer-function-over-method */
/* eslint-disable class-methods-use-this */
/*
* Copyright (C) 2021 StApps
* This program is free software: you can redistribute it and/or modify it
@@ -36,4 +36,3 @@ export class LocateActionChipComponent {
// TODO
}
}

View File

@@ -1,7 +1,7 @@
<ion-chip class='chip-class' (click)='$event.stopPropagation(); onClick()'>
<ion-icon name='location'></ion-icon>
<ion-label>{{'Locate' | translate}}</ion-label>
<ion-chip class="chip-class" (click)="$event.stopPropagation(); onClick()">
<ion-icon name="location"></ion-icon>
<ion-label>{{ 'Locate' | translate }}</ion-label>
<ng-template #loading>
<ion-skeleton-text animated='true'></ion-skeleton-text>
<ion-skeleton-text animated="true"></ion-skeleton-text>
</ng-template>
</ion-chip>

View File

@@ -25,15 +25,26 @@ describe('DataProvider', () => {
let dataFacetsProvider: DataFacetsProvider;
const sampleFacets: SCFacet[] = [
{
buckets: [{key: 'education', count: 4}, {key: 'learn', count: 3}, {key: 'computer', count: 3}],
buckets: [
{key: 'education', count: 4},
{key: 'learn', count: 3},
{key: 'computer', count: 3},
],
field: 'categories',
},
{
buckets: [{key: 'Major One', count: 1}, {key: 'Major Two', count: 2}, {key: 'Major Three' , count: 1}],
buckets: [
{key: 'Major One', count: 1},
{key: 'Major Two', count: 2},
{key: 'Major Three', count: 1},
],
field: 'majors',
},
{
buckets: [{key: 'building', count: 3}, {key: 'room', count: 7}],
buckets: [
{key: 'building', count: 3},
{key: 'room', count: 7},
],
field: 'type',
},
];
@@ -51,16 +62,21 @@ describe('DataProvider', () => {
...sampleThingsMap['academic event'],
];
const sampleBuckets: SCFacetBucket[] = [{key: 'foo', count: 1}, {key: 'bar', count: 2}, {key: 'foo bar', count: 3}];
const sampleBucketsMap: {[key: string]: number} = {foo: 1, bar: 2, 'foo bar': 3};
const sampleBuckets: SCFacetBucket[] = [
{key: 'foo', count: 1},
{key: 'bar', count: 2},
{key: 'foo bar', count: 3},
];
const sampleBucketsMap: {[key: string]: number} = {
'foo': 1,
'bar': 2,
'foo bar': 3,
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [DataModule],
providers: [
DataProvider,
StAppsWebHttpClient,
],
providers: [DataProvider, StAppsWebHttpClient],
});
dataFacetsProvider = TestBed.get(DataFacetsProvider);
});
@@ -78,48 +94,76 @@ describe('DataProvider', () => {
});
it('should convert buckets to buckets map', () => {
expect(dataFacetsProvider.bucketsToMap(sampleBuckets)).toEqual(sampleBucketsMap);
expect(dataFacetsProvider.bucketsToMap(sampleBuckets)).toEqual(
sampleBucketsMap,
);
});
it('should convert buckets map into buckets', () => {
expect(dataFacetsProvider.mapToBuckets(sampleBucketsMap)).toEqual(sampleBuckets);
expect(dataFacetsProvider.mapToBuckets(sampleBucketsMap)).toEqual(
sampleBuckets,
);
});
it('should convert facets into a facets map', () => {
expect(dataFacetsProvider.facetsToMap(sampleFacets)).toEqual(sampleFacetsMap);
expect(dataFacetsProvider.facetsToMap(sampleFacets)).toEqual(
sampleFacetsMap,
);
});
it('should convert facets map into facets', () => {
expect(dataFacetsProvider.mapToFacets(sampleFacetsMap)).toEqual(sampleFacets);
expect(dataFacetsProvider.mapToFacets(sampleFacetsMap)).toEqual(
sampleFacets,
);
});
it('should extract facets (and append them if needed) from the data', () => {
const sampleCombinedFacets: SCFacet[] = [
{
buckets: [
{key: 'computer', count: 3}, {key: 'course', count: 1}, {key: 'education', count: 5},
{key: 'learn', count: 3}, {key: 'library', count: 1}, {key: 'practicum', count: 1},
],
{key: 'computer', count: 3},
{key: 'course', count: 1},
{key: 'education', count: 5},
{key: 'learn', count: 3},
{key: 'library', count: 1},
{key: 'practicum', count: 1},
],
field: 'categories',
},
{
buckets: [{key: 'Major One', count: 2}, {key: 'Major Two', count: 4}, {key: 'Major Three', count: 2}],
buckets: [
{key: 'Major One', count: 2},
{key: 'Major Two', count: 4},
{key: 'Major Three', count: 2},
],
field: 'majors',
},
{
buckets: [{key: 'building', count: 4}, {key: 'academic event', count: 2}, {key: 'person', count: 2}, {key: 'room', count: 8}],
buckets: [
{key: 'building', count: 4},
{key: 'academic event', count: 2},
{key: 'person', count: 2},
{key: 'room', count: 8},
],
field: 'type',
},
];
const checkEqual = (expected: SCFacet[], actual: SCFacet[]) => {
const expectedMap = dataFacetsProvider.facetsToMap(expected);
const actualMap = dataFacetsProvider.facetsToMap(actual);
Object.keys(actualMap).forEach((key) => {
Object.keys(actualMap[key]).forEach((subKey) => {
for (const key of Object.keys(actualMap)) {
for (const subKey of Object.keys(actualMap[key])) {
expect(actualMap[key][subKey]).toBe(expectedMap[key][subKey]);
});
});
}
}
};
checkEqual(dataFacetsProvider.extractFacets(sampleItems, sampleAggregations, sampleFacets), sampleCombinedFacets);
checkEqual(
dataFacetsProvider.extractFacets(
sampleItems,
sampleAggregations,
sampleFacets,
),
sampleCombinedFacets,
);
});
});

View File

@@ -13,16 +13,20 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Injectable} from '@angular/core';
import {SCBackendAggregationConfiguration, SCFacet, SCFacetBucket, SCThing} from '@openstapps/core';
import {
SCBackendAggregationConfiguration,
SCFacet,
SCFacetBucket,
SCThing,
} from '@openstapps/core';
/**
* TODO
*/
@Injectable()
export class DataFacetsProvider {
// tslint:disable-next-line:no-empty
constructor() {
}
// eslint-disable-next-line no-empty, @typescript-eslint/no-empty-function
constructor() {}
/**
* Adds buckets to a map of buckets (e.g. if a buckets array is [{foo: 1}, {bar: 3}],
@@ -32,15 +36,18 @@ export class DataFacetsProvider {
* @param bucketsMap Buckets array transformed into a map
* @param fields A field that should be added to buckets (its map)
*/
// tslint:disable-next-line:prefer-function-over-method
addBuckets(bucketsMap: {[key: string]: number; }, fields: string[]): {[key: string]: number; } {
fields.forEach((field) => {
// eslint-disable-next-line class-methods-use-this
addBuckets(
bucketsMap: {[key: string]: number},
fields: string[],
): {[key: string]: number} {
for (const field of fields) {
if (typeof bucketsMap[field] !== 'undefined') {
bucketsMap[field] = bucketsMap[field] + 1;
} else {
bucketsMap[field] = 1;
}
});
}
return bucketsMap;
}
@@ -50,12 +57,12 @@ export class DataFacetsProvider {
*
* @param buckets Buckets from a facet
*/
// tslint:disable-next-line:prefer-function-over-method
bucketsToMap(buckets: SCFacetBucket[]): {[key: string]: number; } {
const bucketsMap: {[key: string]: number; } = {};
buckets.forEach((bucket) => {
// eslint-disable-next-line class-methods-use-this
bucketsToMap(buckets: SCFacetBucket[]): {[key: string]: number} {
const bucketsMap: {[key: string]: number} = {};
for (const bucket of buckets) {
bucketsMap[bucket.key] = bucket.count;
});
}
return bucketsMap;
}
@@ -70,7 +77,8 @@ export class DataFacetsProvider {
extractFacets(
items: SCThing[],
aggregations: SCBackendAggregationConfiguration[],
facets: SCFacet[] = []): SCFacet[] {
facets: SCFacet[] = [],
): SCFacet[] {
if (items.length === 0) {
if (facets.length === 0) {
return [];
@@ -78,13 +86,16 @@ export class DataFacetsProvider {
return facets;
}
const combinedFacets: SCFacet[] = facets;
const combinedFacetsMap: {[key: string]: {[key: string]: number; }; } = this.facetsToMap(combinedFacets);
items.forEach((item) => {
aggregations.forEach((aggregation) => {
let fieldValues = item[aggregation.fieldName as keyof SCThing] as string | string[] | undefined;
const combinedFacetsMap: {[key: string]: {[key: string]: number}} =
this.facetsToMap(facets);
for (const item of items) {
for (const aggregation of aggregations) {
let fieldValues = item[aggregation.fieldName as keyof SCThing] as
| string
| string[]
| undefined;
if (typeof fieldValues === 'undefined') {
return;
continue;
}
if (typeof fieldValues === 'string') {
fieldValues = [fieldValues];
@@ -94,14 +105,14 @@ export class DataFacetsProvider {
combinedFacetsMap[aggregation.fieldName] || {},
fieldValues,
);
} else if (aggregation.onlyOnTypes.indexOf(item.type) !== -1) {
} else if (aggregation.onlyOnTypes.includes(item.type)) {
combinedFacetsMap[aggregation.fieldName] = this.addBuckets(
combinedFacetsMap[aggregation.fieldName] || {},
fieldValues,
);
}
});
});
}
}
return this.mapToFacets(combinedFacetsMap);
}
@@ -111,11 +122,11 @@ export class DataFacetsProvider {
*
* @param facets Array of facets
*/
facetsToMap(facets: SCFacet[]): {[key: string]: {[key: string]: number; }; } {
const facetsMap: {[key: string]: {[key: string]: number; }; } = {};
facets.forEach((facet) => {
facetsToMap(facets: SCFacet[]): {[key: string]: {[key: string]: number}} {
const facetsMap: {[key: string]: {[key: string]: number}} = {};
for (const facet of facets) {
facetsMap[facet.field] = this.bucketsToMap(facet.buckets);
});
}
return facetsMap;
}
@@ -125,8 +136,8 @@ export class DataFacetsProvider {
*
* @param bucketsMap A map from a buckets array
*/
// tslint:disable-next-line:prefer-function-over-method
mapToBuckets(bucketsMap: {[key: string]: number; }): SCFacetBucket[] {
// eslint-disable-next-line class-methods-use-this
mapToBuckets(bucketsMap: {[key: string]: number}): SCFacetBucket[] {
const buckets: SCFacetBucket[] = [];
for (const key in bucketsMap) {
if (bucketsMap.hasOwnProperty(key)) {
@@ -143,7 +154,7 @@ export class DataFacetsProvider {
*
* @param facetsMap A map from facets array
*/
mapToFacets(facetsMap: {[key: string]: {[key: string]: number; }; }): SCFacet[] {
mapToFacets(facetsMap: {[key: string]: {[key: string]: number}}): SCFacet[] {
const facets: SCFacet[] = [];
for (const key in facetsMap) {
if (facetsMap.hasOwnProperty(key)) {

View File

@@ -25,7 +25,7 @@ export class DataIconPipe implements PipeTransform {
/**
* Mapping from data types to ionic icons to show
*/
typeIconMap: {[type in SCThingType] : string; };
typeIconMap: {[type in SCThingType]: string};
constructor() {
this.typeIconMap = {
@@ -57,9 +57,9 @@ export class DataIconPipe implements PipeTransform {
};
}
/**
* Provide the icon name from the data type
*/
/**
* Provide the icon name from the data type
*/
transform(type: SCThingType): string {
return this.typeIconMap[type];
}

View File

@@ -28,11 +28,7 @@ const dataRoutes: Routes = [
* Module defining routes for data module
*/
@NgModule({
exports: [
RouterModule,
],
imports: [
RouterModule.forChild(dataRoutes),
],
exports: [RouterModule],
imports: [RouterModule.forChild(dataRoutes)],
})
export class DataRoutingModule {}

View File

@@ -34,7 +34,7 @@ export class DataRoutingService {
* @param thing The selected thing
*/
emitChildEvent(thing: SCThings) {
this.childSelectedEvent.next(thing);
this.childSelectedEvent.next(thing);
}
/**

View File

@@ -36,112 +36,110 @@ import {DataProvider} from './data.provider';
import {DataDetailContentComponent} from './detail/data-detail-content.component';
import {DataDetailComponent} from './detail/data-detail.component';
import {AddressDetailComponent} from './elements/address-detail.component';
import {LongInlineText} from './elements/long-inline-text.component';
import {LongInlineTextComponent} from './elements/long-inline-text.component';
import {OffersDetailComponent} from './elements/offers-detail.component';
import {OffersInListComponent} from './elements/offers-in-list.component';
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 {SkeletonListItemComponent} from './elements/skeleton-list-item.component';
import {SkeletonSegmentComponent} from './elements/skeleton-segment-button.component';
import {SkeletonSimpleCardComponent} from './elements/skeleton-simple-card.component';
import {DataListItemComponent} from './list/data-list-item.component';
import {DataListComponent} from './list/data-list.component';
import {FoodDataListComponent} from './list/food-data-list.component';
import {SearchPageComponent} from './list/search-page.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';
import {ArticleListItemComponent} from './types/article/article-list-item.component';
import {CatalogDetailContentComponent} from './types/catalog/catalog-detail-content.component';
import {CatalogListItem} from './types/catalog/catalog-list-item.component';
import {CatalogListItemComponent} from './types/catalog/catalog-list-item.component';
import {DateSeriesDetailContentComponent} from './types/date-series/date-series-detail-content.component';
import {DateSeriesListItem} from './types/date-series/date-series-list-item.component';
import {DateSeriesListItemComponent} from './types/date-series/date-series-list-item.component';
import {DishDetailContentComponent} from './types/dish/dish-detail-content.component';
import {DishListItem} from './types/dish/dish-list-item.component';
import {DishListItemComponent} from './types/dish/dish-list-item.component';
import {EventDetailContentComponent} from './types/event/event-detail-content.component';
import {EventListItemComponent} from './types/event/event-list-item.component';
import {FavoriteDetailContentComponent} from './types/favorite/favorite-detail-content.component';
import {FavoriteListItem} from './types/favorite/favorite-list-item.component';
import {FavoriteListItemComponent} from './types/favorite/favorite-list-item.component';
import {MessageDetailContentComponent} from './types/message/message-detail-content.component';
import {MessageListItem} from './types/message/message-list-item.component';
import {MessageListItemComponent} from './types/message/message-list-item.component';
import {OrganizationDetailContentComponent} from './types/organization/organization-detail-content.component';
import {OrganizationListItem} from './types/organization/organization-list-item.component';
import {OrganizationListItemComponent} from './types/organization/organization-list-item.component';
import {PersonDetailContentComponent} from './types/person/person-detail-content.component';
import {PersonListItem} from './types/person/person-list-item.component';
import {PersonListItemComponent} 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 {PlaceListItemComponent} 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 {SemesterListItemComponent} from './types/semester/semester-list-item.component';
import {VideoDetailContentComponent} from './types/video/video-detail-content.component';
import {VideoListItem} from './types/video/video-list-item.component';
import {VideoListItemComponent} from './types/video/video-list-item.component';
/**
* Module for handling data
*/
@NgModule({
declarations: [
ActionChipListComponent,
AddEventActionChipComponent,
AddEventPopoverComponent,
OffersDetailComponent,
OffersInListComponent,
AddressDetailComponent,
ArticleDetailContentComponent,
ArticleListItem,
SimpleCardComponent,
SkeletonSimpleCard,
ArticleListItemComponent,
CatalogDetailContentComponent,
CatalogListItem,
CatalogListItemComponent,
DataDetailComponent,
DataDetailContentComponent,
FoodDataListComponent,
DataIconPipe,
DataListComponent,
DataListItem,
DataListItemComponent,
DateSeriesDetailContentComponent,
DateSeriesListItem,
DateSeriesListItemComponent,
DishDetailContentComponent,
DishListItem,
DishListItemComponent,
EventDetailContentComponent,
EventListItemComponent,
FavoriteDetailContentComponent,
FavoriteListItem,
LongInlineText,
FavoriteListItemComponent,
FoodDataListComponent,
LocateActionChipComponent,
LongInlineTextComponent,
MessageDetailContentComponent,
MessageListItem,
MessageListItemComponent,
OffersDetailComponent,
OffersInListComponent,
OrganizationDetailContentComponent,
OrganizationListItem,
OrganizationListItemComponent,
OriginDetailComponent,
OriginInListComponent,
PersonDetailContentComponent,
PersonListItem,
PersonListItemComponent,
PlaceDetailContentComponent,
PlaceListItem,
PlaceListItemComponent,
PlaceMensaDetailComponent,
SearchPageComponent,
SemesterDetailContentComponent,
SemesterListItem,
SkeletonListItem,
SkeletonSegment,
SemesterListItemComponent,
SimpleCardComponent,
SkeletonListItemComponent,
SkeletonSegmentComponent,
SkeletonSimpleCardComponent,
VideoDetailContentComponent,
VideoListItem,
DataIconPipe,
ActionChipListComponent,
AddEventActionChipComponent,
LocateActionChipComponent,
],
entryComponents: [
DataListComponent,
VideoListItemComponent,
],
entryComponents: [DataListComponent],
imports: [
IonicModule.forRoot(),
CommonModule,
FormsModule,
DataRoutingModule,
FormsModule,
HttpClientModule,
IonicModule.forRoot(),
MarkdownModule.forRoot(),
MenuModule,
MomentModule.forRoot({
relativeTimeThresholdOptions: {
'm': 59,
m: 59,
},
}),
ScrollingModule,
@@ -149,12 +147,6 @@ import {VideoListItem} from './types/video/video-list-item.component';
TranslateModule.forChild(),
ThingTranslateModule.forChild(),
],
providers: [
DataProvider,
DataFacetsProvider,
Network,
StAppsWebHttpClient,
],
providers: [DataProvider, DataFacetsProvider, Network, StAppsWebHttpClient],
})
export class DataModule {
}
export class DataModule {}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion,@typescript-eslint/ban-ts-comment,@typescript-eslint/no-explicit-any */
/*
* Copyright (C) 2018, 2019 StApps
* This program is free software: you can redistribute it and/or modify it
@@ -15,8 +16,17 @@
import {TestBed} from '@angular/core/testing';
import {Client} from '@openstapps/api/lib/client';
import {
SCDish, SCMessage, SCMultiSearchRequest, SCSaveableThing, SCSearchQuery,
SCSearchResponse, SCSearchValueFilter, SCThing, SCThingOriginType, SCThings, SCThingType,
SCDish,
SCMessage,
SCMultiSearchRequest,
SCSaveableThing,
SCSearchQuery,
SCSearchResponse,
SCSearchValueFilter,
SCThing,
SCThingOriginType,
SCThings,
SCThingType,
} from '@openstapps/core';
import {sampleThingsMap} from '../../_helpers/data/sample-things';
import {StorageProvider} from '../storage/storage.provider';
@@ -66,15 +76,16 @@ describe('DataProvider', () => {
type: SCThingType.Message,
uid: sampleThing.uid,
};
const otherSampleThing: SCMessage = {...sampleThing, uid: 'message-456', name: 'bar'};
const otherSampleThing: SCMessage = {
...sampleThing,
uid: 'message-456',
name: 'bar',
};
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [DataModule],
providers: [
DataProvider,
StAppsWebHttpClient,
],
providers: [DataProvider, StAppsWebHttpClient],
});
storageProvider = TestBed.get(StorageProvider);
dataProvider = TestBed.get(DataProvider);
@@ -128,26 +139,29 @@ describe('DataProvider', () => {
};
dataProvider.backendQueriesLimit = 2;
spyOn(Client.prototype as any, 'multiSearch').and.callFake((req: SCMultiSearchRequest) => ({
then: (callback: any) => {
let i = 0;
for (const key in req) {
if (req.hasOwnProperty(key)) {
i++;
// @ts-ignore
expect(requestCheck[key]).not.toBeNull();
expect(requestCheck[key]).toEqual(req[key]);
// @ts-ignore
requestCheck[key] = null;
// @ts-ignore
req[key] = req[key].toUpperCase();
spyOn(Client.prototype as any, 'multiSearch').and.callFake(
(request_: SCMultiSearchRequest) => ({
then: (callback: any) => {
let i = 0;
for (const key in request_) {
if (request_.hasOwnProperty(key)) {
i++;
// @ts-ignore
expect(requestCheck[key]).not.toBeNull();
expect(requestCheck[key]).toEqual(request_[key]);
// @ts-ignore
// eslint-disable-next-line unicorn/no-null
requestCheck[key] = null;
// @ts-ignore
request_[key] = request_[key].toUpperCase();
}
}
}
expect(i).toBeLessThanOrEqual(dataProvider.backendQueriesLimit);
expect(i).toBeLessThanOrEqual(dataProvider.backendQueriesLimit);
return callback(req);
},
}));
return callback(request_);
},
}),
);
const response = await dataProvider.multiSearch(request);
expect(response).toEqual(responseShould);
@@ -155,10 +169,12 @@ describe('DataProvider', () => {
it('should put an data item into the local database (storage)', async () => {
let providedThing: SCSaveableThing<SCThing>;
spyOn(storageProvider, 'put' as any).and.callFake((_id: any, thing: any) => {
providedThing = thing;
providedThing.origin.created = sampleSavable.origin.created;
});
spyOn(storageProvider, 'put' as any).and.callFake(
(_id: any, thing: any) => {
providedThing = thing;
providedThing.origin.created = sampleSavable.origin.created;
},
);
expect(storageProvider.put).not.toHaveBeenCalled();
expect(providedThing!).not.toBeDefined();
await dataProvider.put(sampleThing);
@@ -170,9 +186,14 @@ describe('DataProvider', () => {
await dataProvider.put(sampleThing);
spyOn(storageProvider, 'get').and.callThrough();
expect(storageProvider.get).not.toHaveBeenCalled();
const providedThing = await dataProvider.get(sampleThing.uid, DataScope.Local);
const providedThing = await dataProvider.get(
sampleThing.uid,
DataScope.Local,
);
providedThing.origin.created = sampleSavable.origin.created;
expect(storageProvider.get).toHaveBeenCalledWith(dataProvider.getDataKey(sampleThing.uid));
expect(storageProvider.get).toHaveBeenCalledWith(
dataProvider.getDataKey(sampleThing.uid),
);
expect(providedThing).toEqual(sampleSavable);
});
@@ -180,11 +201,16 @@ describe('DataProvider', () => {
await dataProvider.put(sampleThing);
await dataProvider.put(otherSampleThing);
const result = await dataProvider.getAll();
expect(Array.from(result.keys()).sort()).toEqual([
dataProvider.getDataKey(sampleThing.uid), dataProvider.getDataKey(otherSampleThing.uid),
expect([...result.keys()].sort()).toEqual([
dataProvider.getDataKey(sampleThing.uid),
dataProvider.getDataKey(otherSampleThing.uid),
]);
expect(result.get(dataProvider.getDataKey(sampleThing.uid))!.data).toEqual(sampleThing);
expect(result.get(dataProvider.getDataKey(otherSampleThing.uid))!.data).toEqual(otherSampleThing);
expect(result.get(dataProvider.getDataKey(sampleThing.uid))!.data).toEqual(
sampleThing,
);
expect(
result.get(dataProvider.getDataKey(otherSampleThing.uid))!.data,
).toEqual(otherSampleThing);
});
it('should provide single data from the backend', async () => {
@@ -196,7 +222,10 @@ describe('DataProvider', () => {
};
});
expect(Client.prototype.getThing).not.toHaveBeenCalled();
const providedThing = await dataProvider.get(sampleThing.uid, DataScope.Remote);
const providedThing = await dataProvider.get(
sampleThing.uid,
DataScope.Remote,
);
expect(Client.prototype.getThing).toHaveBeenCalledWith(sampleThing.uid);
expect(providedThing).toBe(sampleThing);
});
@@ -226,7 +255,9 @@ describe('DataProvider', () => {
await dataProvider.put(sampleThing);
expect(await storageProvider.length()).toBe(1);
await dataProvider.delete(sampleThing.uid);
expect(storageProvider.delete).toHaveBeenCalledWith(dataProvider.getDataKey(sampleThing.uid));
expect(storageProvider.delete).toHaveBeenCalledWith(
dataProvider.getDataKey(sampleThing.uid),
);
expect(await storageProvider.length()).toBe(0);
});
@@ -242,7 +273,7 @@ describe('DataProvider', () => {
dataProvider.getDataKey(otherSampleThing.uid),
);
const result = await storageProvider.getAll();
expect(Array.from(result.keys())).toEqual(['some-uid']);
expect([...result.keys()]).toEqual(['some-uid']);
});
it('should properly check if a data item has already been saved', async () => {

View File

@@ -15,7 +15,8 @@
import {Injectable} from '@angular/core';
import {Client} from '@openstapps/api/lib/client';
import {
SCMultiSearchRequest, SCMultiSearchResponse,
SCMultiSearchRequest,
SCMultiSearchResponse,
SCSearchRequest,
SCSearchResponse,
SCThingOriginType,
@@ -43,7 +44,6 @@ export enum DataScope {
providedIn: 'root',
})
export class DataProvider {
/**
* TODO
*/
@@ -57,26 +57,32 @@ export class DataProvider {
set storagePrefix(storagePrefix) {
this._storagePrefix = storagePrefix;
}
/**
* TODO
*/
private _storagePrefix = 'stapps.data';
/**
* Version of the app (used for the header in communication with the backend)
*/
appVersion = environment.backend_version;
/**
* Maximum number of sub-queries in a multi-query allowed by the backend
*/
backendQueriesLimit = 5;
/**
* TODO
*/
backendUrl = environment.backend_url;
/**
* TODO
*/
client: Client;
/**
* TODO
*/
@@ -88,8 +94,15 @@ export class DataProvider {
* @param stAppsWebHttpClient TODO
* @param storageProvider TODO
*/
constructor(stAppsWebHttpClient: StAppsWebHttpClient, storageProvider: StorageProvider) {
this.client = new Client(stAppsWebHttpClient, this.backendUrl, this.appVersion);
constructor(
stAppsWebHttpClient: StAppsWebHttpClient,
storageProvider: StorageProvider,
) {
this.client = new Client(
stAppsWebHttpClient,
this.backendUrl,
this.appVersion,
);
this.storageProvider = storageProvider;
}
@@ -106,7 +119,7 @@ export class DataProvider {
* Delete all the previously saved data items
*/
async deleteAll(): Promise<void> {
const keys = Array.from((await this.getAll()).keys());
const keys = [...(await this.getAll()).keys()];
return this.storageProvider.delete(...keys);
}
@@ -114,15 +127,23 @@ export class DataProvider {
/**
* Provides a savable thing from the local database using the provided UID
*/
async get(uid: string, scope: DataScope.Local): Promise<SCSaveableThing<SCThings>>;
async get(
uid: string,
scope: DataScope.Local,
): Promise<SCSaveableThing<SCThings>>;
/**
* Provides a thing from the backend
*/
async get(uid: string, scope: DataScope.Remote): Promise<SCThings | SCSaveableThing<SCThings>>;
async get(
uid: string,
scope: DataScope.Remote,
): Promise<SCThings | SCSaveableThing<SCThings>>;
/**
* Provides a thing from both local database and backend
*/
async get(uid: string): Promise<Map<DataScope, SCThings | SCSaveableThing<SCThings>>>;
async get(
uid: string,
): Promise<Map<DataScope, SCThings | SCSaveableThing<SCThings>>>;
/**
* Provides a thing from the local database only, backend only or both, depending on the scope
@@ -130,26 +151,36 @@ export class DataProvider {
* @param uid Unique identifier of a thing
* @param scope From where data should be provided
*/
async get(uid: string, scope?: DataScope):
Promise<SCThings | SCSaveableThing<SCThings> | Map<DataScope, SCThings | SCSaveableThing<SCThings>>> {
if (scope === DataScope.Local) {
return this.storageProvider.get<SCSaveableThing<SCThings>>(this.getDataKey(uid));
}
if (scope === DataScope.Remote) {
return this.client.getThing(uid);
}
const map: Map<DataScope, SCThings | SCSaveableThing<SCThings>> = new Map();
map.set(DataScope.Local, await this.get(uid, DataScope.Local));
map.set(DataScope.Remote, await this.get(uid, DataScope.Remote));
return map;
async get(
uid: string,
scope?: DataScope,
): Promise<
| SCThings
| SCSaveableThing<SCThings>
| Map<DataScope, SCThings | SCSaveableThing<SCThings>>
> {
if (scope === DataScope.Local) {
return this.storageProvider.get<SCSaveableThing<SCThings>>(
this.getDataKey(uid),
);
}
if (scope === DataScope.Remote) {
return this.client.getThing(uid);
}
const map: Map<DataScope, SCThings | SCSaveableThing<SCThings>> = new Map();
map.set(DataScope.Local, await this.get(uid, DataScope.Local));
map.set(DataScope.Remote, await this.get(uid, DataScope.Remote));
return map;
}
/**
* Provides all things saved in the local database
*/
async getAll(): Promise<Map<string, SCSaveableThing<SCThings>>> {
return this.storageProvider.search<SCSaveableThing<SCThings>>(this.storagePrefix);
return this.storageProvider.search<SCSaveableThing<SCThings>>(
this.storagePrefix,
);
}
/**
@@ -175,11 +206,18 @@ export class DataProvider {
*
* @param query - query to send to the backend (auto-splits according to the backend limit)
*/
async multiSearch(query: SCMultiSearchRequest): Promise<SCMultiSearchResponse> {
async multiSearch(
query: SCMultiSearchRequest,
): Promise<SCMultiSearchResponse> {
// partition object into chunks, process those requests in parallel, then merge their responses again
return Object.assign({}, ...(await Promise.all(chunk(toPairs(query), this.backendQueriesLimit)
.map((request) => this.client.multiSearch(fromPairs(request))),
)));
return Object.assign(
{},
...(await Promise.all(
chunk(toPairs(query), this.backendQueriesLimit).map(request =>
this.client.multiSearch(fromPairs(request)),
),
)),
);
}
/**
@@ -188,7 +226,10 @@ export class DataProvider {
* @param item Data item that needs to be saved
* @param [type] Savable type (e.g. 'favorite'); if nothing is provided then type of the thing is used
*/
async put(item: SCThings, type?: SCThingType): Promise<SCSaveableThing<SCThings>> {
async put(
item: SCThings,
type?: SCThingType,
): Promise<SCSaveableThing<SCThings>> {
const savableItem: SCSaveableThing<SCThings> = {
data: item,
name: item.name,
@@ -196,12 +237,15 @@ export class DataProvider {
created: new Date().toISOString(),
type: SCThingOriginType.User,
},
type: (typeof type === 'undefined') ? item.type : type,
type: typeof type === 'undefined' ? item.type : type,
uid: item.uid,
};
// @TODO: Implementation for saving item into the backend (user's account)
return ( this.storageProvider.put<SCSaveableThing<SCThings>>(this.getDataKey(item.uid), savableItem));
return this.storageProvider.put<SCSaveableThing<SCThings>>(
this.getDataKey(item.uid),
savableItem,
);
}
/**
@@ -210,6 +254,6 @@ export class DataProvider {
* @param query - query to send to the backend
*/
async search(query: SCSearchRequest): Promise<SCSearchResponse> {
return (this.client.search(query));
return this.client.search(query);
}
}

View File

@@ -27,5 +27,4 @@ export class DataDetailContentComponent {
* TODO
*/
@Input() item: SCThings;
}

View File

@@ -1,19 +1,68 @@
<stapps-simple-card *ngIf="item.description" [title]="'Description'" [content]="item.description"></stapps-simple-card>
<stapps-simple-card
*ngIf="item.description"
[title]="'Description'"
[content]="item.description"
></stapps-simple-card>
<div [ngSwitch]="true">
<stapps-article-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'article'"></stapps-article-detail-content>
<stapps-catalog-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'catalog'"></stapps-catalog-detail-content>
<stapps-date-series-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'date series'"></stapps-date-series-detail-content>
<stapps-dish-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'dish'"></stapps-dish-detail-content>
<stapps-event-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'academic event'"></stapps-event-detail-content>
<stapps-event-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'sport course'"></stapps-event-detail-content>
<stapps-favorite-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'favorite'"></stapps-favorite-detail-content>
<stapps-message-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'message'"></stapps-message-detail-content>
<stapps-person-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'person'"></stapps-person-detail-content>
<stapps-place-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'building'"></stapps-place-detail-content>
<stapps-place-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'floor'"></stapps-place-detail-content>
<stapps-place-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'point of interest'"></stapps-place-detail-content>
<stapps-place-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'room'"></stapps-place-detail-content>
<stapps-semester-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'semester'"></stapps-semester-detail-content>
<stapps-video-detail-content [item]="item" [language]="language" *ngSwitchCase="item.type === 'video'"></stapps-video-detail-content>
<stapps-origin-detail [origin]="item.origin" ></stapps-origin-detail>
<stapps-article-detail-content
[item]="item"
*ngSwitchCase="item.type === 'article'"
></stapps-article-detail-content>
<stapps-catalog-detail-content
[item]="item"
*ngSwitchCase="item.type === 'catalog'"
></stapps-catalog-detail-content>
<stapps-date-series-detail-content
[item]="item"
*ngSwitchCase="item.type === 'date series'"
></stapps-date-series-detail-content>
<stapps-dish-detail-content
[item]="item"
*ngSwitchCase="item.type === 'dish'"
></stapps-dish-detail-content>
<stapps-event-detail-content
[item]="item"
*ngSwitchCase="item.type === 'academic event'"
></stapps-event-detail-content>
<stapps-event-detail-content
[item]="item"
*ngSwitchCase="item.type === 'sport course'"
></stapps-event-detail-content>
<stapps-favorite-detail-content
[item]="item"
*ngSwitchCase="item.type === 'favorite'"
></stapps-favorite-detail-content>
<stapps-message-detail-content
[item]="item"
*ngSwitchCase="item.type === 'message'"
></stapps-message-detail-content>
<stapps-person-detail-content
[item]="item"
*ngSwitchCase="item.type === 'person'"
></stapps-person-detail-content>
<stapps-place-detail-content
[item]="item"
*ngSwitchCase="item.type === 'building'"
></stapps-place-detail-content>
<stapps-place-detail-content
[item]="item"
*ngSwitchCase="item.type === 'floor'"
></stapps-place-detail-content>
<stapps-place-detail-content
[item]="item"
*ngSwitchCase="item.type === 'point of interest'"
></stapps-place-detail-content>
<stapps-place-detail-content
[item]="item"
*ngSwitchCase="item.type === 'room'"
></stapps-place-detail-content>
<stapps-semester-detail-content
[item]="item"
*ngSwitchCase="item.type === 'semester'"
></stapps-semester-detail-content>
<stapps-video-detail-content
[item]="item"
*ngSwitchCase="item.type === 'video'"
></stapps-video-detail-content>
<stapps-origin-detail [origin]="item.origin"></stapps-origin-detail>
</div>

View File

@@ -1,3 +1,4 @@
/* 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
@@ -16,7 +17,11 @@ import {CUSTOM_ELEMENTS_SCHEMA, DebugElement} from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ActivatedRoute, RouterModule} from '@angular/router';
import {IonRefresher, IonTitle} from '@ionic/angular';
import {TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate/core';
import {
TranslateLoader,
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {sampleThingsMap} from '../../../_helpers/data/sample-things';
import {DataRoutingModule} from '../data-routing.module';
import {DataModule} from '../data.module';
@@ -59,21 +64,26 @@ describe('DataDetailComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterModule.forRoot([]), DataRoutingModule, DataModule,
TranslateModule.forRoot({
loader: {provide: TranslateLoader, useClass: TranslateFakeLoader},
})],
imports: [
RouterModule.forRoot([]),
DataRoutingModule,
DataModule,
TranslateModule.forRoot({
loader: {provide: TranslateLoader, useClass: TranslateFakeLoader},
}),
],
providers: [{provide: ActivatedRoute, useValue: fakeActivatedRoute}],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
.compileComponents();
}).compileComponents();
}));
beforeEach(async () => {
dataProvider = TestBed.get(DataProvider);
translateService = TestBed.get(TranslateService);
refresher = jasmine.createSpyObj('refresher', ['complete']);
spyOn(dataProvider, 'get' as any).and.returnValue(Promise.resolve(sampleThing));
spyOn(dataProvider, 'get' as any).and.returnValue(
Promise.resolve(sampleThing),
);
spyOn(DataDetailComponent.prototype, 'getItem').and.callThrough();
fixture = await TestBed.createComponent(DataDetailComponent);
comp = fixture.componentInstance;
@@ -83,28 +93,33 @@ describe('DataDetailComponent', () => {
await dataProvider.deleteAll();
});
it('should create component', () =>
expect(comp).toBeDefined(),
);
it('should create component', () => expect(comp).toBeDefined());
it('should have appropriate title', async () => {
const title: DebugElement | null = detailPage.query(By.directive(IonTitle));
// eslint-disable-next-line unicorn/no-null
expect(title).not.toBe(null);
expect(title!.nativeElement.textContent).toBe('Foo');
});
it('should get a data item', () => {
comp.getItem(sampleThing.uid);
expect(DataDetailComponent.prototype.getItem).toHaveBeenCalledWith(sampleThing.uid);
expect(DataDetailComponent.prototype.getItem).toHaveBeenCalledWith(
sampleThing.uid,
);
});
it('should get a data item when component is accessed', async () => {
expect(DataDetailComponent.prototype.getItem).toHaveBeenCalledWith(sampleThing.uid);
expect(DataDetailComponent.prototype.getItem).toHaveBeenCalledWith(
sampleThing.uid,
);
});
it('should update the data item when refresh is called', async () => {
await comp.refresh(refresher);
expect(DataDetailComponent.prototype.getItem).toHaveBeenCalledWith(sampleThing.uid);
expect(DataDetailComponent.prototype.getItem).toHaveBeenCalledWith(
sampleThing.uid,
);
expect(refresher.complete).toHaveBeenCalled();
});
});

View File

@@ -17,7 +17,12 @@ import {ActivatedRoute} from '@angular/router';
import {Network} from '@ionic-native/network/ngx';
import {IonRefresher} from '@ionic/angular';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
import {SCLanguageCode, SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
import {
SCLanguageCode,
SCSaveableThing,
SCThings,
SCUuid,
} from '@openstapps/core';
import {DataProvider, DataScope} from '../data.provider';
/**
@@ -44,7 +49,9 @@ export class DataDetailComponent implements OnInit {
/**
* Type guard for SCSavableThing
*/
static isSCSavableThing(thing: SCThings | SCSaveableThing<SCThings>): thing is SCSaveableThing<SCThings> {
static isSCSavableThing(
thing: SCThings | SCSaveableThing<SCThings>,
): thing is SCSaveableThing<SCThings> {
return typeof (thing as SCSaveableThing<SCThings>).data !== 'undefined';
}
@@ -55,10 +62,12 @@ export class DataDetailComponent implements OnInit {
* @param network the network provider
* @param translateService the translation service
*/
constructor(private readonly route: ActivatedRoute,
private readonly dataProvider: DataProvider,
private readonly network: Network,
translateService: TranslateService) {
constructor(
private readonly route: ActivatedRoute,
private readonly dataProvider: DataProvider,
private readonly network: Network,
translateService: TranslateService,
) {
this.language = translateService.currentLang as SCLanguageCode;
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.language = event.lang as SCLanguageCode;
@@ -74,7 +83,8 @@ export class DataDetailComponent implements OnInit {
try {
const item = await this.dataProvider.get(uid, DataScope.Remote);
this.item = DataDetailComponent.isSCSavableThing(item) ? item.data : item;
} catch (_) {
} catch {
// eslint-disable-next-line unicorn/no-null
this.item = null;
}
}
@@ -99,7 +109,9 @@ export class DataDetailComponent implements OnInit {
* @param refresher Refresher component that triggers the update
*/
async refresh(refresher: IonRefresher) {
await this.getItem(this.item?.uid ?? this.route.snapshot.paramMap.get('uid') ?? '');
await this.getItem(
this.item?.uid ?? this.route.snapshot.paramMap.get('uid') ?? '',
);
await refresher.complete();
}
}

View File

@@ -1,23 +1,25 @@
<ion-header>
<ion-toolbar>
<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-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>
<ion-content class="ion-padding">
<ion-refresher slot="fixed" (ionRefresh)="refresh($event.target)">
<ion-refresher-content pullingIcon="chevron-down-outline" pullingText="{{'data.REFRESH_ACTION' | translate}}"
refreshingText="{{'data.REFRESHING' | translate}}">
<ion-refresher-content
pullingIcon="chevron-down-outline"
pullingText="{{ 'data.REFRESH_ACTION' | translate }}"
refreshingText="{{ 'data.REFRESHING' | translate }}"
>
</ion-refresher-content>
</ion-refresher>
<div [ngSwitch]="true">
<ng-container *ngSwitchCase='!item && isDisconnected()'>
<div class='notFoundContainer'>
<ion-icon name='no-connection'>
</ion-icon>
<ng-container *ngSwitchCase="!item && isDisconnected()">
<div class="notFoundContainer">
<ion-icon name="no-connection"> </ion-icon>
<ion-label>
{{ 'data.detail.COULD_NOT_CONNECT' | translate }}
</ion-label>
@@ -25,8 +27,7 @@
</ng-container>
<ng-container *ngSwitchCase="item === null">
<div class="notFoundContainer">
<ion-icon name="broken-link">
</ion-icon>
<ion-icon name="broken-link"> </ion-icon>
<ion-label>
{{ 'data.detail.NOT_FOUND' | translate }}
</ion-label>
@@ -39,14 +40,17 @@
<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-icon
color="medium"
[attr.name]="item.type | dataIcon"
></ion-icon>
</ion-thumbnail>
<ion-grid *ngSwitchDefault>
<ion-row>
<ion-col>
<div class="ion-text-wrap">
<h2 class="name">{{item.name}}</h2>
<ion-note>{{item.type}}</ion-note>
<h2 class="name">{{ item.name }}</h2>
<ion-note>{{ item.type }}</ion-note>
</div>
</ion-col>
</ion-row>

View File

@@ -1,41 +1,57 @@
<ion-card>
<ion-card-header>{{'data.detail.address.TITLE' | translate | titlecase}}</ion-card-header>
<ion-card-header>{{
'data.detail.address.TITLE' | translate | titlecase
}}</ion-card-header>
<ion-card-content>
<ion-grid>
<ion-row>
<ion-col>{{'data.detail.address.STREET' | translate | titlecase}}:</ion-col>
<ion-col
>{{ 'data.detail.address.STREET' | translate | titlecase }}:</ion-col
>
<ion-col width-60 text-right>
{{address.streetAddress}}
{{ address.streetAddress }}
</ion-col>
</ion-row>
<ion-row>
<ion-col>{{'data.detail.address.POSTCODE' | translate | titlecase}}:</ion-col>
<ion-col
>{{
'data.detail.address.POSTCODE' | translate | titlecase
}}:</ion-col
>
<ion-col width-60 text-right>
{{address.postalCode}}
{{ address.postalCode }}
</ion-col>
</ion-row>
<ion-row>
<ion-col>{{'data.detail.address.CITY' | translate | titlecase}}:</ion-col>
<ion-col
>{{ 'data.detail.address.CITY' | translate | titlecase }}:</ion-col
>
<ion-col width-60 text-right>
{{address.addressLocality}}
{{ address.addressLocality }}
</ion-col>
</ion-row>
<ion-row *ngIf="address.addressRegion">
<ion-col>{{'data.detail.address.REGION' | translate | titlecase}}:</ion-col>
<ion-col
>{{ 'data.detail.address.REGION' | translate | titlecase }}:</ion-col
>
<ion-col width-60 text-right>
{{address.addressRegion}}
{{ address.addressRegion }}
</ion-col>
</ion-row>
<ion-row>
<ion-col>{{'data.detail.address.COUNTRY' | translate | titlecase}}:</ion-col>
<ion-col
>{{ 'data.detail.address.COUNTRY' | translate | titlecase }}:</ion-col
>
<ion-col width-60 text-right>
{{address.addressCountry}}
{{ address.addressCountry }}
</ion-col>
</ion-row>
<ion-row *ngIf="address.postOfficeBoxNumber">
<ion-col>{{'data.detail.address.POST_OFFICE_BOX' | translate | titlecase}}</ion-col>
<ion-col>{{
'data.detail.address.POST_OFFICE_BOX' | translate | titlecase
}}</ion-col>
<ion-col width-60 text-right>
{{address.postOfficeBoxNumber}}
{{ address.postOfficeBoxNumber }}
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -21,11 +21,12 @@ import {Component, Input} from '@angular/core';
selector: 'stapps-long-inline-text',
templateUrl: 'long-inline-text.html',
})
export class LongInlineText {
export class LongInlineTextComponent {
/**
* TODO
*/
@Input() size: number;
/**
* TODO
*/

View File

@@ -1 +1,3 @@
<span>{{text | slice:0:size}}<span *ngIf="text.length > size">...</span></span>
<span
>{{ text | slice: 0:size }}<span *ngIf="text.length > size">...</span></span
>

View File

@@ -13,7 +13,10 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCAcademicPriceGroup, SCThingThatCanBeOfferedOffer} from '@openstapps/core';
import {
SCAcademicPriceGroup,
SCThingThatCanBeOfferedOffer,
} from '@openstapps/core';
/**
* TODO
@@ -27,6 +30,7 @@ export class OffersDetailComponent {
* TODO
*/
objectKeys = Object.keys;
/**
* TODO
*/

View File

@@ -1,19 +1,40 @@
<ion-card>
<ion-card-header>{{'data.detail.offers.TITLE' | translate | titlecase}}</ion-card-header>
<ion-card-header>{{
'data.detail.offers.TITLE' | translate | titlecase
}}</ion-card-header>
<ion-card-content>
<div *ngFor="let offer of offers">
<p *ngIf="offer.inPlace">
<ion-icon name="location"></ion-icon>
<a [routerLink]="['/data-detail', offer.inPlace.uid]">{{'name' | thingTranslate: offer.inPlace}}</a>,&nbsp;
<span *ngIf="offer.availabilityStarts">
<ion-icon name="calendar"></ion-icon> {{offer.availabilityStarts | amDateFormat:'ll'}}
<a [routerLink]="['/data-detail', offer.inPlace.uid]">{{
'name' | thingTranslate: offer.inPlace
}}</a
>,&nbsp;
<span
*ngIf="
offer.availabilityRange.gt
? offer.availabilityRange.gt
: offer.availabilityRange.gte
"
>
<ion-icon name="calendar"></ion-icon>
{{
(offer.availabilityRange.gt
? offer.availabilityRange.gt
: offer.availabilityRange.gte
) | amDateFormat: 'll'
}}
</span>
</p>
<ion-grid *ngFor="let group of objectKeys(offer.prices)">
<ion-row>
<ion-col>{{group | titlecase}}</ion-col>
<ion-col>{{ group | titlecase }}</ion-col>
<ion-col width-20 text-right>
<p> {{offer.prices[group] | currency:'EUR':'symbol':undefined:'de'}}</p>
<p>
{{
offer.prices[group] | currency: 'EUR':'symbol':undefined:'de'
}}
</p>
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -13,7 +13,10 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCAcademicPriceGroup, SCThingThatCanBeOfferedOffer} from '@openstapps/core';
import {
SCAcademicPriceGroup,
SCThingThatCanBeOfferedOffer,
} from '@openstapps/core';
/**
* TODO

View File

@@ -1,6 +1,9 @@
<div>
<h2>{{offers[0].prices.default | currency:'EUR':'symbol':undefined:'de'}}</h2>
<h2>
{{ offers[0].prices.default | currency: 'EUR':'symbol':undefined:'de' }}
</h2>
<p *ngIf="offers[0].inPlace">
<ion-icon name="location"></ion-icon>{{offers[0].inPlace.name}}<span *ngIf="offers.length > 1">...</span>
<ion-icon name="location"></ion-icon>{{ offers[0].inPlace.name
}}<span *ngIf="offers.length > 1">...</span>
</p>
</div>

View File

@@ -1,27 +1,61 @@
<ion-card *ngIf="origin.type === 'user'">
<ion-card-header>{{'data.types.origin.TITLE' | translate | titlecase}}: {{'data.types.origin.USER' | translate | titlecase}}</ion-card-header>
<ion-card-header
>{{ 'data.types.origin.TITLE' | translate | titlecase }}:
{{ 'data.types.origin.USER' | translate | titlecase }}</ion-card-header
>
<ion-card-content>
<p>{{'data.types.origin.detail.CREATED' | translate | titlecase}}: {{origin.created | amDateFormat:'ll'}}</p>
<p *ngIf="origin.updated">{{'data.types.origin.detail.UPDATED' | translate | titlecase}}: {{origin.updated | amDateFormat:'ll'}}</p>
<p *ngIf="origin.modified">{{'data.types.origin.detail.MODIFIED' | translate | titlecase}}: {{origin.modified | amDateFormat:'ll'}}</p>
<p *ngIf="origin.name">{{'data.types.origin.detail.MAINTAINER' | translate }}: {{origin.name}}</p>
<p>
{{ 'data.types.origin.detail.CREATED' | translate | titlecase }}:
{{ origin.created | amDateFormat: 'll' }}
</p>
<p *ngIf="origin.updated">
{{ 'data.types.origin.detail.UPDATED' | translate | titlecase }}:
{{ origin.updated | amDateFormat: 'll' }}
</p>
<p *ngIf="origin.modified">
{{ 'data.types.origin.detail.MODIFIED' | translate | titlecase }}:
{{ origin.modified | amDateFormat: 'll' }}
</p>
<p *ngIf="origin.name">
{{ 'data.types.origin.detail.MAINTAINER' | translate }}: {{ origin.name }}
</p>
<p *ngIf="origin.maintainer">
{{'data.types.origin.detail.MAINTAINER' | translate }}: <a [routerLink]="['/data-detail', origin.maintainer.uid]">{{origin.maintainer.name}}</a>
{{ 'data.types.origin.detail.MAINTAINER' | translate }}:
<a [routerLink]="['/data-detail', origin.maintainer.uid]">{{
origin.maintainer.name
}}</a>
</p>
</ion-card-content>
</ion-card>
<ion-card *ngIf="origin.type === 'remote'">
<ion-card-header>{{'data.types.origin.TITLE' | translate | titlecase}}: {{'data.types.origin.REMOTE' | translate | titlecase}}</ion-card-header>
<ion-card-header
>{{ 'data.types.origin.TITLE' | translate | titlecase }}:
{{ 'data.types.origin.REMOTE' | translate | titlecase }}</ion-card-header
>
<ion-card-content>
<p>{{'data.types.origin.detail.INDEXED' | translate | titlecase}}: {{origin.indexed | amDateFormat:'ll'}}</p>
<p *ngIf="origin.modified">{{'data.types.origin.detail.MODIFIED' | translate | titlecase}}: {{origin.modified | amDateFormat:'ll'}}</p>
<p *ngIf="origin.name">{{'data.types.origin.detail.MAINTAINER' | translate }}: {{origin.name}}</p>
<p>
{{ 'data.types.origin.detail.INDEXED' | translate | titlecase }}:
{{ origin.indexed | amDateFormat: 'll' }}
</p>
<p *ngIf="origin.modified">
{{ 'data.types.origin.detail.MODIFIED' | translate | titlecase }}:
{{ origin.modified | amDateFormat: 'll' }}
</p>
<p *ngIf="origin.name">
{{ 'data.types.origin.detail.MAINTAINER' | translate }}: {{ origin.name }}
</p>
<p *ngIf="origin.maintainer">
{{'data.types.origin.detail.MAINTAINER' | translate | titlecase}}: <a [routerLink]="['/data-detail', origin.maintainer.uid]">{{origin.maintainer.name}}</a>
{{ 'data.types.origin.detail.MAINTAINER' | translate | titlecase }}:
<a [routerLink]="['/data-detail', origin.maintainer.uid]">{{
origin.maintainer.name
}}</a>
</p>
<p *ngIf="origin.responsibleEntity">
{{'data.types.origin.detail.RESPONSIBLE' | translate | titlecase}}: <a [routerLink]="['/data-detail', origin.responsibleEntity.uid]">{{origin.responsibleEntity.name}}</a>
{{ 'data.types.origin.detail.RESPONSIBLE' | translate | titlecase }}:
<a [routerLink]="['/data-detail', origin.responsibleEntity.uid]">{{
origin.responsibleEntity.name
}}</a>
</p>
</ion-card-content>
</ion-card>

View File

@@ -1,7 +1,7 @@
<div *ngIf="origin.type === 'user'">
<p>{{origin.created | amDateFormat:'ll'}}</p>
<p>{{ origin.created | amDateFormat: 'll' }}</p>
</div>
<div *ngIf="origin.type === 'remote'">
<p>{{origin.indexed | amDateFormat:'ll'}}</p>
<p>{{ origin.indexed | amDateFormat: 'll' }}</p>
</div>

View File

@@ -27,29 +27,34 @@ export class SimpleCardComponent {
* TODO
*/
areThings = false;
/**
* TODO
*/
@Input() content: string | string[] | SCThing[];
/**
* TODO
*/
@Input() isMarkdown = false;
/**
* TODO
*/
@Input() title: string;
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method
// eslint-disable-next-line class-methods-use-this
isString(data: unknown): data is string {
return typeof data === 'string';
}
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method
// eslint-disable-next-line class-methods-use-this
isThing(something: unknown): something is SCThing {
return isThing(something);
}

View File

@@ -1,26 +1,29 @@
<ion-card>
<ion-card-header>{{title}}</ion-card-header>
<ion-card-header>{{ title }}</ion-card-header>
<ion-card-content>
<ng-container *ngIf="isString(content) then text; else list">
<ng-container *ngIf="isString(content); then text; else list">
</ng-container>
<ng-template #text>
<ng-container *ngIf="isMarkdown; else plainText">
<markdown [data]="content"></markdown>
</ng-container>
<ng-template #plainText>
<p>{{content}}</p>
<p>{{ content }}</p>
</ng-template>
</ng-template>
<ng-template #list>
<ng-container *ngIf="isThing(content[0]) then thingList; else textList">
<ng-container *ngIf="isThing(content[0]); then thingList; else textList">
</ng-container>
<ng-template #thingList>
<a [routerLink]="['/data-detail', thing.uid]" *ngFor="let thing of content">
<p>{{'name' | thingTranslate: thing}}</p>
<a
[routerLink]="['/data-detail', thing.uid]"
*ngFor="let thing of content"
>
<p>{{ 'name' | thingTranslate: thing }}</p>
</a>
</ng-template>
<ng-template #textList>
<p *ngFor="let text of content">{{text}}</p>
<p *ngFor="let text of content">{{ text }}</p>
</ng-template>
</ng-template>
</ion-card-content>

View File

@@ -22,5 +22,4 @@ import {Component} from '@angular/core';
templateUrl: 'skeleton-list-item.html',
styleUrls: ['skeleton-list-item.scss'],
})
export class SkeletonListItem {
}
export class SkeletonListItemComponent {}

View File

@@ -1,18 +1,18 @@
<ion-item>
<ion-thumbnail slot='start' class='ion-margin-end'>
<ion-thumbnail slot="start" class="ion-margin-end">
<ion-skeleton-text animated></ion-skeleton-text>
</ion-thumbnail>
<ion-grid>
<ion-row>
<ion-col>
<h2 class='name'>
<ion-skeleton-text animated style='width: 80%'></ion-skeleton-text>
<h2 class="name">
<ion-skeleton-text animated style="width: 80%"></ion-skeleton-text>
</h2>
<p>
<ion-skeleton-text animated style='width: 80%;'></ion-skeleton-text>
<ion-skeleton-text animated style="width: 80%"></ion-skeleton-text>
</p>
<ion-note>
<ion-skeleton-text animated style='width: 20%'></ion-skeleton-text>
<ion-skeleton-text animated style="width: 20%"></ion-skeleton-text>
</ion-note>
</ion-col>
</ion-row>

View File

@@ -21,5 +21,4 @@ import {Component} from '@angular/core';
selector: 'stapps-skeleton-segment-button',
templateUrl: 'skeleton-segment-button.html',
})
export class SkeletonSegment {
}
export class SkeletonSegmentComponent {}

View File

@@ -1,4 +1,3 @@
<ion-segment-button>
<ion-skeleton-text animated style="width: 85%;"></ion-skeleton-text>
<ion-skeleton-text animated style="width: 85%"></ion-skeleton-text>
</ion-segment-button>

View File

@@ -21,5 +21,4 @@ import {Component} from '@angular/core';
selector: 'stapps-skeleton-simple-card',
templateUrl: 'skeleton-simple-card.html',
})
export class SkeletonSimpleCard {
}
export class SkeletonSimpleCardComponent {}

View File

@@ -3,6 +3,6 @@
<ion-skeleton-text animated style="width: 15%"></ion-skeleton-text>
</ion-card-header>
<ion-card-content>
<p><ion-skeleton-text animated style="width: 85%;"></ion-skeleton-text></p>
<p><ion-skeleton-text animated style="width: 85%"></ion-skeleton-text></p>
</ion-card-content>
</ion-card>

View File

@@ -24,7 +24,7 @@ import {DataRoutingService} from '../data-routing.service';
styleUrls: ['data-list-item.scss'],
templateUrl: 'data-list-item.html',
})
export class DataListItem {
export class DataListItemComponent {
/**
* Whether or not the list item should show a thumbnail
*/

View File

@@ -1,36 +1,90 @@
<ion-item class='ion-text-wrap' button='true' lines='inset' (click)='notifySelect()'>
<div class='item-height-placeholder'></div>
<ion-thumbnail slot='start' *ngIf='!hideThumbnail' class='ion-margin-end'>
<ion-icon color='medium' [attr.name]='item.type | dataIcon'></ion-icon>
<ion-item
class="ion-text-wrap"
button="true"
lines="inset"
(click)="notifySelect()"
>
<div class="item-height-placeholder"></div>
<ion-thumbnail slot="start" *ngIf="!hideThumbnail" class="ion-margin-end">
<ion-icon color="medium" [attr.name]="item.type | dataIcon"></ion-icon>
</ion-thumbnail>
<ion-label class='ion-text-wrap' [ngSwitch]='true'>
<ion-label class="ion-text-wrap" [ngSwitch]="true">
<div>
<stapps-catalog-list-item [item]='item' *ngSwitchCase="item.type === 'catalog'"></stapps-catalog-list-item>
<stapps-date-series-list-item [item]='item'
*ngSwitchCase="item.type === 'date series'"></stapps-date-series-list-item>
<stapps-dish-list-item [item]='item' *ngSwitchCase="item.type === 'dish'"></stapps-dish-list-item>
<stapps-event-list-item [item]='item' *ngSwitchCase="item.type === 'academic event'"></stapps-event-list-item>
<stapps-event-list-item [item]='item' *ngSwitchCase="item.type === 'sport course'"></stapps-event-list-item>
<stapps-favorite-list-item [item]='item' *ngSwitchCase="item.type === 'favorite'"></stapps-favorite-list-item>
<stapps-message-list-item [item]='item' *ngSwitchCase="item.type === 'message'"></stapps-message-list-item>
<stapps-organization-list-item [item]='item'
*ngSwitchCase="item.type === 'organization'"></stapps-organization-list-item>
<stapps-person-list-item [item]='item' *ngSwitchCase="item.type === 'person'"></stapps-person-list-item>
<stapps-place-list-item [item]='item' *ngSwitchCase="item.type === 'building'"></stapps-place-list-item>
<stapps-place-list-item [item]='item' *ngSwitchCase="item.type === 'floor'"></stapps-place-list-item>
<stapps-place-list-item [item]='item' *ngSwitchCase="item.type === 'point of interest'"></stapps-place-list-item>
<stapps-place-list-item [item]='item' *ngSwitchCase="item.type === 'room'"></stapps-place-list-item>
<stapps-semester-list-item [item]='item' *ngSwitchCase="item.type === 'semester'"></stapps-semester-list-item>
<stapps-video-list-item [item]='item' *ngSwitchCase="item.type === 'video'"></stapps-video-list-item>
<stapps-catalog-list-item
[item]="item"
*ngSwitchCase="item.type === 'catalog'"
></stapps-catalog-list-item>
<stapps-date-series-list-item
[item]="item"
*ngSwitchCase="item.type === 'date series'"
></stapps-date-series-list-item>
<stapps-dish-list-item
[item]="item"
*ngSwitchCase="item.type === 'dish'"
></stapps-dish-list-item>
<stapps-event-list-item
[item]="item"
*ngSwitchCase="item.type === 'academic event'"
></stapps-event-list-item>
<stapps-event-list-item
[item]="item"
*ngSwitchCase="item.type === 'sport course'"
></stapps-event-list-item>
<stapps-favorite-list-item
[item]="item"
*ngSwitchCase="item.type === 'favorite'"
></stapps-favorite-list-item>
<stapps-message-list-item
[item]="item"
*ngSwitchCase="item.type === 'message'"
></stapps-message-list-item>
<stapps-organization-list-item
[item]="item"
*ngSwitchCase="item.type === 'organization'"
></stapps-organization-list-item>
<stapps-person-list-item
[item]="item"
*ngSwitchCase="item.type === 'person'"
></stapps-person-list-item>
<stapps-place-list-item
[item]="item"
*ngSwitchCase="item.type === 'building'"
></stapps-place-list-item>
<stapps-place-list-item
[item]="item"
*ngSwitchCase="item.type === 'floor'"
></stapps-place-list-item>
<stapps-place-list-item
[item]="item"
*ngSwitchCase="item.type === 'point of interest'"
></stapps-place-list-item>
<stapps-place-list-item
[item]="item"
*ngSwitchCase="item.type === 'room'"
></stapps-place-list-item>
<stapps-semester-list-item
[item]="item"
*ngSwitchCase="item.type === 'semester'"
></stapps-semester-list-item>
<stapps-video-list-item
[item]="item"
*ngSwitchCase="item.type === 'video'"
></stapps-video-list-item>
<div *ngSwitchDefault>
<h2>
{{'name' | thingTranslate: item}}
{{ 'name' | thingTranslate: item }}
</h2>
<p *ngIf='item.description'>
<stapps-long-inline-text [text]="'description' | thingTranslate: item" [size]='80'></stapps-long-inline-text>
<p *ngIf="item.description">
<stapps-long-inline-text
[text]="'description' | thingTranslate: item"
[size]="80"
></stapps-long-inline-text>
</p>
</div>
<stapps-action-chip-list slot='end' [item]='item'></stapps-action-chip-list>
<stapps-action-chip-list
slot="end"
[item]="item"
></stapps-action-chip-list>
</div>
</ion-label>
</ion-item>

View File

@@ -9,12 +9,9 @@ describe('DataListComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DataListComponent ],
imports: [
TranslateModule.forRoot(),
]
})
.compileComponents();
declarations: [DataListComponent],
imports: [TranslateModule.forRoot()],
}).compileComponents();
}));
beforeEach(() => {

View File

@@ -19,6 +19,7 @@ import {
HostListener,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges,
@@ -36,41 +37,48 @@ import {BehaviorSubject, Observable, Subscription} from 'rxjs';
templateUrl: 'data-list.html',
styleUrls: ['data-list.scss'],
})
export class DataListComponent implements OnChanges, OnInit {
export class DataListComponent implements OnChanges, OnInit, OnDestroy {
/**
* Amount of list items left to show (in percent) that should trigger a data reload
*/
private readonly reloadThreshold = 0.2;
/**
* All SCThings to display
*/
@Input() items?: SCThings[];
/**
* Stream of SCThings for virtual scroll to consume
*/
itemStream = new BehaviorSubject<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 or not the list is to display SCThings of a single type
*/
@Input() singleType = false;
/**
* Items that display the skeleton list
*/
skeletonItems: number[];
/**
* Array of all subscriptions to Observables
*/
subscriptions: Subscription[] = [];
// tslint:disable-next-line: completed-docs
@ViewChild(CdkVirtualScrollViewport) viewPort: CdkVirtualScrollViewport;
/**
@@ -79,38 +87,39 @@ export class DataListComponent implements OnChanges, OnInit {
@HostListener('window.resize', ['$event'])
calcSkeletonItems() {
const itemHeight = 122;
this.skeletonItems = new Array(ceil(window.innerHeight / itemHeight));
this.skeletonItems = Array.from({
length: ceil(window.innerHeight / itemHeight),
});
}
/**
* Uniquely identifies item at a certain list index
*/
// tslint:disable-next-line:prefer-function-over-method
// eslint-disable-next-line class-methods-use-this
identifyItem(_index: number, item: SCThings) {
return item.uid;
}
// tslint:disable-next-line: completed-docs
ngOnChanges(changes: SimpleChanges): void {
if (Array.isArray(this.items) && typeof changes.items !== 'undefined') {
this.itemStream.next(this.items);
}
}
// tslint:disable-next-line: completed-docs
ngOnDestroy(): void {
for (const subscription of this.subscriptions) {
subscription.unsubscribe();
}
}
// tslint:disable-next-line: completed-docs
ngOnInit(): void {
this.calcSkeletonItems();
if (typeof this.resetToTop !== 'undefined') {
this.subscriptions.push(this.resetToTop.subscribe(() => {
this.viewPort.scrollToIndex(0);
}));
this.subscriptions.push(
this.resetToTop.subscribe(() => {
this.viewPort.scrollToIndex(0);
}),
);
}
}
@@ -125,7 +134,10 @@ export class DataListComponent implements OnChanges, OnInit {
* Function to call whenever scroll view visible range changed
*/
scrolled(_index: number) {
if ((this.items?.length ?? 0) - this.viewPort.getRenderedRange().end <= (this.items?.length ?? 0) * this.reloadThreshold) {
if (
(this.items?.length ?? 0) - this.viewPort.getRenderedRange().end <=
(this.items?.length ?? 0) * this.reloadThreshold
) {
this.notifyLoadMore();
}
}

View File

@@ -1,17 +1,27 @@
<ng-container *ngIf="itemStream | async as items">
<cdk-virtual-scroll-viewport itemSize="80" minBufferPx="1500" maxBufferPx="2000" (scrolledIndexChange)="scrolled($event)"
[style.display]="items && items.length ? 'block': 'none'">
<cdk-virtual-scroll-viewport
itemSize="80"
minBufferPx="1500"
maxBufferPx="2000"
(scrolledIndexChange)="scrolled($event)"
[style.display]="items && items.length ? 'block' : 'none'"
>
<ion-list>
<stapps-data-list-item *cdkVirtualFor="let item of items;trackBy: identifyItem" [item]="item"
[hideThumbnail]="singleType"></stapps-data-list-item>
<stapps-data-list-item
*cdkVirtualFor="let item of items; trackBy: identifyItem"
[item]="item"
[hideThumbnail]="singleType"
></stapps-data-list-item>
</ion-list>
</cdk-virtual-scroll-viewport>
</ng-container>
<div [style.display]="items && items.length === 0 ? 'block': 'none'">
<ion-label class='notFoundContainer'>
{{'search.nothing_found' | translate | titlecase}}
<div [style.display]="items && items.length === 0 ? 'block' : 'none'">
<ion-label class="notFoundContainer">
{{ 'search.nothing_found' | translate | titlecase }}
</ion-label>
</div>
<ion-list [style.display]="items ? 'none': 'block'">
<stapps-skeleton-list-item *ngFor="let skeleton of skeletonItems"></stapps-skeleton-list-item>
<ion-list [style.display]="items ? 'none' : 'block'">
<stapps-skeleton-list-item
*ngFor="let skeleton of skeletonItems"
></stapps-skeleton-list-item>
</ion-list>

View File

@@ -37,11 +37,11 @@ export class FoodDataListComponent extends SearchPageComponent {
type: 'value',
},
{
arguments: {
field: 'categories',
value: 'student canteen',
},
type: 'value',
arguments: {
field: 'categories',
value: 'student canteen',
},
type: 'value',
},
{
arguments: {

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, OnInit} from '@angular/core';
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {Router} from '@angular/router';
import {AlertController} from '@ionic/angular';
import {
@@ -38,51 +38,62 @@ import {DataProvider} from '../data.provider';
templateUrl: 'search-page.html',
providers: [ContextMenuService],
})
export class SearchPageComponent implements OnInit {
export class SearchPageComponent implements OnInit, OnDestroy {
/**
* Api query filter
*/
filterQuery: SCSearchFilter | undefined;
/**
* Filters the search should be initialized with
*/
@Input() forcedFilter?: SCSearchFilter;
/**
* Thing counter to start query the next page from
*/
from = 0;
/**
* Container for queried things
*/
items: Promise<SCThings[]>;
/**
* Page size of queries
*/
pageSize = 30;
/**
* Search value from search bar
*/
queryText: string;
/**
* Emits when there is a change in the query (search, sort or filter changed)
*/
queryChanged = new Subject<void>();
/**
* Subject to handle search text changes
*/
queryTextChanged = new Subject<string>();
/**
* Time to wait for search query if search text is changing
*/
searchQueryDueTime = 1000;
/**
* Search response only ever contains a single SCThingType
*/
singleTypeResponse = false;
/**
* Api query sorting
*/
sortQuery: SCSearchSort | undefined;
/**
* Array of all subscriptions to Observables
*/
@@ -110,21 +121,23 @@ export class SearchPageComponent implements OnInit {
) {
this.initialize();
combineLatest(
[this.queryTextChanged.pipe(debounceTime(this.searchQueryDueTime),
combineLatest([
this.queryTextChanged.pipe(
debounceTime(this.searchQueryDueTime),
distinctUntilChanged(),
startWith(this.queryText),
),
this.contextMenuService.filterQueryChanged$.pipe(startWith(this.filterQuery)),
this.contextMenuService.filterQueryChanged$.pipe(
startWith(this.filterQuery),
),
this.contextMenuService.sortQueryChanged$.pipe(startWith(this.sortQuery)),
])
.subscribe(async (query) => {
this.queryText = query[0];
this.filterQuery = query[1];
this.sortQuery = query[2];
this.from = 0;
await this.fetchAndUpdateItems();
this.queryChanged.next();
]).subscribe(async query => {
this.queryText = query[0];
this.filterQuery = query[1];
this.sortQuery = query[2];
this.from = 0;
await this.fetchAndUpdateItems();
this.queryChanged.next();
});
this.fetchAndUpdateItems();
@@ -132,19 +145,21 @@ export class SearchPageComponent implements OnInit {
/**
* Subscribe to 'settings.changed' events
*/
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:
this.subscriptions.push(
this.settingsProvider.settingsActionChanged$.subscribe(
({type, payload}) => {
if (type === 'stapps.settings.changed') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const {category, name, value} = payload!;
this.logger.log(`received event "settings.changed" with category:
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
}
},
));
this.subscriptions.push(this.dataRoutingService.itemSelectListener()
.subscribe((item) => {
}
},
),
this.dataRoutingService.itemSelectListener().subscribe(item => {
void this.router.navigate(['data-detail', item.uid]);
}));
}),
);
}
/**
@@ -185,13 +200,15 @@ export class SearchPageComponent implements OnInit {
};
}
return this.dataProvider.search(searchOptions)
.then(async (result) => {
this.singleTypeResponse = result.facets.find(facet => facet.field === 'type')?.buckets.length === 1;
return this.dataProvider.search(searchOptions).then(
async result => {
this.singleTypeResponse =
result.facets.find(facet => facet.field === 'type')?.buckets
.length === 1;
if (append) {
let items = await this.items;
// append results
items = items.concat(result.data);
items = [...items, ...result.data];
this.items = (async () => items)();
} else {
// override items with results
@@ -201,21 +218,23 @@ export class SearchPageComponent implements OnInit {
return result.data;
})();
}
}, async (err) => {
},
async error => {
const alert: HTMLIonAlertElement = await this.alertController.create({
buttons: ['Dismiss'],
header: 'Error',
subHeader: err.message,
subHeader: error.message,
});
await alert.present();
});
},
);
}
/**
* Set starting values (e.g. forced filter, which can be set in components inheriting this one)
*/
// tslint:disable-next-line:prefer-function-over-method
// eslint-disable-next-line class-methods-use-this
initialize() {
// nothing to do here
}
@@ -223,7 +242,7 @@ export class SearchPageComponent implements OnInit {
/**
* Loads next page of things
*/
// tslint:disable-next-line:no-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async loadMore(): Promise<void> {
this.from += this.pageSize;
await this.fetchAndUpdateItems(true);

View File

@@ -10,10 +10,19 @@
<ion-icon name="options"></ion-icon>
</ion-menu-button>
</ion-buttons>
<ion-searchbar (ngModelChange)="searchStringChanged($event)" [(ngModel)]="queryText"></ion-searchbar>
<ion-searchbar
(ngModelChange)="searchStringChanged($event)"
[(ngModel)]="queryText"
></ion-searchbar>
</ion-toolbar>
</ion-header>
<ion-content>
<stapps-data-list id="data-list" [items]="items | async" [singleType]="singleTypeResponse" (loadmore)="loadMore()" [resetToTop]="queryChanged.asObservable()"></stapps-data-list>
<stapps-data-list
id="data-list"
[items]="items | async"
[singleType]="singleTypeResponse"
(loadmore)="loadMore()"
[resetToTop]="queryChanged.asObservable()"
></stapps-data-list>
</ion-content>

View File

@@ -14,7 +14,10 @@
*/
import {HttpClient, HttpResponse} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {HttpClientInterface, HttpClientRequest} from '@openstapps/api/lib/http-client-interface';
import {
HttpClientInterface,
HttpClientRequest,
} from '@openstapps/api/lib/http-client-interface';
/**
* HttpClient that is based on the Angular HttpClient (@TODO: move it to provider or independent package)
@@ -25,11 +28,11 @@ export class StAppsWebHttpClient implements HttpClientInterface {
*
* @param http TODO
*/
constructor(private readonly http: HttpClient) {
}
constructor(private readonly http: HttpClient) {}
/**
* Make a request
*
* @param requestConfig Configuration of the request
*/
async request<TYPE_OF_BODY>(
@@ -59,14 +62,21 @@ export class StAppsWebHttpClient implements HttpClientInterface {
}
try {
const response: HttpResponse<TYPE_OF_BODY> = await this.http.request<TYPE_OF_BODY>(
requestConfig.method || 'GET', requestConfig.url.toString(), options)
const response: HttpResponse<TYPE_OF_BODY> = await this.http
.request<TYPE_OF_BODY>(
requestConfig.method || 'GET',
requestConfig.url.toString(),
options,
)
.toPromise();
// tslint:disable-next-line:prefer-object-spread
return Object.assign(response, {statusCode: response.status, body: response.body || {}});
} catch (err) {
throw Error(err);
// eslint-disable-next-line prefer-object-spread
return Object.assign(response, {
statusCode: response.status,
body: response.body || {},
});
} catch (error) {
throw new Error(error);
}
}
}

View File

@@ -1,6 +1,16 @@
<stapps-simple-card [title]="'categories' | propertyNameTranslate: item | titlecase" [content]="'categories' | thingTranslate: item">
<stapps-simple-card
[title]="'categories' | propertyNameTranslate: item | titlecase"
[content]="'categories' | thingTranslate: item"
>
</stapps-simple-card>
<stapps-simple-card *ngIf="item.datePublished" [title]="'datePublished' | propertyNameTranslate: item | titlecase"
[content]="item.datePublished | amDateFormat:'ll'"></stapps-simple-card>
<stapps-simple-card [title]="'articleBody' | propertyNameTranslate: item | titlecase" [content]="'articleBody' | thingTranslate: item" [isMarkdown]="true">
<stapps-simple-card
*ngIf="item.datePublished"
[title]="'datePublished' | propertyNameTranslate: item | titlecase"
[content]="item.datePublished | amDateFormat: 'll'"
></stapps-simple-card>
<stapps-simple-card
[title]="'articleBody' | propertyNameTranslate: item | titlecase"
[content]="'articleBody' | thingTranslate: item"
[isMarkdown]="true"
>
</stapps-simple-card>

View File

@@ -14,7 +14,7 @@
*/
import {Component, Input} from '@angular/core';
import {SCArticle} from '@openstapps/core';
import {DataListItem} from '../../list/data-list-item.component';
import {DataListItemComponent} from '../../list/data-list-item.component';
/**
* TODO
@@ -23,18 +23,9 @@ import {DataListItem} from '../../list/data-list-item.component';
selector: 'stapps-article-list-item',
templateUrl: 'article-list-item.html',
})
export class ArticleListItem extends DataListItem {
export class ArticleListItemComponent extends DataListItemComponent {
/**
* TODO
*/
@Input() item: SCArticle;
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method
ngOnInit() {
// TODO: translation
}
}

View File

@@ -1,12 +1,15 @@
<ion-grid>
<ion-row>
<ion-col>
<h2 class="name">{{'name' | thingTranslate: item}}</h2>
<h2 class="name">{{ 'name' | thingTranslate: item }}</h2>
<p *ngIf="item.keywords">
<stapps-long-inline-text [text]="item.keywords.join(', ')" [size]="110"></stapps-long-inline-text>
<stapps-long-inline-text
[text]="item.keywords.join(', ')"
[size]="110"
></stapps-long-inline-text>
</p>
<ion-note>
{{'type' | thingTranslate: item}}
{{ 'type' | thingTranslate: item }}
</ion-note>
</ion-col>
</ion-row>

View File

@@ -1,2 +1,5 @@
<stapps-simple-card [title]="'categories' | propertyNameTranslate: item | titlecase" [content]="'categories' | thingTranslate: item">
<stapps-simple-card
[title]="'categories' | propertyNameTranslate: item | titlecase"
[content]="'categories' | thingTranslate: item"
>
</stapps-simple-card>

View File

@@ -14,7 +14,7 @@
*/
import {Component, Input} from '@angular/core';
import {SCCatalog} from '@openstapps/core';
import {DataListItem} from '../../list/data-list-item.component';
import {DataListItemComponent} from '../../list/data-list-item.component';
/**
* TODO
@@ -23,18 +23,9 @@ import {DataListItem} from '../../list/data-list-item.component';
selector: 'stapps-catalog-list-item',
templateUrl: 'catalog-list-item.html',
})
export class CatalogListItem extends DataListItem {
export class CatalogListItemComponent extends DataListItemComponent {
/**
* TODO
*/
@Input() item: SCCatalog;
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method
ngOnInit() {
// TODO: translation
}
}

View File

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

View File

@@ -1,12 +1,28 @@
<ion-card *ngIf="item.inPlace">
<ion-card-header>
{{'inPlace' | propertyNameTranslate: item | titlecase}}
{{ 'inPlace' | propertyNameTranslate: item | titlecase }}
</ion-card-header>
<ion-card-content>
<ion-icon name="location"></ion-icon> <a [routerLink]="['/data-detail', item.inPlace.uid]">{{'name' | thingTranslate: item.inPlace}}</a>
<stapps-address-detail *ngIf="item.inPlace.address" [address]="item.inPlace.address"></stapps-address-detail>
<ion-icon name="location"></ion-icon>
<a [routerLink]="['/data-detail', item.inPlace.uid]">{{
'name' | thingTranslate: item.inPlace
}}</a>
<stapps-address-detail
*ngIf="item.inPlace.address"
[address]="item.inPlace.address"
></stapps-address-detail>
</ion-card-content>
</ion-card>
<stapps-simple-card [title]="'Duration'" [content]="[item.duration | amDuration:'minutes']"></stapps-simple-card>
<stapps-simple-card *ngIf="item.performers" [title]="'performers' | propertyNameTranslate: item | titlecase" [content]="item.performers"></stapps-simple-card>
<stapps-offers-detail *ngIf="item.offers" [offers]="item.offers"></stapps-offers-detail>
<stapps-simple-card
[title]="'Duration'"
[content]="[item.duration | amDuration: 'minutes']"
></stapps-simple-card>
<stapps-simple-card
*ngIf="item.performers"
[title]="'performers' | propertyNameTranslate: item | titlecase"
[content]="item.performers"
></stapps-simple-card>
<stapps-offers-detail
*ngIf="item.offers"
[offers]="item.offers"
></stapps-offers-detail>

View File

@@ -14,7 +14,7 @@
*/
import {Component, Input} from '@angular/core';
import {SCDateSeries} from '@openstapps/core';
import {DataListItem} from '../../list/data-list-item.component';
import {DataListItemComponent} from '../../list/data-list-item.component';
/**
* TODO
@@ -23,18 +23,9 @@ import {DataListItem} from '../../list/data-list-item.component';
selector: 'stapps-date-series-list-item',
templateUrl: 'date-series-list-item.html',
})
export class DateSeriesListItem extends DataListItem {
export class DateSeriesListItemComponent extends DataListItemComponent {
/**
* TODO
*/
@Input() item: SCDateSeries;
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method
ngOnInit() {
// TODO: translation
}
}

View File

@@ -2,19 +2,29 @@
<ion-row>
<ion-col>
<div class="ion-text-wrap">
<h2 class="name">{{'name' | thingTranslate: item}}</h2>
<h2 class="name">{{ 'name' | thingTranslate: item }}</h2>
<p>
<ion-icon name="calendar"></ion-icon>
<span>
{{item.frequency}}, {{item.dates[0] | amDateFormat:'dddd'}}
<span>({{item.dates[0] | amDateFormat:'ll'}} - {{item.dates[item.dates.length - 1] | amDateFormat:'ll'}})</span>
</span>
{{ item.frequency }}, {{ item.dates[0] | amDateFormat: 'dddd' }}
<span
>({{ item.dates[0] | amDateFormat: 'll' }} -
{{
item.dates[item.dates.length - 1] | amDateFormat: 'll'
}})</span
>
</span>
</p>
<ion-note *ngIf="item.event.type === 'academic event'">{{'categories' | thingTranslate: item.event | join: ', '}}</ion-note>
<ion-note *ngIf="item.event.type === 'academic event'">{{
'categories' | thingTranslate: item.event | join: ', '
}}</ion-note>
</div>
</ion-col>
<ion-col width-20 text-right>
<stapps-offers-in-list *ngIf="item.offers" [offers]="item.offers"></stapps-offers-in-list>
<stapps-offers-in-list
*ngIf="item.offers"
[offers]="item.offers"
></stapps-offers-in-list>
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -1,51 +1,79 @@
<stapps-simple-card [title]="'categories' | propertyNameTranslate: item | titlecase" [content]="'categories' | thingTranslate: item">
<stapps-simple-card
[title]="'categories' | propertyNameTranslate: item | titlecase"
[content]="'categories' | thingTranslate: item"
>
</stapps-simple-card>
<ion-card *ngIf="item.characteristics">
<ion-card-header>{{ 'characteristics' | propertyNameTranslate: item | titlecase }}</ion-card-header>
<ion-card-content *ngFor="let characteristic of ('characteristics' | thingTranslate: item)">
<ion-card-header>{{
'characteristics' | propertyNameTranslate: item | titlecase
}}</ion-card-header>
<ion-card-content
*ngFor="let characteristic of 'characteristics' | thingTranslate: item"
>
<p>
<img *ngIf="characteristic.image"
[src]="characteristic.image" alt=""/><span>&nbsp;{{characteristic.name}}</span>&nbsp;&nbsp;
<img
*ngIf="characteristic.image"
[src]="characteristic.image"
alt=""
/><span>&nbsp;{{ characteristic.name }}</span
>&nbsp;&nbsp;
</p>
</ion-card-content>
</ion-card>
<stapps-offers-detail *ngIf="item.offers" [offers]="item.offers"></stapps-offers-detail>
<stapps-simple-card *ngIf="item.additives" [title]="'additives' | propertyNameTranslate: item" [content]="'additives' | thingTranslate: item | join: ', '">
<stapps-offers-detail
*ngIf="item.offers"
[offers]="item.offers"
></stapps-offers-detail>
<stapps-simple-card
*ngIf="item.additives"
[title]="'additives' | propertyNameTranslate: item"
[content]="'additives' | thingTranslate: item | join: ', '"
>
</stapps-simple-card>
<ion-card *ngIf="item.nutrition">
<ion-card-header>{{'data.types.dish.detail.AVG_NUTRITION_INFO' | translate }}</ion-card-header>
<ion-card-header>{{
'data.types.dish.detail.AVG_NUTRITION_INFO' | translate
}}</ion-card-header>
<ion-card-content>
<ion-grid>
<ion-row *ngIf="item.nutrition.calories">
<ion-col>{{'data.types.dish.detail.CALORIES' | translate }}:</ion-col>
<ion-col>{{ 'data.types.dish.detail.CALORIES' | translate }}:</ion-col>
<ion-col width-20 text-right>
{{item.nutrition.calories | numberLocalized}} kcal
{{ item.nutrition.calories | numberLocalized }} kcal
</ion-col>
</ion-row>
<ion-row *ngIf="item.nutrition.fatContent">
<ion-col>{{'data.types.dish.detail.FAT_TOTAL' | translate }}:</ion-col>
<ion-col>{{ 'data.types.dish.detail.FAT_TOTAL' | translate }}:</ion-col>
<ion-col width-20 text-right>
{{item.nutrition.fatContent | numberLocalized}} g <span *ngIf="item.nutrition.saturatedFatContent">({{'data.types.dish.detail.FAT_SATURATED' | translate }}:
{{item.nutrition.saturatedFatContent | numberLocalized}} g)</span>
{{ item.nutrition.fatContent | numberLocalized }} g
<span *ngIf="item.nutrition.saturatedFatContent"
>({{ 'data.types.dish.detail.FAT_SATURATED' | translate }}:
{{ item.nutrition.saturatedFatContent | numberLocalized }} g)</span
>
</ion-col>
</ion-row>
<ion-row *ngIf="item.nutrition.carbohydrateContent">
<ion-col>{{'data.types.dish.detail.CARBOHYDRATE' | translate }}:</ion-col>
<ion-col
>{{ 'data.types.dish.detail.CARBOHYDRATE' | translate }}:</ion-col
>
<ion-col width-20 text-right>
{{item.nutrition.carbohydrateContent | numberLocalized}} g <span *ngIf="item.nutrition.sugarContent">({{'data.types.dish.detail.SUGAR' | translate }}:
{{item.nutrition.sugarContent | numberLocalized}} g)</span>
{{ item.nutrition.carbohydrateContent | numberLocalized }} g
<span *ngIf="item.nutrition.sugarContent"
>({{ 'data.types.dish.detail.SUGAR' | translate }}:
{{ item.nutrition.sugarContent | numberLocalized }} g)</span
>
</ion-col>
</ion-row>
<ion-row *ngIf="item.nutrition.saltContent">
<ion-col>{{'data.types.dish.detail.SALT' | translate }}:</ion-col>
<ion-col>{{ 'data.types.dish.detail.SALT' | translate }}:</ion-col>
<ion-col width-20 text-right>
{{item.nutrition.saltContent | numberLocalized}} g
{{ item.nutrition.saltContent | numberLocalized }} g
</ion-col>
</ion-row>
<ion-row *ngIf="item.nutrition.proteinContent">
<ion-col>{{'data.types.dish.detail.PROTEIN' | translate }}:</ion-col>
<ion-col>{{ 'data.types.dish.detail.PROTEIN' | translate }}:</ion-col>
<ion-col width-20 text-right>
{{item.nutrition.proteinContent | numberLocalized}} g
{{ item.nutrition.proteinContent | numberLocalized }} g
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -14,8 +14,7 @@
*/
import {Component, Input} from '@angular/core';
import {SCDish} from '@openstapps/core';
// import {SettingsProvider} from '../../../settings/settings.provider';
import {DataListItem} from '../../list/data-list-item.component';
import {DataListItemComponent} from '../../list/data-list-item.component';
/**
* TODO
@@ -24,15 +23,9 @@ import {DataListItem} from '../../list/data-list-item.component';
selector: 'stapps-dish-list-item',
templateUrl: 'dish-list-item.html',
})
export class DishListItem extends DataListItem {
export class DishListItemComponent extends DataListItemComponent {
/**
* TODO
*/
@Input() item: SCDish;
// tslint:disable-next-line: completed-docs prefer-function-over-method
ngOnInit() {
// custom init
}
}

View File

@@ -2,14 +2,17 @@
<ion-row>
<ion-col>
<div class="ion-text-wrap">
<h2 class="name">{{'name' | thingTranslate: item}}</h2>
<p>{{'description' | thingTranslate: item}}</p>
<p>{{'categories' | thingTranslate: item | join: ', '}}</p>
<h2 class="name">{{ 'name' | thingTranslate: item }}</h2>
<p>{{ 'description' | thingTranslate: item }}</p>
<p>{{ 'categories' | thingTranslate: item | join: ', ' }}</p>
</div>
</ion-col>
<ion-col width-10 text-right>
<div class="ion-text-end">
<stapps-offers-in-list *ngIf="item.offers" [offers]="item.offers"></stapps-offers-in-list>
<stapps-offers-in-list
*ngIf="item.offers"
[offers]="item.offers"
></stapps-offers-in-list>
</div>
</ion-col>
</ion-row>

View File

@@ -13,7 +13,12 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCAcademicEvent, SCSportCourse, SCThing, SCTranslations} from '@openstapps/core';
import {
SCAcademicEvent,
SCSportCourse,
SCThing,
SCTranslations,
} from '@openstapps/core';
import {SCThingTranslator} from '@openstapps/core';
/**
@@ -28,18 +33,22 @@ export class EventDetailContentComponent {
* TODO
*/
@Input() item: SCAcademicEvent | SCSportCourse;
/**
* TODO
*/
@Input() language: keyof SCTranslations<SCThing>;
/**
* TODO
*/
objectKeys = Object.keys;
/**
* TODO
*/
translator: SCThingTranslator;
/**
* TODO
*/

View File

@@ -1,14 +1,46 @@
<ng-container *ngIf="item.type === 'academic event'">
<stapps-simple-card *ngIf="item.categories" [title]="'Categories'" [content]="translator.translate(item).categories">
<stapps-simple-card
*ngIf="item.categories"
[title]="'Categories'"
[content]="translator.translate(item).categories"
>
</stapps-simple-card>
<stapps-simple-card *ngIf="item.catalogs" [title]="'Catalogs'" [content]="item.catalogs"></stapps-simple-card>
<stapps-simple-card *ngIf="item.performers" [title]="'Performers'" [content]="item.performers"></stapps-simple-card>
<stapps-simple-card *ngIf="item.organizers" [title]="'Organizers'" [content]="item.organizers"></stapps-simple-card>
<stapps-simple-card *ngIf="item.majors" [title]="'Majors'" [content]="item.majors"></stapps-simple-card>
<stapps-simple-card
*ngIf="item.catalogs"
[title]="'Catalogs'"
[content]="item.catalogs"
></stapps-simple-card>
<stapps-simple-card
*ngIf="item.performers"
[title]="'Performers'"
[content]="item.performers"
></stapps-simple-card>
<stapps-simple-card
*ngIf="item.organizers"
[title]="'Organizers'"
[content]="item.organizers"
></stapps-simple-card>
<stapps-simple-card
*ngIf="item.majors"
[title]="'Majors'"
[content]="item.majors"
></stapps-simple-card>
</ng-container>
<ng-container *ngIf="item.type === 'sport course'">
<stapps-simple-card *ngIf="item.catalogs" [title]="'Catalogs'" [content]="item.catalogs"></stapps-simple-card>
<stapps-simple-card *ngIf="item.performers" [title]="'Performers'" [content]="item.performers"></stapps-simple-card>
<stapps-simple-card *ngIf="item.organizers" [title]="'Organizers'" [content]="item.organizers"></stapps-simple-card>
<stapps-simple-card
*ngIf="item.catalogs"
[title]="'Catalogs'"
[content]="item.catalogs"
></stapps-simple-card>
<stapps-simple-card
*ngIf="item.performers"
[title]="'Performers'"
[content]="item.performers"
></stapps-simple-card>
<stapps-simple-card
*ngIf="item.organizers"
[title]="'Organizers'"
[content]="item.organizers"
></stapps-simple-card>
</ng-container>

View File

@@ -14,7 +14,7 @@
*/
import {Component, Input} from '@angular/core';
import {SCAcademicEvent, SCSportCourse} from '@openstapps/core';
import {DataListItem} from '../../list/data-list-item.component';
import {DataListItemComponent} from '../../list/data-list-item.component';
/**
* TODO
@@ -23,17 +23,9 @@ import {DataListItem} from '../../list/data-list-item.component';
selector: 'stapps-event-list-item',
templateUrl: 'event-list-item.html',
})
export class EventListItemComponent extends DataListItem {
export class EventListItemComponent extends DataListItemComponent {
/**
* TODO
*/
@Input() item: SCAcademicEvent | SCSportCourse;
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method
ngOnInit() {
// TODO: translation
}
}

View File

@@ -2,19 +2,19 @@
<ion-row *ngIf="item.type === 'academic event'">
<ion-col>
<div class="ion-text-wrap">
<h2 class="name">{{item.name}}</h2>
<p *ngIf="item.description">{{item.description}}</p>
<p *ngIf="item.academicTerms">{{item.academicTerms[0].name}}</p>
<ion-note>{{item.type}} ({{item.categories.join(', ')}})</ion-note>
<h2 class="name">{{ item.name }}</h2>
<p *ngIf="item.description">{{ item.description }}</p>
<p *ngIf="item.academicTerms">{{ item.academicTerms[0].name }}</p>
<ion-note>{{ item.type }} ({{ item.categories.join(', ') }})</ion-note>
</div>
</ion-col>
</ion-row>
<ion-row *ngIf="item.type === 'sport course'">
<ion-col>
<h2 class="name">{{item.name}}</h2>
<p *ngIf="item.description">{{item.description}}</p>
<p *ngIf="item.academicTerms">{{item.academicTerms[0].name}}</p>
<ion-note>{{item.type}}</ion-note>
<h2 class="name">{{ item.name }}</h2>
<p *ngIf="item.description">{{ item.description }}</p>
<p *ngIf="item.academicTerms">{{ item.academicTerms[0].name }}</p>
<ion-note>{{ item.type }}</ion-note>
</ion-col>
</ion-row>
</ion-grid>

View File

@@ -14,7 +14,7 @@
*/
import {Component, Input} from '@angular/core';
import {SCFavorite} from '@openstapps/core';
import {DataListItem} from '../../list/data-list-item.component';
import {DataListItemComponent} from '../../list/data-list-item.component';
/**
* TODO
@@ -23,18 +23,9 @@ import {DataListItem} from '../../list/data-list-item.component';
selector: 'stapps-favorite-list-item',
templateUrl: 'favorite-list-item.html',
})
export class FavoriteListItem extends DataListItem {
export class FavoriteListItemComponent extends DataListItemComponent {
/**
* TODO
*/
@Input() item: SCFavorite;
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method
ngOnInit() {
// TODO: translation
}
}

View File

@@ -2,11 +2,21 @@
<ion-row>
<ion-col>
<div class="ion-text-wrap">
<h2 class="name">{{'name' | thingTranslate: item}}: {{'name' | thingTranslate: item.data}}</h2>
<h2 class="name">
{{ 'name' | thingTranslate: item }}:
{{ 'name' | thingTranslate: item.data }}
</h2>
<p *ngIf="item.data.description">
<stapps-long-inline-text [text]="'description' | thingTranslate: item.data" [size]="80"></stapps-long-inline-text>
<stapps-long-inline-text
[text]="'description' | thingTranslate: item.data"
[size]="80"
></stapps-long-inline-text>
</p>
<ion-note>{{'type' | thingTranslate: item}} ({{'type' | thingTranslate: item.data}})</ion-note>
<ion-note
>{{ 'type' | thingTranslate: item }} ({{
'type' | thingTranslate: item.data
}})</ion-note
>
</div>
</ion-col>
<ion-col width-20 text-right>

View File

@@ -1,5 +1,23 @@
<stapps-simple-card [title]="'messageBody' | propertyNameTranslate: item | titlecase" [content]="'messageBody' | thingTranslate: item"></stapps-simple-card>
<stapps-simple-card [title]="'audiences' | propertyNameTranslate: item | titlecase" [content]="'audiences' | thingTranslate: item"></stapps-simple-card>
<stapps-simple-card *ngIf="item.datePublished" [title]="'datePublished' | propertyNameTranslate: item | titlecase" [content]="item.datePublished | amDateFormat:'ll'"></stapps-simple-card>
<stapps-simple-card *ngIf="item.authors" [title]="'authors' | propertyNameTranslate: item | titlecase" [content]="item.authors"></stapps-simple-card>
<stapps-simple-card *ngIf="item.publishers" [title]="'publishers' | propertyNameTranslate: item | titlecase" [content]="item.publishers"></stapps-simple-card>
<stapps-simple-card
[title]="'messageBody' | propertyNameTranslate: item | titlecase"
[content]="'messageBody' | thingTranslate: item"
></stapps-simple-card>
<stapps-simple-card
[title]="'audiences' | propertyNameTranslate: item | titlecase"
[content]="'audiences' | thingTranslate: item"
></stapps-simple-card>
<stapps-simple-card
*ngIf="item.datePublished"
[title]="'datePublished' | propertyNameTranslate: item | titlecase"
[content]="item.datePublished | amDateFormat: 'll'"
></stapps-simple-card>
<stapps-simple-card
*ngIf="item.authors"
[title]="'authors' | propertyNameTranslate: item | titlecase"
[content]="item.authors"
></stapps-simple-card>
<stapps-simple-card
*ngIf="item.publishers"
[title]="'publishers' | propertyNameTranslate: item | titlecase"
[content]="item.publishers"
></stapps-simple-card>

View File

@@ -14,7 +14,7 @@
*/
import {Component, Input} from '@angular/core';
import {SCMessage} from '@openstapps/core';
import {DataListItem} from '../../list/data-list-item.component';
import {DataListItemComponent} from '../../list/data-list-item.component';
/**
* TODO
@@ -23,18 +23,9 @@ import {DataListItem} from '../../list/data-list-item.component';
selector: 'stapps-message-list-item',
templateUrl: 'message-list-item.html',
})
export class MessageListItem extends DataListItem {
export class MessageListItemComponent extends DataListItemComponent {
/**
* TODO
*/
@Input() item: SCMessage;
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method
ngOnInit() {
// TODO: translation
}
}

View File

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

View File

@@ -1,9 +1,15 @@
<ion-card *ngIf="item.inPlace">
<ion-card-header>
{{'inPlace' | propertyNameTranslate: item | titlecase}}
{{ 'inPlace' | propertyNameTranslate: item | titlecase }}
</ion-card-header>
<ion-card-content>
<ion-icon name="location"></ion-icon> <a [routerLink]="['/data-detail', item.inPlace.uid]">{{'name' | thingTranslate: item.inPlace}}</a>
<stapps-address-detail *ngIf="item.inPlace.address" [address]="item.inPlace.address"></stapps-address-detail>
<ion-icon name="location"></ion-icon>
<a [routerLink]="['/data-detail', item.inPlace.uid]">{{
'name' | thingTranslate: item.inPlace
}}</a>
<stapps-address-detail
*ngIf="item.inPlace.address"
[address]="item.inPlace.address"
></stapps-address-detail>
</ion-card-content>
</ion-card>

View File

@@ -14,7 +14,7 @@
*/
import {Component, Input} from '@angular/core';
import {SCOrganization} from '@openstapps/core';
import {DataListItem} from '../../list/data-list-item.component';
import {DataListItemComponent} from '../../list/data-list-item.component';
/**
* TODO
@@ -23,18 +23,9 @@ import {DataListItem} from '../../list/data-list-item.component';
selector: 'stapps-organization-list-item',
templateUrl: 'organization-list-item.html',
})
export class OrganizationListItem extends DataListItem {
export class OrganizationListItemComponent extends DataListItemComponent {
/**
* TODO
*/
@Input() item: SCOrganization;
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method
ngOnInit() {
// TODO: translation
}
}

View File

@@ -2,14 +2,17 @@
<ion-row>
<ion-col>
<div class="ion-text-wrap">
<h2 class="name">{{'name' | thingTranslate: item}}</h2>
<p *ngIf="item.description">{{'description' | thingTranslate: item}}</p>
<ion-note>{{'type' | thingTranslate: item}}</ion-note>
<h2 class="name">{{ 'name' | thingTranslate: item }}</h2>
<p *ngIf="item.description">
{{ 'description' | thingTranslate: item }}
</p>
<ion-note>{{ 'type' | thingTranslate: item }}</ion-note>
</div>
</ion-col>
<ion-col width-20 text-right *ngIf="item.inPlace">
<span *ngIf="item.inPlace">
<ion-icon name="location"></ion-icon> {{'name' | thingTranslate: item.inPlace}}
<ion-icon name="location"></ion-icon>
{{ 'name' | thingTranslate: item.inPlace }}
</span>
</ion-col>
</ion-row>

View File

@@ -1,33 +1,88 @@
<ion-card *ngIf="item.workLocations">
<ion-card-header>
{{'type' | thingTranslate: item.workLocations[0] | titlecase}}
{{ 'type' | thingTranslate: item.workLocations[0] | titlecase }}
</ion-card-header>
<ion-card-content>
<ng-container *ngIf="item.workLocations.length === 1">
<p *ngIf="item.workLocations[0].telephone">{{'telephone' | propertyNameTranslate: item.workLocations[0] | titlecase}}: <a
[href]="'tel:' + item.workLocations[0].telephone">{{item.workLocations[0].telephone}}</a>
<p *ngIf="item.workLocations[0].telephone">
{{
'telephone'
| propertyNameTranslate: item.workLocations[0]
| titlecase
}}:
<a [href]="'tel:' + item.workLocations[0].telephone">{{
item.workLocations[0].telephone
}}</a>
</p>
<p *ngIf="item.workLocations[0].email">{{'email' | propertyNameTranslate: item.workLocations[0] | titlecase}}: <a
[href]="'mailto:' + item.workLocations[0].email">{{item.workLocations[0].email}}</a></p>
<p *ngIf="item.workLocations[0].faxNumber">{{'faxNumber' | propertyNameTranslate: item.workLocations[0] | titlecase}}: {{item.workLocations[0].faxNumber}}</p>
<p *ngIf="item.workLocations[0].url">{{'url' | propertyNameTranslate: item.workLocations[0] | titlecase}}: <a
[href]="item.workLocations[0].url">{{item.workLocations[0].url}}</a>
<p *ngIf="item.workLocations[0].email">
{{
'email' | propertyNameTranslate: item.workLocations[0] | titlecase
}}:
<a [href]="'mailto:' + item.workLocations[0].email">{{
item.workLocations[0].email
}}</a>
</p>
<p *ngIf="item.workLocations[0].areaServed">{{'areaServed' | propertyNameTranslate: item.workLocations[0] | titlecase}}: <a
[routerLink]="[ '/data-detail', item.workLocations[0].areaServed.uid]">{{item.workLocations[0].areaServed.name}}</a>
<p *ngIf="item.workLocations[0].faxNumber">
{{
'faxNumber'
| propertyNameTranslate: item.workLocations[0]
| titlecase
}}: {{ item.workLocations[0].faxNumber }}
</p>
<p *ngIf="item.workLocations[0].url">
{{ 'url' | propertyNameTranslate: item.workLocations[0] | titlecase }}:
<a [href]="item.workLocations[0].url">{{
item.workLocations[0].url
}}</a>
</p>
<p *ngIf="item.workLocations[0].areaServed">
{{
'areaServed'
| propertyNameTranslate: item.workLocations[0]
| titlecase
}}:
<a
[routerLink]="['/data-detail', item.workLocations[0].areaServed.uid]"
>{{ item.workLocations[0].areaServed.name }}</a
>
</p>
</ng-container>
<ion-slides *ngIf="item.workLocations.length > 1" pager="true" class="work-locations">
<ion-slides
*ngIf="item.workLocations.length > 1"
pager="true"
class="work-locations"
>
<ion-slide *ngFor="let workLocation of item.workLocations">
<p *ngIf="workLocation.telephone">{{'telephone' | propertyNameTranslate: item.workLocation | titlecase}}: <a
[href]="'tel:' + workLocation.telephone">{{workLocation.telephone}}</a>
<p *ngIf="workLocation.telephone">
{{
'telephone' | propertyNameTranslate: item.workLocation | titlecase
}}:
<a [href]="'tel:' + workLocation.telephone">{{
workLocation.telephone
}}</a>
</p>
<p style="display:block !important" *ngIf="workLocation.email">{{'email' | propertyNameTranslate: item.workLocation | titlecase}}: <a [href]="'mailto:' + workLocation.email">{{workLocation.email}}</a>
<p style="display: block !important" *ngIf="workLocation.email">
{{ 'email' | propertyNameTranslate: item.workLocation | titlecase }}:
<a [href]="'mailto:' + workLocation.email">{{
workLocation.email
}}</a>
</p>
<p *ngIf="workLocation.faxNumber">{{'faxNumber' | propertyNameTranslate: item.workLocation | titlecase}}: {{workLocation.faxNumber}}</p>
<p *ngIf="workLocation.url">{{'url' | propertyNameTranslate: item.workLocation | titlecase}}: <a [href]="workLocation.url">{{workLocation.url}}</a></p>
<p *ngIf="workLocation.areaServed">{{'areaServed' | propertyNameTranslate: item.workLocation | titlecase}}: <a
[routerLink]="[ '/data-detail', workLocation.areaServed.uid]">{{workLocation.areaServed.name}}</a>
<p *ngIf="workLocation.faxNumber">
{{
'faxNumber' | propertyNameTranslate: item.workLocation | titlecase
}}: {{ workLocation.faxNumber }}
</p>
<p *ngIf="workLocation.url">
{{ 'url' | propertyNameTranslate: item.workLocation | titlecase }}:
<a [href]="workLocation.url">{{ workLocation.url }}</a>
</p>
<p *ngIf="workLocation.areaServed">
{{
'areaServed' | propertyNameTranslate: item.workLocation | titlecase
}}:
<a [routerLink]="['/data-detail', workLocation.areaServed.uid]">{{
workLocation.areaServed.name
}}</a>
</p>
<!-- Used for making the additional space, so that slide pager doesn't show over the text but under it -->
<!-- <div class="stapps-slide-bottom"></div> -->
@@ -35,4 +90,8 @@
</ion-slides>
</ion-card-content>
</ion-card>
<stapps-simple-card *ngIf="item.jobTitles" [title]="'jobTitles' | propertyNameTranslate: item | titlecase" [content]="item.jobTitles"></stapps-simple-card>
<stapps-simple-card
*ngIf="item.jobTitles"
[title]="'jobTitles' | propertyNameTranslate: item | titlecase"
[content]="item.jobTitles"
></stapps-simple-card>

View File

@@ -14,7 +14,7 @@
*/
import {Component, Input} from '@angular/core';
import {SCPerson} from '@openstapps/core';
import {DataListItem} from '../../list/data-list-item.component';
import {DataListItemComponent} from '../../list/data-list-item.component';
/**
* TODO
@@ -23,18 +23,9 @@ import {DataListItem} from '../../list/data-list-item.component';
selector: 'stapps-person-list-item',
templateUrl: 'person-list-item.html',
})
export class PersonListItem extends DataListItem {
export class PersonListItemComponent extends DataListItemComponent {
/**
* TODO
*/
@Input() item: SCPerson;
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method
ngOnInit() {
// TODO: translation
}
}

View File

@@ -2,13 +2,23 @@
<ion-row>
<ion-col>
<div class="ion-text-wrap">
<h2 class="name">{{'name' | thingTranslate: item}} <span *ngIf="item.honorificPrefix">, {{item.honorificPrefix}}</span></h2>
<p *ngIf="item.telephone || item.email"><span *ngIf="item.telephone">
<ion-icon name="call"></ion-icon>&nbsp;{{item.telephone}}&nbsp;
</span><span *ngIf="item.email">
<ion-icon name="mail"></ion-icon>&nbsp;{{item.email}}
</span></p>
<p *ngIf="item.jobTitles">{{item.jobTitles.join(', ') | slice:0:50}}<span *ngIf="item.jobTitles.join(', ').length > 51">...</span></p>
<h2 class="name">
{{ 'name' | thingTranslate: item }}
<span *ngIf="item.honorificPrefix">, {{ item.honorificPrefix }}</span>
</h2>
<p *ngIf="item.telephone || item.email">
<span *ngIf="item.telephone">
<ion-icon name="call"></ion-icon>&nbsp;{{
item.telephone
}}&nbsp; </span
><span *ngIf="item.email">
<ion-icon name="mail"></ion-icon>&nbsp;{{ item.email }}
</span>
</p>
<p *ngIf="item.jobTitles">
{{ item.jobTitles.join(', ') | slice: 0:50
}}<span *ngIf="item.jobTitles.join(', ').length > 51">...</span>
</p>
</div>
</ion-col>
</ion-row>

View File

@@ -13,14 +13,20 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {SCBuilding, SCFloor, SCPointOfInterest, SCRoom, SCThings} from '@openstapps/core';
import {
SCBuilding,
SCFloor,
SCPointOfInterest,
SCRoom,
SCThings,
} from '@openstapps/core';
import {DataProvider} from '../../data.provider';
/**
* TODO
*/
@Component({
providers: [ DataProvider ],
providers: [DataProvider],
selector: 'stapps-place-detail-content',
templateUrl: 'place-detail-content.html',
})
@@ -35,10 +41,8 @@ export class PlaceDetailContentComponent {
*
* @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';
hasCategories(item: SCThings): item is SCThings & {categories: string[]} {
return typeof (item as {categories: string[]}).categories !== 'undefined';
}
/**
@@ -46,9 +50,13 @@ export class PlaceDetailContentComponent {
*
* @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'));
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'))
);
}
}

View File

@@ -1,16 +1,33 @@
<stapps-place-mensa-detail-content [item]="item" [language]="language" *ngIf="isMensaThing(item)"></stapps-place-mensa-detail-content>
<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' | propertyNameTranslate: item | titlecase" [content]="'categories' | thingTranslate: item"></stapps-simple-card>
<stapps-address-detail *ngIf="item.type !== 'floor' && item.address" [address]="item.address"></stapps-address-detail>
<stapps-simple-card
*ngIf="item.type !== 'floor' && item.categories"
[title]="'categories' | propertyNameTranslate: item | titlecase"
[content]="'categories' | thingTranslate: item"
></stapps-simple-card>
<stapps-address-detail
*ngIf="item.type !== 'floor' && item.address"
[address]="item.address"
></stapps-address-detail>
</ng-container>
<ng-container *ngIf="item.type !== 'building'">
<ng-container *ngIf="item.type !== 'building'">
<ion-card *ngIf="item.inPlace">
<ion-card-header>
{{'inPlace' | propertyNameTranslate: item | titlecase}}
{{ 'inPlace' | propertyNameTranslate: item | titlecase }}
</ion-card-header>
<ion-card-content>
<ion-icon name="location"></ion-icon> <a [routerLink]="['/data-detail', item.inPlace.uid]">{{'name' | thingTranslate: item.inPlace}}</a>
<ion-icon name="location"></ion-icon>
<a [routerLink]="['/data-detail', item.inPlace.uid]">{{
'name' | thingTranslate: item.inPlace
}}</a>
</ion-card-content>
</ion-card>
<stapps-address-detail *ngIf="item.inPlace && item.inPlace.address" [address]="item.inPlace.address"></stapps-address-detail>
<stapps-address-detail
*ngIf="item.inPlace && item.inPlace.address"
[address]="item.inPlace.address"
></stapps-address-detail>
</ng-container>

View File

@@ -14,7 +14,7 @@
*/
import {Component, Input} from '@angular/core';
import {SCBuilding, SCFloor, SCPointOfInterest, SCRoom} from '@openstapps/core';
import {DataListItem} from '../../list/data-list-item.component';
import {DataListItemComponent} from '../../list/data-list-item.component';
/**
* TODO
@@ -23,18 +23,9 @@ import {DataListItem} from '../../list/data-list-item.component';
selector: 'stapps-place-list-item',
templateUrl: 'place-list-item.html',
})
export class PlaceListItem extends DataListItem {
export class PlaceListItemComponent extends DataListItemComponent {
/**
* TODO
*/
@Input() item: SCBuilding | SCRoom | SCPointOfInterest | SCFloor;
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method
ngOnInit() {
// TODO: translation
}
}

View File

@@ -2,14 +2,17 @@
<ion-row>
<ion-col>
<div class="ion-text-wrap">
<h2 class="name">{{'name' | thingTranslate: item}} </h2>
<p *ngIf="item.description">{{'description' | thingTranslate: item}} </p>
<ion-note>{{'type' | thingTranslate: item}} </ion-note>
<h2 class="name">{{ 'name' | thingTranslate: item }}</h2>
<p *ngIf="item.description">
{{ 'description' | thingTranslate: item }}
</p>
<ion-note>{{ 'type' | thingTranslate: item }} </ion-note>
</div>
</ion-col>
<ion-col width-20 text-right *ngIf="item.type !== 'building'">
<span *ngIf="item.inPlace">
<ion-icon name="location"></ion-icon>{{'name' | thingTranslate: item.inPlace}}
<ion-icon name="location"></ion-icon
>{{ 'name' | thingTranslate: item.inPlace }}
</span>
</ion-col>
</ion-row>

View File

@@ -29,10 +29,10 @@ import {PlaceMensaService} from './place-mensa-service';
templateUrl: 'place-mensa.html',
})
export class PlaceMensaDetailComponent implements AfterViewInit {
/**
* Map of dishes for each day
*/
// eslint-disable-next-line unicorn/no-null
dishes: Promise<Record<SCISO8601Date, SCDish[]>> | null = null;
/**
@@ -67,6 +67,6 @@ export class PlaceMensaDetailComponent implements AfterViewInit {
*/
ngAfterViewInit() {
this.dishes = this.mensaService.getAllDishes(this.item, this.displayRange);
this.dishes.then((result) => this.selectedDay = keys(result)[0]);
this.dishes.then(result => (this.selectedDay = keys(result)[0]));
}
}

View File

@@ -14,7 +14,13 @@
*/
import {Injectable} from '@angular/core';
import {SCDish, SCISO8601Date, SCMultiSearchRequest, SCPlace, SCThingType} from '@openstapps/core';
import {
SCDish,
SCISO8601Date,
SCMultiSearchRequest,
SCPlace,
SCThingType,
} from '@openstapps/core';
import {keyBy, mapValues, range} from 'lodash-es';
import moment from 'moment';
import {DataProvider} from '../../../../data.provider';
@@ -26,21 +32,20 @@ import {DataProvider} from '../../../../data.provider';
providedIn: 'root',
})
export class PlaceMensaService {
constructor(private dataProvider: DataProvider) {
}
constructor(private dataProvider: DataProvider) {}
/**
* Fetches all dishes for this building
*
* Splits dishes as such that each list contains all dishes that are available at that day.
*/
async getAllDishes(place: SCPlace, days: number): Promise<Record<SCISO8601Date, SCDish[]>> {
async getAllDishes(
place: SCPlace,
days: number,
): Promise<Record<SCISO8601Date, SCDish[]>> {
const request: SCMultiSearchRequest = mapValues(
keyBy(range(days)
.map((i) => moment()
.add(i, 'days')
.toISOString()),
), (date: SCISO8601Date) => ({
keyBy(range(days).map(i => moment().add(i, 'days').toISOString())),
(date: SCISO8601Date) => ({
filter: {
arguments: {
filters: [
@@ -72,8 +77,12 @@ export class PlaceMensaService {
type: 'boolean',
},
size: 1000,
}));
}),
);
return mapValues(await this.dataProvider.multiSearch(request), 'data') as Record<SCISO8601Date, SCDish[]>;
return mapValues(
await this.dataProvider.multiSearch(request),
'data',
) as Record<SCISO8601Date, SCDish[]>;
}
}

View File

@@ -1,24 +1,34 @@
<ng-container>
<div *ngIf="dishes | async as dishes; else loading">
<ion-segment [(ngModel)]="selectedDay">
<ion-segment-button *ngFor="let date of dishes | keyvalue" [value]="date.key">
<ion-label>{{date.key | amDateFormat:'dddd, L'}}</ion-label>
<ion-segment-button
*ngFor="let date of dishes | keyvalue"
[value]="date.key"
>
<ion-label>{{ date.key | amDateFormat: 'dddd, L' }}</ion-label>
</ion-segment-button>
</ion-segment>
<div [ngSwitch]="selectedDay">
<div *ngFor="let date of dishes | keyvalue">
<ion-list *ngSwitchCase="date.key">
<stapps-data-list-item [item]="dish" *ngFor="let dish of date.value"></stapps-data-list-item>
<stapps-data-list-item
[item]="dish"
*ngFor="let dish of date.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>
<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>
<stapps-skeleton-list-item
*ngFor="let skeleton of [1, 2]"
></stapps-skeleton-list-item>
</ion-list>
</ng-template>
</ng-container>

View File

@@ -1,3 +1,13 @@
<stapps-simple-card *ngIf="item.eventsStartDate && item.eventsEndDate"
[title]="('eventsStartDate' | propertyNameTranslate: item | titlecase) + ' - ' + ('eventsEndDate' | propertyNameTranslate: item | titlecase)"
[content]="(item.eventsStartDate | amDateFormat: 'll') + ' - ' + (item.eventsEndDate | amDateFormat: 'll')"></stapps-simple-card>
<stapps-simple-card
*ngIf="item.eventsStartDate && item.eventsEndDate"
[title]="
('eventsStartDate' | propertyNameTranslate: item | titlecase) +
' - ' +
('eventsEndDate' | propertyNameTranslate: item | titlecase)
"
[content]="
(item.eventsStartDate | amDateFormat: 'll') +
' - ' +
(item.eventsEndDate | amDateFormat: 'll')
"
></stapps-simple-card>

View File

@@ -14,7 +14,7 @@
*/
import {Component, Input} from '@angular/core';
import {SCSemester} from '@openstapps/core';
import {DataListItem} from '../../list/data-list-item.component';
import {DataListItemComponent} from '../../list/data-list-item.component';
/**
* TODO
@@ -23,17 +23,9 @@ import {DataListItem} from '../../list/data-list-item.component';
selector: 'stapps-semester-list-item',
templateUrl: 'semester-list-item.html',
})
export class SemesterListItem extends DataListItem {
export class SemesterListItemComponent extends DataListItemComponent {
/**
* TODO
*/
@Input() item: SCSemester;
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method
ngOnInit() {
// TODO: translation
}
}

View File

@@ -2,12 +2,15 @@
<ion-row>
<ion-col>
<div class="ion-text-wrap">
<h2 class="name">{{'name' | thingTranslate: item}}</h2>
<h2 class="name">{{ 'name' | thingTranslate: item }}</h2>
<p>
<ion-icon name="calendar"></ion-icon>
<span>{{item.startDate | amDateFormat: 'll'}} - {{item.endDate | amDateFormat:'ll'}}</span>
<span
>{{ item.startDate | amDateFormat: 'll' }} -
{{ item.endDate | amDateFormat: 'll' }}</span
>
</p>
<ion-note>{{'type' | thingTranslate: item}}</ion-note>
<ion-note>{{ 'type' | thingTranslate: item }}</ion-note>
</div>
</ion-col>
</ion-row>

View File

@@ -1,6 +1,20 @@
<stapps-simple-card *ngIf="item.actors" [title]="'actors' | propertyNameTranslate: item | titlecase" [content]="item.actors"></stapps-simple-card>
<stapps-simple-card *ngIf="item.authors" [title]="'authors' | propertyNameTranslate: item | titlecase" [content]="item.authors"></stapps-simple-card>
<stapps-simple-card *ngIf="item.datePublished" [title]="'datePublished' | propertyNameTranslate: item | titlecase"
[content]="item.datePublished | amDateFormat:'ll'">
<stapps-simple-card
*ngIf="item.actors"
[title]="'actors' | propertyNameTranslate: item | titlecase"
[content]="item.actors"
></stapps-simple-card>
<stapps-simple-card
*ngIf="item.authors"
[title]="'authors' | propertyNameTranslate: item | titlecase"
[content]="item.authors"
></stapps-simple-card>
<stapps-simple-card
*ngIf="item.datePublished"
[title]="'datePublished' | propertyNameTranslate: item | titlecase"
[content]="item.datePublished | amDateFormat: 'll'"
>
</stapps-simple-card>
<stapps-offers-detail *ngIf="item.offers" [offers]="item.offers"></stapps-offers-detail>
<stapps-offers-detail
*ngIf="item.offers"
[offers]="item.offers"
></stapps-offers-detail>

View File

@@ -14,7 +14,7 @@
*/
import {Component, Input} from '@angular/core';
import {SCVideo} from '@openstapps/core';
import {DataListItem} from '../../list/data-list-item.component';
import {DataListItemComponent} from '../../list/data-list-item.component';
/**
* TODO
@@ -23,17 +23,9 @@ import {DataListItem} from '../../list/data-list-item.component';
selector: 'stapps-video-list-item',
templateUrl: 'video-list-item.html',
})
export class VideoListItem extends DataListItem {
export class VideoListItemComponent extends DataListItemComponent {
/**
* TODO
*/
@Input() item: SCVideo;
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method
ngOnInit() {
// TODO: translation
}
}

View File

@@ -2,10 +2,18 @@
<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.duration">{{'duration' | propertyNameTranslate: item | titlecase}}: {{item.duration | amDuration:'seconds'}}</p>
<ion-note>{{'type' | thingTranslate: item}}</ion-note>
<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.duration">
{{ 'duration' | propertyNameTranslate: item | titlecase }}:
{{ item.duration | amDuration: 'seconds' }}
</p>
<ion-note>{{ 'type' | thingTranslate: item }}</ion-note>
</div>
</ion-col>
</ion-row>

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion,@typescript-eslint/ban-ts-comment */
/*
* Copyright (C) 2020 StApps
* This program is free software: you can redistribute it and/or modify it
@@ -12,16 +13,26 @@
* 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 {APP_BASE_HREF, CommonModule, Location, LocationStrategy, PathLocationStrategy} from '@angular/common';
import {
APP_BASE_HREF,
CommonModule,
Location,
LocationStrategy,
PathLocationStrategy,
} from '@angular/common';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {FormsModule} from '@angular/forms';
import {ChildrenOutletContexts, RouterModule, UrlSerializer} from '@angular/router';
import {
ChildrenOutletContexts,
RouterModule,
UrlSerializer,
} from '@angular/router';
import {IonicModule} from '@ionic/angular';
import {TranslateModule,} from '@ngx-translate/core';
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 {ContextMenuService} from '../context/context-menu.service';
import {ContextMenuService} from './context-menu.service';
import {FilterContext, SortContext} from './context-type';
describe('ContextMenuComponent', async () => {
@@ -29,7 +40,6 @@ describe('ContextMenuComponent', async () => {
let instance: ContextMenuComponent;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ContextMenuComponent],
providers: [
@@ -40,7 +50,6 @@ describe('ContextMenuComponent', async () => {
{provide: LocationStrategy, useClass: PathLocationStrategy},
{provide: APP_BASE_HREF, useValue: '/'},
],
// tslint:disable-next-line:object-literal-sort-keys
imports: [
FormsModule,
IonicModule.forRoot(),
@@ -58,23 +67,30 @@ describe('ContextMenuComponent', async () => {
it('should show items in sort context', () => {
instance.sortOption = getSortContextType();
fixture.detectChanges();
const sort: HTMLElement = fixture.debugElement.nativeElement.querySelector('.context-sort');
const sort: HTMLElement =
fixture.debugElement.nativeElement.querySelector('.context-sort');
const sortItem = sort.querySelector('.sort-item');
expect(sortItem!.querySelector('ion-label')!.textContent).toContain('relevance');
expect(sortItem!.querySelector('ion-label')?.textContent).toContain(
'relevance',
);
});
it('should show items in filter context', () => {
instance.filterOption = getFilterContextType();
fixture.detectChanges();
const filter: HTMLElement = fixture.debugElement.nativeElement.querySelector('.context-filter');
const filter: HTMLElement =
fixture.debugElement.nativeElement.querySelector('.context-filter');
const filterItem = filter.querySelector('.filter-group');
expect(filterItem!.querySelector('ion-list-header')!.textContent).toContain('Type');
expect(filterItem!.querySelector('ion-list-header')!.textContent).toContain(
'Type',
);
});
it('should set sort context value and reverse on click', () => {
instance.sortOption = getSortContextType();
fixture.detectChanges();
const sort: HTMLElement = fixture.debugElement.nativeElement.querySelector('.context-sort');
const sort: HTMLElement =
fixture.debugElement.nativeElement.querySelector('.context-sort');
// @ts-ignore
const sortItem: HTMLElement = sort.querySelectorAll('.sort-item')[1];
sortItem!.click();
@@ -93,18 +109,26 @@ describe('ContextMenuComponent', async () => {
instance.filterOption = getFilterContextType();
fixture.detectChanges();
// get filter context div
const filter: HTMLElement = fixture.debugElement.nativeElement.querySelector('.context-filter');
const filter: HTMLElement =
fixture.debugElement.nativeElement.querySelector('.context-filter');
// get all filter groups that represent a facet
const filterGroups = filter.querySelectorAll('.filter-group');
expect(filterGroups.length).toEqual(facets.length);
for (const facet of facets) {
let filterGroup = undefined;
let filterGroup;
// get filter option for facets field
filterGroups.forEach((element) => {
if (element.querySelector('ion-list-header')!.textContent!.toString().toLowerCase().indexOf(facet.field) > -1) {
// eslint-disable-next-line unicorn/no-array-for-each
filterGroups.forEach(element => {
if (
element
.querySelector('ion-list-header')!
.textContent!.toString()
.toLowerCase()
.includes(facet.field)
) {
filterGroup = element;
return;
}
@@ -125,9 +149,13 @@ describe('ContextMenuComponent', async () => {
let filterItem;
for (let i = 0; i < filterItems.length; i++) {
if (filterItems.item(i)
.textContent!.toString().toLowerCase()
.indexOf(bucket.key.toLowerCase()) > 0) {
if (
filterItems
.item(i)
.textContent!.toString()
.toLowerCase()
.indexOf(bucket.key.toLowerCase()) > 0
) {
filterItem = filterItems.item(i);
break;
}
@@ -139,23 +167,27 @@ describe('ContextMenuComponent', async () => {
it('should reset filter', () => {
instance.filterOption = getFilterContextType();
instance.filterOption.options = [{
field: 'type',
buckets: [
{count: 10, key: 'date series', checked: true}
]
}];
instance.filterOption.options = [
{
field: 'type',
buckets: [{count: 10, key: 'date series', checked: true}],
},
];
fixture.detectChanges();
// click reset button
const resetButton: HTMLElement = fixture.debugElement.nativeElement.querySelector('.resetFilterButton');
const resetButton: HTMLElement =
fixture.debugElement.nativeElement.querySelector('.resetFilterButton');
resetButton.click();
expect(instance.filterOption.options[0].buckets[0].checked).toEqual(false);
});
});
/**
*
*/
function getSortContextType(): SortContext {
return {
name: 'sort',
@@ -178,97 +210,102 @@ function getSortContextType(): SortContext {
reversible: true,
value: 'type',
},
]
}
],
};
}
/**
*
*/
function getFilterContextType(): FilterContext {
return {
name: 'filter',
compact: false,
options: facetsMock.filter((facet) => facet.buckets.length > 0).map((facet) => {
return {
buckets: facet.buckets.map((bucket) => {
return {
count: bucket.count,
key: bucket.key,
checked: false,
}
}),
compact: false,
field: facet.field,
onlyOnType: facet.onlyOnType
}
})
}
options: facetsMock
.filter(facet => facet.buckets.length > 0)
.map(facet => {
return {
buckets: facet.buckets.map(bucket => {
return {
count: bucket.count,
key: bucket.key,
checked: false,
};
}),
compact: false,
field: facet.field,
onlyOnType: facet.onlyOnType,
};
}),
};
}
const facetsMock: SCFacet[] = [
{
'buckets': [
buckets: [
{
'count': 60,
'key': 'academic event',
count: 60,
key: 'academic event',
},
{
'count': 160,
'key': 'message',
count: 160,
key: 'message',
},
{
'count': 151,
'key': 'date series',
count: 151,
key: 'date series',
},
{
'count': 106,
'key': 'dish',
count: 106,
key: 'dish',
},
{
'count': 20,
'key': 'building',
count: 20,
key: 'building',
},
],
'field': 'type',
field: 'type',
},
{
'buckets': [
buckets: [
{
'count': 12,
'key': 'Max Mustermann',
count: 12,
key: 'Max Mustermann',
},
{
'count': 2,
'key': 'Foo Bar',
count: 2,
key: 'Foo Bar',
},
],
'field': 'performers',
'onlyOnType': SCThingType.AcademicEvent,
field: 'performers',
onlyOnType: SCThingType.AcademicEvent,
},
{
'buckets': [
buckets: [
{
'count': 5,
'key': 'colloquium',
count: 5,
key: 'colloquium',
},
{
'count': 15,
'key': 'course',
count: 15,
key: 'course',
},
],
'field': 'categories',
'onlyOnType': SCThingType.AcademicEvent,
field: 'categories',
onlyOnType: SCThingType.AcademicEvent,
},
{
'buckets': [
buckets: [
{
'count': 5,
'key': 'employees',
count: 5,
key: 'employees',
},
{
'count': 15,
'key': 'students',
count: 15,
key: 'students',
},
],
'field': 'audiences',
'onlyOnType': SCThingType.Message,
field: 'audiences',
onlyOnType: SCThingType.Message,
},
];

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, OnDestroy} from '@angular/core';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
import {
SCLanguage,
@@ -37,8 +37,7 @@ import {FilterContext, SortContext, SortContextOption} from './context-type';
selector: 'stapps-context',
templateUrl: 'context-menu.html',
})
export class ContextMenuComponent {
export class ContextMenuComponent implements OnDestroy {
/**
* Amount of filter options shown on compact view
*/
@@ -74,24 +73,26 @@ export class ContextMenuComponent {
*/
translator: SCThingTranslator;
constructor(private translateService: TranslateService,
private readonly contextMenuService: ContextMenuService) {
this.language = this.translateService.currentLang as keyof SCTranslations<SCLanguage>;
constructor(
private translateService: TranslateService,
private readonly contextMenuService: ContextMenuService,
) {
this.language = this.translateService
.currentLang as keyof SCTranslations<SCLanguage>;
this.translator = new SCThingTranslator(this.language);
this.subscriptions.push(this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.language = event.lang as keyof SCTranslations<SCLanguage>;
this.translator = new SCThingTranslator(this.language);
}));
this.subscriptions.push(this.contextMenuService.filterContextChanged$.subscribe((filterContext) => {
this.filterOption = filterContext;
}));
this.subscriptions.push(this.contextMenuService.sortOptions.subscribe((sortContext) => {
this.sortOption = sortContext;
}));
this.subscriptions.push(
this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.language = event.lang as keyof SCTranslations<SCLanguage>;
this.translator = new SCThingTranslator(this.language);
}),
this.contextMenuService.filterContextChanged$.subscribe(filterContext => {
this.filterOption = filterContext;
}),
this.contextMenuService.sortOptions.subscribe(sortContext => {
this.sortOption = sortContext;
}),
);
}
/**
@@ -99,21 +100,31 @@ export class ContextMenuComponent {
*/
filterChanged = () => {
this.contextMenuService.contextFilterChanged(this.filterOption);
}
};
/**
* Returns translated property name
*/
getTranslatedPropertyName(property: string, onlyForType?: SCThingType): string {
return (this.translator
// tslint:disable-next-line:no-any
.translatedPropertyNames(onlyForType ?? SCThingType.AcademicEvent) as any)[property];
getTranslatedPropertyName(
property: string,
onlyForType?: SCThingType,
): string {
return (
this.translator.translatedPropertyNames(
onlyForType ?? SCThingType.AcademicEvent,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any
)[property];
}
/**
* Returns translated property value
*/
getTranslatedPropertyValue(onlyForType: SCThingType, field: string, key?: string): string | undefined {
getTranslatedPropertyValue(
onlyForType: SCThingType,
field: string,
key?: string,
): string | undefined {
return this.translator.translatedPropertyValue(onlyForType, field, key);
}
@@ -130,11 +141,13 @@ export class ContextMenuComponent {
* Resets filter options
*/
resetFilter = (option: FilterContext) => {
option.options.forEach((filterFacet) => filterFacet.buckets.forEach((filterBucket) => {
filterBucket.checked = false;
}));
for (const filterFacet of option.options)
for (const filterBucket of filterFacet.buckets) {
filterBucket.checked = false;
}
this.contextMenuService.contextFilterChanged(this.filterOption);
}
};
/**
* Updates selected sort option and updates listener
@@ -151,5 +164,5 @@ export class ContextMenuComponent {
}
}
this.contextMenuService.contextSortChanged(option);
}
};
}

View File

@@ -1,28 +1,38 @@
<ion-menu type="overlay" menuId="context" contentId="data-list" side="end">
<ion-list-header>
<ion-toolbar>
<h3>{{'menu.context.title' | translate | titlecase}}</h3>
<h3>{{ 'menu.context.title' | translate | titlecase }}</h3>
</ion-toolbar>
</ion-list-header>
</ion-list-header>
<ion-content>
<!-- Sort Context -->
<ion-list>
<ion-radio-group class="context-sort" *ngIf="sortOption" [value]="0">
<ion-list-header>
<ion-icon name="swap-vertical-outline"></ion-icon>
<ion-title>{{'menu.context.sort.title' | translate | titlecase}}</ion-title>
<ion-title>{{
'menu.context.sort.title' | translate | titlecase
}}</ion-title>
</ion-list-header>
<ion-item class="sort-item"
*ngFor="let value of sortOption.values, index as i"
(click)="sortChanged(sortOption, sortOption.values[i])">
<ion-label>{{'menu.context.sort.' + value.value | translate | titlecase}}
<ion-item
class="sort-item"
*ngFor="let value of sortOption.values; index as i"
(click)="sortChanged(sortOption, sortOption.values[i])"
>
<ion-label
>{{ 'menu.context.sort.' + value.value | translate | titlecase }}
<span *ngIf="sortOption.value === value.value && value.reversible">
<ion-icon *ngIf="sortOption.reversed" name="arrow-down-outline"></ion-icon>
<ion-icon *ngIf="!sortOption.reversed" name="arrow-up-outline"></ion-icon>
<ion-icon
*ngIf="sortOption.reversed"
name="arrow-down-outline"
></ion-icon>
<ion-icon
*ngIf="!sortOption.reversed"
name="arrow-up-outline"
></ion-icon>
</span>
</ion-label>
<ion-radio slot="end" [value]="i">
</ion-radio>
<ion-radio slot="end" [value]="i"> </ion-radio>
</ion-item>
</ion-radio-group>
</ion-list>
@@ -30,48 +40,99 @@
<div class="context-filter" *ngIf="filterOption">
<ion-list-header>
<ion-icon name="filter-outline"></ion-icon>
<ion-title>{{'menu.context.filter.title' | translate | titlecase}}</ion-title>
<ion-button class="resetFilterButton" fill="clear" color="dark" (click)="resetFilter(filterOption)">
<ion-title>{{
'menu.context.filter.title' | translate | titlecase
}}</ion-title>
<ion-button
class="resetFilterButton"
fill="clear"
color="dark"
(click)="resetFilter(filterOption)"
>
<ion-icon name="trash"></ion-icon>
</ion-button>
</ion-list-header>
<ion-list class="filter-group"
*ngFor="let facet of !filterOption.compact ?
filterOption.options.slice(0, compactFilterOptionCount) : filterOption.options">
<div *ngIf="!facet.field.includes('.')">
<ion-list
class="filter-group"
*ngFor="
let facet of !filterOption.compact
? filterOption.options.slice(0, compactFilterOptionCount)
: filterOption.options
"
>
<div *ngIf="!facet.field.includes('.')">
<ion-list-header class="h3">
<ion-label>
{{(facet.onlyOnType ? getTranslatedPropertyName(facet.field, facet.onlyOnType) : (getTranslatedPropertyName(facet.field))) | titlecase}}
{{facet.onlyOnType ? ' | ' + (getTranslatedPropertyValue(facet.onlyOnType, 'type') | titlecase) : ''}}
{{
(facet.onlyOnType
? getTranslatedPropertyName(facet.field, facet.onlyOnType)
: getTranslatedPropertyName(facet.field)
) | titlecase
}}
{{
facet.onlyOnType
? ' | ' +
(getTranslatedPropertyValue(facet.onlyOnType, 'type')
| titlecase)
: ''
}}
</ion-label>
</ion-list-header>
<div *ngIf="facet.buckets.length > 0">
<ion-item
*ngFor="let bucket of !facet.compact ?
facet.buckets.slice(0, compactFilterOptionCount) : facet.buckets">
*ngFor="
let bucket of !facet.compact
? facet.buckets.slice(0, compactFilterOptionCount)
: facet.buckets
"
>
<ion-label class="filter-item-label">
({{bucket.count}}) {{facet.field === 'type' ? (getTranslatedPropertyValue($any(bucket.key), 'type') | titlecase) :
getTranslatedPropertyValue(facet.onlyOnType, facet.field, bucket.key) | titlecase}}
({{ bucket.count }})
{{
facet.field === 'type'
? (getTranslatedPropertyValue($any(bucket.key), 'type')
| titlecase)
: (getTranslatedPropertyValue(
facet.onlyOnType,
facet.field,
bucket.key
) | titlecase)
}}
</ion-label>
<ion-checkbox
[(ngModel)]="bucket.checked"
(ngModelChange)="filterChanged()"
[value]="{field: facet.field, value: bucket.key, onlyOnType: facet.onlyOnType}">
[(ngModel)]="bucket.checked"
(ngModelChange)="filterChanged()"
[value]="{
field: facet.field,
value: bucket.key,
onlyOnType: facet.onlyOnType
}"
>
</ion-checkbox>
</ion-item>
<ion-button fill="clear"
*ngIf="!facet.compact && facet.buckets.length > compactFilterOptionCount"
(click)="facet.compact = true">
{{'menu.context.filter.showAll' | translate}}
<ion-button
fill="clear"
*ngIf="
!facet.compact &&
facet.buckets.length > compactFilterOptionCount
"
(click)="facet.compact = true"
>
{{ 'menu.context.filter.showAll' | translate }}
</ion-button>
</div>
</div>
</ion-list>
<ion-button fill="clear"
*ngIf="!filterOption.compact && filterOption.options.length > compactFilterOptionCount"
(click)="filterOption.compact = true">
{{'menu.context.filter.showAll' | translate}}
<ion-button
fill="clear"
*ngIf="
!filterOption.compact &&
filterOption.options.length > compactFilterOptionCount
"
(click)="filterOption.compact = true"
>
{{ 'menu.context.filter.showAll' | translate }}
</ion-button>
</div>
</ion-content>

View File

@@ -9,7 +9,7 @@ describe('ContextMenuService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ContextMenuService]
providers: [ContextMenuService],
});
service = TestBed.get(ContextMenuService);
});
@@ -18,38 +18,38 @@ describe('ContextMenuService', () => {
expect(service).toBeTruthy();
});
it('should update filterOptions', (done) => {
service.filterContextChanged$.subscribe((result) => {
it('should update filterOptions', done => {
service.filterContextChanged$.subscribe(result => {
expect(result).toBeDefined();
done();
});
service.updateContextFilter(facetsMock);
});
it('should update filterQuery', (done) => {
it('should update filterQuery', done => {
service.filterContextChanged$.subscribe(() => {
service.contextFilterChanged(filterContext);
});
service.filterQueryChanged$.subscribe((result) => {
service.filterQueryChanged$.subscribe(result => {
expect(result).toBeDefined();
done();
});
service.updateContextFilter(facetsMock);
});
it('should update sortOptions', (done) => {
service.sortContextChanged$.subscribe((result) => {
it('should update sortOptions', done => {
service.sortContextChanged$.subscribe(result => {
expect(result).toBeDefined();
done();
});
service.setContextSort(sortContext);
});
it('should update sortQuery', (done) => {
it('should update sortQuery', done => {
service.sortContextChanged$.subscribe(() => {
service.contextSortChanged(sortContext);
});
service.sortQueryChanged$.subscribe((result) => {
service.sortQueryChanged$.subscribe(result => {
expect(result).toBeDefined();
done();
});
@@ -57,37 +57,37 @@ describe('ContextMenuService', () => {
});
});
const facetsMock: SCFacet[] =
[{
'buckets': [
const facetsMock: SCFacet[] = [
{
buckets: [
{
'count': 60,
'key': 'academic event',
count: 60,
key: 'academic event',
},
{
'count': 160,
'key': 'message',
count: 160,
key: 'message',
},
{
'count': 151,
'key': 'date series',
count: 151,
key: 'date series',
},
{
'count': 106,
'key': 'dish',
count: 106,
key: 'dish',
},
{
'count': 20,
'key': 'building',
count: 20,
key: 'building',
},
{
'count': 20,
'key': 'semester',
count: 20,
key: 'semester',
},
],
'field': 'type',
}];
field: 'type',
},
];
const filterContext: FilterContext = {
name: 'filter',
@@ -97,8 +97,9 @@ const filterContext: FilterContext = {
{
checked: true,
count: 60,
key: 'academic event'
}, {
key: 'academic event',
},
{
checked: false,
count: 160,
key: 'message',
@@ -106,26 +107,28 @@ const filterContext: FilterContext = {
{
checked: false,
count: 151,
key: 'date series'
}, {
key: 'date series',
},
{
checked: false,
count: 106,
key: 'dish'
key: 'dish',
},
{
checked: false,
count: 20,
key: 'building'
key: 'building',
},
{
checked: false,
count: 20,
key: 'semester'
}
key: 'semester',
},
],
field: 'type'
}]
}
field: 'type',
},
],
};
const sortContext: SortContext = {
name: 'sort',
@@ -145,4 +148,4 @@ const sortContext: SortContext = {
value: 'type',
},
],
}
};

View File

@@ -13,16 +13,20 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Injectable} from '@angular/core';
import {SCFacet, SCFacetBucket, SCSearchFilter, SCSearchSort} from '@openstapps/core';
import {SCFacet, SCSearchFilter, SCSearchSort} from '@openstapps/core';
import {Subject} from 'rxjs';
import {FilterBucket, FilterContext, FilterFacet, SortContext} from './context-type';
import {
FilterBucket,
FilterContext,
FilterFacet,
SortContext,
} from './context-type';
/**
* ContextMenuService provides bidirectional communication of context menu options and search queries
*/
@Injectable()
export class ContextMenuService {
/**
* Local filter context object
*/
@@ -31,13 +35,13 @@ export class ContextMenuService {
/**
* Container for the filter context
*/
// tslint:disable-next-line:member-ordering
// eslint-disable-next-line @typescript-eslint/member-ordering
filterOptions = new Subject<FilterContext>();
/**
* Observable filterContext streams
*/
// tslint:disable-next-line:member-ordering
// eslint-disable-next-line @typescript-eslint/member-ordering
filterContextChanged$ = this.filterOptions.asObservable();
/**
@@ -48,19 +52,19 @@ export class ContextMenuService {
/**
* Observable filterContext streams
*/
// tslint:disable-next-line:member-ordering
// eslint-disable-next-line @typescript-eslint/member-ordering
filterQueryChanged$ = this.filterQuery.asObservable();
/**
* Container for the sort context
*/
// tslint:disable-next-line:member-ordering
// eslint-disable-next-line @typescript-eslint/member-ordering
sortOptions = new Subject<SortContext>();
/**
* Observable SortContext streams
*/
// tslint:disable-next-line:member-ordering
// eslint-disable-next-line @typescript-eslint/member-ordering
sortContextChanged$ = this.sortOptions.asObservable();
/**
@@ -71,30 +75,32 @@ export class ContextMenuService {
/**
* Observable SortContext streams
*/
// tslint:disable-next-line:member-ordering
// eslint-disable-next-line @typescript-eslint/member-ordering
sortQueryChanged$ = this.sortQuery.asObservable();
/**
* Returns SCSearchFilter if filterContext value is set, undefined otherwise
*
* @param filterContext FilterContext to build SCSearchFilter from
*/
buildFilterQuery = (filterContext: FilterContext): SCSearchFilter | undefined => {
buildFilterQuery = (
filterContext: FilterContext,
): SCSearchFilter | undefined => {
const filters: SCSearchFilter[] = [];
filterContext.options.forEach((filterFacet) => {
for (const filterFacet of filterContext.options) {
const optionFilters: SCSearchFilter[] = [];
filterFacet.buckets.forEach((filterBucket) => {
for (const filterBucket of filterFacet.buckets) {
if (filterBucket.checked) {
optionFilters.push(
{
arguments: {
field: filterFacet.field,
value: filterBucket.key,
},
type: 'value',
});
optionFilters.push({
arguments: {
field: filterFacet.field,
value: filterBucket.key,
},
type: 'value',
});
}
});
}
if (optionFilters.length > 0) {
filters.push({
arguments: {
@@ -104,7 +110,7 @@ export class ContextMenuService {
type: 'boolean',
});
}
});
}
if (filters.length > 0) {
return {
@@ -117,28 +123,31 @@ export class ContextMenuService {
}
return;
}
};
/**
* Returns SCSearchSort if sorting value is set, undefined otherwise
*
* @param sortContext SortContext to build SCSearchSort from
*/
buildSortQuery = (sortContext: SortContext): SCSearchSort | undefined => {
if (sortContext.value && sortContext.value.length > 0) {
if (sortContext.value === 'name' || sortContext.value === 'type') {
return {
arguments: {
field: sortContext.value,
position: 0,
},
order: sortContext.reversed ? 'desc' : 'asc',
type: 'ducet',
};
}
if (
sortContext.value &&
sortContext.value.length > 0 &&
(sortContext.value === 'name' || sortContext.value === 'type')
) {
return {
arguments: {
field: sortContext.value,
position: 0,
},
order: sortContext.reversed ? 'desc' : 'asc',
type: 'ducet',
};
}
return;
}
};
/**
* Updates filter query from filterContext
@@ -192,7 +201,10 @@ export class ContextMenuService {
* Updates context filter with new facets.
* It preserves the checked status of existing filter options
*/
updateContextFilterOptions = (contextFilter: FilterContext, facets: SCFacet[]) => {
updateContextFilterOptions = (
contextFilter: FilterContext,
facets: SCFacet[],
) => {
const newFilterOptions: FilterFacet[] = [];
// iterate new facets
@@ -206,19 +218,28 @@ export class ContextMenuService {
newFilterOptions.push(newFilterFacet);
// search existing filterOption
const filterOption = contextFilter.options.find((contextFacet: FilterFacet) =>
contextFacet.field === facet.field && contextFacet.onlyOnType === facet.onlyOnType);
facet.buckets.forEach((bucket: SCFacetBucket) => {
const filterOption = contextFilter.options.find(
(contextFacet: FilterFacet) =>
contextFacet.field === facet.field &&
contextFacet.onlyOnType === facet.onlyOnType,
);
for (const bucket of facet.buckets) {
// search existing bucket to preserve checked status
const existingFilterBucket = filterOption ? filterOption.buckets
.find((contextBucket: FilterBucket) => contextBucket.key === bucket.key) : undefined;
const existingFilterBucket = filterOption
? filterOption.buckets.find(
(contextBucket: FilterBucket) =>
contextBucket.key === bucket.key,
)
: undefined;
const filterBucket: FilterBucket = {
checked: existingFilterBucket ? existingFilterBucket.checked : false,
checked: existingFilterBucket
? existingFilterBucket.checked
: false,
count: bucket.count,
key: bucket.key,
};
newFilterFacet.buckets.push(filterBucket);
});
}
}
}
@@ -227,5 +248,5 @@ export class ContextMenuService {
this.contextFilter = contextFilter;
this.filterOptions.next(contextFilter);
}
};
}

View File

@@ -14,8 +14,6 @@
*/
import {SCFacet, SCFacetBucket} from '@openstapps/core';
export type ContextType = FilterContext | SortContext;
/**
* A sort context
*/

View File

@@ -27,24 +27,16 @@ import {NavigationComponent} from './navigation/navigation.component';
* Menu module
*/
@NgModule({
declarations: [
NavigationComponent,
ContextMenuComponent,
],
exports: [
NavigationComponent,
ContextMenuComponent,
],
declarations: [NavigationComponent, ContextMenuComponent],
exports: [NavigationComponent, ContextMenuComponent],
imports: [
CommonModule,
FormsModule,
IonicModule.forRoot(),
TranslateModule.forChild(),
CommonModule,
RouterModule,
SettingsModule,
TranslateModule.forChild(),
],
providers: [
ContextMenuService,
],
providers: [ContextMenuService],
})
export class MenuModule {}

View File

@@ -14,7 +14,12 @@
*/
import {Component} from '@angular/core';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
import {SCAppConfigurationMenuCategory, SCLanguage, SCThingTranslator, SCTranslations} from '@openstapps/core';
import {
SCAppConfigurationMenuCategory,
SCLanguage,
SCThingTranslator,
SCTranslations,
} from '@openstapps/core';
import {NGXLogger} from 'ngx-logger';
import {ConfigProvider} from '../../config/config.provider';
@@ -30,7 +35,6 @@ import {ConfigProvider} from '../../config/config.provider';
templateUrl: 'navigation.html',
})
export class NavigationComponent {
/**
* Possible languages to be used for translation
*/
@@ -46,10 +50,12 @@ export class NavigationComponent {
*/
translator: SCThingTranslator;
constructor(private readonly configProvider: ConfigProvider,
public translateService: TranslateService,
private readonly logger: NGXLogger) {
this.loadMenuEntries();
constructor(
private readonly configProvider: ConfigProvider,
public translateService: TranslateService,
private readonly logger: NGXLogger,
) {
void this.loadMenuEntries();
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.language = event.lang as keyof SCTranslations<SCLanguage>;
this.translator = new SCThingTranslator(this.language);
@@ -62,11 +68,12 @@ export class NavigationComponent {
*/
async loadMenuEntries() {
try {
this.menu = await this.configProvider.getValue('menus') as SCAppConfigurationMenuCategory[];
this.menu = (await this.configProvider.getValue(
'menus',
)) as SCAppConfigurationMenuCategory[];
} catch (error) {
this.logger.error(`error from loading menu entries: ${error}`);
}
}
// openPage(page) {

View File

@@ -6,24 +6,23 @@
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>
<img src="assets/imgs/logo.png" alt=""/>
<img src="assets/imgs/logo.png" alt="" />
<span class="text">StApps</span>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list *ngFor="let category of menu">
<ion-list-header
*ngIf="category.name !== ''">
<ion-list-header *ngIf="category.name !== ''">
<ion-label>
{{category.translations[language].name | titlecase}}
{{ category.translations[language].name | titlecase }}
</ion-label>
</ion-list-header>
<ion-menu-toggle auto-hide="false" *ngFor="let item of category.items">
<ion-item [routerDirection]="'root'" [routerLink]="[item.route]">
<ion-icon slot="end" [name]="item.icon"></ion-icon>
<ion-label>
{{item.translations[language].title | titlecase}}
{{ item.translations[language].title | titlecase }}
</ion-label>
</ion-item>
</ion-menu-toggle>

Some files were not shown because too many files have changed in this diff Show More