mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-02-27 19:32:12 +00:00
refactor: replace TSLint with ESLint
This commit is contained in:
committed by
Jovan Krunić
parent
67fb4a43c9
commit
d696215d08
1
.eslintignore
Normal file
1
.eslintignore
Normal file
@@ -0,0 +1 @@
|
||||
src/app/_helpers/data
|
||||
120
.eslintrc.json
Normal file
120
.eslintrc.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -40,6 +40,11 @@ build:
|
||||
paths:
|
||||
- www
|
||||
|
||||
lint:
|
||||
stage: build
|
||||
script:
|
||||
- npm run lint
|
||||
|
||||
unit:
|
||||
stage: test
|
||||
script:
|
||||
|
||||
49
angular.json
49
angular.json
@@ -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
3486
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -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": {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!
|
||||
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -21,12 +21,7 @@ import {ConfigProvider} from './config.provider';
|
||||
* TODO
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [
|
||||
StorageModule,
|
||||
DataModule,
|
||||
],
|
||||
providers: [
|
||||
ConfigProvider,
|
||||
],
|
||||
imports: [StorageModule, DataModule],
|
||||
providers: [ConfigProvider],
|
||||
})
|
||||
export class ConfigModule {}
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -34,7 +34,7 @@ export class DataRoutingService {
|
||||
* @param thing The selected thing
|
||||
*/
|
||||
emitChildEvent(thing: SCThings) {
|
||||
this.childSelectedEvent.next(thing);
|
||||
this.childSelectedEvent.next(thing);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,5 +27,4 @@ export class DataDetailContentComponent {
|
||||
* TODO
|
||||
*/
|
||||
@Input() item: SCThings;
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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>,
|
||||
<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
|
||||
>,
|
||||
<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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -9,12 +9,9 @@ describe('DataListComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DataListComponent ],
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
declarations: [DataListComponent],
|
||||
imports: [TranslateModule.forRoot()],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> {{characteristic.name}}</span>
|
||||
<img
|
||||
*ngIf="characteristic.image"
|
||||
[src]="characteristic.image"
|
||||
alt=""
|
||||
/><span> {{ characteristic.name }}</span
|
||||
>
|
||||
</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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {{item.telephone}}
|
||||
</span><span *ngIf="item.email">
|
||||
<ion-icon name="mail"></ion-icon> {{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> {{
|
||||
item.telephone
|
||||
}} </span
|
||||
><span *ngIf="item.email">
|
||||
<ion-icon name="mail"></ion-icon> {{ 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>
|
||||
|
||||
@@ -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'))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user