diff --git a/backend/src/main/java/ch/puzzle/okr/OkrApplication.java b/backend/src/main/java/ch/puzzle/okr/OkrApplication.java index ead1b1fb23..c718c0f533 100644 --- a/backend/src/main/java/ch/puzzle/okr/OkrApplication.java +++ b/backend/src/main/java/ch/puzzle/okr/OkrApplication.java @@ -1,11 +1,14 @@ package ch.puzzle.okr; +import ch.puzzle.okr.service.clientconfig.ClientCustomizationProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling +@EnableConfigurationProperties(ClientCustomizationProperties.class) public class OkrApplication { public static void main(String[] args) { SpringApplication.run(OkrApplication.class, args); diff --git a/backend/src/main/java/ch/puzzle/okr/controller/ClientConfigController.java b/backend/src/main/java/ch/puzzle/okr/controller/ClientConfigController.java index 1f449278ca..218da02f8a 100644 --- a/backend/src/main/java/ch/puzzle/okr/controller/ClientConfigController.java +++ b/backend/src/main/java/ch/puzzle/okr/controller/ClientConfigController.java @@ -1,14 +1,13 @@ package ch.puzzle.okr.controller; -import ch.puzzle.okr.service.ClientConfigService; +import ch.puzzle.okr.dto.ClientConfigDto; +import ch.puzzle.okr.service.clientconfig.ClientConfigService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.Map; - @RestController @RequestMapping("/config") public class ClientConfigController { @@ -20,7 +19,7 @@ public ClientConfigController(ClientConfigService configService) { } @GetMapping - public ResponseEntity> getConfig() { + public ResponseEntity getConfig() { return ResponseEntity.status(HttpStatus.OK).body(configService.getConfigBasedOnActiveEnv()); } } diff --git a/backend/src/main/java/ch/puzzle/okr/dto/ClientConfigDto.java b/backend/src/main/java/ch/puzzle/okr/dto/ClientConfigDto.java new file mode 100644 index 0000000000..876be7aa0f --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/dto/ClientConfigDto.java @@ -0,0 +1,7 @@ +package ch.puzzle.okr.dto; + +import java.util.HashMap; + +public record ClientConfigDto(String activeProfile, String issuer, String clientId, String favicon, String logo, + String title, HashMap customStyles) { +} diff --git a/backend/src/main/java/ch/puzzle/okr/service/ClientConfigService.java b/backend/src/main/java/ch/puzzle/okr/service/ClientConfigService.java deleted file mode 100644 index 3763be0d72..0000000000 --- a/backend/src/main/java/ch/puzzle/okr/service/ClientConfigService.java +++ /dev/null @@ -1,29 +0,0 @@ -package ch.puzzle.okr.service; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.util.HashMap; -import java.util.Map; - -@Service -public class ClientConfigService { - - @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") - private String issuer; - - @Value("${spring.profiles.active}") - private String activeProfile; - - @Value("${spring.security.oauth2.resourceserver.opaquetoken.client-id}") - private String clientId; - - public Map getConfigBasedOnActiveEnv() { - HashMap env = new HashMap<>(); - env.put("activeProfile", activeProfile); - env.put("issuer", issuer); - env.put("clientId", clientId); - return env; - } - -} diff --git a/backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientConfigService.java b/backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientConfigService.java new file mode 100644 index 0000000000..f06c7ca325 --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientConfigService.java @@ -0,0 +1,31 @@ +package ch.puzzle.okr.service.clientconfig; + +import ch.puzzle.okr.dto.ClientConfigDto; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class ClientConfigService { + + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") + private String issuer; + + @Value("${spring.profiles.active}") + private String activeProfile; + + @Value("${spring.security.oauth2.resourceserver.opaquetoken.client-id}") + private String clientId; + + private final ClientCustomizationProperties clientCustomizationProperties; + + public ClientConfigService(ClientCustomizationProperties clientCustomizationProperties) { + this.clientCustomizationProperties = clientCustomizationProperties; + } + + public ClientConfigDto getConfigBasedOnActiveEnv() { + return new ClientConfigDto(activeProfile, issuer, clientId, this.clientCustomizationProperties.getFavicon(), + this.clientCustomizationProperties.getLogo(), this.clientCustomizationProperties.getTitle(), + this.clientCustomizationProperties.getCustomStyles()); + } + +} diff --git a/backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientCustomizationProperties.java b/backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientCustomizationProperties.java new file mode 100644 index 0000000000..a992b4d8ab --- /dev/null +++ b/backend/src/main/java/ch/puzzle/okr/service/clientconfig/ClientCustomizationProperties.java @@ -0,0 +1,45 @@ +package ch.puzzle.okr.service.clientconfig; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.HashMap; + +@ConfigurationProperties("okr.clientcustomization") +public class ClientCustomizationProperties { + private String favicon; + private String logo; + private String title; + private HashMap customStyles = new HashMap<>(); + + public void setCustomStyles(HashMap customStyles) { + this.customStyles = customStyles; + } + + public String getFavicon() { + return favicon; + } + + public void setFavicon(String favicon) { + this.favicon = favicon; + } + + public String getLogo() { + return logo; + } + + public void setLogo(String logo) { + this.logo = logo; + } + + public HashMap getCustomStyles() { + return customStyles; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } +} diff --git a/backend/src/main/resources/application-staging.properties b/backend/src/main/resources/application-staging.properties index 805e7beec6..a4f58cfba7 100644 --- a/backend/src/main/resources/application-staging.properties +++ b/backend/src/main/resources/application-staging.properties @@ -4,3 +4,4 @@ logging.level.org.springframework=debug spring.security.oauth2.resourceserver.opaquetoken.client-id=pitc_okr_staging okr.user.champion.usernames=peggimann +okr.clientcustomization.customstyles.okr-topbar-background-color=#ab31ad \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 11729ab8a3..d0c0ad39de 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -50,3 +50,7 @@ okr.jwt.user.username=preferred_username okr.jwt.user.firstname=given_name okr.jwt.user.lastname=family_name okr.jwt.user.email=email + +okr.clientcustomization.favicon=assets/favicon.png +okr.clientcustomization.logo=assets/images/okr-logo.svg +okr.clientcustomization.title=Puzzle OKR diff --git a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql index 8939474bdf..34aec66957 100644 --- a/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql +++ b/backend/src/main/resources/db/h2-db/data-test-h2/V100_0_0__TestData.sql @@ -40,6 +40,7 @@ values (1, 'GJ 22/23-Q4', '2023-04-01', '2023-06-30'), (6, 'GJ 21/22-Q4', '2022-04-01', '2022-06-30'), (7, 'GJ 23/24-Q2', '2023-10-01', '2023-12-31'), (8, 'GJ 23/24-Q3', '2024-01-01', '2024-03-31'), + (9, 'GJ 23/24-Q4', '2024-04-01', '2024-06-30'), (199, 'Backlog', null, null); insert into team (id, version, name) diff --git a/backend/src/test/java/ch/puzzle/okr/controller/ClientConfigControllerIT.java b/backend/src/test/java/ch/puzzle/okr/controller/ClientConfigControllerIT.java index 51daa45cd8..0be1e46dbe 100644 --- a/backend/src/test/java/ch/puzzle/okr/controller/ClientConfigControllerIT.java +++ b/backend/src/test/java/ch/puzzle/okr/controller/ClientConfigControllerIT.java @@ -1,6 +1,6 @@ package ch.puzzle.okr.controller; -import ch.puzzle.okr.service.ClientConfigService; +import ch.puzzle.okr.service.clientconfig.ClientConfigService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; diff --git a/backend/src/test/java/ch/puzzle/okr/service/ClientConfigServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/ClientConfigServiceIT.java index f0f3b31be7..0f2dfa35ed 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/ClientConfigServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/ClientConfigServiceIT.java @@ -1,14 +1,17 @@ package ch.puzzle.okr.service; +import ch.puzzle.okr.dto.ClientConfigDto; +import ch.puzzle.okr.service.clientconfig.ClientConfigService; import ch.puzzle.okr.test.SpringIntegrationTest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; - -import java.util.Map; +import org.springframework.boot.test.context.SpringBootTest; import static org.junit.jupiter.api.Assertions.assertEquals; @SpringIntegrationTest +@SpringBootTest(properties = { "okr.clientcustomization.customstyles.okr-topbar-background-color=#affe00", + "okr.clientcustomization.customstyles.okr-other-css-style=rgba(50,60,70,0.5)", }) class ClientConfigServiceIT { @Autowired @@ -16,10 +19,14 @@ class ClientConfigServiceIT { @Test void saveKeyResultShouldSaveNewKeyResult() { - Map configMap = clientConfigService.getConfigBasedOnActiveEnv(); - - assertEquals("prod", configMap.get("activeProfile")); - assertEquals("http://localhost:8544/realms/pitc", configMap.get("issuer")); + ClientConfigDto clientConfig = clientConfigService.getConfigBasedOnActiveEnv(); + + assertEquals("prod", clientConfig.activeProfile()); + assertEquals("http://localhost:8544/realms/pitc", clientConfig.issuer()); + assertEquals("assets/favicon.png", clientConfig.favicon()); + assertEquals("assets/images/okr-logo.svg", clientConfig.logo()); + assertEquals("#affe00", clientConfig.customStyles().get("okr-topbar-background-color")); + assertEquals("rgba(50,60,70,0.5)", clientConfig.customStyles().get("okr-other-css-style")); } } diff --git a/backend/src/test/java/ch/puzzle/okr/service/persistence/QuarterPersistenceServiceIT.java b/backend/src/test/java/ch/puzzle/okr/service/persistence/QuarterPersistenceServiceIT.java index 8acb8b2bf2..b641dd1dd4 100644 --- a/backend/src/test/java/ch/puzzle/okr/service/persistence/QuarterPersistenceServiceIT.java +++ b/backend/src/test/java/ch/puzzle/okr/service/persistence/QuarterPersistenceServiceIT.java @@ -68,6 +68,7 @@ void shouldReturnCurrentQuarterFutureQuarterAnd4PastQuarters() { @Test void shouldReturnCurrentQuarter() { Quarter quarter = quarterPersistenceService.getCurrentQuarter(); + assertTrue(LocalDate.now().isAfter(quarter.getStartDate())); assertTrue(LocalDate.now().isBefore(quarter.getEndDate())); assertNotNull(quarter.getId()); diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index d11efee938..fa25ac45eb 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -71,6 +71,7 @@ import { ActionPlanComponent } from './action-plan/action-plan.component'; import { CdkDrag, CdkDropList } from '@angular/cdk/drag-drop'; import { TeamManagementComponent } from './shared/dialog/team-management/team-management.component'; import { KeyresultDialogComponent } from './shared/dialog/keyresult-dialog/keyresult-dialog.component'; +import { CustomizationService } from './shared/services/customization.service'; function initOauthFactory(configService: ConfigService, oauthService: OAuthService) { return async () => { @@ -202,4 +203,6 @@ export const MY_FORMATS = { bootstrap: [AppComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) -export class AppModule {} +export class AppModule { + constructor(customizationService: CustomizationService) {} +} diff --git a/frontend/src/app/application-top-bar/application-top-bar.component.html b/frontend/src/app/application-top-bar/application-top-bar.component.html index f95279203a..9ceb0f491e 100644 --- a/frontend/src/app/application-top-bar/application-top-bar.component.html +++ b/frontend/src/app/application-top-bar/application-top-bar.component.html @@ -1,7 +1,7 @@
- okr-logo + okr-logo
diff --git a/frontend/src/app/application-top-bar/application-top-bar.component.scss b/frontend/src/app/application-top-bar/application-top-bar.component.scss index b2c8d632f6..63082ca229 100644 --- a/frontend/src/app/application-top-bar/application-top-bar.component.scss +++ b/frontend/src/app/application-top-bar/application-top-bar.component.scss @@ -12,7 +12,12 @@ z-index: 102; height: inherit; justify-content: space-between; - background-color: $pz-dark-blue; + background-color: var(--okr-topbar-background-color); + + img { + max-height: calc(100% - 16px); + width: auto; + } } .topBarEntry { diff --git a/frontend/src/app/application-top-bar/application-top-bar.component.ts b/frontend/src/app/application-top-bar/application-top-bar.component.ts index 800520390a..873cf185a0 100644 --- a/frontend/src/app/application-top-bar/application-top-bar.component.ts +++ b/frontend/src/app/application-top-bar/application-top-bar.component.ts @@ -1,6 +1,6 @@ -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { OAuthService } from 'angular-oauth2-oidc'; -import { map, ReplaySubject } from 'rxjs'; +import { BehaviorSubject, ReplaySubject, Subscription } from 'rxjs'; import { ConfigService } from '../config.service'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { TeamManagementComponent } from '../shared/dialog/team-management/team-management.component'; @@ -15,13 +15,15 @@ import { isMobileDevice } from '../shared/common'; styleUrls: ['./application-top-bar.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ApplicationTopBarComponent implements OnInit { +export class ApplicationTopBarComponent implements OnInit, OnDestroy { username: ReplaySubject = new ReplaySubject(); menuIsOpen = false; @Input() hasAdminAccess!: ReplaySubject; + logoSrc$ = new BehaviorSubject('assets/images/empty.svg'); private dialogRef!: MatDialogRef | undefined; + private subscription?: Subscription; constructor( private oauthService: OAuthService, @@ -32,20 +34,23 @@ export class ApplicationTopBarComponent implements OnInit { ) {} ngOnInit(): void { - this.configService.config$ - .pipe( - map((config) => { - if (config.activeProfile === 'staging') { - document.getElementById('okrTopbar')!.style.backgroundColor = '#ab31ad'; - } - }), - ) - .subscribe(); + this.subscription = this.configService.config$.subscribe({ + next: (config) => { + if (config.logo) { + this.logoSrc$.next(config.logo); + } + }, + }); if (this.oauthService.hasValidIdToken()) { this.username.next(this.oauthService.getIdentityClaims()['name']); } } + + ngOnDestroy(): void { + this.subscription?.unsubscribe(); + } + logOut() { const currentUrlTree = this.router.createUrlTree([], { queryParams: {} }); this.router.navigateByUrl(currentUrlTree).then(() => { diff --git a/frontend/src/app/config.service.ts b/frontend/src/app/config.service.ts index b311b6b04e..27faea76e1 100644 --- a/frontend/src/app/config.service.ts +++ b/frontend/src/app/config.service.ts @@ -1,14 +1,15 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, shareReplay } from 'rxjs'; +import { ClientConfig } from './shared/types/model/ClientConfig'; @Injectable({ providedIn: 'root', }) export class ConfigService { - public config$: Observable; + public config$: Observable; constructor(private httpClient: HttpClient) { - this.config$ = this.httpClient.get('/config').pipe(shareReplay()); + this.config$ = this.httpClient.get('/config').pipe(shareReplay()); } } diff --git a/frontend/src/app/shared/services/customization.service.spec.ts b/frontend/src/app/shared/services/customization.service.spec.ts new file mode 100644 index 0000000000..9419bb8de8 --- /dev/null +++ b/frontend/src/app/shared/services/customization.service.spec.ts @@ -0,0 +1,129 @@ +import { CustomizationService } from './customization.service'; +import { ConfigService } from '../../config.service'; +import { BehaviorSubject } from 'rxjs'; +import { ClientConfig } from '../types/model/ClientConfig'; + +class CallRecorder { + private calls: { [key: string]: any[] } = {}; + + public add(key: string, value: any): void { + if (!this.calls[key]) { + this.calls[key] = []; + } + this.calls[key].push(value); + } + + public getCallByIdx(key: string, index = 0): any[] { + return this.calls[key][index]; + } + + public getCallCount(key: string): number { + return this.calls[key]?.length ?? 0; + } + + public clear(): void { + this.calls = {}; + } +} + +describe('CustomizationService', () => { + const body: ClientConfig = { + activeProfile: 'test', + issuer: 'some-issuer.com', + clientId: 'my-client-id', + title: 'title', + favicon: 'favicon', + logo: 'logo', + customStyles: { cssVar1: 'foo' }, + }; + + let service: CustomizationService; + let configServiceMock: ConfigService; + let documentMock: Document; + let callRecorder = new CallRecorder(); + let configSubject: BehaviorSubject; + + beforeEach(() => { + configSubject = new BehaviorSubject(body); + configServiceMock = { config$: configSubject.asObservable() } as ConfigService; + callRecorder.clear(); + + documentMock = { + getElementById: (id: string) => { + return { + setAttribute: function () { + callRecorder.add(`${id}-setAttribute`, arguments); + }, + } as unknown as HTMLElement; + }, + querySelector: (selector: string) => { + return { + set innerHTML(value: string) { + callRecorder.add(`${selector}.innerHTML`, arguments); + }, + get style() { + return { + setProperty: function () { + callRecorder.add(`${selector}.style.setProperty`, arguments); + }, + removeProperty: function () { + callRecorder.add(`${selector}.style.removeProperty`, arguments); + }, + }; + }, + }; + }, + } as unknown as Document; + service = new CustomizationService(configServiceMock, documentMock); + }); + + it('should call correct apis when config is ready', () => { + const currentConfig = service.getCurrentConfig(); + expect(currentConfig?.title).toBe(body.title); + expect(currentConfig?.logo).toBe(body.logo); + expect(currentConfig?.favicon).toBe(body.favicon); + expect(currentConfig?.customStyles['cssVar1']).toBe(body.customStyles['cssVar1']); + + expect(callRecorder.getCallCount('title.innerHTML')).toBe(1); + expect(callRecorder.getCallCount('favicon-setAttribute')).toBe(1); + expect(callRecorder.getCallCount('html.style.setProperty')).toBe(1); + expect(callRecorder.getCallCount('html.style.removeProperty')).toBe(0); + + expect(callRecorder.getCallByIdx('title.innerHTML', 0)[0]).toBe('title'); + expect(callRecorder.getCallByIdx('favicon-setAttribute', 0)[0]).toBe('href'); + expect(callRecorder.getCallByIdx('favicon-setAttribute', 0)[1]).toBe('favicon'); + expect(callRecorder.getCallByIdx('html.style.setProperty', 0)[0]).toBe('--cssVar1'); + expect(callRecorder.getCallByIdx('html.style.setProperty', 0)[1]).toBe('foo'); + }); + + it('should update if config changed afterwards', () => { + const bodySecond = { + activeProfile: 'test-second', + issuer: 'some-issuer.com-second', + clientId: 'my-client-id-second', + title: 'title-second', + favicon: 'favicon-second', + logo: 'logo-second', + customStyles: { cssVarNew: 'bar' }, + }; + configSubject.next(bodySecond); + + const currentConfig = service.getCurrentConfig(); + expect(currentConfig?.title).toBe(bodySecond.title); + expect(currentConfig?.logo).toBe(bodySecond.logo); + expect(currentConfig?.favicon).toBe(bodySecond.favicon); + expect(currentConfig?.customStyles['cssVarNew']).toBe(bodySecond.customStyles['cssVarNew']); + expect(currentConfig?.customStyles['cssVar1']).toBe(undefined); + + expect(callRecorder.getCallCount('title.innerHTML')).toBe(2); + expect(callRecorder.getCallCount('favicon-setAttribute')).toBe(2); + expect(callRecorder.getCallCount('html.style.setProperty')).toBe(2); + expect(callRecorder.getCallCount('html.style.removeProperty')).toBe(1); + + expect(callRecorder.getCallByIdx('title.innerHTML', 1)[0]).toBe('title-second'); + expect(callRecorder.getCallByIdx('favicon-setAttribute', 1)[0]).toBe('href'); + expect(callRecorder.getCallByIdx('favicon-setAttribute', 1)[1]).toBe('favicon-second'); + expect(callRecorder.getCallByIdx('html.style.setProperty', 1)[0]).toBe('--cssVarNew'); + expect(callRecorder.getCallByIdx('html.style.setProperty', 1)[1]).toBe('bar'); + }); +}); diff --git a/frontend/src/app/shared/services/customization.service.ts b/frontend/src/app/shared/services/customization.service.ts new file mode 100644 index 0000000000..e96215cc12 --- /dev/null +++ b/frontend/src/app/shared/services/customization.service.ts @@ -0,0 +1,102 @@ +import { Inject, Injectable } from '@angular/core'; +import { ConfigService } from '../../config.service'; +import { CustomizationConfig, CustomStyles } from '../types/model/ClientConfig'; +import { DOCUMENT } from '@angular/common'; + +@Injectable({ + providedIn: 'root', +}) +export class CustomizationService { + private currentConfig?: CustomizationConfig; + + constructor( + configService: ConfigService, + @Inject(DOCUMENT) private document: Document, + ) { + configService.config$.subscribe((config) => { + this.updateCustomizations(config); + }); + } + + public getCurrentConfig() { + return this.currentConfig; + } + + private updateCustomizations(config: CustomizationConfig) { + this.setTitle(config.title); + this.setFavicon(config.favicon); + this.setStyleCustomizations(config.customStyles); + + this.currentConfig = config; + } + + private setFavicon(favicon: string) { + if (!favicon || this.currentConfig?.favicon === favicon) { + return; + } + + if (!this.document) { + return; + } + + this.document.getElementById('favicon')?.setAttribute('href', favicon); + } + + private setTitle(title: string) { + if (!title || this.currentConfig?.title === title) { + return; + } + + if (!this.document) { + return; + } + + this.document.querySelector('title')!.innerHTML = title; + } + + private setStyleCustomizations(customStylesMap: CustomStyles) { + if (!customStylesMap || this.areStylesTheSame(customStylesMap)) { + return; + } + + this.removeStyles(this.currentConfig?.customStyles); + this.setStyles(customStylesMap); + } + + private areStylesTheSame(customStylesMap: CustomStyles) { + return JSON.stringify(this.currentConfig?.customStyles) === JSON.stringify(customStylesMap); + } + + private setStyles(customStylesMap: CustomStyles | undefined) { + if (!customStylesMap) { + return; + } + + if (!this.document) { + return; + } + + const styles = this.document.querySelector('html')!.style; + if (!styles) { + return; + } + + Object.entries(customStylesMap).forEach(([varName, varValue]) => { + styles.setProperty(`--${varName}`, varValue); + }); + } + + private removeStyles(customStylesMap: CustomStyles | undefined) { + if (!customStylesMap) { + return; + } + + const styles = this.document.querySelector('html')!.style; + if (!styles) { + return; + } + Object.keys(customStylesMap).forEach((varName) => { + styles.removeProperty(`--${varName}`); + }); + } +} diff --git a/frontend/src/app/shared/types/model/ClientConfig.ts b/frontend/src/app/shared/types/model/ClientConfig.ts new file mode 100644 index 0000000000..031e4bb55a --- /dev/null +++ b/frontend/src/app/shared/types/model/ClientConfig.ts @@ -0,0 +1,17 @@ +export interface AuthConfig { + activeProfile: string; + issuer: string; + clientId: string; +} + +export interface CustomStyles { + [key: string]: string; +} + +export interface CustomizationConfig { + title: string; + favicon: string; + logo: string; + customStyles: CustomStyles; +} +export interface ClientConfig extends AuthConfig, CustomizationConfig {} diff --git a/frontend/src/assets/images/empty.svg b/frontend/src/assets/images/empty.svg new file mode 100644 index 0000000000..2a63845fcf --- /dev/null +++ b/frontend/src/assets/images/empty.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/index.html b/frontend/src/index.html index c5a7a864f9..7d125adb4c 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -2,10 +2,10 @@ - Puzzle OKR + OKR Tool - + diff --git a/frontend/src/style/_variables.scss b/frontend/src/style/_variables.scss index 6cb19bf774..8fe6a1340c 100644 --- a/frontend/src/style/_variables.scss +++ b/frontend/src/style/_variables.scss @@ -47,4 +47,6 @@ $pz-dark-blue-palette: ( --mdc-text-button-label-text-tracking: normal; --mdc-filled-button-label-text-tracking: normal; --mdc-outlined-button-label-text-tracking: normal; + + --okr-topbar-background-color: #1e5a96; }