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

1
.eslintignore Normal file
View File

@@ -0,0 +1 @@
src/app/_helpers/data

120
.eslintrc.json Normal file
View File

@@ -0,0 +1,120 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"project": [
"tsconfig.json",
"e2e/tsconfig.e2e.json"
],
"createDefaultProgram": true
},
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates",
"plugin:jsdoc/recommended",
"plugin:unicorn/recommended"
],
"plugins": [
"eslint-plugin-unicorn",
"eslint-plugin-jsdoc",
"prettier"
],
"settings": {
"jsdoc": {
"mode": "typescript"
}
},
"rules": {
"unicorn/filename-case": "error",
"unicorn/no-array-callback-reference": "off",
"unicorn/prevent-abbreviations": [
"error",
{
"replacements": {
"ref": false,
"i": false
}
}
],
"unicorn/no-nested-ternary": "off",
"unicorn/better-regex": "off",
"jsdoc/no-types": "error",
"jsdoc/require-param": "off",
"jsdoc/require-param-description": "error",
"jsdoc/check-param-names": "error",
"jsdoc/require-returns": "off",
"jsdoc/require-param-type": "off",
"jsdoc/require-returns-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"@typescript-eslint/lines-between-class-members": [
"error",
"always"
],
"@typescript-eslint/no-explicit-any": "error",
"@angular-eslint/use-lifecycle-interface": "error",
"prettier/prettier": [
"error",
{
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "all",
"bracketSpacing": false,
"arrowParens": "avoid",
"endOfLine": "lf"
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:prettier/recommended",
"plugin:@angular-eslint/template/recommended"
],
"rules": {
"prettier/prettier": [
"error",
{
"parser": "angular",
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "all",
"bracketSpacing": false,
"arrowParens": "avoid",
"endOfLine": "lf"
}
]
}
}
]
}

View File

@@ -40,6 +40,11 @@ build:
paths:
- www
lint:
stage: build
script:
- npm run lint
unit:
stage: test
script:

View File

