mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-20 08:33:11 +00:00
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 {Observable, of} from 'rxjs';
|
||||
import {delay, map} from 'rxjs/operators';
|
||||
import {facetsMock} from './data/sample-facets';
|
||||
import {SampleThings} from './data/sample-things';
|
||||
|
||||
// tslint:disable:no-magic-numbers
|
||||
const sampleIndexResponse: SCIndexResponse = {
|
||||
export const sampleIndexResponse: SCIndexResponse = {
|
||||
app: {
|
||||
campusPolygon: {
|
||||
coordinates: [
|
||||
@@ -105,7 +106,7 @@ const sampleIndexResponse: SCIndexResponse = {
|
||||
},
|
||||
},
|
||||
],
|
||||
name: '',
|
||||
name: 'main menu',
|
||||
translations: {
|
||||
de: {
|
||||
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',
|
||||
route: '/about',
|
||||
@@ -161,13 +147,13 @@ const sampleIndexResponse: SCIndexResponse = {
|
||||
},
|
||||
},
|
||||
],
|
||||
name: '',
|
||||
name: 'Your Study-App',
|
||||
translations: {
|
||||
de: {
|
||||
name: '',
|
||||
name: 'Deine Studi-App',
|
||||
},
|
||||
en: {
|
||||
name: '',
|
||||
name: 'Your Study-App',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -178,7 +164,8 @@ const sampleIndexResponse: SCIndexResponse = {
|
||||
{
|
||||
categories: ['profile'],
|
||||
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,
|
||||
name: 'group',
|
||||
order: 1,
|
||||
@@ -216,7 +203,7 @@ const sampleIndexResponse: SCIndexResponse = {
|
||||
{
|
||||
categories: ['profile'],
|
||||
defaultValue: 'en',
|
||||
description: '',
|
||||
description: 'The language this app is going to use.',
|
||||
inputType: SCSettingInputType.SingleChoice,
|
||||
name: 'language',
|
||||
order: 0,
|
||||
@@ -250,7 +237,8 @@ const sampleIndexResponse: SCIndexResponse = {
|
||||
{
|
||||
categories: ['privacy'],
|
||||
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,
|
||||
name: 'geoLocation',
|
||||
order: 0,
|
||||
@@ -393,16 +381,16 @@ export class FakeBackendInterceptor implements HttpInterceptor {
|
||||
if (request.body.filter.arguments.field === 'uid') {
|
||||
return this.sampleFetcher.getSampleThing(request.body.filter.arguments.value)
|
||||
// tslint:disable-next-line:no-any
|
||||
.pipe(map((sampleData: any) => {
|
||||
return new HttpResponse({status: 200, body: {data: sampleData}});
|
||||
}), delay(this.RESPONSE_DELAY)); // add delay for skeleton screens to be seen (see !16)
|
||||
.pipe(map((sampleData: any) => {
|
||||
return new HttpResponse({status: 200, body: {data: sampleData}});
|
||||
}), delay(this.RESPONSE_DELAY)); // add delay for skeleton screens to be seen (see !16)
|
||||
}
|
||||
}
|
||||
|
||||
return this.sampleFetcher.getSampleThings()
|
||||
// tslint:disable-next-line:no-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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ registerLocaleData(localeDe);
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
*
|
||||
* @param http TODO
|
||||
*/
|
||||
export function createTranslateLoader(http: HttpClient) {
|
||||
@@ -84,4 +84,5 @@ const providers : Provider[] = [
|
||||
providers:
|
||||
environment.use_fake_backend ? [providers, fakeBackendProvider] : providers,
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule {
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export class ConfigProvider {
|
||||
await this.init();
|
||||
} catch (error) {
|
||||
// don't throw ConfigFetchError if saved config is available
|
||||
if (!(error.name === 'ConfigFetchError' && this.initialised)) {
|
||||
if (error.name === 'ConfigFetchError' && !this.initialised) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@ import {CommonModule} from '@angular/common';
|
||||
import {HttpClientModule} from '@angular/common/http';
|
||||
import {NgModule} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {IonicModule} from '@ionic/angular';
|
||||
import {Events, IonicModule} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {MarkdownModule} from 'ngx-markdown';
|
||||
import {MomentModule} from 'ngx-moment';
|
||||
import {MenuModule} from '../menu/menu.module';
|
||||
import {StorageModule} from '../storage/storage.module';
|
||||
import {DataFacetsProvider} from './data-facets.provider';
|
||||
import {DataRoutingModule} from './data-routing.module';
|
||||
@@ -116,6 +117,7 @@ import {VideoListItem} from './types/video/video-list-item.component';
|
||||
DataRoutingModule,
|
||||
HttpClientModule,
|
||||
MarkdownModule.forRoot(),
|
||||
MenuModule,
|
||||
MomentModule.forRoot({
|
||||
relativeTimeThresholdOptions: {
|
||||
'm': 59,
|
||||
@@ -127,6 +129,7 @@ import {VideoListItem} from './types/video/video-list-item.component';
|
||||
providers: [
|
||||
DataProvider,
|
||||
DataFacetsProvider,
|
||||
Events,
|
||||
StAppsWebHttpClient,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -12,92 +12,157 @@
|
||||
* 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 {AlertController} from '@ionic/angular';
|
||||
import {SCThing} from '@openstapps/core';
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {AlertController, Events} from '@ionic/angular';
|
||||
import {
|
||||
SCFacet,
|
||||
SCSearchFilter,
|
||||
SCSearchQuery,
|
||||
SCSearchSort,
|
||||
SCSettingValue,
|
||||
SCSettingValues,
|
||||
SCThing,
|
||||
} from '@openstapps/core';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import {Subject} from 'rxjs';
|
||||
import {debounceTime, distinctUntilChanged} from 'rxjs/operators';
|
||||
import {MenuService} from '../../menu/menu.service';
|
||||
import {DataProvider} from '../data.provider';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* DataListComponent queries things and shows list of things and context menu
|
||||
*/
|
||||
@Component({
|
||||
selector: 'stapps-data-list',
|
||||
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;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* Container for queried things
|
||||
*/
|
||||
items: SCThing[];
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* Page size of queries
|
||||
*/
|
||||
loaded = false;
|
||||
pageSize = 30;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* Search value from search bar
|
||||
*/
|
||||
query: string;
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
queryChanged: Subject<string> = new Subject<string>();
|
||||
queryText: string;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* Subject to handle search text changes
|
||||
*/
|
||||
selectedItem: any;
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
size = 30;
|
||||
queryTextChanged: Subject<string> = new Subject<string>();
|
||||
|
||||
/**
|
||||
*
|
||||
* @param alertController TODO
|
||||
* @param dataProvider TODO
|
||||
* Time to wait for search query if search text is changing
|
||||
*/
|
||||
searchQueryDueTime = 1000;
|
||||
|
||||
/**
|
||||
* Api query sorting
|
||||
*/
|
||||
sortQuery: SCSearchSort | undefined;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param alertController AlertController
|
||||
* @param dataProvider DataProvider
|
||||
* @param events Events
|
||||
* @param menuService MenuService
|
||||
*/
|
||||
constructor(
|
||||
private readonly alertController: AlertController,
|
||||
dataProvider: DataProvider,
|
||||
) {
|
||||
this.dataProvider = dataProvider;
|
||||
this.queryChanged
|
||||
private dataProvider: DataProvider,
|
||||
private readonly events: Events,
|
||||
private readonly menuService: MenuService,
|
||||
) {
|
||||
this.queryTextChanged
|
||||
.pipe(
|
||||
debounceTime(1000),
|
||||
debounceTime(this.searchQueryDueTime),
|
||||
distinctUntilChanged())
|
||||
.subscribe((model) => {
|
||||
this.from = 0;
|
||||
this.query = model;
|
||||
this.fetchItems();
|
||||
this.queryText = model;
|
||||
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> {
|
||||
return this.dataProvider.search({
|
||||
private async fetchAndUpdateItems(append = false): Promise<void> {
|
||||
// build query search options
|
||||
const searchOptions: SCSearchQuery = {
|
||||
from: this.from,
|
||||
query: this.query,
|
||||
size: this.size,
|
||||
} as any)
|
||||
size: this.pageSize,
|
||||
};
|
||||
|
||||
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) => {
|
||||
if (append) {
|
||||
// append results
|
||||
this.items = this.items.concat(result.data);
|
||||
} else {
|
||||
// override items with results
|
||||
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) => {
|
||||
const alert: HTMLIonAlertElement = await this.alertController.create({
|
||||
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> {
|
||||
this.from += this.size;
|
||||
await this.fetchItems();
|
||||
this.from += this.pageSize;
|
||||
await this.fetchAndUpdateItems(true);
|
||||
event.target.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* Initialises the sort context of menuService
|
||||
*/
|
||||
search(query: string) {
|
||||
this.queryChanged.next(query);
|
||||
ngOnInit(): void {
|
||||
// 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-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button></ion-back-button>
|
||||
<ion-menu-button></ion-menu-button>
|
||||
</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-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
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -14,27 +14,37 @@
|
||||
*/
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {NgModule} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {RouterModule} from '@angular/router';
|
||||
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';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* Menu module
|
||||
*/
|
||||
@NgModule({
|
||||
declarations: [
|
||||
NavigationComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
NavigationComponent,
|
||||
ContextMenuComponent,
|
||||
],
|
||||
exports: [
|
||||
NavigationComponent,
|
||||
ContextMenuComponent,
|
||||
],
|
||||
imports: [
|
||||
FormsModule,
|
||||
IonicModule.forRoot(),
|
||||
TranslateModule.forChild(),
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
SettingsModule,
|
||||
],
|
||||
providers: [
|
||||
MenuService,
|
||||
],
|
||||
})
|
||||
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 {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||
import {
|
||||
SCAppConfigurationMenuCategory,
|
||||
SCThing,
|
||||
SCThingTranslator,
|
||||
SCTranslations,
|
||||
} from '@openstapps/core';
|
||||
import {SCAppConfigurationMenuCategory, SCLanguage, SCThingTranslator, SCTranslations} from '@openstapps/core';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
/**
|
||||
* Generated class for the MenuPage page.
|
||||
*
|
||||
@@ -37,34 +30,42 @@ const logger = new Logger();
|
||||
templateUrl: 'navigation.html',
|
||||
})
|
||||
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[];
|
||||
|
||||
/**
|
||||
* Core translator
|
||||
*/
|
||||
translator: SCThingTranslator;
|
||||
|
||||
constructor(private configProvider: ConfigProvider,
|
||||
public translateService: TranslateService) {
|
||||
this.loadMenuEntries().then(() =>
|
||||
logger.log('menuEntries loaded from config: ' + JSON.stringify(this.menu))).catch((error) => {
|
||||
logger.error(error);
|
||||
|
||||
});
|
||||
this.loadMenuEntries();
|
||||
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
|
||||
this.language = event.lang as keyof SCTranslations<any>;
|
||||
this.translator = new SCThingTranslator(this.language, 'en');
|
||||
this.language = event.lang as keyof SCTranslations<SCLanguage>;
|
||||
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
|
||||
ionViewDidLoad() {
|
||||
// console.log('ionViewDidLoad MenuPage');
|
||||
}
|
||||
|
||||
async loadMenuEntries() {
|
||||
this.menu = await this.configProvider.getValue('menus') as SCAppConfigurationMenuCategory[];
|
||||
try {
|
||||
this.menu = await this.configProvider.getValue('menus') as SCAppConfigurationMenuCategory[];
|
||||
} catch (error) {
|
||||
Logger.error(`error from loading menu entries: ${error}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// openPage(page) {
|
||||
|
||||
@@ -10,39 +10,16 @@
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-list *ngFor="let category of menu">
|
||||
<ion-list *ngIf="category.id === 'main'">
|
||||
<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 === '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-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-content>
|
||||
</ion-menu>
|
||||
|
||||
@@ -17,13 +17,13 @@ import {AlertController} from '@ionic/angular';
|
||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||
import {
|
||||
SCLanguage,
|
||||
SCLanguageCode,
|
||||
SCSetting,
|
||||
SCSettingValue,
|
||||
SCSettingValues,
|
||||
SCThingTranslator,
|
||||
SCTranslations,
|
||||
} from '@openstapps/core';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import {SettingsProvider} from '../settings.provider';
|
||||
|
||||
/**
|
||||
@@ -34,6 +34,12 @@ import {SettingsProvider} from '../settings.provider';
|
||||
templateUrl: 'settings-item.html',
|
||||
})
|
||||
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
|
||||
*/
|
||||
@@ -107,8 +113,6 @@ export class SettingsItemComponent implements OnInit {
|
||||
* NgInit
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
Logger.log(JSON.stringify(this.setting));
|
||||
|
||||
this.translatedSetting = this.translator.translate(this.setting);
|
||||
}
|
||||
|
||||
@@ -136,7 +140,7 @@ export class SettingsItemComponent implements OnInit {
|
||||
// handle general settings, with special actions
|
||||
switch (this.setting.name) {
|
||||
case 'language':
|
||||
this.translateService.use(this.setting.value.toString());
|
||||
this.translateService.use(this.setting.value as SCLanguageCode);
|
||||
break;
|
||||
case 'geoLocation':
|
||||
if (!!this.setting.value) {
|
||||
@@ -146,7 +150,9 @@ export class SettingsItemComponent implements OnInit {
|
||||
default:
|
||||
}
|
||||
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 {
|
||||
// reset setting
|
||||
this.setting.value =
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<ion-card *ngIf="setting">
|
||||
<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-content>
|
||||
<ion-note>{{ translatedSetting.description() }}</ion-note>
|
||||
<ion-note *ngIf="!compactView">{{ translatedSetting.description() }}</ion-note>
|
||||
|
||||
<div [ngSwitch]="setting.inputType" *ngIf="isVisible" >
|
||||
<ion-item *ngSwitchCase="'number'">
|
||||
|
||||
@@ -12,12 +12,11 @@
|
||||
* 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 {ChangeDetectorRef, Component} from '@angular/core';
|
||||
import {AlertController, ToastController} from '@ionic/angular';
|
||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||
import {SCLanguage, SCSettingMeta, SCThingTranslator, SCTranslations} from '@openstapps/core';
|
||||
import {SettingsCache, SettingsProvider} from '../settings.provider';
|
||||
|
||||
/**
|
||||
* Settings page component
|
||||
*/
|
||||
@@ -59,11 +58,13 @@ export class SettingsPageComponent {
|
||||
* @param settingsProvider SettingsProvider
|
||||
* @param toastController ToastController
|
||||
* @param translateService TranslateService
|
||||
* @param changeDetectorRef ChangeDetectorRef
|
||||
*/
|
||||
constructor(private readonly alertController: AlertController,
|
||||
private readonly settingsProvider: SettingsProvider,
|
||||
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.translator = new SCThingTranslator(this.language);
|
||||
|
||||
@@ -72,8 +73,6 @@ export class SettingsPageComponent {
|
||||
this.translator = new SCThingTranslator(this.language);
|
||||
});
|
||||
this.settingsCache = {};
|
||||
this.categoriesOrder = settingsProvider.getCategoriesOrder();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,6 +92,9 @@ export class SettingsPageComponent {
|
||||
*/
|
||||
async loadSettings(): Promise<void> {
|
||||
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 {RouterModule, Routes} from '@angular/router';
|
||||
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 {ConfigProvider} from '../config/config.provider';
|
||||
@@ -37,6 +37,9 @@ const settingsRoutes: Routes = [
|
||||
SettingsPageComponent,
|
||||
SettingsItemComponent,
|
||||
],
|
||||
exports: [
|
||||
SettingsItemComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
@@ -46,6 +49,7 @@ const settingsRoutes: Routes = [
|
||||
],
|
||||
providers: [
|
||||
ConfigProvider,
|
||||
Events,
|
||||
Geolocation,
|
||||
SettingsProvider,
|
||||
],
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
*/
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Geolocation} from '@ionic-native/geolocation/ngx';
|
||||
import {Events} from '@ionic/angular';
|
||||
import {
|
||||
SCSetting,
|
||||
SCSettingValue,
|
||||
SCSettingValues,
|
||||
} from '@openstapps/core';
|
||||
import deepmerge from 'deepmerge';
|
||||
import deepMerge from 'deepmerge';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
|
||||
@@ -164,10 +165,12 @@ export class SettingsProvider {
|
||||
* @param storage TODO
|
||||
* @param configProvider TODO
|
||||
* @param geoLocation TODO
|
||||
* @param events TODO
|
||||
*/
|
||||
constructor(private readonly storage: StorageProvider,
|
||||
private readonly configProvider: ConfigProvider,
|
||||
private readonly geoLocation: Geolocation) {
|
||||
private readonly geoLocation: Geolocation,
|
||||
private readonly events: Events) {
|
||||
this.categoriesOrder = [];
|
||||
this.settingsCache = {};
|
||||
}
|
||||
@@ -246,8 +249,9 @@ export class SettingsProvider {
|
||||
}
|
||||
await this.saveSettingValues();
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
public async getValue(category: string, name: string): Promise<unknown> {
|
||||
public async getValue(category: string, name: string): Promise<SCSettingValue | SCSettingValues> {
|
||||
await this.init();
|
||||
if (this.settingExists(category, name)) {
|
||||
// return a copy of the settings value
|
||||
@@ -395,7 +399,7 @@ export class SettingsProvider {
|
||||
const savedSettingsValues: SettingValuesContainer =
|
||||
await this.storage.get<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES);
|
||||
const cacheSettingsValues = this.getSettingValuesFromCache();
|
||||
const mergedSettingValues = deepmerge(savedSettingsValues, cacheSettingsValues);
|
||||
const mergedSettingValues = deepMerge(savedSettingsValues, cacheSettingsValues);
|
||||
await this.storage
|
||||
.put<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES, mergedSettingValues);
|
||||
} 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 name Setting key name
|
||||
* @param value Value to be set
|
||||
@@ -426,8 +431,11 @@ export class SettingsProvider {
|
||||
const setting: SCSetting = this.settingsCache[category].settings[name];
|
||||
const isValueValid = SettingsProvider.validateValue(setting, value);
|
||||
if (isValueValid) {
|
||||
// set and persist new value
|
||||
this.settingsCache[category].settings[name].value = value;
|
||||
await this.saveSettingValues();
|
||||
// publish setting changes
|
||||
this.events.publish('stapps.settings.changed', category, name, value);
|
||||
} else {
|
||||
throw new Error(`Value "${value}" of type
|
||||
${typeof value} is not valid for ${setting.inputType}`);
|
||||
|
||||
Reference in New Issue
Block a user