mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-03-14 18:52:33 +00:00
1481
package-lock.json
generated
1481
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -51,7 +51,7 @@
|
|||||||
"@ngx-translate/http-loader": "4.0.0",
|
"@ngx-translate/http-loader": "4.0.0",
|
||||||
"@openstapps/api": "0.22.0",
|
"@openstapps/api": "0.22.0",
|
||||||
"@openstapps/configuration": "0.22.0",
|
"@openstapps/configuration": "0.22.0",
|
||||||
"@openstapps/core": "0.34.0",
|
"@openstapps/core": "0.36.0",
|
||||||
"@openstapps/logger": "0.4.0",
|
"@openstapps/logger": "0.4.0",
|
||||||
"cordova-android": "8.0.0",
|
"cordova-android": "8.0.0",
|
||||||
"cordova-browser": "6.0.0",
|
"cordova-browser": "6.0.0",
|
||||||
@@ -72,8 +72,8 @@
|
|||||||
"zone.js": "0.9.0"
|
"zone.js": "0.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/architect": "0.13.8",
|
"@angular-devkit/architect": "0.13.10",
|
||||||
"@angular-devkit/build-angular": "0.13.8",
|
"@angular-devkit/build-angular": "0.13.10",
|
||||||
"@angular-devkit/core": "7.3.8",
|
"@angular-devkit/core": "7.3.8",
|
||||||
"@angular-devkit/schematics": "7.3.8",
|
"@angular-devkit/schematics": "7.3.8",
|
||||||
"@angular/cli": "7.3.8",
|
"@angular/cli": "7.3.8",
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
"karma-jasmine-html-reporter": "1.4.2",
|
"karma-jasmine-html-reporter": "1.4.2",
|
||||||
"karma-mocha-reporter": "2.2.5",
|
"karma-mocha-reporter": "2.2.5",
|
||||||
"protractor": "5.4.2",
|
"protractor": "5.4.2",
|
||||||
"surge": "0.20.5",
|
"surge": "0.21.3",
|
||||||
"ts-node": "8.0.3",
|
"ts-node": "8.0.3",
|
||||||
"tslint": "5.15.0",
|
"tslint": "5.15.0",
|
||||||
"typescript": "3.2.4"
|
"typescript": "3.2.4"
|
||||||
|
|||||||
110
src/app/_helpers/data/sample-facets.ts
Normal file
110
src/app/_helpers/data/sample-facets.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019, 2020 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {SCFacet, SCThingType} from '@openstapps/core';
|
||||||
|
|
||||||
|
export const facetsMock: SCFacet[] = [
|
||||||
|
{
|
||||||
|
'buckets': [
|
||||||
|
{
|
||||||
|
'count': 60,
|
||||||
|
'key': 'academic event',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 160,
|
||||||
|
'key': 'message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 151,
|
||||||
|
'key': 'date series',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 106,
|
||||||
|
'key': 'dish',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 20,
|
||||||
|
'key': 'building',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 20,
|
||||||
|
'key': 'semester',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'field': 'type',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'buckets': [
|
||||||
|
{
|
||||||
|
'count': 12,
|
||||||
|
'key': 'Max Mustermann',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 2,
|
||||||
|
'key': 'Foo Bar',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'field': 'performers',
|
||||||
|
'onlyOnType': SCThingType.AcademicEvent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'buckets': [
|
||||||
|
{
|
||||||
|
'count': 5,
|
||||||
|
'key': 'colloquium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 15,
|
||||||
|
'key': 'course',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'field': 'categories',
|
||||||
|
'onlyOnType': SCThingType.AcademicEvent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'buckets': [
|
||||||
|
{
|
||||||
|
'count': 5,
|
||||||
|
'key': 'unipedia',
|
||||||
|
}],
|
||||||
|
'field': 'categories',
|
||||||
|
'onlyOnType': SCThingType.Article,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'buckets': [
|
||||||
|
{
|
||||||
|
'count': 5,
|
||||||
|
'key': 'employees',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 15,
|
||||||
|
'key': 'students',
|
||||||
|
}],
|
||||||
|
'field': 'audiences',
|
||||||
|
'onlyOnType': SCThingType.Message,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'buckets': [
|
||||||
|
{
|
||||||
|
'count': 5,
|
||||||
|
'key': 'main dish',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 15,
|
||||||
|
'key': 'salad',
|
||||||
|
}],
|
||||||
|
'field': 'categories',
|
||||||
|
'onlyOnType': SCThingType.Dish,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -25,10 +25,11 @@ 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 {Observable, of} from 'rxjs';
|
||||||
import {delay, map} from 'rxjs/operators';
|
import {delay, map} from 'rxjs/operators';
|
||||||
|
import {facetsMock} from './data/sample-facets';
|
||||||
import {SampleThings} from './data/sample-things';
|
import {SampleThings} from './data/sample-things';
|
||||||
|
|
||||||
// tslint:disable:no-magic-numbers
|
// tslint:disable:no-magic-numbers
|
||||||
const sampleIndexResponse: SCIndexResponse = {
|
export const sampleIndexResponse: SCIndexResponse = {
|
||||||
app: {
|
app: {
|
||||||
campusPolygon: {
|
campusPolygon: {
|
||||||
coordinates: [
|
coordinates: [
|
||||||
@@ -105,7 +106,7 @@ const sampleIndexResponse: SCIndexResponse = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
name: '',
|
name: 'main menu',
|
||||||
translations: {
|
translations: {
|
||||||
de: {
|
de: {
|
||||||
name: 'Hauptmenü',
|
name: 'Hauptmenü',
|
||||||
@@ -132,21 +133,6 @@ const sampleIndexResponse: SCIndexResponse = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
|
||||||
name: 'Your Study-App',
|
|
||||||
translations: {
|
|
||||||
de: {
|
|
||||||
name: 'Deine Studi-App',
|
|
||||||
},
|
|
||||||
en: {
|
|
||||||
name: 'Your Study-App',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '',
|
|
||||||
id: 'meta',
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
icon: 'information',
|
icon: 'information',
|
||||||
route: '/about',
|
route: '/about',
|
||||||
@@ -161,13 +147,13 @@ const sampleIndexResponse: SCIndexResponse = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
name: '',
|
name: 'Your Study-App',
|
||||||
translations: {
|
translations: {
|
||||||
de: {
|
de: {
|
||||||
name: '',
|
name: 'Deine Studi-App',
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
name: '',
|
name: 'Your Study-App',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -178,7 +164,8 @@ const sampleIndexResponse: SCIndexResponse = {
|
|||||||
{
|
{
|
||||||
categories: ['profile'],
|
categories: ['profile'],
|
||||||
defaultValue: 'student',
|
defaultValue: 'student',
|
||||||
description: '',
|
description: 'The user group the app is going to be used.'
|
||||||
|
+ 'This settings for example is getting used for the predefined price category of mensa meals.',
|
||||||
inputType: SCSettingInputType.SingleChoice,
|
inputType: SCSettingInputType.SingleChoice,
|
||||||
name: 'group',
|
name: 'group',
|
||||||
order: 1,
|
order: 1,
|
||||||
@@ -216,7 +203,7 @@ const sampleIndexResponse: SCIndexResponse = {
|
|||||||
{
|
{
|
||||||
categories: ['profile'],
|
categories: ['profile'],
|
||||||
defaultValue: 'en',
|
defaultValue: 'en',
|
||||||
description: '',
|
description: 'The language this app is going to use.',
|
||||||
inputType: SCSettingInputType.SingleChoice,
|
inputType: SCSettingInputType.SingleChoice,
|
||||||
name: 'language',
|
name: 'language',
|
||||||
order: 0,
|
order: 0,
|
||||||
@@ -250,7 +237,8 @@ const sampleIndexResponse: SCIndexResponse = {
|
|||||||
{
|
{
|
||||||
categories: ['privacy'],
|
categories: ['privacy'],
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
description: '',
|
description: 'Allow the App to use the device location to provide additional information\'s based ' +
|
||||||
|
'on your actual location.',
|
||||||
inputType: SCSettingInputType.SingleChoice,
|
inputType: SCSettingInputType.SingleChoice,
|
||||||
name: 'geoLocation',
|
name: 'geoLocation',
|
||||||
order: 0,
|
order: 0,
|
||||||
@@ -402,7 +390,7 @@ export class FakeBackendInterceptor implements HttpInterceptor {
|
|||||||
return this.sampleFetcher.getSampleThings()
|
return this.sampleFetcher.getSampleThings()
|
||||||
// tslint:disable-next-line:no-any
|
// tslint:disable-next-line:no-any
|
||||||
.pipe(map((sampleData: any) => {
|
.pipe(map((sampleData: any) => {
|
||||||
return new HttpResponse({status: 200, body: {data: sampleData}});
|
return new HttpResponse({status: 200, body: {data: sampleData, facets: facetsMock}});
|
||||||
}), delay(this.RESPONSE_DELAY)); // add delay for skeleton screens to be seen (see !16)
|
}), delay(this.RESPONSE_DELAY)); // add delay for skeleton screens to be seen (see !16)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,4 +84,5 @@ const providers : Provider[] = [
|
|||||||
providers:
|
providers:
|
||||||
environment.use_fake_backend ? [providers, fakeBackendProvider] : providers,
|
environment.use_fake_backend ? [providers, fakeBackendProvider] : providers,
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export class ConfigProvider {
|
|||||||
await this.init();
|
await this.init();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// don't throw ConfigFetchError if saved config is available
|
// don't throw ConfigFetchError if saved config is available
|
||||||
if (!(error.name === 'ConfigFetchError' && this.initialised)) {
|
if (error.name === 'ConfigFetchError' && !this.initialised) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ import {CommonModule} from '@angular/common';
|
|||||||
import {HttpClientModule} from '@angular/common/http';
|
import {HttpClientModule} from '@angular/common/http';
|
||||||
import {NgModule} from '@angular/core';
|
import {NgModule} from '@angular/core';
|
||||||
import {FormsModule} from '@angular/forms';
|
import {FormsModule} from '@angular/forms';
|
||||||
import {IonicModule} from '@ionic/angular';
|
import {Events, IonicModule} from '@ionic/angular';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {MarkdownModule} from 'ngx-markdown';
|
import {MarkdownModule} from 'ngx-markdown';
|
||||||
import {MomentModule} from 'ngx-moment';
|
import {MomentModule} from 'ngx-moment';
|
||||||
|
import {MenuModule} from '../menu/menu.module';
|
||||||
import {StorageModule} from '../storage/storage.module';
|
import {StorageModule} from '../storage/storage.module';
|
||||||
import {DataFacetsProvider} from './data-facets.provider';
|
import {DataFacetsProvider} from './data-facets.provider';
|
||||||
import {DataRoutingModule} from './data-routing.module';
|
import {DataRoutingModule} from './data-routing.module';
|
||||||
@@ -116,6 +117,7 @@ import {VideoListItem} from './types/video/video-list-item.component';
|
|||||||
DataRoutingModule,
|
DataRoutingModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
MarkdownModule.forRoot(),
|
MarkdownModule.forRoot(),
|
||||||
|
MenuModule,
|
||||||
MomentModule.forRoot({
|
MomentModule.forRoot({
|
||||||
relativeTimeThresholdOptions: {
|
relativeTimeThresholdOptions: {
|
||||||
'm': 59,
|
'm': 59,
|
||||||
@@ -127,6 +129,7 @@ import {VideoListItem} from './types/video/video-list-item.component';
|
|||||||
providers: [
|
providers: [
|
||||||
DataProvider,
|
DataProvider,
|
||||||
DataFacetsProvider,
|
DataFacetsProvider,
|
||||||
|
Events,
|
||||||
StAppsWebHttpClient,
|
StAppsWebHttpClient,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,92 +12,157 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {AlertController} from '@ionic/angular';
|
import {AlertController, Events} from '@ionic/angular';
|
||||||
import {SCThing} from '@openstapps/core';
|
import {
|
||||||
|
SCFacet,
|
||||||
|
SCSearchFilter,
|
||||||
|
SCSearchQuery,
|
||||||
|
SCSearchSort,
|
||||||
|
SCSettingValue,
|
||||||
|
SCSettingValues,
|
||||||
|
SCThing,
|
||||||
|
} from '@openstapps/core';
|
||||||
|
import {Logger} from '@openstapps/logger';
|
||||||
import {Subject} from 'rxjs';
|
import {Subject} from 'rxjs';
|
||||||
import {debounceTime, distinctUntilChanged} from 'rxjs/operators';
|
import {debounceTime, distinctUntilChanged} from 'rxjs/operators';
|
||||||
|
import {MenuService} from '../../menu/menu.service';
|
||||||
import {DataProvider} from '../data.provider';
|
import {DataProvider} from '../data.provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* DataListComponent queries things and shows list of things and context menu
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'stapps-data-list',
|
selector: 'stapps-data-list',
|
||||||
templateUrl: 'data-list.html',
|
templateUrl: 'data-list.html',
|
||||||
})
|
})
|
||||||
export class DataListComponent {
|
export class DataListComponent implements OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Api query filter
|
||||||
*/
|
*/
|
||||||
dataProvider: DataProvider;
|
filterQuery: SCSearchFilter | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Thing counter to start query the next page from
|
||||||
*/
|
*/
|
||||||
from = 0;
|
from = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Container for queried things
|
||||||
*/
|
*/
|
||||||
items: SCThing[];
|
items: SCThing[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Page size of queries
|
||||||
*/
|
*/
|
||||||
loaded = false;
|
pageSize = 30;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Search value from search bar
|
||||||
*/
|
*/
|
||||||
query: string;
|
queryText: string;
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
queryChanged: Subject<string> = new Subject<string>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Subject to handle search text changes
|
||||||
*/
|
*/
|
||||||
selectedItem: any;
|
queryTextChanged: Subject<string> = new Subject<string>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Time to wait for search query if search text is changing
|
||||||
*/
|
*/
|
||||||
size = 30;
|
searchQueryDueTime = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Api query sorting
|
||||||
|
*/
|
||||||
|
sortQuery: SCSearchSort | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param alertController TODO
|
* @param alertController AlertController
|
||||||
* @param dataProvider TODO
|
* @param dataProvider DataProvider
|
||||||
|
* @param events Events
|
||||||
|
* @param menuService MenuService
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private readonly alertController: AlertController,
|
private readonly alertController: AlertController,
|
||||||
dataProvider: DataProvider,
|
private dataProvider: DataProvider,
|
||||||
|
private readonly events: Events,
|
||||||
|
private readonly menuService: MenuService,
|
||||||
) {
|
) {
|
||||||
this.dataProvider = dataProvider;
|
this.queryTextChanged
|
||||||
this.queryChanged
|
|
||||||
.pipe(
|
.pipe(
|
||||||
debounceTime(1000),
|
debounceTime(this.searchQueryDueTime),
|
||||||
distinctUntilChanged())
|
distinctUntilChanged())
|
||||||
.subscribe((model) => {
|
.subscribe((model) => {
|
||||||
this.from = 0;
|
this.from = 0;
|
||||||
this.query = model;
|
this.queryText = model;
|
||||||
this.fetchItems();
|
this.fetchAndUpdateItems();
|
||||||
});
|
});
|
||||||
this.fetchItems();
|
|
||||||
|
this.menuService.filterQueryChanged$.subscribe((query) => {
|
||||||
|
this.filterQuery = query;
|
||||||
|
this.fetchAndUpdateItems();
|
||||||
|
});
|
||||||
|
this.menuService.sortQueryChanged$.subscribe((query) => {
|
||||||
|
this.sortQuery = query;
|
||||||
|
this.fetchAndUpdateItems();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fetchAndUpdateItems();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to 'settings.changed' events
|
||||||
|
*/
|
||||||
|
this.events.subscribe('stapps.settings.changed',
|
||||||
|
(category: string, name: string, value: SCSettingValue | SCSettingValues) => {
|
||||||
|
Logger.log(`received event "settings.changed" with category:
|
||||||
|
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Fetches items with set query configuration
|
||||||
|
*
|
||||||
|
* @param append If true fetched data gets appended to existing, override otherwise (default false)
|
||||||
*/
|
*/
|
||||||
private async fetchItems(): Promise<any> {
|
private async fetchAndUpdateItems(append = false): Promise<void> {
|
||||||
return this.dataProvider.search({
|
// build query search options
|
||||||
|
const searchOptions: SCSearchQuery = {
|
||||||
from: this.from,
|
from: this.from,
|
||||||
query: this.query,
|
size: this.pageSize,
|
||||||
size: this.size,
|
};
|
||||||
} as any)
|
|
||||||
|
if (this.queryText && this.queryText.length > 0) {
|
||||||
|
// add query string
|
||||||
|
searchOptions.query = this.queryText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filterQuery) {
|
||||||
|
// add query filtering
|
||||||
|
searchOptions.filter = this.filterQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sortQuery) {
|
||||||
|
// add query sorting
|
||||||
|
searchOptions.sort = [this.sortQuery];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dataProvider.search(searchOptions)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
if (append) {
|
||||||
|
// append results
|
||||||
|
this.items = this.items.concat(result.data);
|
||||||
|
} else {
|
||||||
|
// override items with results
|
||||||
this.items = result.data;
|
this.items = result.data;
|
||||||
this.loaded = true;
|
}
|
||||||
|
// update filter options if result contains facets
|
||||||
|
if (typeof result.facets !== 'undefined') {
|
||||||
|
this.updateContextFilter(result.facets);
|
||||||
|
}
|
||||||
}, async (err) => {
|
}, async (err) => {
|
||||||
const alert: HTMLIonAlertElement = await this.alertController.create({
|
const alert: HTMLIonAlertElement = await this.alertController.create({
|
||||||
buttons: ['Dismiss'],
|
buttons: ['Dismiss'],
|
||||||
@@ -110,18 +175,52 @@ export class DataListComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Loads next page of things
|
||||||
*/
|
*/
|
||||||
|
// tslint:disable-next-line:no-any
|
||||||
async loadMore(event: any): Promise<void> {
|
async loadMore(event: any): Promise<void> {
|
||||||
this.from += this.size;
|
this.from += this.pageSize;
|
||||||
await this.fetchItems();
|
await this.fetchAndUpdateItems(true);
|
||||||
event.target.complete();
|
event.target.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Initialises the sort context of menuService
|
||||||
*/
|
*/
|
||||||
search(query: string) {
|
ngOnInit(): void {
|
||||||
this.queryChanged.next(query);
|
// initialise sort option for context menu
|
||||||
|
this.menuService.setContextSort({
|
||||||
|
name: 'sort',
|
||||||
|
reversed: false,
|
||||||
|
value: 'relevance',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
reversible: false,
|
||||||
|
value: 'relevance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reversible: true,
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reversible: true,
|
||||||
|
value: 'type',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search event of search bar
|
||||||
|
*/
|
||||||
|
searchStringChanged(queryValue: string) {
|
||||||
|
this.queryTextChanged.next(queryValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates context filter in menuService with facets
|
||||||
|
*/
|
||||||
|
updateContextFilter(facets: SCFacet[]) {
|
||||||
|
this.menuService.updateContextFilter(facets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
|
<stapps-context></stapps-context>
|
||||||
<ion-header>
|
<ion-header>
|
||||||
<ion-toolbar>
|
<ion-toolbar>
|
||||||
<ion-buttons slot="start">
|
<ion-buttons slot="start">
|
||||||
<ion-back-button></ion-back-button>
|
<ion-back-button></ion-back-button>
|
||||||
<ion-menu-button></ion-menu-button>
|
<ion-menu-button></ion-menu-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-menu-button menu="context">
|
||||||
|
<ion-icon name="options"></ion-icon>
|
||||||
|
</ion-menu-button>
|
||||||
|
</ion-buttons>
|
||||||
|
|
||||||
<ion-searchbar (ngModelChange)="search($event)" [(ngModel)]="query"></ion-searchbar>
|
<ion-searchbar (ngModelChange)="searchStringChanged($event)" [(ngModel)]="queryText"></ion-searchbar>
|
||||||
|
|
||||||
<!--<ion-title>List</ion-title>-->
|
<!--<ion-title>List</ion-title>-->
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
|
|||||||
274
src/app/modules/menu/context/context-menu.component.spec.ts
Normal file
274
src/app/modules/menu/context/context-menu.component.spec.ts
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {APP_BASE_HREF, CommonModule, Location, LocationStrategy, PathLocationStrategy} from '@angular/common';
|
||||||
|
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {ChildrenOutletContexts, RouterModule, UrlSerializer} from '@angular/router';
|
||||||
|
import {IonicModule} from '@ionic/angular';
|
||||||
|
import {TranslateModule,} from '@ngx-translate/core';
|
||||||
|
import {SCFacet, SCThingType} from '@openstapps/core';
|
||||||
|
import {ContextMenuComponent} from './context-menu.component';
|
||||||
|
import {SettingsModule} from '../../settings/settings.module';
|
||||||
|
import {MenuService} from '../menu.service';
|
||||||
|
import {FilterContext, SortContext} from './context-type';
|
||||||
|
|
||||||
|
describe('ContextMenuComponent', async () => {
|
||||||
|
let fixture: ComponentFixture<ContextMenuComponent>;
|
||||||
|
let instance: ContextMenuComponent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ContextMenuComponent],
|
||||||
|
providers: [
|
||||||
|
ChildrenOutletContexts,
|
||||||
|
Location,
|
||||||
|
UrlSerializer,
|
||||||
|
MenuService,
|
||||||
|
{provide: LocationStrategy, useClass: PathLocationStrategy},
|
||||||
|
{provide: APP_BASE_HREF, useValue: '/'},
|
||||||
|
],
|
||||||
|
// tslint:disable-next-line:object-literal-sort-keys
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
IonicModule.forRoot(),
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
CommonModule,
|
||||||
|
SettingsModule,
|
||||||
|
RouterModule.forRoot([]),
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ContextMenuComponent);
|
||||||
|
instance = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show items in sort context', () => {
|
||||||
|
instance.sortOption = getSortContextType();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const sort: HTMLElement = fixture.debugElement.nativeElement.querySelector('.context-sort');
|
||||||
|
const sortItem = sort.querySelector('.sort-item');
|
||||||
|
expect(sortItem!.querySelector('ion-label')!.textContent).toContain('relevance');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show items in filter context', () => {
|
||||||
|
instance.filterOption = getFilterContextType();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const filter: HTMLElement = fixture.debugElement.nativeElement.querySelector('.context-filter');
|
||||||
|
const filterItem = filter.querySelector('.filter-group');
|
||||||
|
expect(filterItem!.querySelector('ion-list-header')!.textContent).toContain('Type');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set sort context value and reverse on click', () => {
|
||||||
|
instance.sortOption = getSortContextType();
|
||||||
|
fixture.detectChanges();
|
||||||
|
const sort: HTMLElement = fixture.debugElement.nativeElement.querySelector('.context-sort');
|
||||||
|
// @ts-ignore
|
||||||
|
const sortItem: HTMLElement = sort.querySelectorAll('.sort-item')[1];
|
||||||
|
sortItem!.click();
|
||||||
|
expect(instance.sortOption.value).toEqual('name');
|
||||||
|
expect(instance.sortOption.reversed).toBe(false);
|
||||||
|
|
||||||
|
// click again for reverse
|
||||||
|
sortItem!.click();
|
||||||
|
expect(instance.sortOption.reversed).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show all filterable facets', () => {
|
||||||
|
// get set facets with non empty buckets
|
||||||
|
const facets: SCFacet[] = getFilterContextType().options;
|
||||||
|
|
||||||
|
instance.filterOption = getFilterContextType();
|
||||||
|
fixture.detectChanges();
|
||||||
|
// get filter context div
|
||||||
|
const filter: HTMLElement = fixture.debugElement.nativeElement.querySelector('.context-filter');
|
||||||
|
// get all filter groups that represent a facet
|
||||||
|
const filterGroups = filter.querySelectorAll('.filter-group');
|
||||||
|
|
||||||
|
expect(filterGroups.length).toEqual(facets.length);
|
||||||
|
|
||||||
|
for (const facet of facets) {
|
||||||
|
let filterGroup = undefined;
|
||||||
|
|
||||||
|
// get filter option for facets field
|
||||||
|
filterGroups.forEach((element) => {
|
||||||
|
if (element.querySelector('ion-list-header')!.textContent!.toString().toLowerCase().indexOf(facet.field) > -1) {
|
||||||
|
filterGroup = element;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(filterGroup).toBeDefined();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const filterItems = filterGroup.querySelectorAll('.filter-item-label');
|
||||||
|
|
||||||
|
if (filterItems.length !== facet.buckets.length) {
|
||||||
|
console.log(JSON.stringify(facet));
|
||||||
|
}
|
||||||
|
expect(filterItems.length).toEqual(facet.buckets.length);
|
||||||
|
|
||||||
|
// check all buckets are shown
|
||||||
|
for (const bucket of facet.buckets) {
|
||||||
|
let filterItem;
|
||||||
|
|
||||||
|
for (let i = 0; i < filterItems.length; i++) {
|
||||||
|
if (filterItems.item(i)
|
||||||
|
.textContent!.toString().toLowerCase()
|
||||||
|
.indexOf(bucket.key.toLowerCase()) > 0) {
|
||||||
|
filterItem = filterItems.item(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(filterItem).toBeDefined();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset filter', () => {
|
||||||
|
instance.filterOption = getFilterContextType();
|
||||||
|
instance.filterOption.options = [{
|
||||||
|
field: 'type',
|
||||||
|
buckets: [
|
||||||
|
{count: 10, key: 'date series', checked: true}
|
||||||
|
]
|
||||||
|
}];
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// click reset button
|
||||||
|
const resetButton: HTMLElement = fixture.debugElement.nativeElement.querySelector('.resetFilterButton');
|
||||||
|
resetButton.click();
|
||||||
|
|
||||||
|
expect(instance.filterOption.options[0].buckets[0].checked).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getSortContextType(): SortContext {
|
||||||
|
return {
|
||||||
|
name: 'sort',
|
||||||
|
reversed: false,
|
||||||
|
value: 'relevance',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
reversible: false,
|
||||||
|
value: 'relevance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reversible: true,
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reversible: true,
|
||||||
|
value: 'date',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reversible: true,
|
||||||
|
value: 'type',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilterContextType(): FilterContext {
|
||||||
|
return {
|
||||||
|
name: 'filter',
|
||||||
|
compact: false,
|
||||||
|
options: facetsMock.filter((facet) => facet.buckets.length > 0).map((facet) => {
|
||||||
|
return {
|
||||||
|
buckets: facet.buckets.map((bucket) => {
|
||||||
|
return {
|
||||||
|
count: bucket.count,
|
||||||
|
key: bucket.key,
|
||||||
|
checked: false,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
compact: false,
|
||||||
|
field: facet.field,
|
||||||
|
onlyOnType: facet.onlyOnType
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const facetsMock: SCFacet[] = [
|
||||||
|
{
|
||||||
|
'buckets': [
|
||||||
|
{
|
||||||
|
'count': 60,
|
||||||
|
'key': 'academic event',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 160,
|
||||||
|
'key': 'message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 151,
|
||||||
|
'key': 'date series',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 106,
|
||||||
|
'key': 'dish',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 20,
|
||||||
|
'key': 'building',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'field': 'type',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'buckets': [
|
||||||
|
{
|
||||||
|
'count': 12,
|
||||||
|
'key': 'Max Mustermann',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 2,
|
||||||
|
'key': 'Foo Bar',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'field': 'performers',
|
||||||
|
'onlyOnType': SCThingType.AcademicEvent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'buckets': [
|
||||||
|
{
|
||||||
|
'count': 5,
|
||||||
|
'key': 'colloquium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 15,
|
||||||
|
'key': 'course',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'field': 'categories',
|
||||||
|
'onlyOnType': SCThingType.AcademicEvent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'buckets': [
|
||||||
|
{
|
||||||
|
'count': 5,
|
||||||
|
'key': 'employees',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 15,
|
||||||
|
'key': 'students',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'field': 'audiences',
|
||||||
|
'onlyOnType': SCThingType.Message,
|
||||||
|
},
|
||||||
|
];
|
||||||
139
src/app/modules/menu/context/context-menu.component.ts
Normal file
139
src/app/modules/menu/context/context-menu.component.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018, 2019, 2020 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
SCLanguage,
|
||||||
|
SCThingTranslator,
|
||||||
|
SCThingType,
|
||||||
|
SCTranslations,
|
||||||
|
} from '@openstapps/core';
|
||||||
|
import {MenuService} from '../menu.service';
|
||||||
|
import {FilterContext, SortContext, SortContextOption} from './context-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context menu
|
||||||
|
*
|
||||||
|
* It can be configured with sorting types and filtering on facets
|
||||||
|
*
|
||||||
|
* Example:<br>
|
||||||
|
* `<stapps-context (optionChange)="onOptionChange($event)" (settingChange)="onSettingChange($event)"
|
||||||
|
* [sortOption]="SortContext" [filterOption]="FilterContext"></stapps-context>`
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'stapps-context',
|
||||||
|
templateUrl: 'context-menu.html',
|
||||||
|
})
|
||||||
|
export class ContextMenuComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of filter options shown on compact view
|
||||||
|
*/
|
||||||
|
compactFilterOptionCount = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for the filter context
|
||||||
|
*/
|
||||||
|
filterOption: FilterContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible languages to be used for translation
|
||||||
|
*/
|
||||||
|
language: keyof SCTranslations<SCLanguage>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping of SCThingType
|
||||||
|
*/
|
||||||
|
scThingType = SCThingType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for the sort context
|
||||||
|
*/
|
||||||
|
sortOption: SortContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core translator
|
||||||
|
*/
|
||||||
|
translator: SCThingTranslator;
|
||||||
|
|
||||||
|
constructor(private translateService: TranslateService,
|
||||||
|
private readonly menuService: MenuService) {
|
||||||
|
this.language = this.translateService.currentLang as keyof SCTranslations<SCLanguage>;
|
||||||
|
this.translator = new SCThingTranslator(this.language);
|
||||||
|
|
||||||
|
this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
|
||||||
|
this.language = event.lang as keyof SCTranslations<SCLanguage>;
|
||||||
|
this.translator = new SCThingTranslator(this.language);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.menuService.filterContextChanged$.subscribe((filterContext) => {
|
||||||
|
this.filterOption = filterContext;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.menuService.sortOptions.subscribe((sortContext) => {
|
||||||
|
this.sortOption = sortContext;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets selected filter options and updates listener
|
||||||
|
*/
|
||||||
|
filterChanged = () => {
|
||||||
|
this.menuService.contextFilterChanged(this.filterOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns translated property name
|
||||||
|
*/
|
||||||
|
getTranslatedPropertyName(property: string, onlyForType?: SCThingType): string {
|
||||||
|
return (this.translator
|
||||||
|
// tslint:disable-next-line:no-any
|
||||||
|
.translatedPropertyNames(onlyForType ? onlyForType : SCThingType.AcademicEvent) as any)[property];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns translated type of given SCThingType string
|
||||||
|
*/
|
||||||
|
getTranslatedType(scThingType: string) {
|
||||||
|
return this.translator.translatedThingType(scThingType as SCThingType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets filter options
|
||||||
|
*/
|
||||||
|
resetFilter = (option: FilterContext) => {
|
||||||
|
option.options.forEach((filterFacet) => filterFacet.buckets.forEach((filterBucket) => {
|
||||||
|
filterBucket.checked = false;
|
||||||
|
}));
|
||||||
|
this.menuService.contextFilterChanged(this.filterOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates selected sort option and updates listener
|
||||||
|
*/
|
||||||
|
sortChanged = (option: SortContext, value: SortContextOption) => {
|
||||||
|
if (option.value === value.value) {
|
||||||
|
if (value.reversible) {
|
||||||
|
option.reversed = !option.reversed;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
option.value = value.value;
|
||||||
|
if (value.reversible) {
|
||||||
|
option.reversed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.menuService.contextSortChanged(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
src/app/modules/menu/context/context-menu.html
Normal file
80
src/app/modules/menu/context/context-menu.html
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<ion-menu type="overlay" menuId="context" side="end">
|
||||||
|
<ion-card-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="right">
|
||||||
|
<ion-back-button></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>{{'menu.context.title' | translate | titlecase}}</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-card-header>
|
||||||
|
<ion-content>
|
||||||
|
<!-- Sort Context -->
|
||||||
|
<ion-radio-group class="context-sort" *ngIf="sortOption">
|
||||||
|
<ion-list-header>
|
||||||
|
<ion-icon name="swap"></ion-icon>
|
||||||
|
<ion-title>{{'menu.context.sort.title' | translate | titlecase}}</ion-title>
|
||||||
|
</ion-list-header>
|
||||||
|
<ion-item class="sort-item"
|
||||||
|
*ngFor="let value of sortOption.values, index as i"
|
||||||
|
(click)="sortChanged(sortOption, value)">
|
||||||
|
<ion-label>{{'menu.context.sort.' + value.value | translate | titlecase}}
|
||||||
|
<span *ngIf="sortOption.value === value.value && value.reversible">
|
||||||
|
<ion-icon *ngIf="sortOption.reversed" name="arrow-dropdown"></ion-icon>
|
||||||
|
<ion-icon *ngIf="!sortOption.reversed" name="arrow-dropup"></ion-icon>
|
||||||
|
</span>
|
||||||
|
</ion-label>
|
||||||
|
<ion-radio mode="ios" slot="start" [value]="value.value" [checked]="i == 0">
|
||||||
|
|
||||||
|
</ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
<!-- Filter Context -->
|
||||||
|
<div class="context-filter" *ngIf="filterOption">
|
||||||
|
<ion-list-header>
|
||||||
|
<ion-icon name="funnel"></ion-icon>
|
||||||
|
<ion-title>{{'menu.context.filter.title' | translate | titlecase}}</ion-title>
|
||||||
|
<ion-button class="resetFilterButton" fill="clear" color="dark" (click)="resetFilter(filterOption)">
|
||||||
|
<ion-icon name="trash"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</ion-list-header>
|
||||||
|
|
||||||
|
<ion-list class="filter-group"
|
||||||
|
*ngFor="let facet of !filterOption.compact ?
|
||||||
|
filterOption.options.slice(0, compactFilterOptionCount) : filterOption.options">
|
||||||
|
<ion-list-header class="h3">
|
||||||
|
{{(facet.onlyOnType ?
|
||||||
|
getTranslatedPropertyName(facet.field, facet.onlyOnType)
|
||||||
|
: (getTranslatedPropertyName(facet.field))) | titlecase}}
|
||||||
|
{{facet.onlyOnType? ' | ' + (getTranslatedType(facet.onlyOnType) | titlecase) : ''}}
|
||||||
|
</ion-list-header>
|
||||||
|
<div *ngIf="facet.buckets.length > 0">
|
||||||
|
<ion-item
|
||||||
|
*ngFor="let bucket of !facet.compact ?
|
||||||
|
facet.buckets.slice(0, compactFilterOptionCount) : facet.buckets">
|
||||||
|
<ion-label class="filter-item-label">
|
||||||
|
({{bucket.count}}) {{facet.field === 'type' ? (getTranslatedType(bucket.key) | titlecase) : bucket.key | titlecase}}
|
||||||
|
</ion-label>
|
||||||
|
<ion-checkbox
|
||||||
|
mode="ios"
|
||||||
|
[(ngModel)]="bucket.checked"
|
||||||
|
(ngModelChange)="filterChanged()"
|
||||||
|
[value]="{field: facet.field, value: bucket.key, onlyOnType: facet.onlyOnType}">
|
||||||
|
|
||||||
|
</ion-checkbox>
|
||||||
|
</ion-item>
|
||||||
|
<ion-button fill="clear"
|
||||||
|
*ngIf="!facet.compact && facet.buckets.length > compactFilterOptionCount"
|
||||||
|
(click)="facet.compact = true">
|
||||||
|
{{'menu.context.filter.showAll' | translate}}
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</ion-list>
|
||||||
|
<ion-button fill="clear"
|
||||||
|
*ngIf="!filterOption.compact && filterOption.options.length > compactFilterOptionCount"
|
||||||
|
(click)="filterOption.compact = true">
|
||||||
|
{{'menu.context.filter.showAll' | translate}}
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</ion-content>
|
||||||
|
</ion-menu>
|
||||||
|
<ion-router-outlet main></ion-router-outlet>
|
||||||
7
src/app/modules/menu/context/context-menu.scss
Normal file
7
src/app/modules/menu/context/context-menu.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
stapps-navigation {
|
||||||
|
ion-radio {
|
||||||
|
.radio-icon {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/app/modules/menu/context/context-type.ts
Normal file
94
src/app/modules/menu/context/context-type.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {SCFacet, SCFacetBucket} from '@openstapps/core';
|
||||||
|
|
||||||
|
export type ContextType = FilterContext | SortContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sort context
|
||||||
|
*/
|
||||||
|
export interface SortContext {
|
||||||
|
/**
|
||||||
|
* Name of the context
|
||||||
|
*/
|
||||||
|
name: 'sort';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse option
|
||||||
|
*/
|
||||||
|
reversed: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sort value
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort options
|
||||||
|
*/
|
||||||
|
values: SortContextOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A sort context option
|
||||||
|
*/
|
||||||
|
export interface SortContextOption {
|
||||||
|
/**
|
||||||
|
* sort option is reversible
|
||||||
|
*/
|
||||||
|
reversible: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sort option value
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter context
|
||||||
|
*/
|
||||||
|
export interface FilterContext {
|
||||||
|
/**
|
||||||
|
* Compact view of the filter options
|
||||||
|
*/
|
||||||
|
compact?: boolean;
|
||||||
|
/**
|
||||||
|
* Name of the context
|
||||||
|
*/
|
||||||
|
name: 'filter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter values
|
||||||
|
*/
|
||||||
|
options: FilterFacet[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterFacet extends SCFacet {
|
||||||
|
/**
|
||||||
|
* FilterBuckets of a FilterFacet
|
||||||
|
*/
|
||||||
|
buckets: FilterBucket[];
|
||||||
|
/**
|
||||||
|
* Compact view of the option buckets
|
||||||
|
*/
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterBucket extends SCFacetBucket {
|
||||||
|
/**
|
||||||
|
* Sets the Filter active
|
||||||
|
*/
|
||||||
|
checked: boolean;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2018, 2019 StApps
|
* Copyright (C) 2018, 2019, 2020 StApps
|
||||||
* This program is free software: you can redistribute it and/or modify it
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
* under the terms of the GNU General Public License as published by the Free
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
* Software Foundation, version 3.
|
* Software Foundation, version 3.
|
||||||
@@ -14,27 +14,37 @@
|
|||||||
*/
|
*/
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {NgModule} from '@angular/core';
|
import {NgModule} from '@angular/core';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
import {RouterModule} from '@angular/router';
|
import {RouterModule} from '@angular/router';
|
||||||
import {IonicModule} from '@ionic/angular';
|
import {IonicModule} from '@ionic/angular';
|
||||||
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
import {SettingsModule} from '../settings/settings.module';
|
||||||
|
import {ContextMenuComponent} from './context/context-menu.component';
|
||||||
|
import {MenuService} from './menu.service';
|
||||||
import {NavigationComponent} from './navigation/navigation.component';
|
import {NavigationComponent} from './navigation/navigation.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Menu module
|
||||||
*/
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
NavigationComponent,
|
NavigationComponent,
|
||||||
],
|
ContextMenuComponent,
|
||||||
entryComponents: [
|
|
||||||
NavigationComponent,
|
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
NavigationComponent,
|
NavigationComponent,
|
||||||
|
ContextMenuComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
|
FormsModule,
|
||||||
IonicModule.forRoot(),
|
IonicModule.forRoot(),
|
||||||
|
TranslateModule.forChild(),
|
||||||
CommonModule,
|
CommonModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
|
SettingsModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
MenuService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class MenuModule {}
|
export class MenuModule {}
|
||||||
|
|||||||
148
src/app/modules/menu/menu.service.spec.ts
Normal file
148
src/app/modules/menu/menu.service.spec.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import {TestBed} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import {MenuService} from './menu.service';
|
||||||
|
import {SCFacet} from '@openstapps/core';
|
||||||
|
import {FilterContext, SortContext} from './context/context-type';
|
||||||
|
|
||||||
|
describe('MenuService', () => {
|
||||||
|
let service: MenuService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [MenuService]
|
||||||
|
});
|
||||||
|
service = TestBed.get(MenuService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update filterOptions', (done) => {
|
||||||
|
service.filterContextChanged$.subscribe((result) => {
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
service.updateContextFilter(facetsMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update filterQuery', (done) => {
|
||||||
|
service.filterContextChanged$.subscribe(() => {
|
||||||
|
service.contextFilterChanged(filterContext);
|
||||||
|
});
|
||||||
|
service.filterQueryChanged$.subscribe((result) => {
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
service.updateContextFilter(facetsMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update sortOptions', (done) => {
|
||||||
|
service.sortContextChanged$.subscribe((result) => {
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
service.setContextSort(sortContext);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update sortQuery', (done) => {
|
||||||
|
service.sortContextChanged$.subscribe(() => {
|
||||||
|
service.contextSortChanged(sortContext);
|
||||||
|
});
|
||||||
|
service.sortQueryChanged$.subscribe((result) => {
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
service.setContextSort(sortContext);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const facetsMock: SCFacet[] =
|
||||||
|
[{
|
||||||
|
'buckets': [
|
||||||
|
{
|
||||||
|
'count': 60,
|
||||||
|
'key': 'academic event',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 160,
|
||||||
|
'key': 'message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 151,
|
||||||
|
'key': 'date series',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 106,
|
||||||
|
'key': 'dish',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 20,
|
||||||
|
'key': 'building',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'count': 20,
|
||||||
|
'key': 'semester',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'field': 'type',
|
||||||
|
}];
|
||||||
|
|
||||||
|
const filterContext: FilterContext = {
|
||||||
|
name: 'filter',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
buckets: [
|
||||||
|
{
|
||||||
|
checked: true,
|
||||||
|
count: 60,
|
||||||
|
key: 'academic event'
|
||||||
|
}, {
|
||||||
|
checked: false,
|
||||||
|
count: 160,
|
||||||
|
key: 'message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checked: false,
|
||||||
|
count: 151,
|
||||||
|
key: 'date series'
|
||||||
|
}, {
|
||||||
|
checked: false,
|
||||||
|
count: 106,
|
||||||
|
key: 'dish'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checked: false,
|
||||||
|
count: 20,
|
||||||
|
key: 'building'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checked: false,
|
||||||
|
count: 20,
|
||||||
|
key: 'semester'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
field: 'type'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortContext: SortContext = {
|
||||||
|
name: 'sort',
|
||||||
|
reversed: false,
|
||||||
|
value: 'name',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
reversible: false,
|
||||||
|
value: 'relevance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reversible: true,
|
||||||
|
value: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
reversible: true,
|
||||||
|
value: 'type',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
221
src/app/modules/menu/menu.service.ts
Normal file
221
src/app/modules/menu/menu.service.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 StApps
|
||||||
|
* This program is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {SCFacet, SCFacetBucket, SCSearchFilter, SCSearchSort} from '@openstapps/core';
|
||||||
|
import {Subject} from 'rxjs';
|
||||||
|
import {FilterBucket, FilterContext, FilterFacet, SortContext} from './context/context-type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MenuService provides bidirectional communication of context menu options and search queries
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class MenuService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local filter context object
|
||||||
|
*/
|
||||||
|
contextFilter: FilterContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for the filter context
|
||||||
|
*/
|
||||||
|
// tslint:disable-next-line:member-ordering
|
||||||
|
filterOptions = new Subject<FilterContext>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable filterContext streams
|
||||||
|
*/
|
||||||
|
// tslint:disable-next-line:member-ordering
|
||||||
|
filterContextChanged$ = this.filterOptions.asObservable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for the filter query (SCSearchFilter)
|
||||||
|
*/
|
||||||
|
filterQuery = new Subject<SCSearchFilter>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable filterContext streams
|
||||||
|
*/
|
||||||
|
// tslint:disable-next-line:member-ordering
|
||||||
|
filterQueryChanged$ = this.filterQuery.asObservable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for the sort context
|
||||||
|
*/
|
||||||
|
// tslint:disable-next-line:member-ordering
|
||||||
|
sortOptions = new Subject<SortContext>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable SortContext streams
|
||||||
|
*/
|
||||||
|
// tslint:disable-next-line:member-ordering
|
||||||
|
sortContextChanged$ = this.sortOptions.asObservable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for the sort query
|
||||||
|
*/
|
||||||
|
sortQuery = new Subject<SCSearchSort>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observable SortContext streams
|
||||||
|
*/
|
||||||
|
// tslint:disable-next-line:member-ordering
|
||||||
|
sortQueryChanged$ = this.sortQuery.asObservable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns SCSearchFilter if filterContext value is set, undefined otherwise
|
||||||
|
* @param filterContext FilterContext to build SCSearchFilter from
|
||||||
|
*/
|
||||||
|
buildFilterQuery = (filterContext: FilterContext): SCSearchFilter | undefined => {
|
||||||
|
const filters: SCSearchFilter[] = [];
|
||||||
|
|
||||||
|
filterContext.options.forEach((filterFacet) => {
|
||||||
|
filterFacet.buckets.forEach((filterBucket) => {
|
||||||
|
if (filterBucket.checked) {
|
||||||
|
filters.push(
|
||||||
|
{
|
||||||
|
arguments: {
|
||||||
|
field: filterFacet.field,
|
||||||
|
value: filterBucket.key,
|
||||||
|
},
|
||||||
|
type: 'value',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filters.length > 0) {
|
||||||
|
return {
|
||||||
|
arguments: {
|
||||||
|
filters: filters,
|
||||||
|
operation: 'or',
|
||||||
|
},
|
||||||
|
type: 'boolean',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns SCSearchSort if sorting value is set, undefined otherwise
|
||||||
|
* @param sortContext SortContext to build SCSearchSort from
|
||||||
|
*/
|
||||||
|
buildSortQuery = (sortContext: SortContext): SCSearchSort | undefined => {
|
||||||
|
if (sortContext.value && sortContext.value.length > 0) {
|
||||||
|
if (sortContext.value === 'name' || sortContext.value === 'type') {
|
||||||
|
return {
|
||||||
|
arguments: {
|
||||||
|
field: sortContext.value,
|
||||||
|
position: 0,
|
||||||
|
},
|
||||||
|
order: sortContext.reversed ? 'desc' : 'asc',
|
||||||
|
type: 'ducet',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates filter query from filterContext
|
||||||
|
*/
|
||||||
|
contextFilterChanged(filterContext: FilterContext) {
|
||||||
|
this.filterQuery.next(this.buildFilterQuery(filterContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates sort query from sortContext
|
||||||
|
*/
|
||||||
|
contextSortChanged(sortContext: SortContext) {
|
||||||
|
this.sortQuery.next(this.buildSortQuery(sortContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets sort context
|
||||||
|
*/
|
||||||
|
setContextSort(sortContext: SortContext) {
|
||||||
|
this.sortOptions.next(sortContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the filter context options from given facets
|
||||||
|
*/
|
||||||
|
updateContextFilter(facets: SCFacet[]) {
|
||||||
|
// arrange facet field "type" to first position
|
||||||
|
facets.sort((a: SCFacet, b: SCFacet) => {
|
||||||
|
if (a.field === 'type') {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.field === 'type') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.contextFilter) {
|
||||||
|
this.contextFilter = {
|
||||||
|
name: 'filter',
|
||||||
|
options: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateContextFilterOptions(this.contextFilter, facets);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates context filter with new facets.
|
||||||
|
* It preserves the checked status of existing filter options
|
||||||
|
*/
|
||||||
|
updateContextFilterOptions = (contextFilter: FilterContext, facets: SCFacet[]) => {
|
||||||
|
const newFilterOptions: FilterFacet[] = [];
|
||||||
|
|
||||||
|
// iterate new facets
|
||||||
|
for (const facet of facets) {
|
||||||
|
if (facet.buckets.length > 0) {
|
||||||
|
const newFilterFacet: FilterFacet = {
|
||||||
|
buckets: [],
|
||||||
|
field: facet.field,
|
||||||
|
onlyOnType: facet.onlyOnType,
|
||||||
|
};
|
||||||
|
newFilterOptions.push(newFilterFacet);
|
||||||
|
|
||||||
|
// search existing filterOption
|
||||||
|
const filterOption = contextFilter.options.find((contextFacet: FilterFacet) =>
|
||||||
|
contextFacet.field === facet.field && contextFacet.onlyOnType === facet.onlyOnType);
|
||||||
|
facet.buckets.forEach((bucket: SCFacetBucket) => {
|
||||||
|
// search existing bucket to preserve checked status
|
||||||
|
const existingFilterBucket = filterOption ? filterOption.buckets
|
||||||
|
.find((contextBucket: FilterBucket) => contextBucket.key === bucket.key) : undefined;
|
||||||
|
const filterBucket: FilterBucket = {
|
||||||
|
checked: existingFilterBucket ? existingFilterBucket.checked : false,
|
||||||
|
count: bucket.count,
|
||||||
|
key: bucket.key,
|
||||||
|
};
|
||||||
|
newFilterFacet.buckets.push(filterBucket);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update filter options
|
||||||
|
contextFilter.options = newFilterOptions;
|
||||||
|
this.contextFilter = contextFilter;
|
||||||
|
|
||||||
|
this.filterOptions.next(contextFilter);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,17 +14,10 @@
|
|||||||
*/
|
*/
|
||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||||
import {
|
import {SCAppConfigurationMenuCategory, SCLanguage, SCThingTranslator, SCTranslations} from '@openstapps/core';
|
||||||
SCAppConfigurationMenuCategory,
|
|
||||||
SCThing,
|
|
||||||
SCThingTranslator,
|
|
||||||
SCTranslations,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {Logger} from '@openstapps/logger';
|
import {Logger} from '@openstapps/logger';
|
||||||
import {ConfigProvider} from '../../config/config.provider';
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
|
|
||||||
const logger = new Logger();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generated class for the MenuPage page.
|
* Generated class for the MenuPage page.
|
||||||
*
|
*
|
||||||
@@ -37,34 +30,42 @@ const logger = new Logger();
|
|||||||
templateUrl: 'navigation.html',
|
templateUrl: 'navigation.html',
|
||||||
})
|
})
|
||||||
export class NavigationComponent {
|
export class NavigationComponent {
|
||||||
language: keyof SCTranslations<SCThing> = 'en';
|
|
||||||
|
/**
|
||||||
|
* Possible languages to be used for translation
|
||||||
|
*/
|
||||||
|
language: keyof SCTranslations<SCLanguage> = 'en';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Menu entries from config module
|
||||||
|
*/
|
||||||
menu: SCAppConfigurationMenuCategory[];
|
menu: SCAppConfigurationMenuCategory[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Core translator
|
||||||
|
*/
|
||||||
translator: SCThingTranslator;
|
translator: SCThingTranslator;
|
||||||
|
|
||||||
constructor(private configProvider: ConfigProvider,
|
constructor(private configProvider: ConfigProvider,
|
||||||
public translateService: TranslateService) {
|
public translateService: TranslateService) {
|
||||||
this.loadMenuEntries().then(() =>
|
this.loadMenuEntries();
|
||||||
logger.log('menuEntries loaded from config: ' + JSON.stringify(this.menu))).catch((error) => {
|
|
||||||
logger.error(error);
|
|
||||||
|
|
||||||
});
|
|
||||||
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
|
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
|
||||||
this.language = event.lang as keyof SCTranslations<any>;
|
this.language = event.lang as keyof SCTranslations<SCLanguage>;
|
||||||
this.translator = new SCThingTranslator(this.language, 'en');
|
this.translator = new SCThingTranslator(this.language);
|
||||||
});
|
});
|
||||||
this.translator = new SCThingTranslator('en', 'en');
|
this.translator = new SCThingTranslator('en');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* Loads menu entris from configProvider
|
||||||
*/
|
*/
|
||||||
// tslint:disable-next-line:prefer-function-over-method
|
async loadMenuEntries() {
|
||||||
ionViewDidLoad() {
|
try {
|
||||||
// console.log('ionViewDidLoad MenuPage');
|
this.menu = await this.configProvider.getValue('menus') as SCAppConfigurationMenuCategory[];
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`error from loading menu entries: ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadMenuEntries() {
|
|
||||||
this.menu = await this.configProvider.getValue('menus') as SCAppConfigurationMenuCategory[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// openPage(page) {
|
// openPage(page) {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
</ion-header>
|
</ion-header>
|
||||||
<ion-content>
|
<ion-content>
|
||||||
<ion-list *ngFor="let category of menu">
|
<ion-list *ngFor="let category of menu">
|
||||||
<ion-list *ngIf="category.id === 'main'">
|
<ion-list-header
|
||||||
<ion-list-header *ngIf="category.name !== ''">{{category.translations[language].name | titlecase}}</ion-list-header>
|
*ngIf="category.name !== ''">{{category.translations[language].name | titlecase}}</ion-list-header>
|
||||||
<ion-menu-toggle auto-hide="false" *ngFor="let item of category.items">
|
<ion-menu-toggle auto-hide="false" *ngFor="let item of category.items">
|
||||||
<ion-item [routerDirection]="'root'" [routerLink]="[item.route]">
|
<ion-item [routerDirection]="'root'" [routerLink]="[item.route]">
|
||||||
<ion-icon slot="end" [name]="item.icon"></ion-icon>
|
<ion-icon slot="end" [name]="item.icon"></ion-icon>
|
||||||
@@ -21,29 +21,6 @@
|
|||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-menu-toggle>
|
</ion-menu-toggle>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
<ion-list *ngIf="category.id === 'personal'">
|
|
||||||
<ion-list-header *ngIf="category.name !== ''">{{category.translations[language].name | titlecase}}</ion-list-header>
|
|
||||||
<ion-menu-toggle auto-hide="false" *ngFor="let item of category.items">
|
|
||||||
<ion-item [routerDirection]="'root'" [routerLink]="[item.route]">
|
|
||||||
<ion-icon slot="end" [name]="item.icon"></ion-icon>
|
|
||||||
<ion-label>
|
|
||||||
{{item.translations[language].title | titlecase}}
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ion-menu-toggle>
|
|
||||||
</ion-list>
|
|
||||||
<ion-list *ngIf="category.id === 'meta'">
|
|
||||||
<ion-list-header *ngIf="category.name !== ''">{{category.translations[language].name | titlecase}}</ion-list-header>
|
|
||||||
<ion-menu-toggle auto-hide="false" *ngFor="let item of category.items">
|
|
||||||
<ion-item [routerDirection]="'root'" [routerLink]="[item.route]">
|
|
||||||
<ion-icon slot="end" [name]="item.icon"></ion-icon>
|
|
||||||
<ion-label>
|
|
||||||
{{item.translations[language].title | titlecase}}
|
|
||||||
</ion-label>
|
|
||||||
</ion-item>
|
|
||||||
</ion-menu-toggle>
|
|
||||||
</ion-list>
|
|
||||||
</ion-list>
|
|
||||||
</ion-content>
|
</ion-content>
|
||||||
</ion-menu>
|
</ion-menu>
|
||||||
<ion-router-outlet main></ion-router-outlet>
|
<ion-router-outlet main></ion-router-outlet>
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import {AlertController} from '@ionic/angular';
|
|||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
SCLanguage,
|
SCLanguage,
|
||||||
|
SCLanguageCode,
|
||||||
SCSetting,
|
SCSetting,
|
||||||
SCSettingValue,
|
SCSettingValue,
|
||||||
SCSettingValues,
|
SCSettingValues,
|
||||||
SCThingTranslator,
|
SCThingTranslator,
|
||||||
SCTranslations,
|
SCTranslations,
|
||||||
} from '@openstapps/core';
|
} from '@openstapps/core';
|
||||||
import {Logger} from '@openstapps/logger';
|
|
||||||
import {SettingsProvider} from '../settings.provider';
|
import {SettingsProvider} from '../settings.provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,6 +34,12 @@ import {SettingsProvider} from '../settings.provider';
|
|||||||
templateUrl: 'settings-item.html',
|
templateUrl: 'settings-item.html',
|
||||||
})
|
})
|
||||||
export class SettingsItemComponent implements OnInit {
|
export class SettingsItemComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set the setting will be shown as compact view
|
||||||
|
*/
|
||||||
|
@Input() compactView = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag for workaround for selected 'select option' not updating translation
|
* Flag for workaround for selected 'select option' not updating translation
|
||||||
*/
|
*/
|
||||||
@@ -107,8 +113,6 @@ export class SettingsItemComponent implements OnInit {
|
|||||||
* NgInit
|
* NgInit
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
Logger.log(JSON.stringify(this.setting));
|
|
||||||
|
|
||||||
this.translatedSetting = this.translator.translate(this.setting);
|
this.translatedSetting = this.translator.translate(this.setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +140,7 @@ export class SettingsItemComponent implements OnInit {
|
|||||||
// handle general settings, with special actions
|
// handle general settings, with special actions
|
||||||
switch (this.setting.name) {
|
switch (this.setting.name) {
|
||||||
case 'language':
|
case 'language':
|
||||||
this.translateService.use(this.setting.value.toString());
|
this.translateService.use(this.setting.value as SCLanguageCode);
|
||||||
break;
|
break;
|
||||||
case 'geoLocation':
|
case 'geoLocation':
|
||||||
if (!!this.setting.value) {
|
if (!!this.setting.value) {
|
||||||
@@ -146,7 +150,9 @@ export class SettingsItemComponent implements OnInit {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
await this.settingsProvider
|
await this.settingsProvider
|
||||||
.setSettingValue(this.setting.categories[0], this.setting.name, this.setting.value);
|
.setSettingValue(this.setting.categories[0],
|
||||||
|
this.setting.name,
|
||||||
|
this.setting.value);
|
||||||
} else {
|
} else {
|
||||||
// reset setting
|
// reset setting
|
||||||
this.setting.value =
|
this.setting.value =
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<ion-card *ngIf="setting">
|
<ion-card *ngIf="setting">
|
||||||
<ion-card-header>
|
<ion-card-header>
|
||||||
<span>{{ translatedSetting.name() | titlecase }}</span>
|
<span>
|
||||||
|
{{ translatedSetting.name() | titlecase }}
|
||||||
|
<ion-icon *ngIf="compactView" name="information-circle-outline" (click)="presentAlert(translator.translate(setting).name(), translator.translate(setting).description())"></ion-icon>
|
||||||
|
</span>
|
||||||
</ion-card-header>
|
</ion-card-header>
|
||||||
<ion-card-content>
|
<ion-card-content>
|
||||||
<ion-note>{{ translatedSetting.description() }}</ion-note>
|
<ion-note *ngIf="!compactView">{{ translatedSetting.description() }}</ion-note>
|
||||||
|
|
||||||
<div [ngSwitch]="setting.inputType" *ngIf="isVisible" >
|
<div [ngSwitch]="setting.inputType" *ngIf="isVisible" >
|
||||||
<ion-item *ngSwitchCase="'number'">
|
<ion-item *ngSwitchCase="'number'">
|
||||||
|
|||||||
@@ -12,12 +12,11 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component} from '@angular/core';
|
import {ChangeDetectorRef, Component} from '@angular/core';
|
||||||
import {AlertController, ToastController} from '@ionic/angular';
|
import {AlertController, ToastController} from '@ionic/angular';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||||
import {SCLanguage, SCSettingMeta, SCThingTranslator, SCTranslations} from '@openstapps/core';
|
import {SCLanguage, SCSettingMeta, SCThingTranslator, SCTranslations} from '@openstapps/core';
|
||||||
import {SettingsCache, SettingsProvider} from '../settings.provider';
|
import {SettingsCache, SettingsProvider} from '../settings.provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings page component
|
* Settings page component
|
||||||
*/
|
*/
|
||||||
@@ -59,11 +58,13 @@ export class SettingsPageComponent {
|
|||||||
* @param settingsProvider SettingsProvider
|
* @param settingsProvider SettingsProvider
|
||||||
* @param toastController ToastController
|
* @param toastController ToastController
|
||||||
* @param translateService TranslateService
|
* @param translateService TranslateService
|
||||||
|
* @param changeDetectorRef ChangeDetectorRef
|
||||||
*/
|
*/
|
||||||
constructor(private readonly alertController: AlertController,
|
constructor(private readonly alertController: AlertController,
|
||||||
private readonly settingsProvider: SettingsProvider,
|
private readonly settingsProvider: SettingsProvider,
|
||||||
private readonly toastController: ToastController,
|
private readonly toastController: ToastController,
|
||||||
private readonly translateService: TranslateService) {
|
private readonly translateService: TranslateService,
|
||||||
|
private readonly changeDetectorRef: ChangeDetectorRef) {
|
||||||
this.language = translateService.currentLang as keyof SCTranslations<SCLanguage>;
|
this.language = translateService.currentLang as keyof SCTranslations<SCLanguage>;
|
||||||
this.translator = new SCThingTranslator(this.language);
|
this.translator = new SCThingTranslator(this.language);
|
||||||
|
|
||||||
@@ -72,8 +73,6 @@ export class SettingsPageComponent {
|
|||||||
this.translator = new SCThingTranslator(this.language);
|
this.translator = new SCThingTranslator(this.language);
|
||||||
});
|
});
|
||||||
this.settingsCache = {};
|
this.settingsCache = {};
|
||||||
this.categoriesOrder = settingsProvider.getCategoriesOrder();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,6 +92,9 @@ export class SettingsPageComponent {
|
|||||||
*/
|
*/
|
||||||
async loadSettings(): Promise<void> {
|
async loadSettings(): Promise<void> {
|
||||||
this.settingsCache = await this.settingsProvider.getCache();
|
this.settingsCache = await this.settingsProvider.getCache();
|
||||||
|
// categoriesOrder triggers updating the View, because it is used in the ngFor loop
|
||||||
|
this.categoriesOrder = this.settingsProvider.getCategoriesOrder();
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {NgModule} from '@angular/core';
|
|||||||
import {FormsModule} from '@angular/forms';
|
import {FormsModule} from '@angular/forms';
|
||||||
import {RouterModule, Routes} from '@angular/router';
|
import {RouterModule, Routes} from '@angular/router';
|
||||||
import {Geolocation} from '@ionic-native/geolocation/ngx';
|
import {Geolocation} from '@ionic-native/geolocation/ngx';
|
||||||
import {IonicModule} from '@ionic/angular';
|
import {Events, IonicModule} from '@ionic/angular';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
@@ -37,6 +37,9 @@ const settingsRoutes: Routes = [
|
|||||||
SettingsPageComponent,
|
SettingsPageComponent,
|
||||||
SettingsItemComponent,
|
SettingsItemComponent,
|
||||||
],
|
],
|
||||||
|
exports: [
|
||||||
|
SettingsItemComponent,
|
||||||
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -46,6 +49,7 @@ const settingsRoutes: Routes = [
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ConfigProvider,
|
ConfigProvider,
|
||||||
|
Events,
|
||||||
Geolocation,
|
Geolocation,
|
||||||
SettingsProvider,
|
SettingsProvider,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -14,12 +14,13 @@
|
|||||||
*/
|
*/
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {Geolocation} from '@ionic-native/geolocation/ngx';
|
import {Geolocation} from '@ionic-native/geolocation/ngx';
|
||||||
|
import {Events} from '@ionic/angular';
|
||||||
import {
|
import {
|
||||||
SCSetting,
|
SCSetting,
|
||||||
SCSettingValue,
|
SCSettingValue,
|
||||||
SCSettingValues,
|
SCSettingValues,
|
||||||
} from '@openstapps/core';
|
} from '@openstapps/core';
|
||||||
import deepmerge from 'deepmerge';
|
import deepMerge from 'deepmerge';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
|
|
||||||
@@ -164,10 +165,12 @@ export class SettingsProvider {
|
|||||||
* @param storage TODO
|
* @param storage TODO
|
||||||
* @param configProvider TODO
|
* @param configProvider TODO
|
||||||
* @param geoLocation TODO
|
* @param geoLocation TODO
|
||||||
|
* @param events TODO
|
||||||
*/
|
*/
|
||||||
constructor(private readonly storage: StorageProvider,
|
constructor(private readonly storage: StorageProvider,
|
||||||
private readonly configProvider: ConfigProvider,
|
private readonly configProvider: ConfigProvider,
|
||||||
private readonly geoLocation: Geolocation) {
|
private readonly geoLocation: Geolocation,
|
||||||
|
private readonly events: Events) {
|
||||||
this.categoriesOrder = [];
|
this.categoriesOrder = [];
|
||||||
this.settingsCache = {};
|
this.settingsCache = {};
|
||||||
}
|
}
|
||||||
@@ -246,8 +249,9 @@ export class SettingsProvider {
|
|||||||
}
|
}
|
||||||
await this.saveSettingValues();
|
await this.saveSettingValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
|
// publish provider initialised
|
||||||
|
this.events.publish('stapps.settings.initialised');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -339,7 +343,7 @@ export class SettingsProvider {
|
|||||||
*
|
*
|
||||||
* @throws Exception if setting is not provided
|
* @throws Exception if setting is not provided
|
||||||
*/
|
*/
|
||||||
public async getValue(category: string, name: string): Promise<unknown> {
|
public async getValue(category: string, name: string): Promise<SCSettingValue | SCSettingValues> {
|
||||||
await this.init();
|
await this.init();
|
||||||
if (this.settingExists(category, name)) {
|
if (this.settingExists(category, name)) {
|
||||||
// return a copy of the settings value
|
// return a copy of the settings value
|
||||||
@@ -395,7 +399,7 @@ export class SettingsProvider {
|
|||||||
const savedSettingsValues: SettingValuesContainer =
|
const savedSettingsValues: SettingValuesContainer =
|
||||||
await this.storage.get<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES);
|
await this.storage.get<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES);
|
||||||
const cacheSettingsValues = this.getSettingValuesFromCache();
|
const cacheSettingsValues = this.getSettingValuesFromCache();
|
||||||
const mergedSettingValues = deepmerge(savedSettingsValues, cacheSettingsValues);
|
const mergedSettingValues = deepMerge(savedSettingsValues, cacheSettingsValues);
|
||||||
await this.storage
|
await this.storage
|
||||||
.put<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES, mergedSettingValues);
|
.put<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES, mergedSettingValues);
|
||||||
} else {
|
} else {
|
||||||
@@ -412,7 +416,8 @@ export class SettingsProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a valid value of a setting and persists changes in storage
|
* Sets a valid value of a setting and persists changes in storage. Also the changes get published bey Events
|
||||||
|
*
|
||||||
* @param category Category key name
|
* @param category Category key name
|
||||||
* @param name Setting key name
|
* @param name Setting key name
|
||||||
* @param value Value to be set
|
* @param value Value to be set
|
||||||
@@ -426,8 +431,11 @@ export class SettingsProvider {
|
|||||||
const setting: SCSetting = this.settingsCache[category].settings[name];
|
const setting: SCSetting = this.settingsCache[category].settings[name];
|
||||||
const isValueValid = SettingsProvider.validateValue(setting, value);
|
const isValueValid = SettingsProvider.validateValue(setting, value);
|
||||||
if (isValueValid) {
|
if (isValueValid) {
|
||||||
|
// set and persist new value
|
||||||
this.settingsCache[category].settings[name].value = value;
|
this.settingsCache[category].settings[name].value = value;
|
||||||
await this.saveSettingValues();
|
await this.saveSettingValues();
|
||||||
|
// publish setting changes
|
||||||
|
this.events.publish('stapps.settings.changed', category, name, value);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Value "${value}" of type
|
throw new Error(`Value "${value}" of type
|
||||||
${typeof value} is not valid for ${setting.inputType}`);
|
${typeof value} is not valid for ${setting.inputType}`);
|
||||||
|
|||||||
@@ -18,8 +18,19 @@
|
|||||||
"menu": {
|
"menu": {
|
||||||
"context": {
|
"context": {
|
||||||
"title": "Kontext Menü",
|
"title": "Kontext Menü",
|
||||||
"sort": "Sortierung",
|
"sort": {
|
||||||
"filter": "Filter"
|
"title": "Sortierung",
|
||||||
|
"relevance": "Relevanz",
|
||||||
|
"name": "Name",
|
||||||
|
"date": "Datum",
|
||||||
|
"type": "Typ"
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"title": "Filter",
|
||||||
|
"options": "Optionen",
|
||||||
|
"showAll": "alle anzeigen"
|
||||||
|
},
|
||||||
|
"settings": "Einstellungen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
@@ -17,16 +17,27 @@
|
|||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"context": {
|
"context": {
|
||||||
"title": "Context Menu",
|
"title": "context menu",
|
||||||
"sort": "Sort",
|
"sort": {
|
||||||
"filter": "Filter"
|
"title": "sort",
|
||||||
|
"relevance": "relevance",
|
||||||
|
"name": "name",
|
||||||
|
"date": "date",
|
||||||
|
"type": "type"
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"title": "filter",
|
||||||
|
"options": "options",
|
||||||
|
"showAll": "show all"
|
||||||
|
},
|
||||||
|
"settings": "settings"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"resetAlert.title": "Reset all settings?",
|
"resetAlert.title": "Reset all settings?",
|
||||||
"resetAlert.message": "Are you shure to reset all settings to defaults values?",
|
"resetAlert.message": "Are you shure to reset all settings to defaults values?",
|
||||||
"resetAlert.buttonYes": "Yes",
|
"resetAlert.buttonYes": "yes",
|
||||||
"resetAlert.buttonCancel": "Cancel",
|
"resetAlert.buttonCancel": "cancel",
|
||||||
"resetToast.message": "Settings reset",
|
"resetToast.message": "Settings reset",
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"geoLocation": {
|
"geoLocation": {
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ export const environment = {
|
|||||||
backend_url: 'http://localhost:3000',
|
backend_url: 'http://localhost:3000',
|
||||||
backend_version: '1.0.0',
|
backend_version: '1.0.0',
|
||||||
use_fake_backend: true,
|
use_fake_backend: true,
|
||||||
production: true,
|
production: false,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user