@@ -20,7 +20,8 @@
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [{
"assets": [
{
"glob": "**/*",
"input": "src/assets",
"output": "assets"
@@ -36,7 +37,8 @@
"output": "./svg"
}
],
"styles": [{
"styles": [
{
"input": "src/theme/variables.scss"
},
{
@@ -47,10 +49,12 @@
},
"configurations": {
"production": {
"fileReplacements": [{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
@@ -84,8 +88,8 @@
"fake": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.fake.ts"
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.fake.ts"
}
],
"optimization": true,
@@ -144,7 +148,8 @@
"karmaConfig": "src/karma.conf.js",
"styles": [],
"scripts": [],
"assets": [{
"assets": [
{
"glob": "favicon.ico",
"input": "src/",
"output": "/"
@@ -164,10 +169,20 @@
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"builder": "@angular-eslint/builder:lint",
"options": {
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"],
"eslintConfig": ".eslintrc.json",
"ignorePath": ".eslintignore"
}
},
"lint:fix": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"],
"fix": true,
"eslintConfig": ".eslintrc.json",
"ignorePath": ".eslintignore"
}
},
"ionic-cordova-build": {
@@ -213,17 +228,19 @@
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"builder": "@angular-eslint/builder:lint",
"options": {
"tsConfig": "e2e/tsconfig.e2e.json",
"exclude": ["**/node_modules/**"]
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"],
"eslintConfig": ".eslintrc.json",
"ignorePath": ".eslintignore"
}
}
}
}
},
"cli": {
"defaultCollection": "@ionic/angular-toolkit"
"defaultCollection": "@ionic/angular-toolkit",
"analytics": false
},
"schematics": {
"@ionic/angular-toolkit:component": {

3486
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@
"docker:serve": "sudo docker run -p 8100:8100 -p 35729:35729 -p 53703:53703 -v $PWD:/app -it registry.gitlab.com/openstapps/app bash -c \"npm start\"",
"documentation": "compodoc -p tsconfig.json -d docs",
"lint": "ng lint",
"lint:fix": "ng lint:fix",
"ng": "ng",
"postversion": "npm run changelog",
"pree2e": "webdriver-manager clean && webdriver-manager update --gecko false --versions.chrome $(google-chrome --product-version)",
@@ -36,8 +37,7 @@
"start:prod": "ionic serve -- --configuration=production",
"start:fake": "ionic serve -- --configuration=fake",
"start:external": "ionic serve --external",
"test": "ng test",
"tslint": "tslint -p tsconfig.json -c tslint.json 'src/**/*.ts'"
"test": "ng test"
},
"dependencies": {
"@angular/cdk": "12.0.0",
@@ -88,6 +88,11 @@
"@angular-devkit/build-angular": "0.901.13",
"@angular-devkit/core": "9.1.12",
"@angular-devkit/schematics": "9.1.12",
"@angular-eslint/builder": "12.2.0",
"@angular-eslint/eslint-plugin": "12.2.0",
"@angular-eslint/eslint-plugin-template": "12.2.0",
"@angular-eslint/schematics": "1.2.0",
"@angular-eslint/template-parser": "1.2.0",
"@angular/cli": "9.1.12",
"@angular/compiler": "9.1.12",
"@angular/compiler-cli": "9.1.12",
@@ -100,8 +105,15 @@
"@types/jasminewd2": "2.0.6",
"@types/lodash-es": "4.17.4",
"@types/node": "14.14.37",
"@typescript-eslint/eslint-plugin": "4.3.0",
"@typescript-eslint/parser": "4.3.0",
"codelyzer": "5.1.2",
"conventional-changelog-cli": "2.0.12",
"eslint": "7.30.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-jsdoc": "35.4.1",
"eslint-plugin-prettier": "3.4.0",
"eslint-plugin-unicorn": "34.0.1",
"is-docker": "1.1.0",
"jasmine-core": "3.5.0",
"jasmine-spec-reporter": "4.2.1",
@@ -111,10 +123,10 @@
"karma-jasmine": "2.0.1",
"karma-jasmine-html-reporter": "1.4.2",
"karma-mocha-reporter": "2.2.5",
"prettier": "2.3.2",
"protractor": "5.4.2",
"surge": "0.21.3",
"ts-node": "8.0.3",
"tslint": "6.1.3",
"typescript": "3.8.3"
},
"cordova": {

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/>.
*/
/* tslint:disable */
/* eslint-disable */
import moment from 'moment'

View File

@@ -42,22 +42,15 @@ export const sampleAggregations: SCBackendAggregationConfiguration[] = [
},
{
fieldName: 'academicTerms.acronym',
onlyOnTypes: [
SCThingType.AcademicEvent,
SCThingType.SportCourse,
],
onlyOnTypes: [SCThingType.AcademicEvent, SCThingType.SportCourse],
},
{
fieldName: 'academicTerm.acronym',
onlyOnTypes: [
SCThingType.Catalog,
],
onlyOnTypes: [SCThingType.Catalog],
},
{
fieldName: 'majors',
onlyOnTypes: [
SCThingType.AcademicEvent,
],
onlyOnTypes: [SCThingType.AcademicEvent],
},
{
fieldName: 'keywords',

View File

@@ -16,95 +16,98 @@ import {SCFacet, SCThingType} from '@openstapps/core';
export 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',
},
{
'count': 20,
'key': 'semester',
count: 20,
key: 'semester',
},
],
'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': 'unipedia',
}],
'field': 'categories',
'onlyOnType': SCThingType.Article,
count: 5,
key: 'unipedia',
},
],
field: 'categories',
onlyOnType: SCThingType.Article,
},
{
'buckets': [
buckets: [
{
'count': 5,
'key': 'employees',
count: 5,
key: 'employees',
},
{
'count': 15,
'key': 'students',
}],
'field': 'audiences',
'onlyOnType': SCThingType.Message,
count: 15,
key: 'students',
},
],
field: 'audiences',
onlyOnType: SCThingType.Message,
},
{
'buckets': [
buckets: [
{
'count': 5,
'key': 'main dish',
count: 5,
key: 'main dish',
},
{
'count': 15,
'key': 'salad',
}],
'field': 'categories',
'onlyOnType': SCThingType.Dish,
count: 15,
key: 'salad',
},
],
field: 'categories',
onlyOnType: SCThingType.Dish,
},
];

View File

