mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-04-13 01:39:12 +00:00
Compare commits
1 Commits
add-backen
...
142-requir
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c30211ba2 |
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
"@openstapps/backend": minor
|
|
||||||
"@openstapps/core": minor
|
|
||||||
---
|
|
||||||
|
|
||||||
Add the ability to filter by existence of a field
|
|
||||||
9
.changeset/spotty-ducks-cheer.md
Normal file
9
.changeset/spotty-ducks-cheer.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
'@openstapps/app': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Require full reload for setting & language changes
|
||||||
|
|
||||||
|
Setting changes are relatively rare, so it makes little sense
|
||||||
|
going through the effort of ensuring everything is reactive to
|
||||||
|
language changes as well as creating all the pipe bindings etc.
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/** @type {import('syncpack').RcFile} */
|
/** @type {import('syncpack').RcFile} */
|
||||||
const config = {
|
const config = {
|
||||||
semverGroups: [{range: ''}],
|
semverRange: '',
|
||||||
source: ['package.json', '**/package.json'],
|
source: ['package.json', '**/package.json'],
|
||||||
indent: ' ',
|
indent: ' ',
|
||||||
sortFirst: [
|
sortFirst: [
|
||||||
@@ -49,7 +49,7 @@ const config = {
|
|||||||
{
|
{
|
||||||
label: 'Packages should use workspace version',
|
label: 'Packages should use workspace version',
|
||||||
dependencies: ['@openstapps/**'],
|
dependencies: ['@openstapps/**'],
|
||||||
dependencyTypes: ['prod', 'dev', 'peer'],
|
dependencyTypes: ['prod', 'dev'],
|
||||||
packages: ['**'],
|
packages: ['**'],
|
||||||
pinVersion: 'workspace:*',
|
pinVersion: 'workspace:*',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Open StApps Monorepo
|
# <img src="logo-bg.svg" height="24"> Open StApps Monorepo
|
||||||
|
|
||||||
Refer to the [contribution guide](./CONTRIBUTING.md)
|
Refer to the [contribution guide](./CONTRIBUTING.md)
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,3 @@ WORKDIR /app
|
|||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
ENTRYPOINT ["node", "app.js"]
|
ENTRYPOINT ["node", "app.js"]
|
||||||
|
|
||||||
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=12 CMD curl -s --fail --request POST --data '{}' --header 'Content-Type: application/json' http://localhost:3000/ >/dev/null || exit 1
|
|
||||||
|
|||||||
@@ -21,31 +21,16 @@ import {QueryDslSpecificQueryContainer} from '../../types/util.js';
|
|||||||
*/
|
*/
|
||||||
export function buildValueFilter(
|
export function buildValueFilter(
|
||||||
filter: SCSearchValueFilter,
|
filter: SCSearchValueFilter,
|
||||||
):
|
): QueryDslSpecificQueryContainer<'term'> | QueryDslSpecificQueryContainer<'terms'> {
|
||||||
| QueryDslSpecificQueryContainer<'exists'>
|
return Array.isArray(filter.arguments.value)
|
||||||
| QueryDslSpecificQueryContainer<'term'>
|
? {
|
||||||
| QueryDslSpecificQueryContainer<'terms'> {
|
terms: {
|
||||||
switch (typeof filter.arguments.value) {
|
[`${filter.arguments.field}.raw`]: filter.arguments.value,
|
||||||
case 'undefined': {
|
|
||||||
return {
|
|
||||||
exists: {
|
|
||||||
field: filter.arguments.field,
|
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
}
|
: {
|
||||||
case 'string': {
|
|
||||||
return {
|
|
||||||
term: {
|
term: {
|
||||||
[`${filter.arguments.field}.raw`]: filter.arguments.value,
|
[`${filter.arguments.field}.raw`]: filter.arguments.value,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
return {
|
|
||||||
terms: {
|
|
||||||
[`${filter.arguments.field}.raw`]: filter.arguments.value,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
"extends": "@openstapps/tsconfig",
|
"extends": "@openstapps/tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"useUnknownInCatchVariables": false
|
"useUnknownInCatchVariables": false,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true
|
||||||
},
|
},
|
||||||
"exclude": ["lib", "app.js"]
|
"exclude": ["app.js", "lib/"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,3 @@ RUN chown elasticsearch:elasticsearch config/elasticsearch.yml
|
|||||||
USER elasticsearch
|
USER elasticsearch
|
||||||
|
|
||||||
CMD ["/usr/share/elasticsearch/bin/elasticsearch"]
|
CMD ["/usr/share/elasticsearch/bin/elasticsearch"]
|
||||||
|
|
||||||
HEALTHCHECK --interval=10s --timeout=5s --start-period=60s --retries=12 CMD curl --fail -s http://localhost:9200/ >/dev/null || exit 1
|
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ discovery.type: "single-node"
|
|||||||
cluster.routing.allocation.disk.threshold_enabled: false
|
cluster.routing.allocation.disk.threshold_enabled: false
|
||||||
network.bind_host: 0.0.0.0
|
network.bind_host: 0.0.0.0
|
||||||
xpack.security.enabled: false
|
xpack.security.enabled: false
|
||||||
ingest.geoip.downloader.enabled: false
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "@openstapps/tsconfig",
|
"extends": "@openstapps/tsconfig",
|
||||||
"exclude": ["config", "lib", "app.js"]
|
"exclude": ["./config/", "./lib/"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": "@openstapps/tsconfig",
|
"extends": "@openstapps/tsconfig"
|
||||||
"exclude": ["lib", "app.js"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": "@openstapps/tsconfig",
|
"extends": "@openstapps/tsconfig"
|
||||||
"exclude": ["lib", "app.js"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": "@openstapps/tsconfig",
|
"extends": "@openstapps/tsconfig"
|
||||||
"exclude": ["lib", "app.js"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@
|
|||||||
"cordova-plugin-calendar": "5.1.6",
|
"cordova-plugin-calendar": "5.1.6",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"deepmerge": "4.3.1",
|
"deepmerge": "4.3.1",
|
||||||
|
"fast-deep-equal": "3.1.3",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"geojson": "0.5.0",
|
"geojson": "0.5.0",
|
||||||
"ionic-appauth": "0.9.0",
|
"ionic-appauth": "0.9.0",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
SCThingOriginType,
|
SCThingOriginType,
|
||||||
SCThingType,
|
SCThingType,
|
||||||
} from '@openstapps/core';
|
} from '@openstapps/core';
|
||||||
import {CORE_VERSION} from '@openstapps/core';
|
import packageInfo from '@openstapps/core/package.json';
|
||||||
import {Polygon} from 'geojson';
|
import {Polygon} from 'geojson';
|
||||||
|
|
||||||
// provides sample aggregations to be used in tests or backendless development
|
// provides sample aggregations to be used in tests or backendless development
|
||||||
@@ -195,7 +195,7 @@ export const sampleIndexResponse: SCIndexResponse = {
|
|||||||
},
|
},
|
||||||
auth: {},
|
auth: {},
|
||||||
backend: {
|
backend: {
|
||||||
SCVersion: CORE_VERSION,
|
SCVersion: packageInfo.version,
|
||||||
externalRequestTimeout: 5000,
|
externalRequestTimeout: 5000,
|
||||||
hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
|
hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
|
||||||
mappingIgnoredTags: [],
|
mappingIgnoredTags: [],
|
||||||
|
|||||||
80
frontend/app/src/app/animation/splash.ts
Normal file
80
frontend/app/src/app/animation/splash.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import {Animation, AnimationController} from '@ionic/angular';
|
||||||
|
import {iosDuration, iosEasing, mdDuration, mdEasing} from './easings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splash screen animation
|
||||||
|
*/
|
||||||
|
export function splashAnimation(animationCtl: AnimationController): Animation {
|
||||||
|
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||||
|
return animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('opacity', 0, 1)
|
||||||
|
.duration(150)
|
||||||
|
.beforeClearStyles(['visibility'])
|
||||||
|
.addElement(document.querySelector('ion-app')!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMd = document.querySelector('ion-app.md') !== null;
|
||||||
|
const navElement = document.querySelector('stapps-navigation-tabs')!;
|
||||||
|
const navBounds = navElement.getBoundingClientRect();
|
||||||
|
let horizontal = navBounds.width < navBounds.height;
|
||||||
|
if (window.getComputedStyle(navElement).display === 'none') {
|
||||||
|
horizontal = true;
|
||||||
|
}
|
||||||
|
const translate = (amount: number, unit = 'px') =>
|
||||||
|
`translate${horizontal ? 'X' : 'Y'}(${horizontal ? amount * -1 : amount}${unit})`;
|
||||||
|
const duration = 2 * (isMd ? mdDuration : iosDuration);
|
||||||
|
|
||||||
|
const animation = animationCtl
|
||||||
|
.create()
|
||||||
|
.duration(duration)
|
||||||
|
.easing(isMd ? mdEasing : iosEasing)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl.create().beforeClearStyles(['visibility']).addElement(document.querySelector('ion-app')!),
|
||||||
|
)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('transform', translate(horizontal ? 64 : 192), translate(0))
|
||||||
|
.fromTo('opacity', 0, 1)
|
||||||
|
.addElement(document.querySelector('stapps-navigation > ion-split-pane')!),
|
||||||
|
)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('transform', translate(64), translate(0))
|
||||||
|
.addElement(document.querySelectorAll('ion-split-pane > ion-menu > ion-content')),
|
||||||
|
)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('transform', translate(horizontal ? 32 : -72), translate(0))
|
||||||
|
.addElement(document.querySelectorAll('ion-router-outlet > .ion-page > ion-content')!),
|
||||||
|
)
|
||||||
|
.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('transform', translate(100, '%'), translate(0, '%'))
|
||||||
|
.addElement(document.querySelector('stapps-navigation-tabs')!),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!horizontal) {
|
||||||
|
animation.addAnimation(
|
||||||
|
animationCtl
|
||||||
|
.create()
|
||||||
|
.fromTo('background', 'none', 'none')
|
||||||
|
.addElement(document.querySelector('ion-router-outlet')!),
|
||||||
|
);
|
||||||
|
|
||||||
|
const parallax = document
|
||||||
|
.querySelector('ion-router-outlet > .ion-page > ion-content')
|
||||||
|
?.shadowRoot?.querySelector('[part=parallax]');
|
||||||
|
if (parallax) {
|
||||||
|
animation.addAnimation(
|
||||||
|
animationCtl.create().fromTo('translate', '0 256px', '0 0px').addElement(parallax),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return animation;
|
||||||
|
}
|
||||||
@@ -22,28 +22,16 @@ import {environment} from '../environments/environment';
|
|||||||
import {Capacitor} from '@capacitor/core';
|
import {Capacitor} from '@capacitor/core';
|
||||||
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
|
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
|
||||||
import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
|
import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
|
||||||
import {AppVersionService} from './modules/about/app-version.service';
|
|
||||||
import {SplashScreen} from '@capacitor/splash-screen';
|
import {SplashScreen} from '@capacitor/splash-screen';
|
||||||
|
import {AppVersionService} from './modules/about/app-version.service';
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: 'app.component.html',
|
templateUrl: 'app.component.html',
|
||||||
})
|
})
|
||||||
export class AppComponent implements AfterContentInit {
|
export class AppComponent implements AfterContentInit {
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
pages: Array<{
|
pages: Array<{
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
component: unknown;
|
component: unknown;
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
title: string;
|
title: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@@ -65,7 +53,7 @@ export class AppComponent implements AfterContentInit {
|
|||||||
void this.initializeApp();
|
void this.initializeApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngAfterContentInit() {
|
ngAfterContentInit() {
|
||||||
this.scheduleSyncService.init();
|
this.scheduleSyncService.init();
|
||||||
void this.scheduleSyncService.enable();
|
void this.scheduleSyncService.enable();
|
||||||
this.versionService.getPendingReleaseNotes().then(notes => {
|
this.versionService.getPendingReleaseNotes().then(notes => {
|
||||||
@@ -74,24 +62,11 @@ export class AppComponent implements AfterContentInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (document.readyState === 'complete') {
|
|
||||||
this.hideSplash();
|
|
||||||
} else {
|
|
||||||
document.addEventListener('readystatechange', () => {
|
|
||||||
if (document.readyState === 'complete') this.hideSplash();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async hideSplash() {
|
|
||||||
if (Capacitor.isNativePlatform()) {
|
if (Capacitor.isNativePlatform()) {
|
||||||
void SplashScreen.hide();
|
void SplashScreen.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
async initializeApp() {
|
async initializeApp() {
|
||||||
App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => {
|
App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => {
|
||||||
this.zone.run(() => {
|
this.zone.run(() => {
|
||||||
|
|||||||
@@ -25,12 +25,10 @@ import moment from 'moment';
|
|||||||
import 'moment/min/locales';
|
import 'moment/min/locales';
|
||||||
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
|
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
|
||||||
import SwiperCore, {FreeMode, Navigation} from 'swiper';
|
import SwiperCore, {FreeMode, Navigation} from 'swiper';
|
||||||
|
|
||||||
import {environment} from '../environments/environment';
|
import {environment} from '../environments/environment';
|
||||||
import {AppRoutingModule} from './app-routing.module';
|
import {AppRoutingModule} from './app-routing.module';
|
||||||
import {AppComponent} from './app.component';
|
import {AppComponent} from './app.component';
|
||||||
import {CatalogModule} from './modules/catalog/catalog.module';
|
import {CatalogModule} from './modules/catalog/catalog.module';
|
||||||
import {ConfigModule} from './modules/config/config.module';
|
|
||||||
import {ConfigProvider} from './modules/config/config.provider';
|
import {ConfigProvider} from './modules/config/config.provider';
|
||||||
import {DashboardModule} from './modules/dashboard/dashboard.module';
|
import {DashboardModule} from './modules/dashboard/dashboard.module';
|
||||||
import {DataModule} from './modules/data/data.module';
|
import {DataModule} from './modules/data/data.module';
|
||||||
@@ -44,7 +42,6 @@ import {SettingsProvider} from './modules/settings/settings.provider';
|
|||||||
import {StorageModule} from './modules/storage/storage.module';
|
import {StorageModule} from './modules/storage/storage.module';
|
||||||
import {ThingTranslateModule} from './translation/thing-translate.module';
|
import {ThingTranslateModule} from './translation/thing-translate.module';
|
||||||
import {UtilModule} from './util/util.module';
|
import {UtilModule} from './util/util.module';
|
||||||
import {initLogger} from './_helpers/ts-logger';
|
|
||||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||||
import {AboutModule} from './modules/about/about.module';
|
import {AboutModule} from './modules/about/about.module';
|
||||||
import {JobModule} from './modules/jobs/jobs.module';
|
import {JobModule} from './modules/jobs/jobs.module';
|
||||||
@@ -91,28 +88,25 @@ export function initializerFactory(
|
|||||||
) {
|
) {
|
||||||
return async () => {
|
return async () => {
|
||||||
try {
|
try {
|
||||||
initLogger(logger);
|
|
||||||
await storageProvider.init();
|
await storageProvider.init();
|
||||||
await configProvider.init();
|
await configProvider.init();
|
||||||
await settingsProvider.init();
|
if (configProvider.firstSession) {
|
||||||
|
// set language from browser
|
||||||
|
await settingsProvider.setSettingValue(
|
||||||
|
'profile',
|
||||||
|
'language',
|
||||||
|
translateService.getBrowserLang() as SCSettingValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const languageCode = await settingsProvider.getSetting<string>('profile', 'language');
|
||||||
|
// this language will be used as a fallback when a translation isn't found in the current language
|
||||||
|
translateService.setDefaultLang('en');
|
||||||
|
translateService.use(languageCode);
|
||||||
|
moment.locale(languageCode);
|
||||||
|
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
|
||||||
|
setDefaultOptions({locale: dateFnsLocale});
|
||||||
|
dateFnsConfigurationService.setLocale(dateFnsLocale);
|
||||||
try {
|
try {
|
||||||
if (configProvider.firstSession) {
|
|
||||||
// set language from browser
|
|
||||||
await settingsProvider.setSettingValue(
|
|
||||||
'profile',
|
|
||||||
'language',
|
|
||||||
translateService.getBrowserLang() as SCSettingValue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const languageCode = (await settingsProvider.getValue('profile', 'language')) as string;
|
|
||||||
// this language will be used as a fallback when a translation isn't found in the current language
|
|
||||||
translateService.setDefaultLang('en');
|
|
||||||
translateService.use(languageCode);
|
|
||||||
moment.locale(languageCode);
|
|
||||||
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
|
|
||||||
setDefaultOptions({locale: dateFnsLocale});
|
|
||||||
dateFnsConfigurationService.setLocale(dateFnsLocale);
|
|
||||||
|
|
||||||
await defaultAuthService.init();
|
await defaultAuthService.init();
|
||||||
await paiaAuthService.init();
|
await paiaAuthService.init();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -151,11 +145,12 @@ export function createTranslateLoader(http: HttpClient) {
|
|||||||
BrowserAnimationsModule,
|
BrowserAnimationsModule,
|
||||||
CatalogModule,
|
CatalogModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
ConfigModule,
|
|
||||||
DashboardModule,
|
DashboardModule,
|
||||||
DataModule,
|
DataModule,
|
||||||
HebisModule,
|
HebisModule,
|
||||||
IonicModule.forRoot(),
|
IonicModule.forRoot({
|
||||||
|
animated: 'Cypress' in window ? false : undefined,
|
||||||
|
}),
|
||||||
IonIconModule,
|
IonIconModule,
|
||||||
JobModule,
|
JobModule,
|
||||||
FavoritesModule,
|
FavoritesModule,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {ActivatedRoute} from '@angular/router';
|
import {ActivatedRoute} from '@angular/router';
|
||||||
import {SCAboutPage, SCAppConfiguration} from '@openstapps/core';
|
import {SCAboutPage} from '@openstapps/core';
|
||||||
import {ConfigProvider} from '../../config/config.provider';
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
import packageJson from '../../../../../package.json';
|
import packageJson from '../../../../../package.json';
|
||||||
import config from 'capacitor.config';
|
import config from 'capacitor.config';
|
||||||
@@ -42,8 +42,7 @@ export class AboutPageComponent implements OnInit {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const route = this.route.snapshot.url.map(it => it.path).join('/');
|
const route = this.route.snapshot.url.map(it => it.path).join('/');
|
||||||
this.content =
|
this.content = this.configProvider.config.app.aboutPages[route] ?? {};
|
||||||
(this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {};
|
|
||||||
this.version = Capacitor.getPlatform() === 'web' ? 'Web' : await App.getInfo().then(info => info.version);
|
this.version = Capacitor.getPlatform() === 'web' ? 'Web' : await App.getInfo().then(info => info.version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {FormsModule} from '@angular/forms';
|
|||||||
import {IonicModule} from '@ionic/angular';
|
import {IonicModule} from '@ionic/angular';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
|
||||||
import {AboutPageComponent} from './about-page/about-page.component';
|
import {AboutPageComponent} from './about-page/about-page.component';
|
||||||
import {MarkdownModule} from 'ngx-markdown';
|
import {MarkdownModule} from 'ngx-markdown';
|
||||||
import {AboutPageContentComponent} from './about-page/about-page-content.component';
|
import {AboutPageContentComponent} from './about-page/about-page-content.component';
|
||||||
@@ -64,6 +63,5 @@ const settingsRoutes: Routes = [
|
|||||||
ScrollingModule,
|
ScrollingModule,
|
||||||
UtilModule,
|
UtilModule,
|
||||||
],
|
],
|
||||||
providers: [ConfigProvider],
|
|
||||||
})
|
})
|
||||||
export class AboutModule {}
|
export class AboutModule {}
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ import {IPAIAAuthAction} from './paia/paia-auth-action';
|
|||||||
import {AuthActions, IAuthAction} from 'ionic-appauth';
|
import {AuthActions, IAuthAction} from 'ionic-appauth';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import {JSONPath} from 'jsonpath-plus';
|
import {JSONPath} from 'jsonpath-plus';
|
||||||
import {
|
import {SCAuthorizationProviderType, SCUserConfiguration} from '@openstapps/core';
|
||||||
SCAuthorizationProvider,
|
|
||||||
SCAuthorizationProviderType,
|
|
||||||
SCUserConfiguration,
|
|
||||||
SCUserConfigurationMap,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {DefaultAuthService} from './default-auth.service';
|
import {DefaultAuthService} from './default-auth.service';
|
||||||
@@ -37,8 +32,6 @@ const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path';
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class AuthHelperService {
|
export class AuthHelperService {
|
||||||
userConfigurationMap: SCUserConfigurationMap;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private configProvider: ConfigProvider,
|
private configProvider: ConfigProvider,
|
||||||
@@ -47,14 +40,7 @@ export class AuthHelperService {
|
|||||||
private paiaAuth: PAIAAuthService,
|
private paiaAuth: PAIAAuthService,
|
||||||
private browser: SimpleBrowser,
|
private browser: SimpleBrowser,
|
||||||
private alertController: AlertController,
|
private alertController: AlertController,
|
||||||
) {
|
) {}
|
||||||
this.userConfigurationMap =
|
|
||||||
(
|
|
||||||
this.configProvider.getAnyValue('auth') as {
|
|
||||||
default: SCAuthorizationProvider;
|
|
||||||
}
|
|
||||||
).default?.endpoints.mapping ?? {};
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) {
|
public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) {
|
||||||
let message: string | undefined;
|
let message: string | undefined;
|
||||||
@@ -77,9 +63,10 @@ export class AuthHelperService {
|
|||||||
name: '',
|
name: '',
|
||||||
role: 'student',
|
role: 'student',
|
||||||
};
|
};
|
||||||
for (const key in this.userConfigurationMap) {
|
const mapping = this.configProvider.config.auth.default!.endpoints.mapping;
|
||||||
|
for (const key in mapping) {
|
||||||
user[key as keyof SCUserConfiguration] = JSONPath({
|
user[key as keyof SCUserConfiguration] = JSONPath({
|
||||||
path: this.userConfigurationMap[key as keyof SCUserConfiguration] as string,
|
path: mapping[key as keyof SCUserConfiguration] as string,
|
||||||
json: userInfo,
|
json: userInfo,
|
||||||
preventEval: true,
|
preventEval: true,
|
||||||
})[0];
|
})[0];
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
* 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 {
|
import {
|
||||||
AuthorizationRequestHandler,
|
AuthorizationRequestHandler,
|
||||||
AuthorizationServiceConfiguration,
|
AuthorizationServiceConfiguration,
|
||||||
@@ -24,7 +23,6 @@ import {
|
|||||||
} from '@openid/appauth';
|
} from '@openid/appauth';
|
||||||
import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth';
|
import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {SCAuthorizationProvider} from '@openstapps/core';
|
|
||||||
import {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
|
import {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {AuthService} from './auth.service';
|
import {AuthService} from './auth.service';
|
||||||
@@ -67,12 +65,9 @@ export class DefaultAuthService extends AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupConfiguration() {
|
setupConfiguration() {
|
||||||
const authConfig = this.configProvider.getAnyValue('auth') as {
|
this.authConfig = getClientConfig('default', this.configProvider.config.auth);
|
||||||
default: SCAuthorizationProvider;
|
|
||||||
};
|
|
||||||
this.authConfig = getClientConfig('default', authConfig);
|
|
||||||
this.localConfiguration = new AuthorizationServiceConfiguration(
|
this.localConfiguration = new AuthorizationServiceConfiguration(
|
||||||
getEndpointsConfig('default', authConfig),
|
getEndpointsConfig('default', this.configProvider.config.auth),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
* 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 {
|
import {
|
||||||
AuthorizationError,
|
AuthorizationError,
|
||||||
AuthorizationRequest,
|
AuthorizationRequest,
|
||||||
@@ -47,7 +46,6 @@ import {PAIAAuthorizationResponse} from './paia-authorization-response';
|
|||||||
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
|
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
|
||||||
import {PAIATokenResponse} from './paia-token-response';
|
import {PAIATokenResponse} from './paia-token-response';
|
||||||
import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action';
|
import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action';
|
||||||
import {SCAuthorizationProvider} from '@openstapps/core';
|
|
||||||
import {ConfigProvider} from '../../config/config.provider';
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods';
|
import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods';
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
@@ -154,11 +152,10 @@ export class PAIAAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupConfiguration() {
|
setupConfiguration() {
|
||||||
const authConfig = this.configProvider.getAnyValue('auth') as {
|
this.authConfig = getClientConfig('paia', this.configProvider.config.auth);
|
||||||
paia: SCAuthorizationProvider;
|
this.localConfiguration = new AuthorizationServiceConfiguration(
|
||||||
};
|
getEndpointsConfig('paia', this.configProvider.config.auth),
|
||||||
this.authConfig = getClientConfig('paia', authConfig);
|
);
|
||||||
this.localConfiguration = new AuthorizationServiceConfiguration(getEndpointsConfig('paia', authConfig));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected notifyActionListers(action: IPAIAAuthAction) {
|
protected notifyActionListers(action: IPAIAAuthAction) {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
* 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 {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
|
import {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {ICalEvent} from './ical/ical';
|
import {ICalEvent} from './ical/ical';
|
||||||
@@ -35,14 +34,14 @@ export class CalendarService {
|
|||||||
|
|
||||||
goToDateClicked = this.goToDate.asObservable();
|
goToDateClicked = this.goToDate.asObservable();
|
||||||
|
|
||||||
calendarName = 'StApps';
|
calendarName: string;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
constructor(
|
constructor(
|
||||||
readonly calendar: Calendar,
|
readonly calendar: Calendar,
|
||||||
private readonly configProvider: ConfigProvider,
|
private readonly configProvider: ConfigProvider,
|
||||||
) {
|
) {
|
||||||
this.calendarName = (this.configProvider.getValue('name') as string) ?? 'StApps';
|
this.calendarName = this.configProvider.config.app.name ?? 'StApps';
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCalendar(): Promise<CalendarInfo | undefined> {
|
async createCalendar(): Promise<CalendarInfo | undefined> {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {IonicModule} from '@ionic/angular';
|
|||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {MomentModule} from 'ngx-moment';
|
import {MomentModule} from 'ngx-moment';
|
||||||
import {DataModule} from '../data/data.module';
|
import {DataModule} from '../data/data.module';
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {CatalogComponent} from './catalog.component';
|
import {CatalogComponent} from './catalog.component';
|
||||||
import {UtilModule} from '../../util/util.module';
|
import {UtilModule} from '../../util/util.module';
|
||||||
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
|
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
|
||||||
@@ -46,6 +45,5 @@ const catalogRoutes: Routes = [
|
|||||||
DataModule,
|
DataModule,
|
||||||
UtilModule,
|
UtilModule,
|
||||||
],
|
],
|
||||||
providers: [SettingsProvider],
|
|
||||||
})
|
})
|
||||||
export class CatalogModule {}
|
export class CatalogModule {}
|
||||||
|
|||||||
@@ -16,12 +16,6 @@ import {TestBed} from '@angular/core/testing';
|
|||||||
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {ConfigProvider, STORAGE_KEY_CONFIG} from './config.provider';
|
import {ConfigProvider, STORAGE_KEY_CONFIG} from './config.provider';
|
||||||
import {
|
|
||||||
ConfigFetchError,
|
|
||||||
ConfigInitError,
|
|
||||||
SavedConfigNotAvailable,
|
|
||||||
WrongConfigVersionInStorage,
|
|
||||||
} from './errors';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {sampleIndexResponse} from '../../_helpers/data/sample-configuration';
|
import {sampleIndexResponse} from '../../_helpers/data/sample-configuration';
|
||||||
|
|
||||||
|
|||||||
@@ -14,19 +14,14 @@
|
|||||||
*/
|
*/
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {Client} from '@openstapps/api';
|
import {Client} from '@openstapps/api';
|
||||||
import {SCAppConfiguration, SCIndexResponse} from '@openstapps/core';
|
import {SCIndexResponse} from '@openstapps/core';
|
||||||
import {CORE_VERSION} from '@openstapps/core';
|
import packageInfo from '@openstapps/core/package.json';
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {environment} from '../../../environments/environment';
|
import {environment} from '../../../environments/environment';
|
||||||
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {
|
import equals from 'fast-deep-equal/es6';
|
||||||
ConfigFetchError,
|
import {BehaviorSubject} from 'rxjs';
|
||||||
ConfigInitError,
|
|
||||||
ConfigValueNotAvailable,
|
|
||||||
SavedConfigNotAvailable,
|
|
||||||
WrongConfigVersionInStorage,
|
|
||||||
} from './errors';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key to store config in storage module
|
* Key to store config in storage module
|
||||||
@@ -35,6 +30,17 @@ import {
|
|||||||
*/
|
*/
|
||||||
export const STORAGE_KEY_CONFIG = 'stapps.config';
|
export const STORAGE_KEY_CONFIG = 'stapps.config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes an object deeply immutable
|
||||||
|
*/
|
||||||
|
function deepFreeze<T extends object>(object: T) {
|
||||||
|
for (const key of Object.keys(object)) {
|
||||||
|
const value = (object as Record<string, unknown>)[key];
|
||||||
|
if (typeof value === 'object' && !Object.isFrozen(value)) deepFreeze(value!);
|
||||||
|
}
|
||||||
|
return Object.freeze(object);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides configuration
|
* Provides configuration
|
||||||
*/
|
*/
|
||||||
@@ -50,18 +56,23 @@ export class ConfigProvider {
|
|||||||
/**
|
/**
|
||||||
* App configuration as IndexResponse
|
* App configuration as IndexResponse
|
||||||
*/
|
*/
|
||||||
config: SCIndexResponse;
|
config: Readonly<SCIndexResponse>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version of the @openstapps/core package that app is using
|
* Version of the @openstapps/core package that app is using
|
||||||
*/
|
*/
|
||||||
scVersion = CORE_VERSION;
|
scVersion = packageInfo.version;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* First session indicator (config not found in storage)
|
* First session indicator (config not found in storage)
|
||||||
*/
|
*/
|
||||||
firstSession = true;
|
firstSession = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the config requires an update
|
||||||
|
*/
|
||||||
|
needsUpdate$ = new BehaviorSubject(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor, initialise api client
|
* Constructor, initialise api client
|
||||||
* @param storageProvider StorageProvider to load persistent configuration
|
* @param storageProvider StorageProvider to load persistent configuration
|
||||||
@@ -76,104 +87,35 @@ export class ConfigProvider {
|
|||||||
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
|
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches configuration from backend
|
|
||||||
*/
|
|
||||||
async fetch(): Promise<SCIndexResponse> {
|
|
||||||
try {
|
|
||||||
return await this.client.handshake(this.scVersion);
|
|
||||||
} catch {
|
|
||||||
throw new ConfigFetchError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value of an app configuration
|
|
||||||
* @param attribute requested attribute from app configuration
|
|
||||||
*/
|
|
||||||
public getValue(attribute: keyof SCAppConfiguration) {
|
|
||||||
if (this.config.app[attribute] !== undefined) {
|
|
||||||
return this.config.app[attribute];
|
|
||||||
}
|
|
||||||
throw new ConfigValueNotAvailable(attribute);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a value of the configuration (not only app configuration)
|
|
||||||
* @param attribute requested attribute from the configuration
|
|
||||||
*/
|
|
||||||
public getAnyValue(attribute: keyof SCIndexResponse) {
|
|
||||||
if (this.config[attribute] !== undefined) {
|
|
||||||
return this.config[attribute];
|
|
||||||
}
|
|
||||||
throw new ConfigValueNotAvailable(attribute);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialises the ConfigProvider
|
* Initialises the ConfigProvider
|
||||||
* @throws ConfigInitError if no configuration could be loaded.
|
|
||||||
* @throws WrongConfigVersionInStorage if fetch failed and saved config has wrong SCVersion
|
|
||||||
*/
|
*/
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
let loadError;
|
this.config = (await this.storageProvider.has(STORAGE_KEY_CONFIG))
|
||||||
let fetchError;
|
? await this.storageProvider.get<SCIndexResponse>(STORAGE_KEY_CONFIG)
|
||||||
// load saved configuration
|
: undefined!;
|
||||||
try {
|
this.firstSession = !this.config;
|
||||||
this.config = await this.loadLocal();
|
|
||||||
this.firstSession = false;
|
const updatedConfig = this.client.handshake(this.scVersion).then(async fetchedConfig => {
|
||||||
this.logger.log(`initialised configuration from storage`);
|
if (!equals(fetchedConfig, this.config)) {
|
||||||
if (this.config.backend.SCVersion !== this.scVersion) {
|
await this.storageProvider.put(STORAGE_KEY_CONFIG, fetchedConfig);
|
||||||
loadError = new WrongConfigVersionInStorage(this.scVersion, this.config.backend.SCVersion);
|
this.logger.log(`Config updated`);
|
||||||
|
this.needsUpdate$.next(true);
|
||||||
|
this.needsUpdate$.complete();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
return fetchedConfig;
|
||||||
loadError = error;
|
});
|
||||||
}
|
|
||||||
// fetch remote configuration from backend
|
|
||||||
try {
|
|
||||||
const fetchedConfig: SCIndexResponse = await this.fetch();
|
|
||||||
await this.set(fetchedConfig);
|
|
||||||
this.logger.log(`initialised configuration from remote`);
|
|
||||||
} catch (error) {
|
|
||||||
fetchError = error;
|
|
||||||
}
|
|
||||||
// check for occurred errors and throw them
|
|
||||||
if (loadError !== undefined && fetchError !== undefined) {
|
|
||||||
throw new ConfigInitError();
|
|
||||||
}
|
|
||||||
if (loadError !== undefined) {
|
|
||||||
this.logger.warn(loadError);
|
|
||||||
}
|
|
||||||
if (fetchError !== undefined) {
|
|
||||||
this.logger.warn(fetchError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
this.config ??= await updatedConfig;
|
||||||
* Returns saved configuration from StorageModule
|
this.config = deepFreeze(this.config);
|
||||||
* @throws SavedConfigNotAvailable if no configuration could be loaded
|
|
||||||
*/
|
if (this.config.backend.SCVersion !== this.scVersion) {
|
||||||
async loadLocal(): Promise<SCIndexResponse> {
|
this.logger.warn(
|
||||||
// get local configuration
|
'Incompatible config, expected',
|
||||||
if (await this.storageProvider.has(STORAGE_KEY_CONFIG)) {
|
this.scVersion,
|
||||||
return this.storageProvider.get<SCIndexResponse>(STORAGE_KEY_CONFIG);
|
'but got',
|
||||||
|
this.config.backend.SCVersion,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
throw new SavedConfigNotAvailable();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the configuration from the provider
|
|
||||||
* @param config configuration to save
|
|
||||||
*/
|
|
||||||
async save(config: SCIndexResponse): Promise<void> {
|
|
||||||
await this.storageProvider.put(STORAGE_KEY_CONFIG, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the configuration in the module and writes it into app storage
|
|
||||||
* @param config SCIndexResponse to set
|
|
||||||
*/
|
|
||||||
async set(config: SCIndexResponse): Promise<void> {
|
|
||||||
this.config = config;
|
|
||||||
await this.save(this.config);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 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 {AppError} from '../../_helpers/errors';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when fetching from backend fails
|
|
||||||
*/
|
|
||||||
export class ConfigFetchError extends AppError {
|
|
||||||
constructor() {
|
|
||||||
super('ConfigFetchError', 'App configuration could not be fetched!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when the ConfigProvider could be initialised
|
|
||||||
*/
|
|
||||||
export class ConfigInitError extends AppError {
|
|
||||||
constructor() {
|
|
||||||
super('ConfigInitError', 'App configuration could not be initialised!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when the requested config value is not available
|
|
||||||
*/
|
|
||||||
export class ConfigValueNotAvailable extends AppError {
|
|
||||||
constructor(valueKey: string) {
|
|
||||||
super('ConfigValueNotAvailable', `No attribute "${valueKey}" in config available!`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when no saved config is available
|
|
||||||
*/
|
|
||||||
export class SavedConfigNotAvailable extends AppError {
|
|
||||||
constructor() {
|
|
||||||
super('SavedConfigNotAvailable', 'No saved app configuration available.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error that is thrown when the SCVersion of the saved config is not compatible with the app
|
|
||||||
*/
|
|
||||||
export class WrongConfigVersionInStorage extends AppError {
|
|
||||||
constructor(correctVersion: string, savedVersion: string) {
|
|
||||||
super(
|
|
||||||
'WrongConfigVersionInStorage',
|
|
||||||
`The saved configs backend version ${savedVersion} ` +
|
|
||||||
`does not equal the configured backend version ${correctVersion} of the app.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,6 @@ import {SwiperModule} from 'swiper/angular';
|
|||||||
import {TranslateModule, TranslatePipe} from '@ngx-translate/core';
|
import {TranslateModule, TranslatePipe} from '@ngx-translate/core';
|
||||||
import {MomentModule} from 'ngx-moment';
|
import {MomentModule} from 'ngx-moment';
|
||||||
import {DataModule} from '../data/data.module';
|
import {DataModule} from '../data/data.module';
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {DashboardComponent} from './dashboard.component';
|
import {DashboardComponent} from './dashboard.component';
|
||||||
import {SearchSectionComponent} from './sections/search-section/search-section.component';
|
import {SearchSectionComponent} from './sections/search-section/search-section.component';
|
||||||
import {NewsSectionComponent} from './sections/news-section/news-section.component';
|
import {NewsSectionComponent} from './sections/news-section/news-section.component';
|
||||||
@@ -70,6 +69,6 @@ const catalogRoutes: Routes = [
|
|||||||
NewsModule,
|
NewsModule,
|
||||||
JobModule,
|
JobModule,
|
||||||
],
|
],
|
||||||
providers: [SettingsProvider, TranslatePipe],
|
providers: [TranslatePipe],
|
||||||
})
|
})
|
||||||
export class DashboardModule {}
|
export class DashboardModule {}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {ScheduleProvider} from '../calendar/schedule.provider';
|
|||||||
import {GeoNavigationDirective} from '../map/geo-navigation.directive';
|
import {GeoNavigationDirective} from '../map/geo-navigation.directive';
|
||||||
import {MapWidgetComponent} from '../map/widget/map-widget.component';
|
import {MapWidgetComponent} from '../map/widget/map-widget.component';
|
||||||
import {MenuModule} from '../menu/menu.module';
|
import {MenuModule} from '../menu/menu.module';
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {StorageModule} from '../storage/storage.module';
|
import {StorageModule} from '../storage/storage.module';
|
||||||
import {ActionChipListComponent} from './chips/action-chip-list.component';
|
import {ActionChipListComponent} from './chips/action-chip-list.component';
|
||||||
import {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component';
|
import {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component';
|
||||||
@@ -214,7 +213,6 @@ import {ShareButtonComponent} from './elements/share-button.component';
|
|||||||
StAppsWebHttpClient,
|
StAppsWebHttpClient,
|
||||||
CalendarService,
|
CalendarService,
|
||||||
RoutingStackService,
|
RoutingStackService,
|
||||||
SettingsProvider,
|
|
||||||
{
|
{
|
||||||
provide: SimpleBrowser,
|
provide: SimpleBrowser,
|
||||||
useFactory: browserFactory,
|
useFactory: browserFactory,
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
import {Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core';
|
import {Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
import {ModalController} from '@ionic/angular';
|
import {ModalController} from '@ionic/angular';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
|
||||||
import {SCLanguageCode, SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
|
|
||||||
import {DataProvider, DataScope} from '../data.provider';
|
import {DataProvider, DataScope} from '../data.provider';
|
||||||
import {FavoritesService} from '../../favorites/favorites.service';
|
import {FavoritesService} from '../../favorites/favorites.service';
|
||||||
import {take} from 'rxjs/operators';
|
import {take} from 'rxjs/operators';
|
||||||
import {Network} from '@capacitor/network';
|
import {Network} from '@capacitor/network';
|
||||||
import {DataListContext} from '../list/data-list.component';
|
import {DataListContext} from '../list/data-list.component';
|
||||||
|
import {lastValueFrom} from 'rxjs';
|
||||||
|
|
||||||
export interface ExternalDataLoadEvent {
|
export interface ExternalDataLoadEvent {
|
||||||
uid: SCUuid;
|
uid: SCUuid;
|
||||||
@@ -29,6 +29,13 @@ export interface ExternalDataLoadEvent {
|
|||||||
resolve: (item: SCThings | null | undefined) => void;
|
resolve: (item: SCThings | null | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard for SCSavableThing
|
||||||
|
*/
|
||||||
|
function isSCSavableThing(thing: SCThings | SCSaveableThing): thing is SCSaveableThing {
|
||||||
|
return (thing as SCSaveableThing).data !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Component to display an SCThing detailed
|
* A Component to display an SCThing detailed
|
||||||
*/
|
*/
|
||||||
@@ -53,11 +60,6 @@ export class DataDetailComponent implements OnInit {
|
|||||||
|
|
||||||
@Input() autoRouteDataPath = true;
|
@Input() autoRouteDataPath = true;
|
||||||
|
|
||||||
/**
|
|
||||||
* The language of the item
|
|
||||||
*/
|
|
||||||
language: SCLanguageCode;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicating wether internet connectivity is given or not
|
* Indicating wether internet connectivity is given or not
|
||||||
*/
|
*/
|
||||||
@@ -79,20 +81,12 @@ export class DataDetailComponent implements OnInit {
|
|||||||
|
|
||||||
@Output() loadItem: EventEmitter<ExternalDataLoadEvent> = new EventEmitter<ExternalDataLoadEvent>();
|
@Output() loadItem: EventEmitter<ExternalDataLoadEvent> = new EventEmitter<ExternalDataLoadEvent>();
|
||||||
|
|
||||||
/**
|
|
||||||
* Type guard for SCSavableThing
|
|
||||||
*/
|
|
||||||
static isSCSavableThing(thing: SCThings | SCSaveableThing): thing is SCSaveableThing {
|
|
||||||
return (thing as SCSaveableThing).data !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly route: ActivatedRoute,
|
protected readonly route: ActivatedRoute,
|
||||||
router: Router,
|
router: Router,
|
||||||
private readonly dataProvider: DataProvider,
|
private readonly dataProvider: DataProvider,
|
||||||
private readonly favoritesService: FavoritesService,
|
private readonly favoritesService: FavoritesService,
|
||||||
readonly modalController: ModalController,
|
readonly modalController: ModalController,
|
||||||
translateService: TranslateService,
|
|
||||||
) {
|
) {
|
||||||
this.inputItem = router.getCurrentNavigation()?.extras.state?.item;
|
this.inputItem = router.getCurrentNavigation()?.extras.state?.item;
|
||||||
if (!this.inputItem?.origin) {
|
if (!this.inputItem?.origin) {
|
||||||
@@ -100,10 +94,6 @@ export class DataDetailComponent implements OnInit {
|
|||||||
// This can happen, for example, when detail views use `inPlace` list items
|
// This can happen, for example, when detail views use `inPlace` list items
|
||||||
delete this.inputItem;
|
delete this.inputItem;
|
||||||
}
|
}
|
||||||
this.language = translateService.currentLang as SCLanguageCode;
|
|
||||||
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.language = event.lang as SCLanguageCode;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.isDisconnected = new Promise(async resolve => {
|
this.isDisconnected = new Promise(async resolve => {
|
||||||
const isConnected = (await Network.getStatus()).connected;
|
const isConnected = (await Network.getStatus()).connected;
|
||||||
@@ -126,13 +116,8 @@ export class DataDetailComponent implements OnInit {
|
|||||||
)
|
)
|
||||||
: this.dataProvider.get(uid, DataScope.Remote)));
|
: this.dataProvider.get(uid, DataScope.Remote)));
|
||||||
|
|
||||||
this.item = item
|
// eslint-disable-next-line unicorn/no-null
|
||||||
? // eslint-disable-next-line unicorn/no-null
|
this.item = item ? (isSCSavableThing(item) ? item.data : item) : null;
|
||||||
DataDetailComponent.isSCSavableThing(item)
|
|
||||||
? item.data
|
|
||||||
: item
|
|
||||||
: // eslint-disable-next-line unicorn/no-null
|
|
||||||
null;
|
|
||||||
} catch {
|
} catch {
|
||||||
// eslint-disable-next-line unicorn/no-null
|
// eslint-disable-next-line unicorn/no-null
|
||||||
this.item = null;
|
this.item = null;
|
||||||
@@ -144,14 +129,10 @@ export class DataDetailComponent implements OnInit {
|
|||||||
await this.getItem(uid ?? '', false);
|
await this.getItem(uid ?? '', false);
|
||||||
// fallback to the saved item (from favorites)
|
// fallback to the saved item (from favorites)
|
||||||
if (this.item === null) {
|
if (this.item === null) {
|
||||||
this.favoritesService
|
const item = await lastValueFrom(this.favoritesService.get(uid).pipe(take(1)));
|
||||||
.get(uid)
|
if (item) {
|
||||||
.pipe(take(1))
|
this.item = item.data;
|
||||||
.subscribe(item => {
|
}
|
||||||
if (item !== undefined) {
|
|
||||||
this.item = item.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export class OffersInListComponent {
|
|||||||
@Input() set offers(it: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>) {
|
@Input() set offers(it: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>) {
|
||||||
this._offers = it;
|
this._offers = it;
|
||||||
this.price = it[0].prices?.default;
|
this.price = it[0].prices?.default;
|
||||||
this.settingsProvider.getSetting('profile', 'group').then(group => {
|
this.settingsProvider.getSetting<string>('profile', 'group').then(group => {
|
||||||
this.price = it[0].prices?.[(group.value as string).replace(/s$/, '') as never];
|
this.price = it[0].prices?.[group.replace(/s$/, '') as never];
|
||||||
});
|
});
|
||||||
|
|
||||||
const availabilities = new Set(it.map(offer => offer.availability));
|
const availabilities = new Set(it.map(offer => offer.availability));
|
||||||
|
|||||||
@@ -17,14 +17,7 @@ import {ActivatedRoute, Router} from '@angular/router';
|
|||||||
import {Keyboard} from '@capacitor/keyboard';
|
import {Keyboard} from '@capacitor/keyboard';
|
||||||
import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular';
|
import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular';
|
||||||
import {Capacitor} from '@capacitor/core';
|
import {Capacitor} from '@capacitor/core';
|
||||||
import {
|
import {SCFacet, SCSearchFilter, SCSearchQuery, SCSearchSort, SCThings} from '@openstapps/core';
|
||||||
SCFacet,
|
|
||||||
SCFeatureConfiguration,
|
|
||||||
SCSearchFilter,
|
|
||||||
SCSearchQuery,
|
|
||||||
SCSearchSort,
|
|
||||||
SCThings,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {combineLatest, Subject} from 'rxjs';
|
import {combineLatest, Subject} from 'rxjs';
|
||||||
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
|
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
|
||||||
@@ -170,9 +163,8 @@ export class SearchPageComponent implements OnInit {
|
|||||||
private readonly route: ActivatedRoute,
|
private readonly route: ActivatedRoute,
|
||||||
protected positionService: PositionService,
|
protected positionService: PositionService,
|
||||||
private readonly configProvider: ConfigProvider,
|
private readonly configProvider: ConfigProvider,
|
||||||
animationController: AnimationController,
|
|
||||||
) {
|
) {
|
||||||
this.routeAnimation = searchPageSwitchAnimation(animationController);
|
this.routeAnimation = searchPageSwitchAnimation(inject(AnimationController));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -323,16 +315,6 @@ export class SearchPageComponent implements OnInit {
|
|||||||
this.queryChanged.next();
|
this.queryChanged.next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.settingsProvider.settingsActionChanged$
|
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
|
||||||
.subscribe(({type, payload}) => {
|
|
||||||
if (type === 'stapps.settings.changed') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const {category, name, value} = payload!;
|
|
||||||
this.logger.log(`received event "settings.changed" with category:
|
|
||||||
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.dataRoutingService
|
this.dataRoutingService
|
||||||
.itemSelectListener()
|
.itemSelectListener()
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
.pipe(takeUntilDestroyed(this.destroy$))
|
||||||
@@ -342,12 +324,8 @@ export class SearchPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
this.isHebisAvailable =
|
||||||
const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
|
this.configProvider.config.app.features.plugins?.['hebis-plugin']?.urlPath !== undefined;
|
||||||
this.isHebisAvailable = !!features.plugins?.['hebis-plugin']?.urlPath;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
SCRatingResponse,
|
SCRatingResponse,
|
||||||
SCRatingRoute,
|
SCRatingRoute,
|
||||||
SCUserGroup,
|
SCUserGroup,
|
||||||
SCUserGroupSetting,
|
|
||||||
SCUuid,
|
SCUuid,
|
||||||
} from '@openstapps/core';
|
} from '@openstapps/core';
|
||||||
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
|
||||||
@@ -63,9 +62,7 @@ export class RatingProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get userGroup(): Promise<SCUserGroup> {
|
private get userGroup(): Promise<SCUserGroup> {
|
||||||
return this.settingsProvider
|
return this.settingsProvider.getSetting<SCUserGroup>('profile', 'group');
|
||||||
.getSetting('profile', 'group')
|
|
||||||
.then(it => (it as SCUserGroupSetting).value as SCUserGroup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getStoredRatings(): Promise<RatingStorage> {
|
private async getStoredRatings(): Promise<RatingStorage> {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export class PlaceMensaService {
|
|||||||
sort: [
|
sort: [
|
||||||
{
|
{
|
||||||
arguments: {
|
arguments: {
|
||||||
field: `offers.prices.${(priceGroup.value as string).replace(/s$/, '')}`,
|
field: `offers.prices.${(priceGroup as string).replace(/s$/, '')}`,
|
||||||
},
|
},
|
||||||
order: 'desc',
|
order: 'desc',
|
||||||
type: 'generic',
|
type: 'generic',
|
||||||
|
|||||||
@@ -12,21 +12,13 @@
|
|||||||
* 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, OnInit} from '@angular/core';
|
import {Component, inject, OnInit} from '@angular/core';
|
||||||
import {AlertController, AnimationController} from '@ionic/angular';
|
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
|
||||||
import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators';
|
import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators';
|
||||||
import {combineLatest} from 'rxjs';
|
import {combineLatest} from 'rxjs';
|
||||||
import {SCThingType} from '@openstapps/core';
|
import {SCThingType} from '@openstapps/core';
|
||||||
import {FavoritesService} from './favorites.service';
|
import {FavoritesService} from './favorites.service';
|
||||||
import {DataRoutingService} from '../data/data-routing.service';
|
|
||||||
import {ContextMenuService} from '../menu/context/context-menu.service';
|
import {ContextMenuService} from '../menu/context/context-menu.service';
|
||||||
import {SearchPageComponent} from '../data/list/search-page.component';
|
import {SearchPageComponent} from '../data/list/search-page.component';
|
||||||
import {DataProvider} from '../data/data.provider';
|
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {PositionService} from '../map/position.service';
|
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,34 +34,7 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
|
|||||||
|
|
||||||
showNavigation = false;
|
showNavigation = false;
|
||||||
|
|
||||||
constructor(
|
private favoritesService = inject(FavoritesService);
|
||||||
alertController: AlertController,
|
|
||||||
dataProvider: DataProvider,
|
|
||||||
contextMenuService: ContextMenuService,
|
|
||||||
settingsProvider: SettingsProvider,
|
|
||||||
logger: NGXLogger,
|
|
||||||
dataRoutingService: DataRoutingService,
|
|
||||||
router: Router,
|
|
||||||
route: ActivatedRoute,
|
|
||||||
positionService: PositionService,
|
|
||||||
private favoritesService: FavoritesService,
|
|
||||||
configProvider: ConfigProvider,
|
|
||||||
animationController: AnimationController,
|
|
||||||
) {
|
|
||||||
super(
|
|
||||||
alertController,
|
|
||||||
dataProvider,
|
|
||||||
contextMenuService,
|
|
||||||
settingsProvider,
|
|
||||||
logger,
|
|
||||||
dataRoutingService,
|
|
||||||
router,
|
|
||||||
route,
|
|
||||||
positionService,
|
|
||||||
configProvider,
|
|
||||||
animationController,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
super.ngOnInit(false);
|
super.ngOnInit(false);
|
||||||
@@ -96,16 +61,6 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
|
|||||||
this.queryChanged.next();
|
this.queryChanged.next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.settingsProvider.settingsActionChanged$
|
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
|
||||||
.subscribe(({type, payload}) => {
|
|
||||||
if (type === 'stapps.settings.changed') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const {category, name, value} = payload!;
|
|
||||||
this.logger.log(`received event "settings.changed" with category:
|
|
||||||
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.dataRoutingService
|
this.dataRoutingService
|
||||||
.itemSelectListener()
|
.itemSelectListener()
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
.pipe(takeUntilDestroyed(this.destroy$))
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
} from '@openstapps/core';
|
} from '@openstapps/core';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {DataProvider} from '../data/data.provider';
|
import {DataProvider} from '../data/data.provider';
|
||||||
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
|
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import {ThingTranslateService} from '../../translation/thing-translate.service';
|
import {ThingTranslateService} from '../../translation/thing-translate.service';
|
||||||
import {BehaviorSubject, Observable} from 'rxjs';
|
import {BehaviorSubject, Observable} from 'rxjs';
|
||||||
@@ -41,11 +40,6 @@ import {debounceTime, map} from 'rxjs/operators';
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class FavoritesService {
|
export class FavoritesService {
|
||||||
/**
|
|
||||||
* Translation pipe
|
|
||||||
*/
|
|
||||||
thingTranslatePipe: ThingTranslatePipe;
|
|
||||||
|
|
||||||
favorites = new BehaviorSubject<Map<string, SCFavorite>>(new Map<string, SCFavorite>());
|
favorites = new BehaviorSubject<Map<string, SCFavorite>>(new Map<string, SCFavorite>());
|
||||||
|
|
||||||
// using debounce time 0 allows change detection to run through async suspension
|
// using debounce time 0 allows change detection to run through async suspension
|
||||||
@@ -93,8 +87,8 @@ export class FavoritesService {
|
|||||||
return items.sort((a, b) => {
|
return items.sort((a, b) => {
|
||||||
return (
|
return (
|
||||||
new Intl.Collator(this.translate.currentLang).compare(
|
new Intl.Collator(this.translate.currentLang).compare(
|
||||||
this.thingTranslatePipe.transform(field, a),
|
this.thingTranslate.get(a, field) as string,
|
||||||
this.thingTranslatePipe.transform(field, b),
|
this.thingTranslate.get(b, field) as string,
|
||||||
) * reverse
|
) * reverse
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -124,7 +118,6 @@ export class FavoritesService {
|
|||||||
private readonly translate: TranslateService,
|
private readonly translate: TranslateService,
|
||||||
private readonly thingTranslate: ThingTranslateService,
|
private readonly thingTranslate: ThingTranslateService,
|
||||||
) {
|
) {
|
||||||
this.thingTranslatePipe = new ThingTranslatePipe(this.translate, this.thingTranslate);
|
|
||||||
void this.emitAll();
|
void this.emitAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +178,9 @@ export class FavoritesService {
|
|||||||
const textFilteredItems: SCIndexableThings[] = [];
|
const textFilteredItems: SCIndexableThings[] = [];
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (
|
if (
|
||||||
this.thingTranslatePipe.transform('name', item).toLowerCase().includes(queryText.toLowerCase())
|
(this.thingTranslate.get(item, 'name') as string)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(queryText.toLowerCase())
|
||||||
) {
|
) {
|
||||||
textFilteredItems.push(item);
|
textFilteredItems.push(item);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {DaiaAvailabilityResponse, DaiaHolding, DaiaService} from './protocol/res
|
|||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {SCFeatureConfiguration} from '@openstapps/core';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
|
|
||||||
@@ -67,7 +66,7 @@ export class DaiaDataProvider {
|
|||||||
async getAvailability(id: string): Promise<DaiaHolding[] | undefined> {
|
async getAvailability(id: string): Promise<DaiaHolding[] | undefined> {
|
||||||
if (this.daiaServiceUrl === undefined) {
|
if (this.daiaServiceUrl === undefined) {
|
||||||
try {
|
try {
|
||||||
const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
|
const features = this.configProvider.config.app.features;
|
||||||
if (features.extern?.daia?.url) {
|
if (features.extern?.daia?.url) {
|
||||||
this.daiaServiceUrl = features.extern?.daia?.url;
|
this.daiaServiceUrl = features.extern?.daia?.url;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -114,16 +114,6 @@ export class HebisSearchPageComponent extends SearchPageComponent implements OnI
|
|||||||
this.queryChanged.next();
|
this.queryChanged.next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.settingsProvider.settingsActionChanged$
|
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
|
||||||
.subscribe(({type, payload}) => {
|
|
||||||
if (type === 'stapps.settings.changed') {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const {category, name, value} = payload!;
|
|
||||||
this.logger.log(`received event "settings.changed" with category:
|
|
||||||
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.dataRoutingService
|
this.dataRoutingService
|
||||||
.itemSelectListener()
|
.itemSelectListener()
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
.pipe(takeUntilDestroyed(this.destroy$))
|
||||||
|
|||||||
@@ -12,14 +12,9 @@
|
|||||||
* 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 {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {JQueryRequestor, Requestor} from '@openid/appauth';
|
import {JQueryRequestor, Requestor} from '@openid/appauth';
|
||||||
import {
|
import {SCAuthorizationProviderType, SCFeatureConfigurationExtern} from '@openstapps/core';
|
||||||
SCAuthorizationProviderType,
|
|
||||||
SCFeatureConfiguration,
|
|
||||||
SCFeatureConfigurationExtern,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types';
|
import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types';
|
||||||
import {HebisDataProvider} from '../../hebis/hebis-data.provider';
|
import {HebisDataProvider} from '../../hebis/hebis-data.provider';
|
||||||
import {PAIATokenResponse} from '../../auth/paia/paia-token-response';
|
import {PAIATokenResponse} from '../../auth/paia/paia-token-response';
|
||||||
@@ -53,9 +48,7 @@ export class LibraryAccountService {
|
|||||||
private readonly toastController: ToastController,
|
private readonly toastController: ToastController,
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const config: SCFeatureConfigurationExtern = (
|
const config: SCFeatureConfigurationExtern = configProvider.config.app.features.extern!.paia;
|
||||||
configProvider.getValue('features') as SCFeatureConfiguration
|
|
||||||
).extern!.paia;
|
|
||||||
this.baseUrl = config.url;
|
this.baseUrl = config.url;
|
||||||
this.authType = config.authProvider as SCAuthorizationProviderType;
|
this.authType = config.authProvider as SCAuthorizationProviderType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {LeafletModule} from '@asymmetrik/ngx-leaflet';
|
|||||||
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
|
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
|
||||||
import {IonicModule} from '@ionic/angular';
|
import {IonicModule} from '@ionic/angular';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {Polygon} from 'geojson';
|
|
||||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {DataFacetsProvider} from '../data/data-facets.provider';
|
import {DataFacetsProvider} from '../data/data-facets.provider';
|
||||||
@@ -42,7 +41,7 @@ import {GeoNavigationDirective} from './geo-navigation.directive';
|
|||||||
*/
|
*/
|
||||||
export function initMapConfigFactory(configProvider: ConfigProvider, mapProvider: MapProvider) {
|
export function initMapConfigFactory(configProvider: ConfigProvider, mapProvider: MapProvider) {
|
||||||
return async () => {
|
return async () => {
|
||||||
mapProvider.defaultPolygon = (await configProvider.getValue('campusPolygon')) as Polygon;
|
mapProvider.defaultPolygon = configProvider.config.app.campusPolygon;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export class MapProvider {
|
|||||||
private positionService: PositionService,
|
private positionService: PositionService,
|
||||||
private configProvider: ConfigProvider,
|
private configProvider: ConfigProvider,
|
||||||
) {
|
) {
|
||||||
this.defaultPolygon = this.configProvider.getValue('campusPolygon') as Polygon;
|
this.defaultPolygon = this.configProvider.config.app.campusPolygon;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,11 +13,11 @@
|
|||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component, Input} from '@angular/core';
|
import {Component, Input} from '@angular/core';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {SCThingType} from '@openstapps/core';
|
||||||
import {SCLanguage, SCThingTranslator, SCThingType, SCTranslations} from '@openstapps/core';
|
|
||||||
import {ContextMenuService} from './context-menu.service';
|
import {ContextMenuService} from './context-menu.service';
|
||||||
import {FilterContext, FilterFacet, SortContext, SortContextOption} from './context-type.js';
|
import {FilterContext, FilterFacet, SortContext, SortContextOption} from './context-type.js';
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
|
import {ThingTranslateService} from '../../../translation/thing-translate.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The context menu
|
* The context menu
|
||||||
@@ -59,11 +59,6 @@ export class ContextMenuComponent {
|
|||||||
return this.filterOption.options.filter(it => it.buckets.length > 0);
|
return this.filterOption.options.filter(it => it.buckets.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Possible languages to be used for translation
|
|
||||||
*/
|
|
||||||
language: keyof SCTranslations<SCLanguage>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapping of SCThingType
|
* Mapping of SCThingType
|
||||||
*/
|
*/
|
||||||
@@ -74,22 +69,10 @@ export class ContextMenuComponent {
|
|||||||
*/
|
*/
|
||||||
sortOption: SortContext;
|
sortOption: SortContext;
|
||||||
|
|
||||||
/**
|
|
||||||
* Core translator
|
|
||||||
*/
|
|
||||||
translator: SCThingTranslator;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private translateService: TranslateService,
|
|
||||||
private readonly contextMenuService: ContextMenuService,
|
private readonly contextMenuService: ContextMenuService,
|
||||||
|
private readonly thingTranslateService: ThingTranslateService,
|
||||||
) {
|
) {
|
||||||
this.language = this.translateService.currentLang as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
|
|
||||||
this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => {
|
|
||||||
this.language = event.lang as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
});
|
|
||||||
this.contextMenuService.filterContextChanged$.pipe(takeUntilDestroyed()).subscribe(filterContext => {
|
this.contextMenuService.filterContextChanged$.pipe(takeUntilDestroyed()).subscribe(filterContext => {
|
||||||
this.filterOption = filterContext;
|
this.filterOption = filterContext;
|
||||||
});
|
});
|
||||||
@@ -109,7 +92,7 @@ export class ContextMenuComponent {
|
|||||||
* Returns translated property value
|
* Returns translated property value
|
||||||
*/
|
*/
|
||||||
getTranslatedPropertyValue(onlyForType: SCThingType, field: string, key?: string): string | undefined {
|
getTranslatedPropertyValue(onlyForType: SCThingType, field: string, key?: string): string | undefined {
|
||||||
return this.translator.translatedPropertyValue(onlyForType, field, key);
|
return this.thingTranslateService.translator.translatedPropertyValue(onlyForType, field, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,74 +13,23 @@
|
|||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {SCAppConfigurationMenuCategory} from '@openstapps/core';
|
||||||
import {
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
SCAppConfigurationMenuCategory,
|
|
||||||
SCLanguage,
|
|
||||||
SCThingTranslator,
|
|
||||||
SCTranslations,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {NavigationService} from './navigation.service';
|
|
||||||
import config from 'capacitor.config';
|
|
||||||
import {SettingsProvider} from '../../settings/settings.provider';
|
|
||||||
import {BreakpointObserver} from '@angular/cdk/layout';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generated class for the MenuPage page.
|
|
||||||
*
|
|
||||||
* See https://ionicframework.com/docs/components/#navigation for more info on
|
|
||||||
* Ionic pages and navigation.
|
|
||||||
*/
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'stapps-navigation',
|
selector: 'stapps-navigation',
|
||||||
styleUrls: ['navigation.scss'],
|
styleUrls: ['navigation.scss'],
|
||||||
templateUrl: 'navigation.html',
|
templateUrl: 'navigation.html',
|
||||||
})
|
})
|
||||||
export class NavigationComponent implements OnInit {
|
export class NavigationComponent implements OnInit {
|
||||||
showTabbar = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the app
|
|
||||||
*/
|
|
||||||
appName = config.appName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Possible languages to be used for translation
|
|
||||||
*/
|
|
||||||
language: keyof SCTranslations<SCLanguage>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menu entries from config module
|
* Menu entries from config module
|
||||||
*/
|
*/
|
||||||
menu: SCAppConfigurationMenuCategory[];
|
menu: SCAppConfigurationMenuCategory[];
|
||||||
|
|
||||||
/**
|
constructor(private config: ConfigProvider) {}
|
||||||
* Core translator
|
|
||||||
*/
|
|
||||||
translator: SCThingTranslator;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public translateService: TranslateService,
|
|
||||||
private navigationService: NavigationService,
|
|
||||||
private settingsProvider: SettingsProvider,
|
|
||||||
private responsive: BreakpointObserver,
|
|
||||||
) {
|
|
||||||
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.language = event.lang as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.responsive.observe(['(min-width: 768px)']).subscribe(result => {
|
|
||||||
this.showTabbar = !result.matches;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.language = (await this.settingsProvider.getValue(
|
this.menu = this.config.config.app.menus;
|
||||||
'profile',
|
|
||||||
'language',
|
|
||||||
)) as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
this.menu = await this.navigationService.getMenu();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,11 @@
|
|||||||
class="menu-category"
|
class="menu-category"
|
||||||
>
|
>
|
||||||
<ion-icon slot="end" [name]="category.icon"></ion-icon>
|
<ion-icon slot="end" [name]="category.icon"></ion-icon>
|
||||||
<ion-label> {{ category.translations[language]?.title | titlecase }} </ion-label>
|
<ion-label> {{ 'title' | translateSimple: category | titlecase }} </ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item *ngFor="let item of category.items" [rootLink]="item.route" [redirectedFrom]="item.route">
|
<ion-item *ngFor="let item of category.items" [rootLink]="item.route" [redirectedFrom]="item.route">
|
||||||
<ion-icon slot="end" [name]="item.icon"></ion-icon>
|
<ion-icon slot="end" [name]="item.icon"></ion-icon>
|
||||||
<ion-label> {{ item.translations[language]?.title | titlecase }} </ion-label>
|
<ion-label> {{ 'title' | translateSimple: item | titlecase }} </ion-label>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ import {IonIconModule} from '../../../util/ion-icon/ion-icon.module';
|
|||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {RouterModule} from '@angular/router';
|
import {RouterModule} from '@angular/router';
|
||||||
import {OfflineNoticeComponent} from './offline-notice.component';
|
import {OfflineNoticeComponent} from './offline-notice.component';
|
||||||
|
import {ThingTranslateModule} from '../../../translation/thing-translate.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [RootLinkDirective, NavigationComponent, TabsComponent, OfflineNoticeComponent],
|
declarations: [RootLinkDirective, NavigationComponent, TabsComponent, OfflineNoticeComponent],
|
||||||
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule],
|
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule, ThingTranslateModule],
|
||||||
exports: [TabsComponent, RootLinkDirective, NavigationComponent],
|
exports: [TabsComponent, RootLinkDirective, NavigationComponent],
|
||||||
})
|
})
|
||||||
export class NavigationModule {}
|
export class NavigationModule {}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ stapps-navigation-tabs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stapps-offline-notice.needs-reload ~ ion-split-pane,
|
||||||
stapps-offline-notice.has-error ~ ion-split-pane,
|
stapps-offline-notice.has-error ~ ion-split-pane,
|
||||||
stapps-offline-notice.is-offline ~ ion-split-pane {
|
stapps-offline-notice.is-offline ~ ion-split-pane {
|
||||||
margin-top: calc(var(--font-size-md) + 2 * var(--spacing-sm));
|
margin-top: calc(var(--font-size-md) + 2 * var(--spacing-sm));
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 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 {SCAppConfigurationMenuCategory} from '@openstapps/core';
|
|
||||||
import {ConfigProvider} from '../../config/config.provider';
|
|
||||||
import {NGXLogger} from 'ngx-logger';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class NavigationService {
|
|
||||||
constructor(
|
|
||||||
private configProvider: ConfigProvider,
|
|
||||||
private logger: NGXLogger,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async getMenu() {
|
|
||||||
let menu: SCAppConfigurationMenuCategory[] = [];
|
|
||||||
try {
|
|
||||||
menu = this.configProvider.getValue('menus') as SCAppConfigurationMenuCategory[];
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`error from loading menu entries: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,10 @@ import {InternetConnectionService} from '../../../util/internet-connection.servi
|
|||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||||
|
import {SettingsProvider} from '../../settings/settings.provider';
|
||||||
|
import {AnimationController} from '@ionic/angular';
|
||||||
|
import {filter, race} from 'rxjs';
|
||||||
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'stapps-offline-notice',
|
selector: 'stapps-offline-notice',
|
||||||
@@ -28,12 +32,17 @@ export class OfflineNoticeComponent {
|
|||||||
|
|
||||||
@HostBinding('class.has-error') hasError = false;
|
@HostBinding('class.has-error') hasError = false;
|
||||||
|
|
||||||
|
@HostBinding('class.needs-reload') needsReload = false;
|
||||||
|
|
||||||
@ViewChild('spinIcon', {read: ElementRef}) spinIcon: ElementRef;
|
@ViewChild('spinIcon', {read: ElementRef}) spinIcon: ElementRef;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly offlineProvider: InternetConnectionService,
|
readonly offlineProvider: InternetConnectionService,
|
||||||
readonly router: Router,
|
readonly router: Router,
|
||||||
readonly logger: NGXLogger,
|
readonly logger: NGXLogger,
|
||||||
|
readonly animationCtl: AnimationController,
|
||||||
|
settingsProvider: SettingsProvider,
|
||||||
|
configProvider: ConfigProvider,
|
||||||
) {
|
) {
|
||||||
this.offlineProvider.offline$.pipe(takeUntilDestroyed()).subscribe(isOffline => {
|
this.offlineProvider.offline$.pipe(takeUntilDestroyed()).subscribe(isOffline => {
|
||||||
this.isOffline = isOffline;
|
this.isOffline = isOffline;
|
||||||
@@ -41,6 +50,15 @@ export class OfflineNoticeComponent {
|
|||||||
this.offlineProvider.error$.pipe(takeUntilDestroyed()).subscribe(hasError => {
|
this.offlineProvider.error$.pipe(takeUntilDestroyed()).subscribe(hasError => {
|
||||||
this.hasError = hasError;
|
this.hasError = hasError;
|
||||||
});
|
});
|
||||||
|
race(
|
||||||
|
settingsProvider.needsReload$.pipe(filter(it => it)),
|
||||||
|
configProvider.needsUpdate$.pipe(filter(it => it)),
|
||||||
|
)
|
||||||
|
.pipe(takeUntilDestroyed())
|
||||||
|
.subscribe(() => {
|
||||||
|
console.log('aha!');
|
||||||
|
this.needsReload = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
retry() {
|
retry() {
|
||||||
@@ -49,4 +67,15 @@ export class OfflineNoticeComponent {
|
|||||||
this.spinIcon.nativeElement.classList.add('spin');
|
this.spinIcon.nativeElement.classList.add('spin');
|
||||||
this.offlineProvider.retry();
|
this.offlineProvider.retry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reloadPage() {
|
||||||
|
await this.animationCtl
|
||||||
|
.create()
|
||||||
|
.duration(100)
|
||||||
|
.fromTo('opacity', 1, 0)
|
||||||
|
.addElement(document.querySelector('ion-app')!)
|
||||||
|
.play();
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
<ion-icon #spinIcon slot="start" [size]="16" [weight]="800" name="refresh"></ion-icon>
|
<ion-icon #spinIcon slot="start" [size]="16" [weight]="800" name="refresh"></ion-icon>
|
||||||
<ion-label>{{ 'app.errors.CONNECTION_ERROR' | translate }}</ion-label>
|
<ion-label>{{ 'app.errors.CONNECTION_ERROR' | translate }}</ion-label>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
<ion-button class="reload" color="warning" (click)="reloadPage()">
|
||||||
|
<ion-icon slot="start" [size]="16" [weight]="800" name="refresh"></ion-icon>
|
||||||
|
<ion-label>{{ 'settings.reloadPage' | translate }}</ion-label>
|
||||||
|
</ion-button>
|
||||||
<ion-button class="close" fill="clear" color="light" (click)="offlineProvider.dismissError()"
|
<ion-button class="close" fill="clear" color="light" (click)="offlineProvider.dismissError()"
|
||||||
><ion-icon [size]="16" [weight]="800" name="close" slot="icon-only"></ion-icon
|
><ion-icon [size]="16" [weight]="800" name="close" slot="icon-only"></ion-icon
|
||||||
></ion-button>
|
></ion-button>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
transition: all 150ms ease;
|
transition: all 150ms ease;
|
||||||
|
|
||||||
|
&.needs-reload,
|
||||||
&.is-offline,
|
&.is-offline,
|
||||||
&.has-error {
|
&.has-error {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.needs-reload > .reload,
|
||||||
&.is-offline > .offline-button,
|
&.is-offline > .offline-button,
|
||||||
&.has-error > .close,
|
&.has-error > .close,
|
||||||
&.has-error > .error-button {
|
&.has-error > .error-button {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
import type {AnimationBuilder} from '@ionic/angular';
|
import type {AnimationBuilder} from '@ionic/angular';
|
||||||
import {AnimationController} from '@ionic/angular';
|
import {AnimationController} from '@ionic/angular';
|
||||||
import type {AnimationOptions} from '@ionic/angular/providers/nav-controller';
|
import type {AnimationOptions} from '@ionic/angular/providers/nav-controller';
|
||||||
|
import {iosDuration, iosEasing, mdDuration, mdEasing} from '../../../animation/easings';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -23,10 +24,11 @@ import type {AnimationOptions} from '@ionic/angular/providers/nav-controller';
|
|||||||
export function tabsTransition(animationController: AnimationController): AnimationBuilder {
|
export function tabsTransition(animationController: AnimationController): AnimationBuilder {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return (_baseElement: HTMLElement, options: AnimationOptions | any) => {
|
return (_baseElement: HTMLElement, options: AnimationOptions | any) => {
|
||||||
const duration = options.duration || 350;
|
const isMd = document.querySelector('ion-app.md') !== null;
|
||||||
const contentExitDuration = options.contentExitDuration || 100;
|
const duration = isMd ? mdDuration : iosDuration;
|
||||||
|
const easing = isMd ? mdEasing : iosEasing;
|
||||||
|
|
||||||
const rootTransition = animationController.create().duration(duration);
|
const rootTransition = animationController.create().duration(duration).easing(easing);
|
||||||
|
|
||||||
const enterTransition = animationController
|
const enterTransition = animationController
|
||||||
.create()
|
.create()
|
||||||
@@ -39,23 +41,15 @@ export function tabsTransition(animationController: AnimationController): Animat
|
|||||||
.addElement(options.leavingEl);
|
.addElement(options.leavingEl);
|
||||||
const exitTransition = animationController
|
const exitTransition = animationController
|
||||||
.create()
|
.create()
|
||||||
.duration(contentExitDuration * 2)
|
|
||||||
.easing('cubic-bezier(0.87, 0, 0.13, 1)')
|
|
||||||
.fromTo('opacity', '1', '0')
|
.fromTo('opacity', '1', '0')
|
||||||
.addElement(options.leavingEl.querySelector('ion-header'));
|
.addElement(options.leavingEl.querySelector('ion-header'));
|
||||||
const contentExit = animationController
|
const contentExit = animationController
|
||||||
.create()
|
.create()
|
||||||
.easing('linear')
|
|
||||||
.duration(contentExitDuration)
|
|
||||||
.fromTo('opacity', '1', '0')
|
.fromTo('opacity', '1', '0')
|
||||||
.addElement(options.leavingEl.querySelectorAll(':scope > *:not(ion-header)'));
|
.addElement(options.leavingEl.querySelectorAll(':scope > *:not(ion-header)'));
|
||||||
const contentEnter = animationController
|
const contentEnter = animationController
|
||||||
.create()
|
.create()
|
||||||
.delay(contentExitDuration)
|
.fromTo('transform', 'scale(1.05)', 'scale(1)')
|
||||||
.duration(duration - contentExitDuration)
|
|
||||||
.easing('cubic-bezier(0.16, 1, 0.3, 1)')
|
|
||||||
.fromTo('transform', 'scale(1.025)', 'scale(1)')
|
|
||||||
.fromTo('opacity', '0', '1')
|
|
||||||
.addElement(options.enteringEl.querySelectorAll(':scope > *:not(ion-header)'));
|
.addElement(options.enteringEl.querySelectorAll(':scope > *:not(ion-header)'));
|
||||||
|
|
||||||
rootTransition.addAnimation([enterTransition, contentExit, contentEnter, exitTransition, exitZIndex]);
|
rootTransition.addAnimation([enterTransition, contentExit, contentEnter, exitTransition, exitZIndex]);
|
||||||
|
|||||||
@@ -12,17 +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 {Component} from '@angular/core';
|
||||||
import {NavigationEnd, Router} from '@angular/router';
|
import {NavigationEnd, Router} from '@angular/router';
|
||||||
import {
|
import {SCAppConfigurationMenuCategory} from '@openstapps/core';
|
||||||
SCAppConfigurationMenuCategory,
|
|
||||||
SCLanguage,
|
|
||||||
SCThingTranslator,
|
|
||||||
SCTranslations,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import {ConfigProvider} from '../../config/config.provider';
|
import {ConfigProvider} from '../../config/config.provider';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import {NGXLogger} from 'ngx-logger';
|
import {NGXLogger} from 'ngx-logger';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -31,21 +25,11 @@ import {NGXLogger} from 'ngx-logger';
|
|||||||
styleUrls: ['./tabs.component.scss'],
|
styleUrls: ['./tabs.component.scss'],
|
||||||
})
|
})
|
||||||
export class TabsComponent {
|
export class TabsComponent {
|
||||||
/**
|
|
||||||
* Possible languages to be used for translation
|
|
||||||
*/
|
|
||||||
language: keyof SCTranslations<SCLanguage>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menu entries from config module
|
* Menu entries from config module
|
||||||
*/
|
*/
|
||||||
menu: SCAppConfigurationMenuCategory[];
|
menu: SCAppConfigurationMenuCategory[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Core translator
|
|
||||||
*/
|
|
||||||
translator: SCThingTranslator;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of selected tab
|
* Name of selected tab
|
||||||
*/
|
*/
|
||||||
@@ -57,8 +41,6 @@ export class TabsComponent {
|
|||||||
private readonly logger: NGXLogger,
|
private readonly logger: NGXLogger,
|
||||||
private readonly router: Router,
|
private readonly router: Router,
|
||||||
) {
|
) {
|
||||||
this.language = this.translateService.currentLang as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
void this.loadMenuEntries();
|
void this.loadMenuEntries();
|
||||||
this.router.events.subscribe((event: unknown) => {
|
this.router.events.subscribe((event: unknown) => {
|
||||||
if (event instanceof NavigationEnd) {
|
if (event instanceof NavigationEnd) {
|
||||||
@@ -66,11 +48,6 @@ export class TabsComponent {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.selectTab(router.url);
|
this.selectTab(router.url);
|
||||||
|
|
||||||
translateService.onLangChange?.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.language = event.lang as keyof SCTranslations<SCLanguage>;
|
|
||||||
this.translator = new SCThingTranslator(this.language);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +55,7 @@ export class TabsComponent {
|
|||||||
*/
|
*/
|
||||||
async loadMenuEntries() {
|
async loadMenuEntries() {
|
||||||
try {
|
try {
|
||||||
const menus = (await this.configProvider.getValue('menus')) as SCAppConfigurationMenuCategory[];
|
const menus = this.configProvider.config.app.menus;
|
||||||
|
|
||||||
const menu = menus.slice(0, 5);
|
const menu = menus.slice(0, 5);
|
||||||
if (menu) {
|
if (menu) {
|
||||||
|
|||||||
@@ -46,6 +46,6 @@
|
|||||||
[tab]="category.title"
|
[tab]="category.title"
|
||||||
>
|
>
|
||||||
<ion-icon [name]="category.icon"></ion-icon>
|
<ion-icon [name]="category.icon"></ion-icon>
|
||||||
<ion-label>{{ category.translations[language]?.title | titlecase }}</ion-label>
|
<ion-label>{{ 'title' | translateSimple: category | titlecase }}</ion-label>
|
||||||
</ion-tab-button>
|
</ion-tab-button>
|
||||||
</ion-tab-bar>
|
</ion-tab-bar>
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<ng-container *ngFor="let setting of settings">
|
|
||||||
<stapps-chip-filter
|
|
||||||
[displayValue]="setting | settingValueTranslate | titlecase"
|
|
||||||
[value]="setting"
|
|
||||||
[active]="!!filtersMap.get($any(setting.name))"
|
|
||||||
(toggle)="stateChanged($any($event))"
|
|
||||||
>
|
|
||||||
</stapps-chip-filter>
|
|
||||||
</ng-container>
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2022 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, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
|
||||||
import {newsFilterSettingsFieldsMapping, NewsFilterSettingsNames} from '../../news-filter-settings';
|
|
||||||
import {SCSearchValueFilter, SCSetting} from '@openstapps/core';
|
|
||||||
import {DataProvider} from '../../../data/data.provider';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'stapps-news-settings-filter',
|
|
||||||
templateUrl: './news-settings-filter.component.html',
|
|
||||||
styleUrls: ['./news-settings-filter.component.scss'],
|
|
||||||
})
|
|
||||||
export class NewsSettingsFilterComponent implements OnInit {
|
|
||||||
/**
|
|
||||||
* A map of the filters where the keys are settings names
|
|
||||||
*/
|
|
||||||
filtersMap = new Map<NewsFilterSettingsNames, SCSearchValueFilter>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emits the current filters
|
|
||||||
*/
|
|
||||||
@Output() filtersChanged = new EventEmitter<SCSearchValueFilter[]>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provided settings to show the filters for
|
|
||||||
*/
|
|
||||||
@Input() settings: SCSetting[];
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
for (const setting of this.settings) {
|
|
||||||
this.filtersMap.set(
|
|
||||||
setting.name as NewsFilterSettingsNames,
|
|
||||||
DataProvider.createValueFilter(
|
|
||||||
newsFilterSettingsFieldsMapping[setting.name as NewsFilterSettingsNames],
|
|
||||||
setting.value as string,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.filtersChanged.emit([...this.filtersMap.values()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* To be executed when a chip filter has been enabled/disabled
|
|
||||||
* @param setting The value of the filter
|
|
||||||
*/
|
|
||||||
stateChanged(setting: SCSetting) {
|
|
||||||
if (this.filtersMap.get(setting.name as NewsFilterSettingsNames) === undefined) {
|
|
||||||
this.filtersMap.set(
|
|
||||||
setting.name as NewsFilterSettingsNames,
|
|
||||||
DataProvider.createValueFilter(
|
|
||||||
newsFilterSettingsFieldsMapping[setting.name as NewsFilterSettingsNames],
|
|
||||||
setting.value as string,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.filtersMap.delete(setting.name as NewsFilterSettingsNames);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.filtersChanged.emit([...this.filtersMap.values()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,13 +20,11 @@ import {TranslateModule} from '@ngx-translate/core';
|
|||||||
import {MomentModule} from 'ngx-moment';
|
import {MomentModule} from 'ngx-moment';
|
||||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
import {DataModule} from '../data/data.module';
|
import {DataModule} from '../data/data.module';
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
|
||||||
import {NewsItemComponent} from './item/news-item.component';
|
import {NewsItemComponent} from './item/news-item.component';
|
||||||
import {NewsPageComponent} from './page/news-page.component';
|
import {NewsPageComponent} from './page/news-page.component';
|
||||||
import {SkeletonNewsItemComponent} from './item/skeleton-news-item.component';
|
import {SkeletonNewsItemComponent} from './item/skeleton-news-item.component';
|
||||||
import {ChipFilterComponent} from '../data/chips/filter/chip-filter.component';
|
import {ChipFilterComponent} from '../data/chips/filter/chip-filter.component';
|
||||||
import {SettingsModule} from '../settings/settings.module';
|
import {SettingsModule} from '../settings/settings.module';
|
||||||
import {NewsSettingsFilterComponent} from './elements/news-filter-settings/news-settings-filter.component';
|
|
||||||
import {UtilModule} from '../../util/util.module';
|
import {UtilModule} from '../../util/util.module';
|
||||||
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
|
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
|
||||||
|
|
||||||
@@ -36,13 +34,7 @@ const newsRoutes: Routes = [{path: 'news', component: NewsPageComponent}];
|
|||||||
* News Module
|
* News Module
|
||||||
*/
|
*/
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [NewsPageComponent, SkeletonNewsItemComponent, NewsItemComponent, ChipFilterComponent],
|
||||||
NewsPageComponent,
|
|
||||||
SkeletonNewsItemComponent,
|
|
||||||
NewsItemComponent,
|
|
||||||
ChipFilterComponent,
|
|
||||||
NewsSettingsFilterComponent,
|
|
||||||
],
|
|
||||||
imports: [
|
imports: [
|
||||||
IonicModule.forRoot(),
|
IonicModule.forRoot(),
|
||||||
ThingTranslateModule.forChild(),
|
ThingTranslateModule.forChild(),
|
||||||
@@ -56,7 +48,6 @@ const newsRoutes: Routes = [{path: 'news', component: NewsPageComponent}];
|
|||||||
SettingsModule,
|
SettingsModule,
|
||||||
UtilModule,
|
UtilModule,
|
||||||
],
|
],
|
||||||
providers: [SettingsProvider],
|
|
||||||
exports: [NewsItemComponent],
|
exports: [NewsItemComponent],
|
||||||
})
|
})
|
||||||
export class NewsModule {}
|
export class NewsModule {}
|
||||||
|
|||||||
@@ -19,17 +19,18 @@ import {
|
|||||||
SCSearchBooleanFilter,
|
SCSearchBooleanFilter,
|
||||||
SCSearchFilter,
|
SCSearchFilter,
|
||||||
SCSearchQuery,
|
SCSearchQuery,
|
||||||
SCSearchValueFilter,
|
|
||||||
SCSetting,
|
|
||||||
} from '@openstapps/core';
|
} from '@openstapps/core';
|
||||||
import {DataProvider} from '../data/data.provider';
|
import {DataProvider} from '../data/data.provider';
|
||||||
import {
|
|
||||||
newsFilterSettingsCategory,
|
|
||||||
newsFilterSettingsFieldsMapping,
|
|
||||||
NewsFilterSettingsNames,
|
|
||||||
} from './news-filter-settings';
|
|
||||||
import {SettingsProvider} from '../settings/settings.provider';
|
import {SettingsProvider} from '../settings/settings.provider';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mapping between settings and corresponding data fields for building a value filter
|
||||||
|
*/
|
||||||
|
const newsFilterSettingsFieldsMapping = [
|
||||||
|
['language', 'inLanguage'],
|
||||||
|
['group', 'audiences'],
|
||||||
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for providing news messages
|
* Service for providing news messages
|
||||||
*/
|
*/
|
||||||
@@ -42,28 +43,22 @@ export class NewsProvider {
|
|||||||
private settingsProvider: SettingsProvider,
|
private settingsProvider: SettingsProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getCurrentSettings(): Promise<SCSetting[]> {
|
/**
|
||||||
const settings: SCSetting[] = [];
|
* Gets the news filter based on user group and language settings
|
||||||
for (const settingName of Object.keys(newsFilterSettingsFieldsMapping) as NewsFilterSettingsNames[]) {
|
*/
|
||||||
settings.push(await this.settingsProvider.getSetting(newsFilterSettingsCategory, settingName));
|
|
||||||
}
|
|
||||||
return settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCurrentFilters(): Promise<SCSearchFilter[]> {
|
async getCurrentFilters(): Promise<SCSearchFilter[]> {
|
||||||
const settings = await this.getCurrentSettings();
|
return Promise.all(
|
||||||
const filtersMap = new Map<NewsFilterSettingsNames, SCSearchValueFilter>();
|
newsFilterSettingsFieldsMapping.map(
|
||||||
for (const setting of settings) {
|
async ([setting, field]) =>
|
||||||
filtersMap.set(
|
({
|
||||||
setting.name as NewsFilterSettingsNames,
|
type: 'value',
|
||||||
DataProvider.createValueFilter(
|
arguments: {
|
||||||
newsFilterSettingsFieldsMapping[setting.name as NewsFilterSettingsNames],
|
field,
|
||||||
setting.value as string,
|
value: await this.settingsProvider.getSetting('profile', setting),
|
||||||
),
|
},
|
||||||
);
|
}) satisfies SCSearchFilter,
|
||||||
}
|
),
|
||||||
|
);
|
||||||
return [...filtersMap.values()];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
* 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, OnInit} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {IonRefresher} from '@ionic/angular';
|
import {IonRefresher} from '@ionic/angular';
|
||||||
import {SCMessage, SCSearchFilter, SCSearchValueFilter, SCSetting} from '@openstapps/core';
|
import {SCMessage} from '@openstapps/core';
|
||||||
import {NewsProvider} from '../news.provider';
|
import {NewsProvider} from '../news.provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,7 +25,7 @@ import {NewsProvider} from '../news.provider';
|
|||||||
templateUrl: 'news-page.html',
|
templateUrl: 'news-page.html',
|
||||||
styleUrls: ['news-page.scss'],
|
styleUrls: ['news-page.scss'],
|
||||||
})
|
})
|
||||||
export class NewsPageComponent implements OnInit {
|
export class NewsPageComponent {
|
||||||
/**
|
/**
|
||||||
* Thing counter to start query the next page from
|
* Thing counter to start query the next page from
|
||||||
*/
|
*/
|
||||||
@@ -51,24 +51,20 @@ export class NewsPageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
elementSize = [300, 300];
|
elementSize = [300, 300];
|
||||||
|
|
||||||
/**
|
constructor(private newsProvider: NewsProvider) {
|
||||||
* Relevant settings
|
this.fetchNews();
|
||||||
*/
|
}
|
||||||
settings: SCSetting[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Active filters
|
|
||||||
*/
|
|
||||||
filters: SCSearchFilter[];
|
|
||||||
|
|
||||||
constructor(private newsProvider: NewsProvider) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch news from the backend
|
* Fetch news from the backend
|
||||||
*/
|
*/
|
||||||
async fetchNews() {
|
async fetchNews() {
|
||||||
this.from = this.pageSize;
|
this.from = this.pageSize;
|
||||||
this.news = await this.newsProvider.getList(this.pageSize, 0, [...this.filters]);
|
this.news = await this.newsProvider.getList(
|
||||||
|
this.pageSize,
|
||||||
|
0,
|
||||||
|
await this.newsProvider.getCurrentFilters(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,7 +73,11 @@ export class NewsPageComponent implements OnInit {
|
|||||||
async loadMore(infiniteScrollElement?: HTMLIonInfiniteScrollElement, more = this.pageSize): Promise<void> {
|
async loadMore(infiniteScrollElement?: HTMLIonInfiniteScrollElement, more = this.pageSize): Promise<void> {
|
||||||
const from = this.from;
|
const from = this.from;
|
||||||
this.from += more;
|
this.from += more;
|
||||||
const fetchedNews = await this.newsProvider.getList(more, from, [...this.filters]);
|
const fetchedNews = await this.newsProvider.getList(
|
||||||
|
more,
|
||||||
|
from,
|
||||||
|
await this.newsProvider.getCurrentFilters(),
|
||||||
|
);
|
||||||
|
|
||||||
this.news = [...this.news, ...fetchedNews];
|
this.news = [...this.news, ...fetchedNews];
|
||||||
await infiniteScrollElement?.complete();
|
await infiniteScrollElement?.complete();
|
||||||
@@ -96,13 +96,6 @@ export class NewsPageComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the local variables on component initialization
|
|
||||||
*/
|
|
||||||
async ngOnInit() {
|
|
||||||
this.settings = await this.newsProvider.getCurrentSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the shown list
|
* Updates the shown list
|
||||||
* @param refresher Refresher component that triggers the update
|
* @param refresher Refresher component that triggers the update
|
||||||
@@ -116,13 +109,4 @@ export class NewsPageComponent implements OnInit {
|
|||||||
await refresher.complete();
|
await refresher.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Executed when filters have been changed
|
|
||||||
* @param filters Current filters to be used
|
|
||||||
*/
|
|
||||||
toggleFilter(filters: SCSearchValueFilter[]) {
|
|
||||||
this.filters = filters;
|
|
||||||
void this.fetchNews();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,17 +32,6 @@
|
|||||||
>
|
>
|
||||||
</ion-refresher-content>
|
</ion-refresher-content>
|
||||||
</ion-refresher>
|
</ion-refresher>
|
||||||
<ion-grid>
|
|
||||||
<ion-row>
|
|
||||||
<ion-col size="12">
|
|
||||||
<stapps-news-settings-filter
|
|
||||||
*ngIf="settings"
|
|
||||||
[settings]="settings"
|
|
||||||
(filtersChanged)="toggleFilter($event)"
|
|
||||||
></stapps-news-settings-filter>
|
|
||||||
</ion-col>
|
|
||||||
</ion-row>
|
|
||||||
</ion-grid>
|
|
||||||
<div class="news-grid">
|
<div class="news-grid">
|
||||||
<ng-container *ngIf="!news">
|
<ng-container *ngIf="!news">
|
||||||
<stapps-skeleton-news-item *ngFor="let skeleton of [1, 2, 3, 4, 5]"></stapps-skeleton-news-item>
|
<stapps-skeleton-news-item *ngFor="let skeleton of [1, 2, 3, 4, 5]"></stapps-skeleton-news-item>
|
||||||
|
|||||||
@@ -14,8 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
import {Component, Input} from '@angular/core';
|
import {Component, Input} from '@angular/core';
|
||||||
import {AlertController} from '@ionic/angular';
|
import {AlertController} from '@ionic/angular';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
|
||||||
import {SCLanguageCode, SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
|
|
||||||
import {SettingsProvider} from '../settings.provider';
|
import {SettingsProvider} from '../settings.provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,23 +41,10 @@ export class SettingsItemComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() setting: SCSetting;
|
@Input() setting: SCSetting;
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param alertCtrl AlertController
|
|
||||||
* @param translateService TranslateService
|
|
||||||
* @param settingsProvider SettingProvider
|
|
||||||
*/
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly alertCtrl: AlertController,
|
private readonly alertCtrl: AlertController,
|
||||||
private readonly translateService: TranslateService,
|
|
||||||
private readonly settingsProvider: SettingsProvider,
|
private readonly settingsProvider: SettingsProvider,
|
||||||
) {
|
) {}
|
||||||
translateService.onLangChange.subscribe((_event: LangChangeEvent) => {
|
|
||||||
this.isVisible = false;
|
|
||||||
// TODO: Issue #53 check workaround for selected 'select option' not updating translation
|
|
||||||
setTimeout(() => (this.isVisible = true));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows alert with given title and message and a 'ok' button
|
* Shows alert with given title and message and a 'ok' button
|
||||||
@@ -82,14 +68,6 @@ export class SettingsItemComponent {
|
|||||||
this.setting.value !== undefined &&
|
this.setting.value !== undefined &&
|
||||||
SettingsProvider.validateValue(this.setting, this.setting.value)
|
SettingsProvider.validateValue(this.setting, this.setting.value)
|
||||||
) {
|
) {
|
||||||
// handle general settings, with special actions
|
|
||||||
switch (this.setting.name) {
|
|
||||||
case 'language': {
|
|
||||||
this.translateService.use(this.setting.value as SCLanguageCode);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
await this.settingsProvider.setSettingValue(
|
await this.settingsProvider.setSettingValue(
|
||||||
this.setting.categories[0],
|
this.setting.categories[0],
|
||||||
this.setting.name,
|
this.setting.name,
|
||||||
@@ -97,7 +75,7 @@ export class SettingsItemComponent {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// reset setting
|
// reset setting
|
||||||
this.setting.value = (await this.settingsProvider.getValue(
|
this.setting.value = (await this.settingsProvider.getSetting(
|
||||||
this.setting.categories[0],
|
this.setting.categories[0],
|
||||||
this.setting.name,
|
this.setting.name,
|
||||||
)) as SCSettingValue | SCSettingValues;
|
)) as SCSettingValue | SCSettingValues;
|
||||||
|
|||||||
@@ -47,14 +47,6 @@ export class SettingsPageComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
settingsCache: SettingsCache;
|
settingsCache: SettingsCache;
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param alertController AlertController
|
|
||||||
* @param settingsProvider SettingsProvider
|
|
||||||
* @param toastController ToastController
|
|
||||||
* @param translateService TranslateService
|
|
||||||
* @param changeDetectorRef ChangeDetectorRef
|
|
||||||
*/
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly alertController: AlertController,
|
private readonly alertController: AlertController,
|
||||||
private readonly settingsProvider: SettingsProvider,
|
private readonly settingsProvider: SettingsProvider,
|
||||||
|
|||||||
@@ -26,20 +26,6 @@
|
|||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<ng-container *ngFor="let categoryKey of categoriesOrder">
|
<ng-container *ngFor="let categoryKey of categoriesOrder">
|
||||||
<ion-list *ngIf="objectKeys(settingsCache).includes(categoryKey)">
|
<ion-list *ngIf="objectKeys(settingsCache).includes(categoryKey)">
|
||||||
<!-- <ion-item-divider>
|
|
||||||
<h2>
|
|
||||||
{{
|
|
||||||
'categories[0]'
|
|
||||||
| thingTranslate
|
|
||||||
: $any(
|
|
||||||
settingsCache[categoryKey]?.settings[
|
|
||||||
objectKeys(settingsCache[categoryKey]?.settings)[0]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
| titlecase
|
|
||||||
}}
|
|
||||||
</h2>
|
|
||||||
</ion-item-divider> -->
|
|
||||||
<stapps-settings-item
|
<stapps-settings-item
|
||||||
*ngFor="let settingKeys of objectKeys(settingsCache[categoryKey].settings)"
|
*ngFor="let settingKeys of objectKeys(settingsCache[categoryKey].settings)"
|
||||||
[setting]="settingsCache[categoryKey].settings[settingKeys]"
|
[setting]="settingsCache[categoryKey].settings[settingKeys]"
|
||||||
@@ -49,7 +35,7 @@
|
|||||||
|
|
||||||
<calendar-sync-settings></calendar-sync-settings>
|
<calendar-sync-settings></calendar-sync-settings>
|
||||||
|
|
||||||
<ion-button expand="block" (click)="presentResetAlert()" fill="outline">
|
<ion-button expand="block" (click)="presentResetAlert()" fill="outline" color="danger">
|
||||||
{{ 'settings.resetSettings' | translate }}
|
{{ 'settings.resetSettings' | translate }}
|
||||||
<ion-icon slot="start" name="device_reset"></ion-icon>
|
<ion-icon slot="start" name="device_reset"></ion-icon>
|
||||||
</ion-button>
|
</ion-button>
|
||||||
|
|||||||
@@ -12,11 +12,8 @@
|
|||||||
* 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 {Pipe, PipeTransform} from '@angular/core';
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
|
||||||
import {SCSetting} from '@openstapps/core';
|
import {SCSetting} from '@openstapps/core';
|
||||||
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
|
|
||||||
import {ThingTranslateService} from '../../translation/thing-translate.service';
|
import {ThingTranslateService} from '../../translation/thing-translate.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,14 +24,10 @@ import {ThingTranslateService} from '../../translation/thing-translate.service';
|
|||||||
pure: true,
|
pure: true,
|
||||||
})
|
})
|
||||||
export class SettingTranslatePipe implements PipeTransform {
|
export class SettingTranslatePipe implements PipeTransform {
|
||||||
constructor(
|
constructor(private readonly thingTranslate: ThingTranslateService) {}
|
||||||
private readonly translate: TranslateService,
|
|
||||||
private readonly thingTranslate: ThingTranslateService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
transform(setting: SCSetting): string | undefined {
|
transform(setting: SCSetting) {
|
||||||
const thingTranslatePipe = new ThingTranslatePipe(this.translate, this.thingTranslate);
|
const translatedSettingValues = this.thingTranslate.get(setting, 'values') as string;
|
||||||
const translatedSettingValues = thingTranslatePipe.transform('values', setting);
|
|
||||||
|
|
||||||
return translatedSettingValues
|
return translatedSettingValues
|
||||||
? String(translatedSettingValues[setting.values?.indexOf(setting.value as string) as number])
|
? String(translatedSettingValues[setting.values?.indexOf(setting.value as string) as number])
|
||||||
|
|||||||
@@ -18,13 +18,10 @@ import {FormsModule} from '@angular/forms';
|
|||||||
import {RouterModule, Routes} from '@angular/router';
|
import {RouterModule, Routes} from '@angular/router';
|
||||||
import {IonicModule} from '@ionic/angular';
|
import {IonicModule} from '@ionic/angular';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
|
|
||||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
|
||||||
import {SettingsItemComponent} from './item/settings-item.component';
|
import {SettingsItemComponent} from './item/settings-item.component';
|
||||||
import {SettingsPageComponent} from './page/settings-page.component';
|
import {SettingsPageComponent} from './page/settings-page.component';
|
||||||
import {SettingTranslatePipe} from './setting-translate.pipe';
|
import {SettingTranslatePipe} from './setting-translate.pipe';
|
||||||
import {SettingsProvider} from './settings.provider';
|
|
||||||
import {CalendarSyncSettingsComponent} from './page/calendar-sync-settings.component';
|
import {CalendarSyncSettingsComponent} from './page/calendar-sync-settings.component';
|
||||||
import {ScheduleProvider} from '../calendar/schedule.provider';
|
import {ScheduleProvider} from '../calendar/schedule.provider';
|
||||||
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
|
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
|
||||||
@@ -60,13 +57,6 @@ const settingsRoutes: Routes = [{path: 'settings', component: SettingsPageCompon
|
|||||||
RouterModule.forChild(settingsRoutes),
|
RouterModule.forChild(settingsRoutes),
|
||||||
UtilModule,
|
UtilModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [ScheduleSyncService, CalendarService, ScheduleProvider, ThingTranslatePipe],
|
||||||
ScheduleSyncService,
|
|
||||||
ConfigProvider,
|
|
||||||
SettingsProvider,
|
|
||||||
CalendarService,
|
|
||||||
ScheduleProvider,
|
|
||||||
ThingTranslatePipe,
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class SettingsModule {}
|
export class SettingsModule {}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
|
import {SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
|
||||||
import deepMerge from 'deepmerge';
|
import deepMerge from 'deepmerge';
|
||||||
import {Subject} from 'rxjs';
|
import {BehaviorSubject, Subject} from 'rxjs';
|
||||||
import {ConfigProvider} from '../config/config.provider';
|
import {ConfigProvider} from '../config/config.provider';
|
||||||
import {StorageProvider} from '../storage/storage.provider';
|
import {StorageProvider} from '../storage/storage.provider';
|
||||||
|
|
||||||
@@ -89,7 +89,9 @@ export interface SettingsAction {
|
|||||||
/**
|
/**
|
||||||
* Provider for app settings
|
* Provider for app settings
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
export class SettingsProvider {
|
export class SettingsProvider {
|
||||||
/**
|
/**
|
||||||
* Source of settings actions
|
* Source of settings actions
|
||||||
@@ -103,16 +105,16 @@ export class SettingsProvider {
|
|||||||
*/
|
*/
|
||||||
categoriesOrder: string[];
|
categoriesOrder: string[];
|
||||||
|
|
||||||
/**
|
|
||||||
* Settings actions observable
|
|
||||||
*/
|
|
||||||
settingsActionChanged$ = this.settingsActionSource.asObservable();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache for the imported settings
|
* Cache for the imported settings
|
||||||
*/
|
*/
|
||||||
settingsCache: SettingsCache;
|
settingsCache: SettingsCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the page needs a reload
|
||||||
|
*/
|
||||||
|
needsReload$ = new BehaviorSubject(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if all given values are valid to possible values in given settingInput
|
* Return true if all given values are valid to possible values in given settingInput
|
||||||
* @param possibleValues Possible values
|
* @param possibleValues Possible values
|
||||||
@@ -148,9 +150,7 @@ export class SettingsProvider {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return Array.isArray(possibleValues) && possibleValues.includes(enteredValue);
|
||||||
possibleValues !== undefined && Array.isArray(possibleValues) && possibleValues.includes(enteredValue)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,7 +206,7 @@ export class SettingsProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an Setting to the Cache if not exist and set undefined value to defaultValue
|
* Add a Setting to the Cache if not exist and set undefined value to defaultValue
|
||||||
* @param setting Setting with categories, defaultValue, name, input type and valid values
|
* @param setting Setting with categories, defaultValue, name, input type and valid values
|
||||||
*/
|
*/
|
||||||
private addSetting(setting: SCSetting): void {
|
private addSetting(setting: SCSetting): void {
|
||||||
@@ -281,34 +281,20 @@ export class SettingsProvider {
|
|||||||
return this.categoriesOrder;
|
return this.categoriesOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns copy of a setting if exist
|
|
||||||
* @param category the category of requested setting
|
|
||||||
* @param name the name of requested setting
|
|
||||||
* @throws Exception if setting is not provided
|
|
||||||
*/
|
|
||||||
public async getSetting(category: string, name: string): Promise<SCSetting> {
|
|
||||||
await this.init();
|
|
||||||
if (this.settingExists(category, name)) {
|
|
||||||
// return a copy of the settings
|
|
||||||
return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name]));
|
|
||||||
}
|
|
||||||
throw new Error(`Setting "${name}" not provided`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns copy of a settings value if exist
|
* Returns copy of a settings value if exist
|
||||||
* @param category the category of requested setting
|
* @param category the category of requested setting
|
||||||
* @param name the name of requested setting
|
* @param name the name of requested setting
|
||||||
* @throws Exception if setting is not provided
|
* @throws Exception if setting is not provided
|
||||||
*/
|
*/
|
||||||
public async getValue(category: string, name: string): Promise<SCSettingValue | SCSettingValues> {
|
public async getSetting<T extends SCSettingValue | SCSettingValues>(
|
||||||
await this.init();
|
category: 'profile' | string,
|
||||||
if (this.settingExists(category, name)) {
|
name: string,
|
||||||
// return a copy of the settings value
|
): Promise<T> {
|
||||||
return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name].value));
|
const settings = await this.storage.get<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES);
|
||||||
}
|
const value = settings[category]?.[name];
|
||||||
throw new Error(`Setting "${name}" not provided`);
|
if (!value) throw new Error(`Setting "${name}" not provided`);
|
||||||
|
return value as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -319,8 +305,8 @@ export class SettingsProvider {
|
|||||||
this.needsInit = false;
|
this.needsInit = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings: SCSetting[] = this.configProvider.getValue('settings') as SCSetting[];
|
const settings: SCSetting[] = this.configProvider.config.app.settings;
|
||||||
for (const setting of settings) this.addSetting(setting);
|
for (const setting of settings) this.addSetting(JSON.parse(JSON.stringify(setting)));
|
||||||
|
|
||||||
for (const category of Object.keys(this.settingsCache)) {
|
for (const category of Object.keys(this.settingsCache)) {
|
||||||
if (!this.categoriesOrder.includes(category)) {
|
if (!this.categoriesOrder.includes(category)) {
|
||||||
@@ -347,7 +333,6 @@ export class SettingsProvider {
|
|||||||
: this.settingsCache[categoryKey].settings[settingKey].defaultValue;
|
: this.settingsCache[categoryKey].settings[settingKey].defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.saveSettingValues();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,6 +382,9 @@ export class SettingsProvider {
|
|||||||
this.getSettingValuesFromCache(),
|
this.getSettingValuesFromCache(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.needsReload$.next(true);
|
||||||
|
this.needsReload$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2019 StApps
|
* Copyright (C) 2023 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.
|
||||||
@@ -12,16 +12,15 @@
|
|||||||
* 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 {NgModule} from '@angular/core';
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
import {DataModule} from '../data/data.module';
|
|
||||||
import {StorageModule} from '../storage/storage.module';
|
|
||||||
import {ConfigProvider} from './config.provider';
|
|
||||||
|
|
||||||
/**
|
@Injectable()
|
||||||
* TODO
|
@Pipe({
|
||||||
*/
|
name: 'entries',
|
||||||
@NgModule({
|
pure: true,
|
||||||
imports: [StorageModule, DataModule],
|
|
||||||
providers: [ConfigProvider],
|
|
||||||
})
|
})
|
||||||
export class ConfigModule {}
|
export class EntriesPipe implements PipeTransform {
|
||||||
|
transform<T>(value: Record<string | number | symbol, T>): T[] {
|
||||||
|
return Object.values(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2021 StApps
|
* Copyright (C) 2023 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.
|
||||||
@@ -12,21 +12,23 @@
|
|||||||
* 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 {SCSettingCategories, SCThingsField} from '@openstapps/core';
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
/**
|
|
||||||
* Category of settings to use for news filter
|
@Injectable()
|
||||||
*/
|
@Pipe({
|
||||||
export const newsFilterSettingsCategory: SCSettingCategories = 'profile';
|
name: 'join',
|
||||||
/**
|
pure: true,
|
||||||
* Settings to use for news filter
|
})
|
||||||
*/
|
export class ArrayJoinPipe implements PipeTransform {
|
||||||
export type NewsFilterSettingsNames = 'language' | 'group';
|
transform(anArray: unknown[] | unknown, separator: string | unknown): string {
|
||||||
/**
|
if (typeof separator !== 'string' || separator.length <= 0) {
|
||||||
* The mapping between settings and corresponding data fields for building a value filter
|
return '';
|
||||||
*/
|
}
|
||||||
export const newsFilterSettingsFieldsMapping: {
|
|
||||||
[key in NewsFilterSettingsNames]: SCThingsField;
|
if (!Array.isArray(anArray)) {
|
||||||
} = {
|
throw new SyntaxError(`Wrong parameter in ArrayJoinPipe. Expected a valid Array, received: ${anArray}`);
|
||||||
language: 'inLanguage',
|
}
|
||||||
group: 'audiences',
|
|
||||||
};
|
return anArray.join(separator);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,416 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2023 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, OnDestroy, Pipe, PipeTransform} from '@angular/core';
|
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
|
||||||
import moment from 'moment';
|
|
||||||
import {Subscription} from 'rxjs';
|
|
||||||
import {logger} from '../_helpers/ts-logger';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'join',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class ArrayJoinPipe implements PipeTransform {
|
|
||||||
value = '';
|
|
||||||
|
|
||||||
transform(anArray: unknown[] | unknown, separator: string | unknown): string {
|
|
||||||
if (typeof separator !== 'string' || separator.length <= 0) {
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(anArray)) {
|
|
||||||
throw new SyntaxError(`Wrong parameter in ArrayJoinPipe. Expected a valid Array, received: ${anArray}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = anArray.join(separator);
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'entries',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class EntriesPipe implements PipeTransform {
|
|
||||||
transform<T>(value: Record<string | number | symbol, T>): T[] {
|
|
||||||
return Object.values(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'toUnix',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class ToUnixPipe implements PipeTransform {
|
|
||||||
transform(value: string | number | Date | null | undefined): number {
|
|
||||||
return (value instanceof Date ? value : new Date(value ?? 0)).valueOf();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'sentencecase',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class SentenceCasePipe implements PipeTransform {
|
|
||||||
value = '';
|
|
||||||
|
|
||||||
transform(aString: string | unknown): string {
|
|
||||||
if (typeof aString !== 'string') {
|
|
||||||
throw new SyntaxError(
|
|
||||||
`Wrong parameter in StringSplitPipe. Expected a valid String, received: ${aString}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = aString.slice(0, 1).toUpperCase() + aString.slice(1);
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'split',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class StringSplitPipe implements PipeTransform {
|
|
||||||
value = new Array<unknown>();
|
|
||||||
|
|
||||||
transform(aString: string | unknown, splitter: string | unknown): unknown[] {
|
|
||||||
if (typeof splitter !== 'string' || splitter.length <= 0) {
|
|
||||||
return this.value as never;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof aString !== 'string') {
|
|
||||||
throw new SyntaxError(
|
|
||||||
`Wrong parameter in StringSplitPipe. Expected a valid String, received: ${aString}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = aString.split(splitter);
|
|
||||||
|
|
||||||
return this.value as never;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'durationLocalized',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class DurationLocalizedPipe implements PipeTransform, OnDestroy {
|
|
||||||
locale: string;
|
|
||||||
|
|
||||||
onLangChange?: Subscription;
|
|
||||||
|
|
||||||
value: string;
|
|
||||||
|
|
||||||
frequencyPrefixes: {[iso6391Code: string]: string} = {
|
|
||||||
de: 'alle',
|
|
||||||
en: 'every',
|
|
||||||
es: 'cada',
|
|
||||||
pt: 'a cada',
|
|
||||||
fr: 'tous les',
|
|
||||||
cn: '每',
|
|
||||||
ru: 'kаждые',
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(private readonly translate: TranslateService) {
|
|
||||||
this.locale = translate.currentLang;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed === false) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value An ISO 8601 duration string
|
|
||||||
* @param isFrequency Boolean indicating if this duration is to be interpreted as repeat frequency
|
|
||||||
*/
|
|
||||||
transform(value: string | unknown, isFrequency = false): string {
|
|
||||||
this.updateValue(value, isFrequency);
|
|
||||||
this._dispose();
|
|
||||||
if (this.onLangChange?.closed === true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.locale = event.lang;
|
|
||||||
this.updateValue(value, isFrequency);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(value: string | unknown, isFrequency = false): void {
|
|
||||||
if (typeof value !== 'string') {
|
|
||||||
logger.warn(`durationLocalized pipe unable to parse input: ${value}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isFrequency) {
|
|
||||||
const fequencyPrefix = Object.keys(this.frequencyPrefixes).filter(element =>
|
|
||||||
this.locale.includes(element),
|
|
||||||
);
|
|
||||||
this.value = [
|
|
||||||
fequencyPrefix.length > 0 ? this.frequencyPrefixes[fequencyPrefix[0]] : this.frequencyPrefixes.en,
|
|
||||||
moment.duration(value).humanize(),
|
|
||||||
].join(' ');
|
|
||||||
} else {
|
|
||||||
this.value = moment.duration(value).humanize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'metersLocalized',
|
|
||||||
pure: false,
|
|
||||||
})
|
|
||||||
export class MetersLocalizedPipe implements PipeTransform, OnDestroy {
|
|
||||||
locale: string;
|
|
||||||
|
|
||||||
onLangChange?: Subscription;
|
|
||||||
|
|
||||||
value = '';
|
|
||||||
|
|
||||||
constructor(private readonly translate: TranslateService) {
|
|
||||||
this.locale = translate.currentLang;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed === false) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
transform(value: string | number | unknown): string {
|
|
||||||
this.updateValue(value);
|
|
||||||
this._dispose();
|
|
||||||
if (this.onLangChange?.closed === true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.locale = event.lang;
|
|
||||||
this.updateValue(value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(value: string | number | unknown) {
|
|
||||||
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
||||||
logger.warn(`metersLocalized pipe unable to parse input: ${value}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const imperialLocale = ['US', 'UK', 'LR', 'MM'].some(term => this.locale.includes(term));
|
|
||||||
const meters = typeof value === 'string' ? Number.parseFloat(value) : (value as number);
|
|
||||||
|
|
||||||
if (imperialLocale) {
|
|
||||||
const yards = meters * 1.0936;
|
|
||||||
const options = {
|
|
||||||
style: 'unit',
|
|
||||||
unit: yards >= 1760 ? 'mile' : 'yard',
|
|
||||||
maximumFractionDigits: yards >= 1760 ? 1 : 0,
|
|
||||||
} as unknown as Intl.NumberFormatOptions;
|
|
||||||
this.value = new Intl.NumberFormat(this.locale, options).format(yards >= 1760 ? yards / 1760 : yards);
|
|
||||||
} else {
|
|
||||||
const options = {
|
|
||||||
style: 'unit',
|
|
||||||
unit: meters >= 1000 ? 'kilometer' : 'meter',
|
|
||||||
maximumFractionDigits: meters >= 1000 ? 1 : 0,
|
|
||||||
} as unknown as Intl.NumberFormatOptions;
|
|
||||||
this.value = new Intl.NumberFormat(this.locale, options).format(
|
|
||||||
meters >= 1000 ? meters / 1000 : meters,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'isNaN',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class IsNaNPipe implements PipeTransform {
|
|
||||||
transform(value: unknown): boolean {
|
|
||||||
return Number.isNaN(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'isNumeric',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class IsNumericPipe implements PipeTransform {
|
|
||||||
transform(value: unknown): boolean {
|
|
||||||
return !Number.isNaN(
|
|
||||||
typeof value === 'number' ? value : typeof value === 'string' ? Number.parseFloat(value) : Number.NaN,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'numberLocalized',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class NumberLocalizedPipe implements PipeTransform, OnDestroy {
|
|
||||||
locale: string;
|
|
||||||
|
|
||||||
onLangChange?: Subscription;
|
|
||||||
|
|
||||||
value: string;
|
|
||||||
|
|
||||||
constructor(private readonly translate: TranslateService) {
|
|
||||||
this.locale = translate.currentLang;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed === false) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value The number to be formatted
|
|
||||||
* @param formatOptions Formatting options to include.
|
|
||||||
* As specified by Intl.NumberFormatOptions as comma seperated key:value pairs.
|
|
||||||
*/
|
|
||||||
transform(value: string | number | unknown, formatOptions?: string): string {
|
|
||||||
this.updateValue(value, formatOptions);
|
|
||||||
this._dispose();
|
|
||||||
if (this.onLangChange?.closed === true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.locale = event.lang;
|
|
||||||
this.updateValue(value, formatOptions);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(value: string | number | unknown, formatOptions?: string): void {
|
|
||||||
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
||||||
logger.warn(`numberLocalized pipe unable to parse input: ${value}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const options = formatOptions
|
|
||||||
?.split(',')
|
|
||||||
.map(element => element.split(':'))
|
|
||||||
.reduce(
|
|
||||||
(accumulator, [key, value_]) => ({
|
|
||||||
...accumulator,
|
|
||||||
[key.trim()]: value_.trim(),
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
) as Intl.NumberFormatOptions;
|
|
||||||
const float = typeof value === 'string' ? Number.parseFloat(value) : (value as number);
|
|
||||||
this.value = new Intl.NumberFormat(this.locale, options).format(float);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
@Pipe({
|
|
||||||
name: 'dateFormat',
|
|
||||||
pure: true,
|
|
||||||
})
|
|
||||||
export class DateLocalizedFormatPipe implements PipeTransform, OnDestroy {
|
|
||||||
locale: string;
|
|
||||||
|
|
||||||
onLangChange?: Subscription;
|
|
||||||
|
|
||||||
value: string;
|
|
||||||
|
|
||||||
constructor(private readonly translate: TranslateService) {
|
|
||||||
this.locale = translate.currentLang;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed === false) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param value The date to be formatted
|
|
||||||
* @param formatOptions Dateformat options to include.
|
|
||||||
* As specified by Intl.DateTimeFormatOptions as comma seperated key:value pairs
|
|
||||||
* Default is year,month,day,hour and minute in numeric representation e.g. (en-US) "8/6/2021, 10:35"
|
|
||||||
*/
|
|
||||||
transform(value: string | unknown, formatOptions?: string): string {
|
|
||||||
this.updateValue(value, formatOptions);
|
|
||||||
this._dispose();
|
|
||||||
if (this.onLangChange?.closed === true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
|
||||||
this.locale = event.lang;
|
|
||||||
this.updateValue(value, formatOptions);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(value: string | Date | unknown, formatOptions?: string): void {
|
|
||||||
if (typeof value !== 'string' && Object.prototype.toString.call(value) !== '[object Date]') {
|
|
||||||
logger.warn(`dateFormat pipe unable to parse input: ${value}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const options = formatOptions
|
|
||||||
?.split(',')
|
|
||||||
.map(element => element.split(':'))
|
|
||||||
.reduce(
|
|
||||||
(accumulator, [key, value_]) => ({
|
|
||||||
...accumulator,
|
|
||||||
[key.trim()]: value_.trim(),
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
) as Intl.DateTimeFormatOptions;
|
|
||||||
const date = typeof value === 'string' ? Date.parse(value) : (value as Date);
|
|
||||||
this.value = new Intl.DateTimeFormat(
|
|
||||||
this.locale,
|
|
||||||
options ?? {
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
},
|
|
||||||
).format(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
|
import {logger} from '../../_helpers/ts-logger';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@Pipe({
|
||||||
|
name: 'dateFormat',
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class DateLocalizedFormatPipe implements PipeTransform {
|
||||||
|
constructor(private readonly translate: TranslateService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param value The date to be formatted
|
||||||
|
* @param formatOptions Dateformat options to include.
|
||||||
|
* As specified by Intl.DateTimeFormatOptions as comma seperated key:value pairs
|
||||||
|
* Default is year,month,day,hour and minute in numeric representation e.g. (en-US) "8/6/2021, 10:35"
|
||||||
|
*/
|
||||||
|
transform(value: string | unknown, formatOptions?: string): string {
|
||||||
|
if (typeof value !== 'string' && Object.prototype.toString.call(value) !== '[object Date]') {
|
||||||
|
logger.warn(`dateFormat pipe unable to parse input: ${value}`);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const options = formatOptions
|
||||||
|
?.split(',')
|
||||||
|
.map(element => element.split(':'))
|
||||||
|
.reduce(
|
||||||
|
(accumulator, [key, value_]) => ({
|
||||||
|
...accumulator,
|
||||||
|
[key.trim()]: value_.trim(),
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
) as Intl.DateTimeFormatOptions;
|
||||||
|
const date = typeof value === 'string' ? Date.parse(value) : (value as Date);
|
||||||
|
return new Intl.DateTimeFormat(
|
||||||
|
this.translate.currentLang,
|
||||||
|
options ?? {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
},
|
||||||
|
).format(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
|
import {logger} from '../../_helpers/ts-logger';
|
||||||
|
import moment from 'moment/moment';
|
||||||
|
|
||||||
|
const frequencyPrefixes: {[iso6391Code: string]: string} = {
|
||||||
|
de: 'alle',
|
||||||
|
en: 'every',
|
||||||
|
es: 'cada',
|
||||||
|
pt: 'a cada',
|
||||||
|
fr: 'tous les',
|
||||||
|
cn: '每',
|
||||||
|
ru: 'kаждые',
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@Pipe({
|
||||||
|
name: 'durationLocalized',
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class DurationLocalizedPipe implements PipeTransform {
|
||||||
|
constructor(private readonly translate: TranslateService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param value An ISO 8601 duration string
|
||||||
|
* @param isFrequency Boolean indicating if this duration is to be interpreted as repeat frequency
|
||||||
|
*/
|
||||||
|
transform(value: string | unknown, isFrequency = false): string {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
logger.warn(`durationLocalized pipe unable to parse input: ${value}`);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFrequency) {
|
||||||
|
const fequencyPrefix = Object.keys(frequencyPrefixes).filter(element =>
|
||||||
|
this.translate.currentLang.includes(element),
|
||||||
|
);
|
||||||
|
return [
|
||||||
|
fequencyPrefix.length > 0 ? frequencyPrefixes[fequencyPrefix[0]] : frequencyPrefixes.en,
|
||||||
|
moment.duration(value).humanize(),
|
||||||
|
].join(' ');
|
||||||
|
} else {
|
||||||
|
return moment.duration(value).humanize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
frontend/app/src/app/translation/date-time/to-unix.pipe.ts
Normal file
12
frontend/app/src/app/translation/date-time/to-unix.pipe.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@Pipe({
|
||||||
|
name: 'toUnix',
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class ToUnixPipe implements PipeTransform {
|
||||||
|
transform(value: string | number | Date | null | undefined): number {
|
||||||
|
return (value instanceof Date ? value : new Date(value ?? 0)).valueOf();
|
||||||
|
}
|
||||||
|
}
|
||||||
12
frontend/app/src/app/translation/numbers/is-nan.pipe.ts
Normal file
12
frontend/app/src/app/translation/numbers/is-nan.pipe.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@Pipe({
|
||||||
|
name: 'isNaN',
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class IsNaNPipe implements PipeTransform {
|
||||||
|
transform(value: unknown): boolean {
|
||||||
|
return Number.isNaN(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
frontend/app/src/app/translation/numbers/is-numeric.pipe.ts
Normal file
14
frontend/app/src/app/translation/numbers/is-numeric.pipe.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@Pipe({
|
||||||
|
name: 'isNumeric',
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class IsNumericPipe implements PipeTransform {
|
||||||
|
transform(value: unknown): boolean {
|
||||||
|
return !Number.isNaN(
|
||||||
|
typeof value === 'number' ? value : typeof value === 'string' ? Number.parseFloat(value) : Number.NaN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
|
import {logger} from '../../_helpers/ts-logger';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@Pipe({
|
||||||
|
name: 'metersLocalized',
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class MetersLocalizedPipe implements PipeTransform {
|
||||||
|
constructor(private readonly translate: TranslateService) {}
|
||||||
|
|
||||||
|
transform(value: string | number | unknown): string {
|
||||||
|
if (typeof value !== 'string' && typeof value !== 'number') {
|
||||||
|
logger.warn(`metersLocalized pipe unable to parse input: ${value}`);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const imperialLocale = ['US', 'UK', 'LR', 'MM'].some(term => this.translate.currentLang.includes(term));
|
||||||
|
const meters = typeof value === 'string' ? Number.parseFloat(value) : (value as number);
|
||||||
|
|
||||||
|
if (imperialLocale) {
|
||||||
|
const yards = meters * 1.0936;
|
||||||
|
const options = {
|
||||||
|
style: 'unit',
|
||||||
|
unit: yards >= 1760 ? 'mile' : 'yard',
|
||||||
|
maximumFractionDigits: yards >= 1760 ? 1 : 0,
|
||||||
|
} as unknown as Intl.NumberFormatOptions;
|
||||||
|
return new Intl.NumberFormat(this.translate.currentLang, options).format(
|
||||||
|
yards >= 1760 ? yards / 1760 : yards,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const options = {
|
||||||
|
style: 'unit',
|
||||||
|
unit: meters >= 1000 ? 'kilometer' : 'meter',
|
||||||
|
maximumFractionDigits: meters >= 1000 ? 1 : 0,
|
||||||
|
} as unknown as Intl.NumberFormatOptions;
|
||||||
|
return new Intl.NumberFormat(this.translate.currentLang, options).format(
|
||||||
|
meters >= 1000 ? meters / 1000 : meters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
|
import {logger} from '../../_helpers/ts-logger';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@Pipe({
|
||||||
|
name: 'numberLocalized',
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class NumberLocalizedPipe implements PipeTransform {
|
||||||
|
constructor(private readonly translate: TranslateService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param value The number to be formatted
|
||||||
|
* @param formatOptions Formatting options to include.
|
||||||
|
* As specified by Intl.NumberFormatOptions as comma seperated key:value pairs.
|
||||||
|
*/
|
||||||
|
transform(value: string | number | unknown, formatOptions?: string): string {
|
||||||
|
if (typeof value !== 'string' && typeof value !== 'number') {
|
||||||
|
logger.warn(`numberLocalized pipe unable to parse input: ${value}`);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const options = formatOptions
|
||||||
|
?.split(',')
|
||||||
|
.map(element => element.split(':'))
|
||||||
|
.reduce(
|
||||||
|
(accumulator, [key, value_]) => ({
|
||||||
|
...accumulator,
|
||||||
|
[key.trim()]: value_.trim(),
|
||||||
|
}),
|
||||||
|
{},
|
||||||
|
) as Intl.NumberFormatOptions;
|
||||||
|
const float = typeof value === 'string' ? Number.parseFloat(value) : (value as number);
|
||||||
|
return new Intl.NumberFormat(this.translate.currentLang, options).format(float);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,40 +12,23 @@
|
|||||||
* 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 {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core';
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
import {Subscription} from 'rxjs';
|
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
|
||||||
import {ThingTranslateService} from './thing-translate.service';
|
import {ThingTranslateService} from './thing-translate.service';
|
||||||
import {isThing, SCThings, SCThingType} from '@openstapps/core';
|
import {isThing, SCThings, SCThingType} from '@openstapps/core';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'propertyNameTranslate',
|
name: 'propertyNameTranslate',
|
||||||
pure: false, // required to update the value when the promise is resolved
|
pure: true,
|
||||||
})
|
})
|
||||||
export class PropertyNameTranslatePipe implements PipeTransform, OnDestroy {
|
export class PropertyNameTranslatePipe implements PipeTransform {
|
||||||
value: unknown;
|
constructor(private readonly thingTranslate: ThingTranslateService) {}
|
||||||
|
|
||||||
lastKey?: string;
|
|
||||||
|
|
||||||
lastType: string;
|
|
||||||
|
|
||||||
onLangChange: Subscription;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly translate: TranslateService,
|
|
||||||
private readonly thingTranslate: ThingTranslateService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
updateValue(key: string, type: string): void {
|
|
||||||
this.value = this.thingTranslate.getPropertyName(type as SCThingType, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
transform<K extends string, Q extends keyof Extract<SCThings, {type: K}>>(query: Q, type: K): string;
|
transform<K extends string, Q extends keyof Extract<SCThings, {type: K}>>(query: Q, type: K): string;
|
||||||
transform<T extends SCThings, K extends keyof T>(query: K, thing: T): string;
|
transform<T extends SCThings, K extends keyof T>(query: K, thing: T): string;
|
||||||
transform(query: unknown, thingOrType: SCThings | string | unknown): unknown {
|
transform(query: unknown, thingOrType: SCThings | string | unknown): unknown {
|
||||||
if (typeof query !== 'string' || query.length <= 0) {
|
if (typeof query !== 'string' || query.length <= 0) {
|
||||||
return query;
|
return query as never;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isThing(thingOrType) && typeof thingOrType !== 'string') {
|
if (!isThing(thingOrType) && typeof thingOrType !== 'string') {
|
||||||
@@ -54,37 +37,9 @@ export class PropertyNameTranslatePipe implements PipeTransform, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// store the params, in case they change
|
return this.thingTranslate.getPropertyName(
|
||||||
this.lastKey = query;
|
isThing(thingOrType) ? thingOrType.type : (thingOrType as SCThingType),
|
||||||
this.lastType = typeof thingOrType === 'string' ? thingOrType : thingOrType.type;
|
query,
|
||||||
|
);
|
||||||
this.updateValue(query, this.lastType);
|
|
||||||
|
|
||||||
// if there is a subscription to onLangChange, clean it
|
|
||||||
this._dispose();
|
|
||||||
|
|
||||||
if (this.onLangChange?.closed ?? true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe(() => {
|
|
||||||
if (typeof this.lastKey === 'string') {
|
|
||||||
this.lastKey = undefined; // we want to make sure it doesn't return the same value until it's been updated
|
|
||||||
this.updateValue(query, this.lastType);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean any existing subscription to change events
|
|
||||||
*/
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@Pipe({
|
||||||
|
name: 'sentencecase',
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class SentenceCasePipe implements PipeTransform {
|
||||||
|
transform(aString: string | unknown): string {
|
||||||
|
if (typeof aString !== 'string') {
|
||||||
|
throw new SyntaxError(
|
||||||
|
`Wrong parameter in StringSplitPipe. Expected a valid String, received: ${aString}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return aString.slice(0, 1).toUpperCase() + aString.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
frontend/app/src/app/translation/strings/split.pipe.ts
Normal file
22
frontend/app/src/app/translation/strings/split.pipe.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@Pipe({
|
||||||
|
name: 'split',
|
||||||
|
pure: true,
|
||||||
|
})
|
||||||
|
export class StringSplitPipe implements PipeTransform {
|
||||||
|
transform(aString: string | unknown, splitter: string | unknown): unknown[] {
|
||||||
|
if (typeof splitter !== 'string' || splitter.length <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof aString !== 'string') {
|
||||||
|
throw new SyntaxError(
|
||||||
|
`Wrong parameter in StringSplitPipe. Expected a valid String, received: ${aString}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return aString.split(splitter);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,25 +13,23 @@
|
|||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {ModuleWithProviders, NgModule, Provider} from '@angular/core';
|
import {ModuleWithProviders, NgModule, Provider} from '@angular/core';
|
||||||
import {
|
|
||||||
ArrayJoinPipe,
|
|
||||||
DateLocalizedFormatPipe,
|
|
||||||
DurationLocalizedPipe,
|
|
||||||
EntriesPipe,
|
|
||||||
IsNaNPipe,
|
|
||||||
IsNumericPipe,
|
|
||||||
MetersLocalizedPipe,
|
|
||||||
NumberLocalizedPipe,
|
|
||||||
SentenceCasePipe,
|
|
||||||
StringSplitPipe,
|
|
||||||
ToUnixPipe,
|
|
||||||
} from './common-string-pipes';
|
|
||||||
import {ThingTranslateDefaultParser, ThingTranslateParser} from './thing-translate.parser';
|
import {ThingTranslateDefaultParser, ThingTranslateParser} from './thing-translate.parser';
|
||||||
import {ThingTranslatePipe} from './thing-translate.pipe';
|
import {ThingTranslatePipe} from './thing-translate.pipe';
|
||||||
import {ThingTranslateService} from './thing-translate.service';
|
import {ThingTranslateService} from './thing-translate.service';
|
||||||
import {IonIconModule} from '../util/ion-icon/ion-icon.module';
|
import {IonIconModule} from '../util/ion-icon/ion-icon.module';
|
||||||
import {TranslateSimplePipe} from './translate-simple.pipe';
|
import {TranslateSimplePipe} from './translate-simple.pipe';
|
||||||
import {PropertyNameTranslatePipe} from './property-name-translate.pipe';
|
import {PropertyNameTranslatePipe} from './property-name-translate.pipe';
|
||||||
|
import {DateLocalizedFormatPipe} from './date-time/date-format.pipe';
|
||||||
|
import {NumberLocalizedPipe} from './numbers/number-localized.pipe';
|
||||||
|
import {IsNaNPipe} from './numbers/is-nan.pipe';
|
||||||
|
import {IsNumericPipe} from './numbers/is-numeric.pipe';
|
||||||
|
import {MetersLocalizedPipe} from './numbers/meters-localized.pipe';
|
||||||
|
import {DurationLocalizedPipe} from './date-time/duration-localized.pipe';
|
||||||
|
import {ToUnixPipe} from './date-time/to-unix.pipe';
|
||||||
|
import {ArrayJoinPipe} from './collections/join.pipe';
|
||||||
|
import {StringSplitPipe} from './strings/split.pipe';
|
||||||
|
import {SentenceCasePipe} from './strings/sentence-case.pipe';
|
||||||
|
import {EntriesPipe} from './collections/entries.pipe';
|
||||||
|
|
||||||
export interface ThingTranslateModuleConfig {
|
export interface ThingTranslateModuleConfig {
|
||||||
parser?: Provider;
|
parser?: Provider;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
* 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 {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/member-ordering, class-methods-use-this */
|
/* eslint-disable @typescript-eslint/member-ordering, class-methods-use-this */
|
||||||
@@ -51,10 +50,3 @@ export class ThingTranslateDefaultParser extends ThingTranslateParser {
|
|||||||
return property;
|
return property;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO
|
|
||||||
*/
|
|
||||||
export function isDefined<T>(value?: T | null): value is T {
|
|
||||||
return value !== undefined && value !== null;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,35 +12,17 @@
|
|||||||
* 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 {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core';
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
|
||||||
import {isThing, SCThings, SCThingWithoutReferences} from '@openstapps/core';
|
import {isThing, SCThings, SCThingWithoutReferences} from '@openstapps/core';
|
||||||
import {Subscription} from 'rxjs';
|
|
||||||
import {ThingTranslateService} from './thing-translate.service';
|
import {ThingTranslateService} from './thing-translate.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'thingTranslate',
|
name: 'thingTranslate',
|
||||||
pure: false, // required to update the value when the promise is resolved
|
pure: true,
|
||||||
})
|
})
|
||||||
export class ThingTranslatePipe implements PipeTransform, OnDestroy {
|
export class ThingTranslatePipe implements PipeTransform {
|
||||||
value: unknown;
|
constructor(private readonly thingTranslate: ThingTranslateService) {}
|
||||||
|
|
||||||
lastKey?: string;
|
|
||||||
|
|
||||||
lastThing: SCThingWithoutReferences;
|
|
||||||
|
|
||||||
onLangChange: Subscription;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly translate: TranslateService,
|
|
||||||
// private readonly _ref: ChangeDetectorRef,
|
|
||||||
private readonly thingTranslate: ThingTranslateService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
updateValue(key: string, thing: SCThingWithoutReferences): void {
|
|
||||||
this.value = this.thingTranslate.get(thing as SCThings, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
transform<T extends SCThingWithoutReferences, P extends string[] | string | keyof T>(
|
transform<T extends SCThingWithoutReferences, P extends string[] | string | keyof T>(
|
||||||
query: P,
|
query: P,
|
||||||
@@ -56,37 +38,6 @@ export class ThingTranslatePipe implements PipeTransform, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// store the params, in case they change
|
return this.thingTranslate.get(thing as SCThings, query) as never;
|
||||||
this.lastKey = query;
|
|
||||||
this.lastThing = thing;
|
|
||||||
|
|
||||||
this.updateValue(query, thing);
|
|
||||||
|
|
||||||
// if there is a subscription to onLangChange, clean it
|
|
||||||
this._dispose();
|
|
||||||
|
|
||||||
if (this.onLangChange?.closed ?? true) {
|
|
||||||
this.onLangChange = this.translate.onLangChange.subscribe(() => {
|
|
||||||
if (typeof this.lastKey === 'string') {
|
|
||||||
this.lastKey = undefined; // we want to make sure it doesn't return the same value until it's been updated
|
|
||||||
this.updateValue(query, thing);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.value as never;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean any existing subscription to change events
|
|
||||||
*/
|
|
||||||
private _dispose(): void {
|
|
||||||
if (this.onLangChange?.closed) {
|
|
||||||
this.onLangChange?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this._dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,21 +13,9 @@
|
|||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import {
|
import {SCLanguageCode, SCThings, SCThingTranslator, SCThingType} from '@openstapps/core';
|
||||||
SCLanguage,
|
import {ThingTranslateParser} from './thing-translate.parser';
|
||||||
SCLanguageCode,
|
|
||||||
SCThings,
|
|
||||||
SCThingTranslator,
|
|
||||||
SCThingType,
|
|
||||||
SCTranslations,
|
|
||||||
} from '@openstapps/core';
|
|
||||||
import moment from 'moment';
|
|
||||||
import {isDefined, ThingTranslateParser} from './thing-translate.parser';
|
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
|
||||||
import {setDefaultOptions} from 'date-fns';
|
|
||||||
import {DateFnsConfigurationService} from 'ngx-date-fns';
|
|
||||||
import {getDateFnsLocale} from './dfns-locale';
|
|
||||||
|
|
||||||
// export const DEFAULT_LANGUAGE = new InjectionToken<string>('DEFAULT_LANGUAGE');
|
// export const DEFAULT_LANGUAGE = new InjectionToken<string>('DEFAULT_LANGUAGE');
|
||||||
|
|
||||||
@@ -40,28 +28,16 @@ export class ThingTranslateService {
|
|||||||
translator: SCThingTranslator;
|
translator: SCThingTranslator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param translateService Instance of Angular TranslateService
|
* @param translateService Instance of Angular TranslateService
|
||||||
* @param parser An instance of the parser currently used
|
* @param parser An instance of the parser currently used
|
||||||
* @param dfnsConfiguration the date fns configuration
|
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private readonly translateService: TranslateService,
|
translateService: TranslateService,
|
||||||
public parser: ThingTranslateParser,
|
public parser: ThingTranslateParser,
|
||||||
private dfnsConfiguration: DateFnsConfigurationService,
|
|
||||||
) {
|
) {
|
||||||
this.translator = new SCThingTranslator(
|
this.translator = new SCThingTranslator(
|
||||||
(translateService.currentLang ?? translateService.defaultLang) as SCLanguageCode,
|
(translateService.currentLang ?? translateService.defaultLang) as SCLanguageCode,
|
||||||
);
|
);
|
||||||
/** set the default language from configuration */
|
|
||||||
this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => {
|
|
||||||
this.translator.language = event.lang as keyof SCTranslations<SCLanguage>;
|
|
||||||
moment.locale(event.lang);
|
|
||||||
getDateFnsLocale(event.lang as SCLanguageCode).then(locale => {
|
|
||||||
setDefaultOptions({locale});
|
|
||||||
this.dfnsConfiguration.setLocale(locale);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,7 +59,7 @@ export class ThingTranslateService {
|
|||||||
keyPath: string | string[],
|
keyPath: string | string[],
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
): string | number | boolean | object {
|
): string | number | boolean | object {
|
||||||
if (!isDefined(keyPath) || keyPath.length === 0) {
|
if (!keyPath || keyPath.length === 0) {
|
||||||
throw new Error(`Parameter "keyPath" required`);
|
throw new Error(`Parameter "keyPath" required`);
|
||||||
}
|
}
|
||||||
if (Array.isArray(keyPath)) {
|
if (Array.isArray(keyPath)) {
|
||||||
@@ -101,10 +77,10 @@ export class ThingTranslateService {
|
|||||||
*/
|
*/
|
||||||
public getPropertyName(type: SCThingType, keyPath: string | string[]): string {
|
public getPropertyName(type: SCThingType, keyPath: string | string[]): string {
|
||||||
const translatedPropertyNames = this.translator.translatedPropertyNames(type);
|
const translatedPropertyNames = this.translator.translatedPropertyNames(type);
|
||||||
if (!isDefined(translatedPropertyNames)) {
|
if (!translatedPropertyNames) {
|
||||||
throw new Error(`Parameter "type" is an invalid SCThingType`);
|
throw new Error(`Parameter "type" is an invalid SCThingType`);
|
||||||
}
|
}
|
||||||
if (!isDefined(keyPath) || keyPath.length === 0) {
|
if (!keyPath || keyPath.length === 0) {
|
||||||
throw new Error(`Parameter "keyPath" required`);
|
throw new Error(`Parameter "keyPath" required`);
|
||||||
}
|
}
|
||||||
if (Array.isArray(keyPath)) {
|
if (Array.isArray(keyPath)) {
|
||||||
|
|||||||
@@ -12,59 +12,29 @@
|
|||||||
* 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 {DestroyRef, inject, Injectable, Pipe, PipeTransform} from '@angular/core';
|
import {Injectable, Pipe, PipeTransform} from '@angular/core';
|
||||||
import {TranslateService} from '@ngx-translate/core';
|
import {TranslateService} from '@ngx-translate/core';
|
||||||
import {get} from '@openstapps/collection-utils';
|
import {get} from '@openstapps/collection-utils';
|
||||||
import {Subscription} from 'rxjs';
|
|
||||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@Pipe({
|
@Pipe({
|
||||||
name: 'translateSimple',
|
name: 'translateSimple',
|
||||||
pure: false,
|
pure: true,
|
||||||
})
|
})
|
||||||
export class TranslateSimplePipe implements PipeTransform {
|
export class TranslateSimplePipe implements PipeTransform {
|
||||||
value: unknown;
|
|
||||||
|
|
||||||
query: unknown;
|
|
||||||
|
|
||||||
thing: unknown;
|
|
||||||
|
|
||||||
onLangChange: Subscription;
|
|
||||||
|
|
||||||
destroy$ = inject(DestroyRef);
|
|
||||||
|
|
||||||
constructor(private readonly translate: TranslateService) {}
|
constructor(private readonly translate: TranslateService) {}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
private updateValue() {
|
|
||||||
try {
|
|
||||||
this.value =
|
|
||||||
get(
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(this.thing as any).translations[this.translate.currentLang] ?? this.thing,
|
|
||||||
this.query as string,
|
|
||||||
) ?? this.thing;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`${this.query}: ${error}`);
|
|
||||||
this.value = this.thing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
transform<T extends object, P extends keyof T>(query: P, thing: T): T[P];
|
transform<T extends object, P extends keyof T>(query: P, thing: T): T[P];
|
||||||
transform<T extends object, P extends string | string[]>(query: P, thing: T): P | unknown {
|
transform<T extends object, P extends string | string[]>(query: P, thing: T): P | unknown {
|
||||||
// store the params, in case they change
|
try {
|
||||||
this.query = query;
|
return (get(
|
||||||
this.thing = thing;
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(thing as any).translations[this.translate.currentLang] ?? thing,
|
||||||
this.updateValue();
|
query as string,
|
||||||
|
) ?? thing) as never;
|
||||||
this.onLangChange ??= this.translate.onLangChange
|
} catch (error) {
|
||||||
.pipe(takeUntilDestroyed(this.destroy$))
|
console.warn(`${query.toString()}: ${error}`);
|
||||||
.subscribe(() => {
|
return thing as never;
|
||||||
this.updateValue();
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return this.value as never;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -550,6 +550,7 @@
|
|||||||
"resetToast.message": "Einstellungen wurden zurückgesetzt",
|
"resetToast.message": "Einstellungen wurden zurückgesetzt",
|
||||||
"title": "Einstellungen",
|
"title": "Einstellungen",
|
||||||
"resetSettings": "Einstellungen zurücksetzen",
|
"resetSettings": "Einstellungen zurücksetzen",
|
||||||
|
"reloadPage": "Aktualisierung erforderlich",
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Kalender",
|
"title": "Kalender",
|
||||||
"sync": {
|
"sync": {
|
||||||
|
|||||||
@@ -550,6 +550,7 @@
|
|||||||
"resetToast.message": "Settings reset",
|
"resetToast.message": "Settings reset",
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"resetSettings": "Reset Settings",
|
"resetSettings": "Reset Settings",
|
||||||
|
"reloadPage": "Reload required",
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"title": "Calendar",
|
"title": "Calendar",
|
||||||
"sync": {
|
"sync": {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="color-scheme" content="light dark" />
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
<meta name="theme-color" content="#3880ff" />
|
||||||
<title>StApps</title>
|
<title>StApps</title>
|
||||||
|
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||||
</head>
|
</head>
|
||||||
<body style="background-color: var(--ion-color-primary)">
|
<body style="background: var(--ion-color-primary)">
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,9 +5,11 @@
|
|||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"outDir": "./dist/out-tsc",
|
"outDir": "./dist/out-tsc",
|
||||||
"declaration": false,
|
"declaration": false,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
"skipLibCheck": false,
|
"skipLibCheck": false,
|
||||||
"isolatedModules": false,
|
"isolatedModules": false,
|
||||||
"strictPropertyInitialization": false,
|
"strictPropertyInitialization": false,
|
||||||
|
"resolveJsonModule": true,
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"useDefineForClassFields": false,
|
"useDefineForClassFields": false,
|
||||||
|
|||||||
@@ -30,13 +30,13 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/changelog-git": "0.1.14",
|
"@changesets/changelog-git": "0.1.14",
|
||||||
"@changesets/cli": "2.26.1",
|
"@changesets/cli": "2.26.1",
|
||||||
|
"prettier": "3.1.1",
|
||||||
"cobertura-merge": "1.0.4",
|
"cobertura-merge": "1.0.4",
|
||||||
"deepmerge": "4.3.1",
|
"deepmerge": "4.3.1",
|
||||||
"dotenv-cli": "7.2.1",
|
"dotenv-cli": "7.2.1",
|
||||||
"glob": "10.2.7",
|
"glob": "10.2.7",
|
||||||
"junit-report-merger": "6.0.2",
|
"junit-report-merger": "6.0.2",
|
||||||
"prettier": "3.1.1",
|
"syncpack": "10.5.1",
|
||||||
"syncpack": "12.3.0",
|
|
||||||
"turbo": "1.10.16",
|
"turbo": "1.10.16",
|
||||||
"turbo-ignore": "1.10.16",
|
"turbo-ignore": "1.10.16",
|
||||||
"typedoc": "0.24.8",
|
"typedoc": "0.24.8",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": "@openstapps/tsconfig",
|
"extends": "@openstapps/tsconfig"
|
||||||
"exclude": ["lib", "app.js"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": "@openstapps/tsconfig",
|
"extends": "@openstapps/tsconfig"
|
||||||
"exclude": ["lib", "app.js"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": "@openstapps/tsconfig",
|
"extends": "@openstapps/tsconfig"
|
||||||
"exclude": ["lib", "app.js"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": "@openstapps/tsconfig",
|
"extends": "@openstapps/tsconfig"
|
||||||
"exclude": ["lib", "app.js"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,5 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"stripInternal": true
|
"stripInternal": true
|
||||||
},
|
}
|
||||||
"exclude": ["lib", "app.js"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,15 @@
|
|||||||
"typedoc": "0.24.8",
|
"typedoc": "0.24.8",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.1.6"
|
||||||
},
|
},
|
||||||
|
"tsup": {
|
||||||
|
"entry": [
|
||||||
|
"src/index.ts"
|
||||||
|
],
|
||||||
|
"sourcemap": true,
|
||||||
|
"clean": true,
|
||||||
|
"format": "esm",
|
||||||
|
"outDir": "lib"
|
||||||
|
},
|
||||||
"prettier": "@openstapps/prettier-config",
|
"prettier": "@openstapps/prettier-config",
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
export const CORE_VERSION = process.env.CORE_VERSION!;
|
|
||||||
|
|
||||||
export * from './guards.js';
|
export * from './guards.js';
|
||||||
export * from './meta.js';
|
export * from './meta.js';
|
||||||
export * from './translator.js';
|
export * from './translator.js';
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ export interface SCValueFilterArguments extends SCSearchAbstractFilterArguments
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Value to filter. One or more values has to match the field exactly.
|
* Value to filter. One or more values has to match the field exactly.
|
||||||
*
|
|
||||||
* Leaving the value out will check if the field exists.
|
|
||||||
*/
|
*/
|
||||||
value?: string | string[];
|
value: string | string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": "@openstapps/tsconfig",
|
"extends": "@openstapps/tsconfig"
|
||||||
"exclude": ["lib", "app.js"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
import {defineConfig} from 'tsup';
|
|
||||||
import packageJson from './package.json' assert {type: 'json'};
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
entry: ['src/index.ts'],
|
|
||||||
sourcemap: true,
|
|
||||||
clean: true,
|
|
||||||
format: 'esm',
|
|
||||||
outDir: 'lib',
|
|
||||||
env: {
|
|
||||||
CORE_VERSION: packageJson.version,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user