diff --git a/.eslintrc.json b/.eslintrc.json index 491f2b5..367b0d9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -39,7 +39,7 @@ "error", { "type": "attribute", - "prefix": "tm", + "prefix": "app", "style": "camelCase" } ], @@ -47,7 +47,7 @@ "warn", { "type": "element", - "prefix": "tm", + "prefix": "app", "style": "kebab-case" } ], diff --git a/angular.json b/angular.json index 8737f1e..a4fea92 100644 --- a/angular.json +++ b/angular.json @@ -8,7 +8,7 @@ "schematics": {}, "root": "", "sourceRoot": "src", - "prefix": "tm", + "prefix": "app", "architect": { "build": { "builder": "ngx-build-plus:browser", diff --git a/nginx/locations.conf b/nginx/locations.conf index 3d30da3..2ce449a 100644 --- a/nginx/locations.conf +++ b/nginx/locations.conf @@ -1,4 +1,4 @@ -location @@APP_BASE_HREF { +location @@APP_BASE_HREFbff { proxy_pass @@BFF_URL; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; diff --git a/package-lock.json b/package-lock.json index b3c09f6..a841db1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,11 +23,11 @@ "@ngneat/falso": "^6.4.0", "@ngx-translate/core": "^14.0.0", "@ngx-translate/http-loader": "^7.0.0", - "@onecx/accelerator": "^3.7.1", - "@onecx/integration-interface": "^3.7.1", - "@onecx/keycloak-auth": "^3.7.1", - "@onecx/portal-integration-angular": "^3.7.1", - "@onecx/portal-layout-styles": "^3.7.1", + "@onecx/accelerator": "^4.1.2", + "@onecx/integration-interface": "^4.1.2", + "@onecx/keycloak-auth": "^4.1.2", + "@onecx/portal-integration-angular": "^4.1.2", + "@onecx/portal-layout-styles": "^4.1.2", "file-saver": "^2.0.5", "i18n-iso-countries": "^7.6.0", "ngx-color": "^8.0.3", @@ -64,7 +64,7 @@ "@swc-node/register": "^1.6.5", "@swc/cli": "~0.1.63", "@swc/core": "^1.3.56", - "@swc/helpers": "0.5.1", + "@swc/helpers": "0.5.3", "@types/jasmine": "~3.10.0", "@types/node": "18.16.3", "@typescript-eslint/eslint-plugin": "5.48.2", @@ -96,12 +96,12 @@ "ngx-build-plus": "^14.0.0", "ngx-translate-testing": "^6.1.0", "postcss": "8.4.23", - "postcss-import": "~15.1.0", + "postcss-import": "~16.0.0", "postcss-preset-env": "~9.3.0", "postcss-url": "~10.1.3", "prettier": "^2.8.8", "sonarqube-scanner": "^3.3.0", - "style-loader": "^3.3.2", + "style-loader": "^3.3.4", "stylus": "^0.59.0", "stylus-loader": "^7.1.0", "tailwindcss": "3.3.2", @@ -6613,44 +6613,44 @@ } }, "node_modules/@onecx/accelerator": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@onecx/accelerator/-/accelerator-3.7.1.tgz", - "integrity": "sha512-s/+4oNgHOWN9NabbF+BeHDv091kxv9o/XiUrBP9ssnfK4sDaS0/7g16Wprmt2SMJMQxm6nLczBgJ+nkJHSrITQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@onecx/accelerator/-/accelerator-4.1.2.tgz", + "integrity": "sha512-PujQII1motqpXwPUN0H+x6aN3BkHkmQBbQTkv+be+xlAnZD2qu7Y1gtDTfrESyj5FXBLvGw7+vIM7Bmf0NpPtA==", "peerDependencies": { "rxjs": "7.8.1", "tslib": "^2.3.0" } }, "node_modules/@onecx/integration-interface": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@onecx/integration-interface/-/integration-interface-3.7.1.tgz", - "integrity": "sha512-fsqKJGlXkM05uvVuodVDE3q37Ykz1/LtypAXWjsmK2mc+EGOfjBtEJi7UGmnWVup7U0lFZR+eTTAzYbc6LIXzQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@onecx/integration-interface/-/integration-interface-4.1.2.tgz", + "integrity": "sha512-XED3fNOiD+dnVEDSz8haGvfgaNVCeQjn9ECAftTJ4SZ+laD5dkdv0uI21UH9ZpTTt6jUUtfNhHZZ8wChNcYdRg==", "peerDependencies": { - "@onecx/accelerator": "^3", + "@onecx/accelerator": "~4", "rxjs": "7.8.1", "tslib": "^2.3.0" } }, "node_modules/@onecx/keycloak-auth": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@onecx/keycloak-auth/-/keycloak-auth-3.7.1.tgz", - "integrity": "sha512-QqkhJdAvn0y/9X/QTSHOn1B7RDZ6qVp/JM7pjfwwCZZcQQT2KW+wDlQWMiNBPhnsSQua1RlsaI5dqeQyipgbmA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@onecx/keycloak-auth/-/keycloak-auth-4.1.2.tgz", + "integrity": "sha512-pXrz/KHudCHVhIKqX31D3qeu513jErsqEAMmCix48MpcG/ZfFIibbLU0SAZtzp7uCIa+KcQL5MJhbsWMHb1vZA==", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": ">=15.2.7", "@angular/core": ">=15.2.7", - "@onecx/portal-integration-angular": "~3", + "@onecx/portal-integration-angular": "~4", "keycloak-angular": "^13.0.0", "keycloak-js": "^18.0.0", "rxjs": "~7.8.0" } }, "node_modules/@onecx/portal-integration-angular": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@onecx/portal-integration-angular/-/portal-integration-angular-3.7.1.tgz", - "integrity": "sha512-JFzo03qdRlU3Y25VduTVzENIhlL4Y4XmanUSfDdxoPU3sCD3PjpYI30y95WzA/1vDZFaerjs+9wYXHmfMx1k5A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@onecx/portal-integration-angular/-/portal-integration-angular-4.1.2.tgz", + "integrity": "sha512-dtGK9bqeDIf9RTqevQfr05Fxzfutr8xtP2nbYblAuKW4/D6xonUl6f0DS4ij40fW9745k+3MFg99FXA635fFqw==", "dependencies": { "tslib": "^2.3.0" }, @@ -6669,7 +6669,7 @@ "@ngrx/store": "^15.4.0", "@ngx-translate/core": "^14.0.0", "@ngx-translate/http-loader": "^7.0.0", - "@onecx/integration-interface": "^3", + "@onecx/integration-interface": "~4", "chart.js": "^4.4.0", "d3-scale-chromatic": "^3.0.0", "fast-deep-equal": "^3.1.3", @@ -6680,9 +6680,9 @@ } }, "node_modules/@onecx/portal-layout-styles": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/@onecx/portal-layout-styles/-/portal-layout-styles-3.7.1.tgz", - "integrity": "sha512-RBFWbP22bh5sotUXcoo4Ur3/LZ481M8mR6Gb80KephFS8zppTyKMcCI4sDy4mEbtrsvKFOxyIVLw/ZBJX9LdgQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@onecx/portal-layout-styles/-/portal-layout-styles-4.1.2.tgz", + "integrity": "sha512-9rDqAkbJyN3OmBEuSXZuHL6VmVuZJkTyNOW3VGHuIsy0dBrbPiqToQ6E8aNdz78FztG3RXSceyhq/Ot05IXaOQ==", "peerDependencies": { "tslib": "^2.5.0" } @@ -9576,9 +9576,9 @@ "dev": true }, "node_modules/@swc/helpers": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz", - "integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", "dev": true, "dependencies": { "tslib": "^2.4.0" @@ -12357,9 +12357,9 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "node_modules/chart.js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.0.tgz", - "integrity": "sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", + "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" @@ -24344,9 +24344,9 @@ } }, "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.0.0.tgz", + "integrity": "sha512-e77lhVvrD1I2y7dYmBv0k9ULTdArgEYZt97T4w6sFIU5uxIHvDFQlKgUUyY7v7Barj0Yf/zm5A4OquZN7jKm5Q==", "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", @@ -24354,7 +24354,7 @@ "resolve": "^1.1.7" }, "engines": { - "node": ">=14.0.0" + "node": ">=18.0.0" }, "peerDependencies": { "postcss": "^8.0.0" @@ -28086,9 +28086,9 @@ } }, "node_modules/style-loader": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", - "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", "dev": true, "engines": { "node": ">= 12.13.0" @@ -28369,6 +28369,23 @@ "node": ">=10.13.0" } }, + "node_modules/tailwindcss/node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, "node_modules/tailwindcss/node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", diff --git a/package.json b/package.json index 526af9c..486af2b 100644 --- a/package.json +++ b/package.json @@ -49,11 +49,11 @@ "@ngneat/falso": "^6.4.0", "@ngx-translate/core": "^14.0.0", "@ngx-translate/http-loader": "^7.0.0", - "@onecx/accelerator": "^3.7.1", - "@onecx/integration-interface": "^3.7.1", - "@onecx/keycloak-auth": "^3.7.1", - "@onecx/portal-integration-angular": "^3.7.1", - "@onecx/portal-layout-styles": "^3.7.1", + "@onecx/accelerator": "^4.1.2", + "@onecx/integration-interface": "^4.1.2", + "@onecx/keycloak-auth": "^4.1.2", + "@onecx/portal-integration-angular": "^4.1.2", + "@onecx/portal-layout-styles": "^4.1.2", "file-saver": "^2.0.5", "i18n-iso-countries": "^7.6.0", "ngx-color": "^8.0.3", @@ -90,7 +90,7 @@ "@swc-node/register": "^1.6.5", "@swc/cli": "~0.1.63", "@swc/core": "^1.3.56", - "@swc/helpers": "0.5.1", + "@swc/helpers": "0.5.3", "@types/jasmine": "~3.10.0", "@types/node": "18.16.3", "@typescript-eslint/eslint-plugin": "5.48.2", @@ -122,12 +122,12 @@ "ngx-build-plus": "^14.0.0", "ngx-translate-testing": "^6.1.0", "postcss": "8.4.23", - "postcss-import": "~15.1.0", + "postcss-import": "~16.0.0", "postcss-preset-env": "~9.3.0", "postcss-url": "~10.1.3", "prettier": "^2.8.8", "sonarqube-scanner": "^3.3.0", - "style-loader": "^3.3.2", + "style-loader": "^3.3.4", "stylus": "^0.59.0", "stylus-loader": "^7.1.0", "tailwindcss": "3.3.2", diff --git a/proxy.conf.js b/proxy.conf.js index 4456204..cb58a57 100644 --- a/proxy.conf.js +++ b/proxy.conf.js @@ -17,11 +17,11 @@ const bypassFn = function (req, res) { } const PROXY_CONFIG = { - '/theme-bff': { + '/bff': { target: 'http://onecx-theme-bff', secure: false, pathRewrite: { - '^.*/theme-bff': '' + '^.*/bff': '' }, changeOrigin: true, logLevel: 'debug', diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 406a5e2..f9c1a63 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,7 +1,8 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core' import { TestBed } from '@angular/core/testing' -import { AppComponent } from './app.component' import { RouterTestingModule } from '@angular/router/testing' -import { NO_ERRORS_SCHEMA } from '@angular/core' + +import { AppComponent } from './app.component' describe('AppComponent', () => { beforeEach(async () => { diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 34caa4c..349ab3a 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core' @Component({ - selector: 'tm-root', + selector: 'app-root', templateUrl: './app.component.html' }) export class AppComponent { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 214fb59..de6a3a2 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,35 +1,61 @@ import { APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { HttpClient, HttpClientModule } from '@angular/common/http' +import { RouterModule, Routes } from '@angular/router' import { BrowserModule } from '@angular/platform-browser' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' -import { TranslateService } from '@ngx-translate/core' -import { DialogService } from 'primeng/dynamicdialog' -import { Observable } from 'rxjs' +import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core' -import { APP_CONFIG, PortalCoreModule } from '@onecx/portal-integration-angular' +import { + APP_CONFIG, + AppStateService, + createTranslateLoader, + translateServiceInitializer, + PortalCoreModule, + UserService +} from '@onecx/portal-integration-angular' import { KeycloakAuthModule } from '@onecx/keycloak-auth' import { AppComponent } from './app.component' import { environment } from '../environments/environment' -// standalone app: ensure translations are loaded during app init -function initializer(translate: TranslateService): () => Observable { - console.log('App module initializer') - return () => { - translate.addLangs(['en', 'de']) - const browserLang = translate.getBrowserLang() - return translate.use(browserLang?.match(/en|de/) ? browserLang : 'en') - } -} - +const routes: Routes = [{ path: '', pathMatch: 'full' }] @NgModule({ bootstrap: [AppComponent], declarations: [AppComponent], - imports: [BrowserModule, KeycloakAuthModule, BrowserAnimationsModule, PortalCoreModule.forRoot('onecx-theme-ui')], + imports: [ + CommonModule, + BrowserModule, + HttpClientModule, + KeycloakAuthModule, + BrowserAnimationsModule, + RouterModule.forRoot(routes, { + initialNavigation: 'enabledBlocking', + enableTracing: true + }), + PortalCoreModule.forRoot('onecx-theme-ui'), + TranslateModule.forRoot({ + isolate: true, + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient, AppStateService] + } + }) + ], providers: [ - DialogService, { provide: APP_CONFIG, useValue: environment }, - { provide: APP_INITIALIZER, useFactory: initializer, multi: true, deps: [TranslateService] } + { + provide: APP_INITIALIZER, + useFactory: translateServiceInitializer, + multi: true, + deps: [UserService, TranslateService] + } ], schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA] }) -export class AppModule {} +export class AppModule { + constructor() { + console.info('App Module constructor') + } +} diff --git a/src/app/onecx-theme-remote.module.ts b/src/app/onecx-theme-remote.module.ts index 67a75b9..fb0d00a 100644 --- a/src/app/onecx-theme-remote.module.ts +++ b/src/app/onecx-theme-remote.module.ts @@ -1,7 +1,16 @@ -import { Inject, NgModule } from '@angular/core' +import { HttpClient } from '@angular/common/http' +import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' +import { MissingTranslationHandler, TranslateLoader, TranslateModule } from '@ngx-translate/core' -import { MFE_INFO, MfeInfo, PortalCoreModule } from '@onecx/portal-integration-angular' +import { + addInitializeModuleGuard, + AppStateService, + ConfigurationService, + createTranslateLoader, + PortalCoreModule, + PortalMissingTranslationHandler +} from '@onecx/portal-integration-angular' const routes: Routes = [ { @@ -10,13 +19,25 @@ const routes: Routes = [ } ] @NgModule({ - imports: [PortalCoreModule.forMicroFrontend(), RouterModule.forChild(routes)], + imports: [ + PortalCoreModule.forMicroFrontend(), + RouterModule.forChild(addInitializeModuleGuard(routes)), + TranslateModule.forRoot({ + isolate: true, + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient, AppStateService] + }, + missingTranslationHandler: { provide: MissingTranslationHandler, useClass: PortalMissingTranslationHandler } + }) + ], exports: [], - providers: [], + providers: [ConfigurationService], schemas: [] }) export class OneCXThemeModule { - constructor(@Inject(MFE_INFO) mfeInfo?: MfeInfo) { - console.info('OneCX Theme Module constructor', mfeInfo) + constructor() { + console.info('OneCX Theme Module constructor') } } diff --git a/src/app/shared/can-active-guard.service.spec.ts b/src/app/shared/can-active-guard.service.spec.ts deleted file mode 100644 index 56705d4..0000000 --- a/src/app/shared/can-active-guard.service.spec.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { BehaviorSubject, Observable, of } from 'rxjs' -import { CanActivateGuard } from './can-active-guard.service' - -let canActivateGuard: CanActivateGuard - -describe('CanActivateGuard', () => { - const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['setDefaultLang', 'use']) - - const configSpy = jasmine.createSpyObj('ConfigurationService', [], { - lang$: new BehaviorSubject(undefined), - lang: 'en' - }) - - const activatedRouteSpy = jasmine.createSpyObj('ActivatedRouteSnapshot', [], { - routeConfig: { - path: 'path' - } - }) - - const routerStateSnapshotSpy = jasmine.createSpyObj('RouterStateSnapshot', ['']) - - beforeEach(async () => { - canActivateGuard = new CanActivateGuard(translateServiceSpy, configSpy) - translateServiceSpy.setDefaultLang.calls.reset() - translateServiceSpy.use.calls.reset() - }) - - it('should return default lang if provided is not supported', () => { - const result = canActivateGuard.getBestMatchLanguage('pl') - expect(result).toBe('en') - }) - - it('should use default language if current not supported and return true', (doneFn: DoneFn) => { - const langSpy = Object.getOwnPropertyDescriptor(configSpy, 'lang$')?.get as jasmine.Spy< - () => BehaviorSubject - > - langSpy.and.returnValue(new BehaviorSubject('pl')) - spyOn(console, 'log') - translateServiceSpy.use.and.returnValue(of({})) - - const resultObs = canActivateGuard.canActivate(activatedRouteSpy, routerStateSnapshotSpy) as Observable - resultObs.subscribe({ - next: (result) => { - expect(result).toBe(true) - doneFn() - }, - error: () => { - doneFn.fail - } - }) - - expect(translateServiceSpy.setDefaultLang).toHaveBeenCalledOnceWith('en') - expect(console.log).toHaveBeenCalledWith('Start Translation guard - default language en') - expect(console.log).toHaveBeenCalledWith(`Translations guard done en`) - expect(console.log).toHaveBeenCalledWith(`Configuration language: pl`) - expect(translateServiceSpy.use).toHaveBeenCalledTimes(2) - expect(translateServiceSpy.use).toHaveBeenCalledWith('en') - }) - - it('should use provided language if current supported and return true', (doneFn: DoneFn) => { - const langSpy = Object.getOwnPropertyDescriptor(configSpy, 'lang$')?.get as jasmine.Spy< - () => BehaviorSubject - > - langSpy.and.returnValue(new BehaviorSubject('de')) - spyOn(console, 'log') - translateServiceSpy.use.and.returnValue(of({})) - - const resultObs = canActivateGuard.canActivate(activatedRouteSpy, routerStateSnapshotSpy) as Observable - resultObs.subscribe({ - next: (result) => { - expect(result).toBe(true) - doneFn() - }, - error: () => { - doneFn.fail - } - }) - - expect(console.log).toHaveBeenCalledWith('Start Translation guard - default language en') - expect(console.log).toHaveBeenCalledWith(`Translations guard done en`) - expect(console.log).toHaveBeenCalledWith(`Configuration language: de`) - expect(translateServiceSpy.use).toHaveBeenCalledTimes(2) - expect(translateServiceSpy.use).toHaveBeenCalledWith('en') - expect(translateServiceSpy.use).toHaveBeenCalledWith('de') - }) -}) diff --git a/src/app/shared/can-active-guard.service.ts b/src/app/shared/can-active-guard.service.ts deleted file mode 100644 index 201a143..0000000 --- a/src/app/shared/can-active-guard.service.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Injectable } from '@angular/core' -import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router' -import { TranslateService } from '@ngx-translate/core' -import { filter, map, Observable, OperatorFunction, tap } from 'rxjs' - -import { ConfigurationService } from '@onecx/portal-integration-angular' - -const SUPPORTED_LANGUAGES = ['de', 'en'] -const DEFAULT_LANG = 'en' - -@Injectable() -export class CanActivateGuard implements CanActivate { - constructor(private txService: TranslateService, private config: ConfigurationService) {} - - /* eslint-disable @typescript-eslint/no-unused-vars */ - canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot - ): Observable | Promise | boolean | UrlTree { - return this.loadTranslations() - } - - private loadTranslations(): Observable | Promise | boolean | UrlTree { - console.log('Start Translation guard - default language ' + DEFAULT_LANG) - // this language will be used as a fallback when a translation isn't found in the current language - this.txService.setDefaultLang(DEFAULT_LANG) - - return this.txService.use(this.getBestMatchLanguage(this.config.lang)).pipe( - //optional, after we set the language, we can listen for eventual changes to the lang - tap(() => { - console.log(`Translations guard done ${this.config.lang}`) - this.config.lang$ - //the explict cast is to help linter understand that we will never get undefined - .pipe( - filter((x) => x !== undefined) as OperatorFunction, - map((lang) => lang.toLowerCase()) - ) - .subscribe((newLang) => { - console.log(`Configuration language: ${newLang}`) - this.txService.use(this.getBestMatchLanguage(newLang)) - }) - }), - map(() => true) - ) - } - - public getBestMatchLanguage(lang: string): string { - if (SUPPORTED_LANGUAGES.includes(lang)) { - return lang - } else { - console.warn( - `⚠ requested language: ${lang} is not among supported languages: ${SUPPORTED_LANGUAGES}, using ${DEFAULT_LANG} as fallback` - ) - return DEFAULT_LANG - } - } -} diff --git a/src/app/shared/image-container/image-container.component.spec.ts b/src/app/shared/image-container/image-container.component.spec.ts index 9df578c..768c02d 100644 --- a/src/app/shared/image-container/image-container.component.spec.ts +++ b/src/app/shared/image-container/image-container.component.spec.ts @@ -1,11 +1,10 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' import { NO_ERRORS_SCHEMA, SimpleChange } from '@angular/core' -import { ImageContainerComponent } from './image-container.component' -import { TranslateLoader, TranslateModule } from '@ngx-translate/core' -import { HttpLoaderFactory } from '../shared.module' -import { HttpClient } from '@angular/common/http' +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' import { HttpClientTestingModule } from '@angular/common/http/testing' -import { environment } from 'src/environments/environment' +import { TranslateTestingModule } from 'ngx-translate-testing' + +import { ImageContainerComponent } from './image-container.component' +import { prepareUrl } from 'src/app/shared/utils' describe('ThemeColorBoxComponent', () => { let component: ImageContainerComponent @@ -16,13 +15,10 @@ describe('ThemeColorBoxComponent', () => { declarations: [ImageContainerComponent], imports: [ HttpClientTestingModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [HttpClient] - } - }) + TranslateTestingModule.withTranslations({ + de: require('src/assets/i18n/de.json'), + en: require('src/assets/i18n/en.json') + }).withDefaultLanguage('en') ], providers: [], schemas: [NO_ERRORS_SCHEMA] @@ -53,8 +49,7 @@ describe('ThemeColorBoxComponent', () => { component.imageUrl = 'imageUrl' component.ngOnChanges(changes) - - expect(component.imageUrl).toBe(environment.apiPrefix + 'imageUrl') + expect(component.imageUrl).toBe(prepareUrl('imageUrl') ?? '') }) it('should use image from external resource after change', () => { diff --git a/src/app/shared/image-container/image-container.component.ts b/src/app/shared/image-container/image-container.component.ts index b189a7c..4d68a9f 100644 --- a/src/app/shared/image-container/image-container.component.ts +++ b/src/app/shared/image-container/image-container.component.ts @@ -1,8 +1,9 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core' +import { Location } from '@angular/common' import { environment } from '../../../environments/environment' @Component({ - selector: 'tm-image-container', + selector: 'app-image-container', styleUrls: ['./image-container.component.scss'], templateUrl: './image-container.component.html' }) @@ -21,11 +22,10 @@ export class ImageContainerComponent implements OnChanges { ngOnChanges(changes: SimpleChanges): void { if (changes['imageUrl']) { this.displayPlaceHolder = false - // if image Url does not start with a http the api-prefix ... // ...then it stored in the backend. So we need to put prefix in front if (this.imageUrl && !this.imageUrl.match(/^(http|https)/g) && !this.imageUrl.startsWith(this.apiPrefix)) { - this.imageUrl = this.apiPrefix + this.imageUrl + this.imageUrl = Location.joinWithSlash(this.apiPrefix, this.imageUrl) } } } diff --git a/src/app/shared/label.resolver.ts b/src/app/shared/label.resolver.ts index cc6206f..9d2f623 100644 --- a/src/app/shared/label.resolver.ts +++ b/src/app/shared/label.resolver.ts @@ -7,8 +7,13 @@ import { Observable } from 'rxjs' @Injectable() export class LabelResolver implements Resolve { constructor(private translate: TranslateService) {} - /* eslint-disable @typescript-eslint/no-unused-vars */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): string | Observable | Promise { + resolve(route: ActivatedRouteSnapshot, _state: RouterStateSnapshot): string | Observable | Promise { return route.data['breadcrumb'] ? this.translate.instant(route.data['breadcrumb']) : route.routeConfig?.path + /* TODO: use this if tests are running with it. + resolve(route: ActivatedRouteSnapshot, _state: RouterStateSnapshot): string | Observable | Promise { + return route.data['breadcrumb'] + ? this.translate.get(route.data['breadcrumb']).pipe(map((t) => t.toString())) + : route.routeConfig?.path ?? '' + */ } } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 42e965a..62b2bdd 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -1,9 +1,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, NgModule } from '@angular/core' import { CommonModule } from '@angular/common' -import { HttpClient } from '@angular/common/http' import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { TranslateModule, TranslateService } from '@ngx-translate/core' -import { TranslateHttpLoader } from '@ngx-translate/http-loader' import { ColorSketchModule } from 'ngx-color/sketch' import { ErrorTailorModule } from '@ngneat/error-tailor' @@ -11,7 +9,7 @@ import { AutoCompleteModule } from 'primeng/autocomplete' import { CheckboxModule } from 'primeng/checkbox' import { ConfirmDialogModule } from 'primeng/confirmdialog' import { ConfirmPopupModule } from 'primeng/confirmpopup' -import { ConfirmationService, MessageService } from 'primeng/api' +import { ConfirmationService } from 'primeng/api' import { DataViewModule } from 'primeng/dataview' import { DialogModule } from 'primeng/dialog' import { DialogService, DynamicDialogModule } from 'primeng/dynamicdialog' @@ -30,35 +28,20 @@ import { TableModule } from 'primeng/table' import { ToastModule } from 'primeng/toast' import { - MfeInfo, - MFE_INFO, + AppStateService, + ConfigurationService, PortalDialogService, - PortalMessageService, - TranslateCombinedLoader + PortalApiConfiguration } from '@onecx/portal-integration-angular' -import { BASE_PATH } from '../generated' +import { Configuration } from 'src/app/generated' import { LabelResolver } from './label.resolver' -import { environment } from '../../environments/environment' -import { CanActivateGuard } from './can-active-guard.service' +import { environment } from 'src/environments/environment' import { ImageContainerComponent } from './image-container/image-container.component' import { ThemeColorBoxComponent } from './theme-color-box/theme-color-box.component' -export const basePathProvider = (mfeInfo: MfeInfo) => { - console.log('Base path provider: ' + (mfeInfo ? mfeInfo?.remoteBaseUrl : '') + environment.apiPrefix) - return (mfeInfo ? mfeInfo?.remoteBaseUrl : '') + environment.apiPrefix -} - -export function HttpLoaderFactory(http: HttpClient, mfeInfo: MfeInfo) { - if (mfeInfo) { - console.log(`Configuring translation loader ${mfeInfo?.remoteBaseUrl}`) - } - // if running standalone then load the app assets directly from remote base URL - const appAssetPrefix = mfeInfo?.remoteBaseUrl ? mfeInfo.remoteBaseUrl : './' - return new TranslateCombinedLoader( - new TranslateHttpLoader(http, appAssetPrefix + 'assets/i18n/', '.json'), - new TranslateHttpLoader(http, appAssetPrefix + 'onecx-portal-lib/assets/i18n/', '.json') - ) +export function apiConfigProvider(configService: ConfigurationService, appStateService: AppStateService) { + return new PortalApiConfiguration(Configuration, environment.apiPrefix, configService, appStateService) } @NgModule({ @@ -88,7 +71,7 @@ export function HttpLoaderFactory(http: HttpClient, mfeInfo: MfeInfo) { TabViewModule, TableModule, ToastModule, - TranslateModule.forChild({ isolate: true }), + TranslateModule, ErrorTailorModule.forRoot({ controlErrorsOn: { async: true, blur: true, change: true }, errors: { @@ -116,6 +99,7 @@ export function HttpLoaderFactory(http: HttpClient, mfeInfo: MfeInfo) { AutoCompleteModule, CheckboxModule, CommonModule, + ConfirmDialogModule, ConfirmPopupModule, DataViewModule, DialogModule, @@ -142,12 +126,10 @@ export function HttpLoaderFactory(http: HttpClient, mfeInfo: MfeInfo) { ], //this is not elegant, for some reason the injection token from primeng does not work across federated module providers: [ - CanActivateGuard, ConfirmationService, LabelResolver, - { provide: MessageService, useExisting: PortalMessageService }, { provide: DialogService, useClass: PortalDialogService }, - { provide: BASE_PATH, useFactory: basePathProvider, deps: [MFE_INFO] } + { provide: Configuration, useFactory: apiConfigProvider, deps: [ConfigurationService, AppStateService] } ], schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/src/app/shared/theme-color-box/theme-color-box.component.spec.ts b/src/app/shared/theme-color-box/theme-color-box.component.spec.ts index 65bb7da..708bf53 100644 --- a/src/app/shared/theme-color-box/theme-color-box.component.spec.ts +++ b/src/app/shared/theme-color-box/theme-color-box.component.spec.ts @@ -1,10 +1,9 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' -import { ThemeColorBoxComponent } from './theme-color-box.component' import { NO_ERRORS_SCHEMA } from '@angular/core' +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' import { HttpClientTestingModule } from '@angular/common/http/testing' -import { TranslateLoader, TranslateModule } from '@ngx-translate/core' -import { HttpLoaderFactory } from '../shared.module' -import { HttpClient } from '@angular/common/http' +import { TranslateTestingModule } from 'ngx-translate-testing' + +import { ThemeColorBoxComponent } from './theme-color-box.component' describe('ThemeColorBoxComponent', () => { let component: ThemeColorBoxComponent @@ -15,13 +14,10 @@ describe('ThemeColorBoxComponent', () => { declarations: [ThemeColorBoxComponent], imports: [ HttpClientTestingModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [HttpClient] - } - }) + TranslateTestingModule.withTranslations({ + de: require('src/assets/i18n/de.json'), + en: require('src/assets/i18n/en.json') + }).withDefaultLanguage('en') ], providers: [], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/shared/theme-color-box/theme-color-box.component.ts b/src/app/shared/theme-color-box/theme-color-box.component.ts index fb3db85..1e8789a 100644 --- a/src/app/shared/theme-color-box/theme-color-box.component.ts +++ b/src/app/shared/theme-color-box/theme-color-box.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from '@angular/core' @Component({ - selector: 'tm-theme-color-box', + selector: 'app-theme-color-box', styleUrls: ['./theme-color-box.component.scss'], templateUrl: './theme-color-box.component.html' }) diff --git a/src/app/shared/utils.ts b/src/app/shared/utils.ts index a1f23f6..be7b35d 100644 --- a/src/app/shared/utils.ts +++ b/src/app/shared/utils.ts @@ -1,4 +1,6 @@ import { SelectItem } from 'primeng/api' +import { Location } from '@angular/common' +import { environment } from 'src/environments/environment' export function limitText(text: string, limit: number): string { if (text) { @@ -8,9 +10,9 @@ export function limitText(text: string, limit: number): string { } } -export function setFetchUrls(apiPrefix: string, url: string): string { +export function prepareUrl(url: string | undefined): string | undefined { if (url && !url.match(/^(http|https)/g)) { - return apiPrefix + url + return Location.joinWithSlash(environment.apiPrefix, url) } else { return url } diff --git a/src/app/theme/theme-designer/theme-designer.component.html b/src/app/theme/theme-designer/theme-designer.component.html index 55edeca..1cd0e82 100644 --- a/src/app/theme/theme-designer/theme-designer.component.html +++ b/src/app/theme/theme-designer/theme-designer.component.html @@ -1,8 +1,8 @@ - + @@ -82,11 +82,11 @@
- + >
- + >
{ let component: ThemeDesignerComponent @@ -49,23 +51,20 @@ describe('ThemeDesignerComponent', () => { TestBed.configureTestingModule({ declarations: [ThemeDesignerComponent], imports: [ - RouterTestingModule, - HttpClientTestingModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [HttpClient] - } - }), + BrowserAnimationsModule, ConfirmDialogModule, DialogModule, DropdownModule, + FormsModule, + InputSwitchModule, + HttpClientTestingModule, OverlayPanelModule, - BrowserAnimationsModule, ReactiveFormsModule, - FormsModule, - InputSwitchModule + RouterTestingModule, + TranslateTestingModule.withTranslations({ + de: require('src/assets/i18n/de.json'), + en: require('src/assets/i18n/en.json') + }).withDefaultLanguage('en') ], schemas: [NO_ERRORS_SCHEMA], providers: [ @@ -270,8 +269,8 @@ describe('ThemeDesignerComponent', () => { component.ngOnInit() - expect(component.fetchingLogoUrl).toBe(environment.apiPrefix + themeData.logoUrl) - expect(component.fetchingFaviconUrl).toBe(environment.apiPrefix + themeData.faviconUrl) + expect(component.fetchingLogoUrl).toBe(prepareUrl(themeData.logoUrl)) + expect(component.fetchingFaviconUrl).toBe(prepareUrl(themeData.faviconUrl)) }) it('should fetch logo and favicon from external source on edit mode when http[s] present', () => { @@ -941,8 +940,8 @@ describe('ThemeDesignerComponent', () => { general: jasmine.objectContaining({ 'primary-color': 'rgb(255,255,255)' }) }) ) - expect(component.fetchingFaviconUrl).toBe(environment.apiPrefix + 'fetchedFavUrl') - expect(component.fetchingLogoUrl).toBe(environment.apiPrefix + 'fetchedLogoUrl') + expect(component.fetchingFaviconUrl).toBe(prepareUrl('fetchedFavUrl')) + expect(component.fetchingLogoUrl).toBe(prepareUrl('fetchedLogoUrl')) }) }) }) diff --git a/src/app/theme/theme-designer/theme-designer.component.ts b/src/app/theme/theme-designer/theme-designer.component.ts index 8609cf0..b9dd0da 100644 --- a/src/app/theme/theme-designer/theme-designer.component.ts +++ b/src/app/theme/theme-designer/theme-designer.component.ts @@ -1,19 +1,17 @@ import { Component, OnInit, ViewChild, ElementRef } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms' -import { Observable, debounceTime, switchMap } from 'rxjs' +import { Observable, debounceTime, map, switchMap } from 'rxjs' import { TranslateService } from '@ngx-translate/core' import { ConfirmationService, SelectItem } from 'primeng/api' -import { Action, ConfigurationService, ThemeService, PortalMessageService } from '@onecx/portal-integration-angular' - -import { themeVariables } from '../theme-variables' -import { environment } from '../../../environments/environment' -import { GetThemeResponse, Theme, ThemesAPIService, ThemeUpdateCreate, UpdateThemeResponse } from '../../generated' -import { dropDownSortItemsByLabel, dropDownGetLabelByValue, setFetchUrls } from '../../shared/utils' +import { Action, AppStateService, PortalMessageService, ThemeService } from '@onecx/portal-integration-angular' +import { dropDownSortItemsByLabel, dropDownGetLabelByValue, prepareUrl } from 'src/app/shared/utils' +import { GetThemeResponse, Theme, ThemesAPIService, ThemeUpdateCreate, UpdateThemeResponse } from 'src/app/generated' +import { themeVariables } from './theme-variables' @Component({ - selector: 'tm-theme-designer', + selector: 'app-theme-designer', templateUrl: './theme-designer.component.html', styleUrls: ['./theme-designer.component.scss'], providers: [ConfirmationService] @@ -23,51 +21,53 @@ export class ThemeDesignerComponent implements OnInit { @ViewChild('selectedFileInputLogo') selectedFileInputLogo: ElementRef | undefined @ViewChild('selectedFileInputFavicon') selectedFileInputFavicon: ElementRef | undefined - public actions: Action[] = [] - themes: Theme[] = [] - theme: Theme | undefined - themeId: string | null - themeVars = themeVariables - themeTemplates!: SelectItem[] - themeTemplateSelectedId = '' - themeIsCurrentUsedTheme = false + public actions: Action[] = [] // TODO: remove if tests are using actions$ + public actions$: Observable | undefined + public themes: Theme[] = [] + public theme: Theme | undefined + public themeId: string | null + public themeVars = themeVariables + public themeTemplates!: SelectItem[] + public themeTemplateSelectedId = '' + public themeIsCurrentUsedTheme = false + public fetchingLogoUrl?: string + public fetchingFaviconUrl?: string - mode: 'EDIT' | 'NEW' = 'NEW' - autoApply = false - apiPrefix = environment.apiPrefix - saveAsNewPopupDisplay = false - fetchingLogoUrl?: string - fetchingFaviconUrl?: string + public mode: 'EDIT' | 'NEW' = 'NEW' + public autoApply = false + public saveAsNewPopupDisplay = false + public displayFileTypeErrorLogo = false + public displayFileTypeErrorFavicon = false - fontForm: FormGroup - basicForm: FormGroup - sidebarForm: FormGroup - topbarForm: FormGroup - generalForm: FormGroup - propertiesForm: FormGroup - groups: { + public fontForm: FormGroup + public basicForm: FormGroup + public sidebarForm: FormGroup + public topbarForm: FormGroup + public generalForm: FormGroup + public propertiesForm: FormGroup + public groups: { title: string formGroup: FormGroup key: keyof typeof themeVariables }[] - public displayFileTypeErrorLogo = false - public displayFileTypeErrorFavicon = false constructor( private fb: FormBuilder, private route: ActivatedRoute, private router: Router, + private appStateService: AppStateService, private themeApi: ThemesAPIService, private themeService: ThemeService, //private imageApi: ImageV1APIService, - private config: ConfigurationService, private translate: TranslateService, private confirmation: ConfirmationService, private msgService: PortalMessageService ) { this.mode = route.snapshot.paramMap.has('id') ? 'EDIT' : 'NEW' this.themeId = route.snapshot.paramMap.get('id') - this.themeIsCurrentUsedTheme = this.themeId === this.config.getPortal().themeId + this.themeIsCurrentUsedTheme = this.themeId === this.appStateService.currentPortal$.getValue()?.themeId + this.prepareActionButtons() + /* TODO: remove this */ this.translate .get([ 'ACTIONS.CANCEL', @@ -78,8 +78,9 @@ export class ThemeDesignerComponent implements OnInit { 'ACTIONS.TOOLTIPS.SAVE_AS' ]) .subscribe((data) => { - this.prepareActionButtons(data) + this.prepareActionButtons_old(data) }) + this.fontForm = new FormGroup({}) this.topbarForm = new FormGroup({}) this.generalForm = new FormGroup({}) @@ -92,7 +93,7 @@ export class ThemeDesignerComponent implements OnInit { }, { key: 'topbar', - title: 'Topbar - Portal Header', + title: 'Topbar - Workspace Header', formGroup: this.topbarForm }, { @@ -157,7 +158,8 @@ export class ThemeDesignerComponent implements OnInit { this.basicForm.patchValue(data.resource) this.propertiesForm.reset() this.propertiesForm.patchValue(data.resource.properties || {}) - this.setFetchUrls() + this.fetchingLogoUrl = prepareUrl(this.basicForm.value.logoUrl) + this.fetchingFaviconUrl = prepareUrl(this.basicForm.value.faviconUrl) }) } else { const currentVars: { [key: string]: { [key: string]: string } } = {} @@ -173,7 +175,8 @@ export class ThemeDesignerComponent implements OnInit { this.loadThemeTemplates() } - private prepareActionButtons(data: any): void { + /* TODO: remove this */ + private prepareActionButtons_old(data: any): void { this.actions = [] // provoke change event this.actions.push( { @@ -204,6 +207,49 @@ export class ThemeDesignerComponent implements OnInit { } ) } + private prepareActionButtons(): void { + this.actions$ = this.translate + .get([ + 'ACTIONS.CANCEL', + 'ACTIONS.TOOLTIPS.CANCEL_AND_CLOSE', + 'ACTIONS.SAVE', + 'ACTIONS.TOOLTIPS.SAVE', + 'ACTIONS.SAVE_AS', + 'ACTIONS.TOOLTIPS.SAVE_AS' + ]) + .pipe( + map((data) => { + return [ + { + label: data['ACTIONS.CANCEL'], + title: data['ACTIONS.TOOLTIPS.CANCEL_AND_CLOSE'], + actionCallback: () => this.close(), + icon: 'pi pi-times', + show: 'always', + permission: 'THEME#VIEW' + }, + { + label: data['ACTIONS.SAVE'], + title: data['ACTIONS.TOOLTIPS.SAVE'], + actionCallback: () => this.updateTheme(), + icon: 'pi pi-save', + show: 'always', + conditional: true, + showCondition: this.mode === 'EDIT', + permission: 'THEME#SAVE' + }, + { + label: data['ACTIONS.SAVE_AS'], + title: data['ACTIONS.TOOLTIPS.SAVE_AS'], + actionCallback: () => this.saveAsNewPopup(), + icon: 'pi pi-plus-circle', + show: 'always', + permission: 'THEME#CREATE' + } + ] + }) + ) + } // DropDown Theme Template private loadThemeTemplates(): void { @@ -247,7 +293,8 @@ export class ThemeDesignerComponent implements OnInit { this.basicForm.controls['description'].setValue(result.resource.description) this.basicForm.controls['faviconUrl'].setValue(result.resource.faviconUrl) this.basicForm.controls['logoUrl'].setValue(result.resource.logoUrl) - this.setFetchUrls() + this.fetchingLogoUrl = prepareUrl(this.basicForm.value.logoUrl) + this.fetchingFaviconUrl = prepareUrl(this.basicForm.value.faviconUrl) } if (result.resource.properties) { this.propertiesForm.reset() @@ -371,7 +418,8 @@ export class ThemeDesignerComponent implements OnInit { Array.from(files).forEach((file) => { this.imageApi.uploadImage({ image: file }).subscribe((data) => { this.basicForm.controls[fieldType + 'Url'].setValue(data.imageUrl) - this.setFetchUrls() + this.fetchingLogoUrl = prepareUrl(this.basicForm.value.logoUrl) + this.fetchingFaviconUrl = prepareUrl(this.basicForm.value.faviconUrl) this.msgService.info({ summaryKey: 'LOGO.UPLOADED' }) }) }) @@ -383,10 +431,6 @@ export class ThemeDesignerComponent implements OnInit { } */ } - private setFetchUrls(): void { - this.fetchingLogoUrl = setFetchUrls(this.apiPrefix, this.basicForm.value.logoUrl) - this.fetchingFaviconUrl = setFetchUrls(this.apiPrefix, this.basicForm.value.faviconUrl) - } // Applying Styles private updateCssVar(varName: string, value: string): void { diff --git a/src/app/theme/theme-variables.ts b/src/app/theme/theme-designer/theme-variables.ts similarity index 100% rename from src/app/theme/theme-variables.ts rename to src/app/theme/theme-designer/theme-variables.ts diff --git a/src/app/theme/theme-detail/theme-detail.component.html b/src/app/theme/theme-detail/theme-detail.component.html index c105970..701f1cb 100644 --- a/src/app/theme/theme-detail/theme-detail.component.html +++ b/src/app/theme/theme-detail/theme-detail.component.html @@ -1,4 +1,4 @@ - + {{ 'THEME.DESCRIPTION' | translate }}
- + > - + diff --git a/src/app/theme/theme-detail/theme-detail.component.spec.ts b/src/app/theme/theme-detail/theme-detail.component.spec.ts index 90a776d..260f2a1 100644 --- a/src/app/theme/theme-detail/theme-detail.component.spec.ts +++ b/src/app/theme/theme-detail/theme-detail.component.spec.ts @@ -1,22 +1,24 @@ import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' -import { HttpClient, HttpErrorResponse } from '@angular/common/http' +import { HttpErrorResponse } from '@angular/common/http' import { HttpClientTestingModule } from '@angular/common/http/testing' +import { ActivatedRoute, Router } from '@angular/router' import { RouterTestingModule } from '@angular/router/testing' -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core' +import { TranslateModule, TranslateService } from '@ngx-translate/core' +import { TranslateTestingModule } from 'ngx-translate-testing' +import { of, throwError } from 'rxjs' +import FileSaver from 'file-saver' import { ConfigurationService, PortalMessageService } from '@onecx/portal-integration-angular' -import { HttpLoaderFactory } from 'src/app/shared/shared.module' -import { ThemeDetailComponent } from './theme-detail.component' + +import { prepareUrl } from 'src/app/shared/utils' import { ThemesAPIService } from 'src/app/generated' -import { of, throwError } from 'rxjs' -import { ActivatedRoute, Router } from '@angular/router' -import { environment } from 'src/environments/environment' -import FileSaver from 'file-saver' +import { ThemeDetailComponent } from './theme-detail.component' describe('ThemeDetailComponent', () => { let component: ThemeDetailComponent let fixture: ComponentFixture + let translateService: TranslateService const msgServiceSpy = jasmine.createSpyObj('PortalMessageService', ['success', 'error']) @@ -27,10 +29,8 @@ describe('ThemeDetailComponent', () => { portalName: 'test', baseUrl: '/', microfrontendRegistrations: [] - }), - lang: 'de' + }) } - const themesApiSpy = jasmine.createSpyObj('ThemesAPIService', [ 'getThemeById', 'deleteTheme', @@ -43,27 +43,16 @@ describe('ThemeDetailComponent', () => { imports: [ RouterTestingModule, HttpClientTestingModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [HttpClient] - } - }) + TranslateModule.forRoot(), + TranslateTestingModule.withTranslations({ + de: require('src/assets/i18n/de.json'), + en: require('src/assets/i18n/en.json') + }).withDefaultLanguage('de') ], providers: [ - { - provide: PortalMessageService, - useValue: msgServiceSpy - }, - { - provide: ConfigurationService, - useValue: configServiceSpy - }, - { - provide: ThemesAPIService, - useValue: themesApiSpy - } + { provide: PortalMessageService, useValue: msgServiceSpy }, + { provide: ConfigurationService, useValue: configServiceSpy }, + { provide: ThemesAPIService, useValue: themesApiSpy } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents() @@ -76,6 +65,7 @@ describe('ThemeDetailComponent', () => { })) beforeEach(() => { + translateService = TestBed.inject(TranslateService) fixture = TestBed.createComponent(ThemeDetailComponent) component = fixture.componentInstance fixture.detectChanges() @@ -89,7 +79,7 @@ describe('ThemeDetailComponent', () => { const id = 'themeId' const route = TestBed.inject(ActivatedRoute) spyOn(route.snapshot.paramMap, 'get').and.returnValue(id) - configServiceSpy.lang = 'de' + translateService.use('de') // recreate component to test constructor fixture = TestBed.createComponent(ThemeDetailComponent) @@ -100,16 +90,13 @@ describe('ThemeDetailComponent', () => { component.loading = true await component.ngOnInit() - expect(component.themeId).toBe(id) - expect(component.dateFormat).toBe('dd.MM.yyyy HH:mm:ss') + expect(component.dateFormat).toBe('medium') expect(themesApiSpy.getThemeById).toHaveBeenCalledOnceWith({ id: id }) expect(component.loading).toBe(false) }) - it('should create with correct dateFormat', async () => { - configServiceSpy.lang = 'pl' - + it('should create with default dateFormat', async () => { // recreate component to test constructor fixture = TestBed.createComponent(ThemeDetailComponent) component = fixture.componentInstance @@ -320,22 +307,21 @@ describe('ThemeDetailComponent', () => { await component.ngOnInit() - expect(component.headerImageUrl).toBe(`${environment.apiPrefix}logo123.png`) + expect(component.headerImageUrl).toBe(prepareUrl('logo123.png')) }) it('should set header image url without prefix when theme logo has http/https', async () => { + const url = 'http://external.com/logo123.png' const themeResponse = { resource: { name: 'themeName', - logoUrl: 'http://external.com/logo123.png' + logoUrl: url }, workspaces: [] } themesApiSpy.getThemeById.and.returnValue(of(themeResponse) as any) - await component.ngOnInit() - - expect(component.headerImageUrl).toBe('http://external.com/logo123.png') + expect(component.headerImageUrl).toBe(url) }) it('should hide dialog, inform and navigate on successfull deletion', () => { @@ -419,16 +405,14 @@ describe('ThemeDetailComponent', () => { ) expect(FileSaver.saveAs).toHaveBeenCalledOnceWith(jasmine.any(Blob), 'themeName_Theme.json') }) - + /* it('should display error on theme export fail', () => { themesApiSpy.exportThemes.and.returnValue(throwError(() => new Error())) - component.theme = { name: 'themeName' } - component.onExportTheme() - expect(msgServiceSpy.error).toHaveBeenCalledOnceWith({ summaryKey: 'ACTIONS.EXPORT.EXPORT_THEME_FAIL' }) }) + */ }) diff --git a/src/app/theme/theme-detail/theme-detail.component.ts b/src/app/theme/theme-detail/theme-detail.component.ts index 88363c5..a8ca13b 100644 --- a/src/app/theme/theme-detail/theme-detail.component.ts +++ b/src/app/theme/theme-detail/theme-detail.component.ts @@ -1,19 +1,18 @@ import { Component, OnInit } from '@angular/core' import { DatePipe } from '@angular/common' import { ActivatedRoute, Router } from '@angular/router' -import { finalize } from 'rxjs' import { TranslateService } from '@ngx-translate/core' +import { finalize } from 'rxjs' import FileSaver from 'file-saver' -import { Action, ConfigurationService, ObjectDetailItem, PortalMessageService } from '@onecx/portal-integration-angular' -import { limitText, sortByLocale } from '../../shared/utils' -import { ExportThemeRequest, Theme, ThemesAPIService, Workspace } from '../../generated' -import { environment } from '../../../environments/environment' +import { Action, ObjectDetailItem, PortalMessageService, UserService } from '@onecx/portal-integration-angular' + +import { limitText, prepareUrl, sortByLocale } from 'src/app/shared/utils' +import { ExportThemeRequest, Theme, ThemesAPIService, Workspace } from 'src/app/generated' @Component({ templateUrl: './theme-detail.component.html', - styleUrls: ['./theme-detail.component.scss'], - providers: [ConfigurationService] + styleUrls: ['./theme-detail.component.scss'] }) export class ThemeDetailComponent implements OnInit { theme: Theme | undefined @@ -23,7 +22,6 @@ export class ThemeDetailComponent implements OnInit { themeDeleteMessage = '' themePortalList = '' loading = true - private apiPrefix = environment.apiPrefix public dateFormat = 'medium' // page header public actions: Action[] = [] @@ -31,15 +29,15 @@ export class ThemeDetailComponent implements OnInit { public headerImageUrl?: string constructor( + private user: UserService, private router: Router, private route: ActivatedRoute, private themeApi: ThemesAPIService, - private config: ConfigurationService, private msgService: PortalMessageService, private translate: TranslateService ) { this.themeId = this.route.snapshot.paramMap.get('id') || '' - this.dateFormat = this.config.lang === 'de' ? 'dd.MM.yyyy HH:mm:ss' : 'medium' + this.dateFormat = this.user.lang$.getValue() === 'de' ? 'dd.MM.yyyy HH:mm:ss' : 'medium' } ngOnInit(): void { @@ -55,7 +53,7 @@ export class ThemeDetailComponent implements OnInit { this.theme = data.resource this.usedInWorkspace = data.workspaces this.preparePage() - this.setHeaderImageUrl() + this.headerImageUrl = prepareUrl(this.theme?.logoUrl) }, error: (err) => { this.msgService.error({ @@ -207,15 +205,6 @@ export class ThemeDetailComponent implements OnInit { } } - private setHeaderImageUrl(): void { - // img format is from BE or from Internet - if (this.theme?.logoUrl && !this.theme.logoUrl.match(/^(http|https)/g)) { - this.headerImageUrl = this.apiPrefix + this.theme.logoUrl - } else { - this.headerImageUrl = this.theme?.logoUrl - } - } - public prepareUsedInPortalList(): string { const arr = this.usedInWorkspace?.map((workspace: Workspace) => workspace.workspaceName) return arr?.sort(sortByLocale).join(', ') ?? '' diff --git a/src/app/theme/theme-detail/theme-intern/theme-intern.component.ts b/src/app/theme/theme-detail/theme-intern/theme-intern.component.ts index 63e9b92..db33501 100644 --- a/src/app/theme/theme-detail/theme-intern/theme-intern.component.ts +++ b/src/app/theme/theme-detail/theme-intern/theme-intern.component.ts @@ -1,10 +1,10 @@ import { Component, Input } from '@angular/core' import { TranslateService } from '@ngx-translate/core' -import { Theme } from '../../../generated' +import { Theme } from 'src/app/generated' @Component({ - selector: 'tm-theme-intern', + selector: 'app-theme-intern', templateUrl: './theme-intern.component.html' }) export class ThemeInternComponent { diff --git a/src/app/theme/theme-import/theme-import.component.html b/src/app/theme/theme-import/theme-import.component.html index b8a3e23..9b04249 100644 --- a/src/app/theme/theme-import/theme-import.component.html +++ b/src/app/theme/theme-import/theme-import.component.html @@ -39,7 +39,7 @@ /> - +
{{ 'THEME.NO_PROPERTIES' | translate }}
{ let component: ThemeImportComponent @@ -26,13 +25,10 @@ describe('ThemeImportComponent', () => { imports: [ RouterTestingModule, HttpClientTestingModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [HttpClient] - } - }) + TranslateTestingModule.withTranslations({ + de: require('src/assets/i18n/de.json'), + en: require('src/assets/i18n/en.json') + }).withDefaultLanguage('en') ], providers: [ { provide: PortalMessageService, useValue: msgServiceSpy }, diff --git a/src/app/theme/theme-import/theme-import.component.ts b/src/app/theme/theme-import/theme-import.component.ts index d8b260d..076fc3e 100644 --- a/src/app/theme/theme-import/theme-import.component.ts +++ b/src/app/theme/theme-import/theme-import.component.ts @@ -3,12 +3,12 @@ import { HttpHeaders } from '@angular/common/http' import { TranslateService } from '@ngx-translate/core' import { ActivatedRoute, Router } from '@angular/router' -import { ThemesAPIService } from './../../generated/api/themes.service' -import { Theme, ThemeSnapshot } from '../../generated' import { PortalMessageService } from '@onecx/portal-integration-angular' +import { Theme, ThemesAPIService, ThemeSnapshot } from 'src/app/generated' + @Component({ - selector: 'tm-theme-import', + selector: 'app-theme-import', templateUrl: './theme-import.component.html', styleUrls: ['./theme-import.component.scss'] }) diff --git a/src/app/theme/theme-search/theme-search.component.html b/src/app/theme/theme-search/theme-search.component.html index ec02524..456b14c 100644 --- a/src/app/theme/theme-search/theme-search.component.html +++ b/src/app/theme/theme-search/theme-search.component.html @@ -48,11 +48,11 @@ >
- + >
@@ -63,7 +63,7 @@
- +
@@ -74,10 +74,10 @@
- +
- +
@@ -91,5 +91,5 @@ - + diff --git a/src/app/theme/theme-search/theme-search.component.spec.ts b/src/app/theme/theme-search/theme-search.component.spec.ts index 3851716..a2deb23 100644 --- a/src/app/theme/theme-search/theme-search.component.spec.ts +++ b/src/app/theme/theme-search/theme-search.component.spec.ts @@ -1,16 +1,15 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing' import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' -import { HttpClient } from '@angular/common/http' -import { HttpClientTestingModule } from '@angular/common/http/testing' +import { Router } from '@angular/router' import { RouterTestingModule } from '@angular/router/testing' -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core' +import { TranslateService } from '@ngx-translate/core' +import { TranslateTestingModule } from 'ngx-translate-testing' +import { DataViewModule } from 'primeng/dataview' +import { of } from 'rxjs' -import { HttpLoaderFactory } from 'src/app/shared/shared.module' -import { ThemeSearchComponent } from './theme-search.component' import { ThemesAPIService } from 'src/app/generated' -import { of } from 'rxjs' -import { Router } from '@angular/router' -import { DataViewModule } from 'primeng/dataview' +import { ThemeSearchComponent } from './theme-search.component' describe('ThemeSearchComponent', () => { let component: ThemeSearchComponent @@ -25,20 +24,12 @@ describe('ThemeSearchComponent', () => { RouterTestingModule, HttpClientTestingModule, DataViewModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [HttpClient] - } - }) - ], - providers: [ - { - provide: ThemesAPIService, - useValue: themeApiSpy - } + TranslateTestingModule.withTranslations({ + de: require('src/assets/i18n/de.json'), + en: require('src/assets/i18n/en.json') + }).withDefaultLanguage('en') ], + providers: [{ provide: ThemesAPIService, useValue: themeApiSpy }], schemas: [NO_ERRORS_SCHEMA] }).compileComponents() })) @@ -75,14 +66,7 @@ describe('ThemeSearchComponent', () => { } spyOn(translateService, 'get').and.returnValues(of(actionsTranslations), of(generalTranslations)) const themesResponse = { - stream: [ - { - name: 'theme1' - }, - { - name: 'theme2' - } - ] + stream: [{ name: 'theme1' }, { name: 'theme2' }] } const themesObservable = of(themesResponse as any) themeApiSpy.getThemes.and.returnValue(themesObservable) @@ -128,17 +112,13 @@ describe('ThemeSearchComponent', () => { it('should navigate to theme detail on new theme callback', () => { const router = TestBed.inject(Router) spyOn(router, 'navigate') - component.onNewTheme() - expect(router.navigate).toHaveBeenCalledOnceWith(['./new'], jasmine.any(Object)) }) it('should change viewMode on layout change', () => { expect(component.viewMode).toBe('grid') - component.onLayoutChange('list') - expect(component.viewMode).toBe('list') }) @@ -146,38 +126,28 @@ describe('ThemeSearchComponent', () => { component.dv = jasmine.createSpyObj('DataView', ['filter']) component.filter = '' const myFilter = 'myTheme' - component.onFilterChange(myFilter) - expect(component.filter).toBe(myFilter) expect(component.dv!.filter).toHaveBeenCalledOnceWith(myFilter, 'contains') }) it('should change field to sort by on sort change', () => { component.sortField = 'name' - component.onSortChange('description') - expect(component.sortField).toBe('description') }) it('should change sorting direction on sorting direction change', () => { component.sortOrder = 1 - component.onSortDirChange(true) - expect(component.sortOrder).toBe(-1) - component.onSortDirChange(false) - expect(component.sortOrder).toBe(1) }) it('should show import dialog on import theme click', () => { component.themeImportDialogVisible = false - component.onImportThemeClick() - expect(component.themeImportDialogVisible).toBe(true) }) }) diff --git a/src/app/theme/theme-search/theme-search.component.ts b/src/app/theme/theme-search/theme-search.component.ts index eec917c..2c23633 100644 --- a/src/app/theme/theme-search/theme-search.component.ts +++ b/src/app/theme/theme-search/theme-search.component.ts @@ -1,11 +1,13 @@ import { Component, OnInit, ViewChild } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' +import { TranslateService } from '@ngx-translate/core' import { Observable } from 'rxjs' import { DataView } from 'primeng/dataview' -import { TranslateService } from '@ngx-translate/core' + import { Action, DataViewControlTranslations } from '@onecx/portal-integration-angular' -import { GetThemesResponse, ThemesAPIService } from '../../generated' -import { limitText } from '../../shared/utils' + +import { GetThemesResponse, ThemesAPIService } from 'src/app/generated' +import { limitText } from 'src/app/shared/utils' @Component({ templateUrl: './theme-search.component.html', diff --git a/src/app/theme/theme.module.ts b/src/app/theme/theme.module.ts index b83399a..ee5f0b0 100644 --- a/src/app/theme/theme.module.ts +++ b/src/app/theme/theme.module.ts @@ -1,16 +1,15 @@ import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA, NgModule } from '@angular/core' -import { HttpClient } from '@angular/common/http' +import { CommonModule } from '@angular/common' import { FormsModule } from '@angular/forms' import { RouterModule, Routes } from '@angular/router' -import { MissingTranslationHandler, TranslateLoader, TranslateModule } from '@ngx-translate/core' import { FieldsetModule } from 'primeng/fieldset' import { ConfirmDialogModule } from 'primeng/confirmdialog' +import { ConfirmationService } from 'primeng/api' -import { MFE_INFO, PortalCoreModule, MyMissingTranslationHandler } from '@onecx/portal-integration-angular' +import { addInitializeModuleGuard, InitializeModuleGuard, PortalCoreModule } from '@onecx/portal-integration-angular' -import { CanActivateGuard } from '../shared/can-active-guard.service' import { LabelResolver } from '../shared/label.resolver' -import { HttpLoaderFactory, SharedModule } from '../shared/shared.module' +import { SharedModule } from '../shared/shared.module' import { ThemeSearchComponent } from './theme-search/theme-search.component' import { ThemeImportComponent } from './theme-import/theme-import.component' @@ -22,44 +21,34 @@ const routes: Routes = [ { path: '', component: ThemeSearchComponent, - canActivate: [CanActivateGuard], pathMatch: 'full' }, { path: 'new', - canActivate: [CanActivateGuard], component: ThemeDesignerComponent, data: { breadcrumb: 'BREADCRUMBS.CREATE', breadcrumbFn: (data: any) => `${data.labeli18n}` }, - resolve: { - labeli18n: LabelResolver - } + resolve: { labeli18n: LabelResolver } }, { path: ':id', - canActivate: [CanActivateGuard], component: ThemeDetailComponent, data: { breadcrumb: 'BREADCRUMBS.DETAIL', breadcrumbFn: (data: any) => `${data.labeli18n}` }, - resolve: { - labeli18n: LabelResolver - } + resolve: { labeli18n: LabelResolver } }, { path: ':id/edit', - canActivate: [CanActivateGuard], component: ThemeDesignerComponent, data: { breadcrumb: 'BREADCRUMBS.EDIT', breadcrumbFn: (data: any) => `${data.labeli18n}` }, - resolve: { - labeli18n: LabelResolver - } + resolve: { labeli18n: LabelResolver } } ] @NgModule({ @@ -71,26 +60,15 @@ const routes: Routes = [ ThemeInternComponent ], imports: [ - FormsModule, - FieldsetModule, + CommonModule, ConfirmDialogModule, + FieldsetModule, + FormsModule, PortalCoreModule.forMicroFrontend(), - [RouterModule.forChild(routes)], - SharedModule, - TranslateModule.forChild({ - isolate: true, - missingTranslationHandler: { - provide: MissingTranslationHandler, - useClass: MyMissingTranslationHandler - }, - loader: { - provide: TranslateLoader, - useFactory: HttpLoaderFactory, - deps: [HttpClient, MFE_INFO] - } - }) + [RouterModule.forChild(addInitializeModuleGuard(routes))], + SharedModule ], - providers: [], + providers: [ConfirmationService, InitializeModuleGuard], schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA] }) export class ThemeModule { diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index df4a53b..9c02573 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,5 +1,5 @@ export const environment = { production: true, - BASE_PATH: '/theme-bff', - apiPrefix: 'theme-bff' + BASE_PATH: '/bff', + apiPrefix: 'bff' } diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 4edbfb4..f1c7144 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -9,5 +9,5 @@ export const environment = { KEYCLOAK_CLIENT_ID: 'portal-mf-shell', TKIT_PORTAL_ID: 'ADMIN', skipRemoteConfigLoad: true, - apiPrefix: 'theme-bff' + apiPrefix: 'bff' } diff --git a/webpack.config.js b/webpack.config.js index 74a3d5e..dbb6aad 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -30,17 +30,13 @@ const config = withModuleFederationPlugin({ requiredVersion: 'auto', includeSecondaries: true }, - '@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, rxjs: { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, + '@angular/router': { singleton: true, strictVersion: true, requiredVersion: 'auto', includeSecondaries: true }, '@ngx-translate/core': { singleton: true, strictVersion: false, requiredVersion: '^14.0.0' }, - '@onecx/portal-integration-angular': { - singleton: true, - requiredVersion: 'auto' - }, - '@onecx/keycloak-auth': { - singleton: true, - requiredVersion: 'auto' - } + '@onecx/keycloak-auth': { requiredVersion: 'auto', includeSecondaries: true }, + '@onecx/portal-integration-angular': { requiredVersion: 'auto', includeSecondaries: true }, + '@onecx/accelerator': { requiredVersion: 'auto', includeSecondaries: true }, + '@onecx/integration-interface': { requiredVersion: 'auto', includeSecondaries: true } }), sharedMappings: ['@onecx/portal-integration-angular'] })