@@ -15,15 +15,29 @@
import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {
SCAcademicEvent, SCArticle, SCBook, SCBuilding, SCCatalog,
SCDateSeries, SCDish, SCFavorite, SCMessage, SCPerson, SCRoom, SCSearchFilter,
SCThing, SCThingOriginType, SCThingType, SCToDo, SCToDoPriority,
SCAcademicEvent,
SCArticle,
SCBook,
SCBuilding,
SCCatalog,
SCDateSeries,
SCDish,
SCFavorite,
SCMessage,
SCPerson,
SCRoom,
SCSearchFilter,
SCThing,
SCThingOriginType,
SCThingType,
SCToDo,
SCToDoPriority,
} from '@openstapps/core';
import {Observable, of} from 'rxjs';
import {checkFilter} from '../fakesearch/filters';
import {sampleResources} from './resources/test-resources';
// tslint:disable:no-magic-numbers
/* eslint-disable */
const sampleMessages: SCMessage[] = [
{
audiences: ['students'],
@@ -385,9 +399,9 @@ export class SampleThings {
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method no-any
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-explicit-any
getSampleThing(uid: string): Observable<any[]> {
// tslint:disable-next-line:no-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sampleThings: any[] = [];
for (const resource of sampleResources) {
if (resource.instance.uid as SCThingType === uid) {
@@ -403,12 +417,12 @@ export class SampleThings {
/**
* TODO
*/
// tslint:disable-next-line:prefer-function-over-method no-any
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-explicit-any
getSampleThings(filter?: SCSearchFilter): Observable<any[]> {
// tslint:disable-next-line:no-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sampleThings: any[] = [];
for (const resource of sampleResources) {
// tslint:disable-next-line:max-line-length
// eslint-disable-next-line max-len
// if ([SCThingType.Video].includes(resource.instance.type as SCThingType)) {
if (typeof filter === 'undefined' || checkFilter(resource.instance as SCThing, filter)) {
sampleThings.push(resource.instance);

View File

@@ -18,7 +18,10 @@
*/
export class AppError extends Error {
/**
* Instantiate a new error
* TODO
*
* @param name Name of the error
* @param message Message of the error
*/
constructor(name: string, message: string) {
super(message);

View File

@@ -22,14 +22,20 @@ import {
HTTP_INTERCEPTORS,
} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {SCIndexResponse, SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
import {
SCIndexResponse,
SCSettingInputType,
SCThingOriginType,
SCThingType,
} from '@openstapps/core';
import {Observable, of} from 'rxjs';
import {delay, map} from 'rxjs/operators';
import {dependencies} from '../../../package.json';
import {facetsMock} from './data/sample-facets';
import {SampleThings} from './data/sample-things';
// tslint:disable:no-magic-numbers
/* eslint-disable unicorn/no-abusive-eslint-disable */
/* eslint-disable */
export const sampleIndexResponse: SCIndexResponse = {
app: {
campusPolygon: {
@@ -313,7 +319,7 @@ export const sampleIndexResponse: SCIndexResponse = {
},
};
// tslint:enable:no-magic-numbers
/* eslint-enable no-magic-numbers */
/**
* TODO
@@ -338,7 +344,7 @@ export class FakeBackendInterceptor implements HttpInterceptor {
/**
* TODO
*/
// tslint:disable-next-line:no-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (request.method === 'POST') {
if (request.url.endsWith('/') && request.method === 'POST') {
@@ -350,7 +356,7 @@ export class FakeBackendInterceptor implements HttpInterceptor {
if (typeof request.body.filter !== 'undefined' && typeof request.body.filter.arguments !== 'undefined') {
if (request.body.filter.arguments.field === 'uid') {
return this.sampleFetcher.getSampleThing(request.body.filter.arguments.value)
// tslint:disable-next-line:no-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.pipe(map((sampleData: any) => {
return new HttpResponse({status: 200, body: {data: sampleData}});
}), delay(this.RESPONSE_DELAY)); // add delay for skeleton screens to be seen (see !16)
@@ -358,7 +364,7 @@ export class FakeBackendInterceptor implements HttpInterceptor {
}
return this.sampleFetcher.getSampleThings(request.body.filter)
// tslint:disable-next-line:no-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.pipe(map((sampleData: any) => {
return new HttpResponse({status: 200, body: {data: sampleData, facets: facetsMock}});
}), delay(this.RESPONSE_DELAY)); // add delay for skeleton screens to be seen (see !16)

View File

@@ -13,7 +13,12 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCSearchBooleanFilter, SCSearchFilter, SCSearchValueFilter, SCThing} from '@openstapps/core';
import {
SCSearchBooleanFilter,
SCSearchFilter,
SCSearchValueFilter,
SCThing,
} from '@openstapps/core';
import {logger} from '../ts-logger';
/**
@@ -21,17 +26,19 @@ import {logger} from '../ts-logger';
*/
export function checkFilter(thing: SCThing, filter: SCSearchFilter): boolean {
switch (filter.type) {
case 'availability': /*TODO*/
case 'availability' /*TODO*/:
break;
case 'boolean':
return applyBooleanFilter(thing, filter);
case 'distance': /*TODO*/
case 'distance' /*TODO*/:
break;
case 'value':
return applyValueFilter(thing, filter);
}
void logger.error(`Not implemented filter method "${filter.type}" in fake backend!`);
void logger.error(
`Not implemented filter method "${filter.type}" in fake backend!`,
);
return false;
}
@@ -39,11 +46,18 @@ export function checkFilter(thing: SCThing, filter: SCSearchFilter): boolean {
/**
* Checks if a value filter applies to an SCThing
*/
function applyValueFilter(thing: SCThing, filter: SCSearchValueFilter): boolean {
function applyValueFilter(
thing: SCThing,
filter: SCSearchValueFilter,
): boolean {
const path = filter.arguments.field.split('.');
const thingFieldValue = traverseToFieldPath(thing, path, filter.arguments.value);
const thingFieldValue = traverseToFieldPath(
thing,
path,
filter.arguments.value,
);
if (!(thingFieldValue.found)) {
if (!thingFieldValue.found) {
return false;
}
@@ -54,32 +68,40 @@ function applyValueFilter(thing: SCThing, filter: SCSearchValueFilter): boolean
* Object that can be accessed using foo[bar]
*/
interface IndexableObject {
// tslint:disable-next-line:no-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
/**
* Result of a search for a field and comparison to a desired value
*/
type FieldSearchResult = {
/**
* Weather the field was found
*/
found: true;
type FieldSearchResult =
| {
/**
* Weather the field was found
*/
found: true;
/**
* The result of the comparison
*/
result: boolean;
} | {
/**
* Weather the field was found
*/
found: false;
};
/**
* The result of the comparison
*/
result: boolean;
}
| {
/**
* Weather the field was found
*/
found: false;
};
// tslint:disable-next-line:completed-docs
function traverseToFieldPath(value: IndexableObject, path: string[], desiredFieldValue: unknown): FieldSearchResult {
/**
* TODO
*/
function traverseToFieldPath(
value: IndexableObject,
path: string[],
desiredFieldValue: unknown,
): FieldSearchResult {
if (path.length === 0) {
void logger.error(`Value filter provided with zero length path`);
@@ -90,7 +112,7 @@ function traverseToFieldPath(value: IndexableObject, path: string[], desiredFiel
const nestedProperty = value[path[0]];
if (path.length === 1) {
return esStyleFieldHandler(nestedProperty, (nestedValue) => {
return esStyleFieldHandler(nestedProperty, nestedValue => {
return {
found: true,
result: nestedValue === desiredFieldValue,
@@ -98,13 +120,14 @@ function traverseToFieldPath(value: IndexableObject, path: string[], desiredFiel
});
}
return esStyleFieldHandler(nestedProperty, (nestedValue) => {
return esStyleFieldHandler(nestedProperty, nestedValue => {
if (typeof nestedValue === 'object') {
return traverseToFieldPath(
nestedValue as IndexableObject,
// tslint:disable-next-line:no-magic-numbers
// eslint-disable-next-line no-magic-numbers
path.slice(1),
desiredFieldValue);
desiredFieldValue,
);
}
return {found: false};
@@ -117,8 +140,10 @@ function traverseToFieldPath(value: IndexableObject, path: string[], desiredFiel
/**
* ES treats arrays like normal fields
*/
function esStyleFieldHandler<T>(field: T | T[],
handler: (value: T) => FieldSearchResult): FieldSearchResult {
function esStyleFieldHandler<T>(
field: T | T[],
handler: (value: T) => FieldSearchResult,
): FieldSearchResult {
if (Array.isArray(field)) {
for (const nestedField of field) {
const result = handler(nestedField);
@@ -138,7 +163,10 @@ function esStyleFieldHandler<T>(field: T | T[],
/**
* Checks if a boolean filter applies to an SCThing
*/
function applyBooleanFilter(thing: SCThing, filter: SCSearchBooleanFilter): boolean {
function applyBooleanFilter(
thing: SCThing,
filter: SCSearchBooleanFilter,
): boolean {
let out = false;
switch (filter.arguments.operation) {
@@ -164,7 +192,9 @@ function applyBooleanFilter(thing: SCThing, filter: SCSearchBooleanFilter): bool
return false;
}
void logger.error(`Not implemented boolean filter "${filter.arguments.operation}"`);
void logger.error(
`Not implemented boolean filter "${filter.arguments.operation}"`,
);
return false;
}

View File

@@ -16,4 +16,4 @@ import {NGXLogger} from 'ngx-logger';
export let logger: NGXLogger;
export const initLogger = (newLogger: NGXLogger) => logger = newLogger;
export const initLogger = (newLogger: NGXLogger) => (logger = newLogger);

View File

@@ -15,9 +15,7 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
const routes: Routes = [
{path: '', redirectTo: '/news', pathMatch: 'full'},
];
const routes: Routes = [{path: '', redirectTo: '/news', pathMatch: 'full'}];
/**
* TODO

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
* Copyright (C) 2018, 2019 StApps
* This program is free software: you can redistribute it and/or modify it
@@ -24,10 +25,9 @@ import {ThingTranslateService} from './translation/thing-translate.service';
import {AppComponent} from './app.component';
import {ConfigProvider} from './modules/config/config.provider';
import {SettingsProvider} from './modules/settings/settings.provider';
import {NGXLogger} from "ngx-logger";
import {NGXLogger} from 'ngx-logger';
describe('AppComponent', () => {
let statusBarSpy: jasmine.SpyObj<StatusBar>;
let splashScreenSpy: jasmine.SpyObj<SplashScreen>;
let platformReadySpy: any;
@@ -42,15 +42,21 @@ describe('AppComponent', () => {
statusBarSpy = jasmine.createSpyObj('StatusBar', ['styleDefault']);
splashScreenSpy = jasmine.createSpyObj('SplashScreen', ['hide']);
platformReadySpy = Promise.resolve();
platformSpy = jasmine.createSpyObj('Platform', { ready: platformReadySpy });
translateServiceSpy = jasmine.createSpyObj('TranslateService', ['setDefaultLang', 'use']);
thingTranslateServiceSpy = jasmine.createSpyObj('ThingTranslateService', ['init']);
settingsProvider = jasmine.createSpyObj('SettingsProvider',
['getSettingValue', 'provideSetting', 'setCategoriesOrder']);
configProvider = jasmine.createSpyObj('ConfigProvider',
['init']);
ngxLogger = jasmine.createSpyObj('NGXLogger',
['log', 'error', 'warn']);
platformSpy = jasmine.createSpyObj('Platform', {ready: platformReadySpy});
translateServiceSpy = jasmine.createSpyObj('TranslateService', [
'setDefaultLang',
'use',
]);
thingTranslateServiceSpy = jasmine.createSpyObj('ThingTranslateService', [
'init',
]);
settingsProvider = jasmine.createSpyObj('SettingsProvider', [
'getSettingValue',
'provideSetting',
'setCategoriesOrder',
]);
configProvider = jasmine.createSpyObj('ConfigProvider', ['init']);
ngxLogger = jasmine.createSpyObj('NGXLogger', ['log', 'error', 'warn']);
TestBed.configureTestingModule({
declarations: [AppComponent],
@@ -83,5 +89,4 @@ describe('AppComponent', () => {
});
// TODO: add more tests!
});

View File

@@ -47,19 +47,19 @@ export class AppComponent {
* @param platform TODO
* @param statusBar TODO
* @param splashScreen TODO
* @param translateService TODO
* @param thingTranslateService TODO
* @param settingsProvider TODO
* @param configProvider TODO
* @param logger An angular logger
*/
constructor(private readonly platform: Platform,
private readonly statusBar: StatusBar,
private readonly splashScreen: SplashScreen,
private readonly settingsProvider: SettingsProvider,
private readonly configProvider: ConfigProvider,
private readonly logger: NGXLogger) {
this.initializeApp();
constructor(
private readonly platform: Platform,
private readonly statusBar: StatusBar,
private readonly splashScreen: SplashScreen,
private readonly settingsProvider: SettingsProvider,
private readonly configProvider: ConfigProvider,
private readonly logger: NGXLogger,
) {
void this.initializeApp();
}
/**
@@ -67,32 +67,32 @@ export class AppComponent {
*/
async initializeApp() {
// tslint:disable-next-line: no-floating-promises
this.platform.ready()
.then(async () => {
// Okay, so the platform is ready and our plugins are available.
// Here you can do any higher level native things you might need.
this.statusBar.styleDefault();
this.splashScreen.hide();
this.platform.ready().then(async () => {
// Okay, so the platform is ready and our plugins are available.
// Here you can do any higher level native things you might need.
this.statusBar.styleDefault();
this.splashScreen.hide();
// initialise the configProvider
try {
await this.configProvider.init();
} catch (error) {
if (typeof error.name !== 'undefined') {
if (error.name === 'ConfigInitError') {
// @TODO: Issue #43 handle initialisation error and inform user
}
}
this.logger.error(error);
// initialise the configProvider
try {
await this.configProvider.init();
} catch (error) {
if (
typeof error.name !== 'undefined' &&
error.name === 'ConfigInitError'
) {
// TODO: Issue #43 handle initialisation error and inform user
}
this.logger.error(error);
}
// set order of categories in settings
this.settingsProvider.setCategoriesOrder([
'profile',
'privacy',
'credentials',
'others',
]);
});
// set order of categories in settings
this.settingsProvider.setCategoriesOrder([
'profile',
'privacy',
'credentials',
'others',
]);
});
}
}

View File

@@ -12,7 +12,12 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {CommonModule, HashLocationStrategy, LocationStrategy, registerLocaleData} from '@angular/common';
import {
CommonModule,
HashLocationStrategy,
LocationStrategy,
registerLocaleData,
} from '@angular/common';
import {HttpClient} from '@angular/common/http';
import localeDe from '@angular/common/locales/de';
import {APP_INITIALIZER, NgModule, Provider} from '@angular/core';
@@ -21,7 +26,11 @@ import {RouteReuseStrategy} from '@angular/router';
import {SplashScreen} from '@ionic-native/splash-screen/ngx';
import {StatusBar} from '@ionic-native/status-bar/ngx';
import {IonicModule, IonicRouteStrategy} from '@ionic/angular';
import {TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate/core';
import {
TranslateLoader,
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import {TranslateHttpLoader} from '@ngx-translate/http-loader';
import moment from 'moment';
import 'moment/min/locales';
@@ -47,21 +56,33 @@ registerLocaleData(localeDe);
/**
* Initializes settings from Config before other components
*
* @param logger TODO
* @param settingsProvider provider of settings (e.g. language that has been set)
* @param configProvider TODO
* @param translateService TODO
*/
export function initSettingsFactory(logger: NGXLogger,
settingsProvider: SettingsProvider,
configProvider: ConfigProvider,
translateService: TranslateService) {
export function initSettingsFactory(
logger: NGXLogger,
settingsProvider: SettingsProvider,
configProvider: ConfigProvider,
translateService: TranslateService,
) {
return async () => {
initLogger(logger);
await settingsProvider.init();
try {
// set language from settings
if (configProvider.firstSession) {
await settingsProvider.setSettingValue('profile', 'language', translateService.getBrowserLang());
await settingsProvider.setSettingValue(
'profile',
'language',
translateService.getBrowserLang(),
);
}
const languageCode = (await settingsProvider.getValue('profile', 'language')) as string;
const languageCode = (await settingsProvider.getValue(
'profile',
'language',
)) as string;
// this language will be used as a fallback when a translation isn't found in the current language
translateService.setDefaultLang('en');
translateService.use(languageCode);
@@ -81,7 +102,7 @@ export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
const providers : Provider[] = [
const providers: Provider[] = [
StatusBar,
SplashScreen,
{
@@ -95,7 +116,7 @@ const providers : Provider[] = [
{
provide: APP_INITIALIZER,
multi: true,
deps: [NGXLogger,SettingsProvider,ConfigProvider,TranslateService],
deps: [NGXLogger, SettingsProvider, ConfigProvider, TranslateService],
useFactory: initSettingsFactory,
},
];
@@ -122,14 +143,18 @@ const providers : Provider[] = [
loader: {
deps: [HttpClient],
provide: TranslateLoader,
useFactory: (createTranslateLoader),
useFactory: createTranslateLoader,
},
}),
// use maximal logging level when not in production, minimal (log only fatal errors) in production
LoggerModule.forRoot({level: environment.production ? NgxLoggerLevel.FATAL : NgxLoggerLevel.TRACE}),
LoggerModule.forRoot({
level: environment.production
? NgxLoggerLevel.FATAL
: NgxLoggerLevel.TRACE,
}),
],
providers:
environment.use_fake_backend ? [providers, fakeBackendProvider] : providers,
providers: environment.use_fake_backend
? [providers, fakeBackendProvider]
: providers,
})
export class AppModule {
}
export class AppModule {}

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>

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