diff --git a/backend/src/main/java/ch/puzzle/okr/multitenancy/customization/TenantClientCustomizationProvider.java b/backend/src/main/java/ch/puzzle/okr/multitenancy/customization/TenantClientCustomizationProvider.java index 0085cedd8c..3749a7ec1f 100644 --- a/backend/src/main/java/ch/puzzle/okr/multitenancy/customization/TenantClientCustomizationProvider.java +++ b/backend/src/main/java/ch/puzzle/okr/multitenancy/customization/TenantClientCustomizationProvider.java @@ -16,14 +16,18 @@ public class TenantClientCustomizationProvider { private static final String CUSTOM_STYLES_PREFIX = "okr.tenants.{0}.clientcustomization.customstyles"; private static final String TOPBAR_BACKGROUND_COLOR = ".okr-topbar-background-color"; private static final String BANNER_BACKGROUND_COLOR = ".okr-banner-background-color"; + private static final String OVERVIEW_BACKGROUND_COLOR = ".okr-overview-background-color"; + private static final String TEAM_HEADER_COLOR = ".okr-team-header-color"; + private static final String ADD_OBJECTIVE_TEXT_COLOR = ".okr-add-objective-text-color"; + private static final String ADD_OBJECTIVE_ICON = ".okr-add-objective-icon"; + private static final String ADD_OBJECTIVE_OUTLINE_COLOR = ".okr-add-objective-outline-color"; private final Map tenantCustomizations = new HashMap<>(); - private final List customCssStyles = List.of(TOPBAR_BACKGROUND_COLOR, BANNER_BACKGROUND_COLOR); + private final List customCssStyles = List.of(TOPBAR_BACKGROUND_COLOR, BANNER_BACKGROUND_COLOR, + OVERVIEW_BACKGROUND_COLOR, TEAM_HEADER_COLOR, ADD_OBJECTIVE_TEXT_COLOR, ADD_OBJECTIVE_ICON, + ADD_OBJECTIVE_OUTLINE_COLOR); private final Environment env; - record CssConfigItem(String cssName, String cssValue) implements Serializable { - } - public TenantClientCustomizationProvider(final @Value("${okr.tenant-ids}") String[] tenantIds, Environment env) { this.env = env; for (String tenantId : tenantIds) { @@ -78,4 +82,7 @@ String extractCssNameFromPropertyName(String propertyName, String tenantId) { private String formatWithTenant(String stringWithTenantPlaceholder, String tenantId) { return MessageFormat.format(stringWithTenantPlaceholder, tenantId); } + + record CssConfigItem(String cssName, String cssValue) implements Serializable { + } } \ No newline at end of file diff --git a/backend/src/main/resources/application-demo.properties b/backend/src/main/resources/application-demo.properties index 6ca314d820..a4822a5e68 100644 --- a/backend/src/main/resources/application-demo.properties +++ b/backend/src/main/resources/application-demo.properties @@ -1,22 +1,22 @@ # logging level for staging logging.level.org.springframework=debug - spring.security.oauth2.resourceserver.opaquetoken.client-id=pitc_okr_demo - # flyway fill database with demo data spring.flyway.locations=classpath:db/migration,classpath:db/data-migration-demo,classpath:db/callback - -# pit client customization -okr.tenants.pitc.clientcustomization.logo=assets/images/okr-logo.svg +# pitc client customization +okr.tenants.pitc.clientcustomization.logo=assets/images/okr-demo-logo.svg okr.tenants.pitc.clientcustomization.triangles=assets/images/triangles-okr-header.svg okr.tenants.pitc.clientcustomization.background-logo=assets/images/puzzle-p.svg okr.tenants.pitc.clientcustomization.favicon=assets/favicon.png okr.tenants.pitc.clientcustomization.title=Puzzle OKR +okr.tenants.pitc.clientcustomization.helpSiteUrl=https://wiki.puzzle.ch/Puzzle/OKRs okr.tenants.pitc.clientcustomization.customstyles.okr-topbar-background-color=#1e5a96 okr.tenants.pitc.clientcustomization.customstyles.okr-banner-background-color=#dcedf9 -okr.tenants.pitc.clientcustomization.helpSiteUrl=https://wiki.puzzle.ch/Puzzle/OKRs - - +okr.tenants.pitc.clientcustomization.customstyles.okr-overview-background-color=#1e5a96 +okr.tenants.pitc.clientcustomization.customstyles.okr-team-header-color=#ffffff +okr.tenants.pitc.clientcustomization.customstyles.okr-add-objective-text-color=#e5e8eb +okr.tenants.pitc.clientcustomization.customstyles.okr-add-objective-icon=/assets/icons/new-icon-demo.svg +okr.tenants.pitc.clientcustomization.customstyles.okr-add-objective-outline-color=#e5e8eb # acme client customization okr.tenants.acme.clientcustomization.logo=assets/images/okr-logo-acme.svg okr.tenants.acme.clientcustomization.triangles=assets/images/triangles-okr-acme-header.svg diff --git a/backend/src/main/resources/application-dev.properties b/backend/src/main/resources/application-dev.properties index 0083f63248..0918d25dc8 100644 --- a/backend/src/main/resources/application-dev.properties +++ b/backend/src/main/resources/application-dev.properties @@ -1,27 +1,22 @@ # show actions from entity manager #spring.jpa.show-sql=true #spring.jpa.properties.hibernate.format_sql=true - # debug options logging.level.ch.puzzle.okr=DEBUG logging.level.org.springframework.security=DEBUG # server.servlet.context-path=/resource-server # flyway fill database with data spring.flyway.locations=classpath:db/migration,classpath:db/data-migration,classpath:db/callback - # TENANT Configuration okr.tenant-ids=pitc,acme okr.datasource.driver-class-name=org.postgresql.Driver - # security connect.src=http://localhost:8544 http://localhost:8545 - # hibernate hibernate.connection.url=jdbc:postgresql://localhost:5432/okr hibernate.connection.username=user hibernate.connection.password=pwd hibernate.multiTenancy=SCHEMA - # pitc okr.tenants.pitc.datasource.url=jdbc:postgresql://localhost:5432/okr okr.tenants.pitc.datasource.username=pitc @@ -31,7 +26,6 @@ okr.tenants.pitc.user.champion.emails=gl@gl.com okr.tenants.pitc.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8544/realms/pitc/protocol/openid-connect/certs okr.tenants.pitc.security.oauth2.frontend.issuer-url=http://localhost:8544/realms/pitc okr.tenants.pitc.security.oauth2.frontend.client-id=pitc_okr_staging - # acme okr.tenants.acme.datasource.url=jdbc:postgresql://localhost:5432/okr okr.tenants.acme.datasource.username=acme @@ -41,8 +35,7 @@ okr.tenants.acme.user.champion.emails=gl@acme.com okr.tenants.acme.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8545/realms/acme/protocol/openid-connect/certs okr.tenants.acme.security.oauth2.frontend.issuer-url=http://localhost:8545/realms/acme okr.tenants.acme.security.oauth2.frontend.client-id=acme_okr_staging - -# pit client customization +# pitc client customization okr.tenants.pitc.clientcustomization.logo=assets/images/okr-logo.svg okr.tenants.pitc.clientcustomization.triangles=assets/images/triangles-okr-header.svg okr.tenants.pitc.clientcustomization.background-logo=assets/images/puzzle-p.svg @@ -51,7 +44,6 @@ okr.tenants.pitc.clientcustomization.title=Puzzle OKR okr.tenants.pitc.clientcustomization.helpSiteUrl=https://wiki.puzzle.ch/Puzzle/OKRs okr.tenants.pitc.clientcustomization.customstyles.okr-topbar-background-color=#1e5a96 okr.tenants.pitc.clientcustomization.customstyles.okr-banner-background-color=#dcedf9 - # acme client customization okr.tenants.acme.clientcustomization.logo=assets/images/okr-logo-acme.svg okr.tenants.acme.clientcustomization.triangles=assets/images/triangles-okr-acme-header.svg diff --git a/backend/src/main/resources/application-staging.properties b/backend/src/main/resources/application-staging.properties index 1981bca769..57ade77501 100644 --- a/backend/src/main/resources/application-staging.properties +++ b/backend/src/main/resources/application-staging.properties @@ -44,7 +44,7 @@ okr.tenants.pitc.clientcustomization.triangles=assets/images/triangles-okr-heade okr.tenants.pitc.clientcustomization.background-logo=assets/images/puzzle-p.svg okr.tenants.pitc.clientcustomization.favicon=assets/favicon.png okr.tenants.pitc.clientcustomization.title=Puzzle OKR -okr.tenants.pitc.clientcustomization.customstyles.okr-topbar-background-color=#1e5a96 +okr.tenants.pitc.clientcustomization.customstyles.okr-topbar-background-color=#641e96 okr.tenants.pitc.clientcustomization.customstyles.okr-banner-background-color=#dcedf9 okr.tenants.pitc.clientcustomization.helpSiteUrl=https://wiki.puzzle.ch/Puzzle/OKRs diff --git a/frontend/src/app/components/application-top-bar/application-top-bar.component.html b/frontend/src/app/components/application-top-bar/application-top-bar.component.html index 4d764f97ea..549b425893 100644 --- a/frontend/src/app/components/application-top-bar/application-top-bar.component.html +++ b/frontend/src/app/components/application-top-bar/application-top-bar.component.html @@ -1,14 +1,7 @@
- okr-logo + okr-logo
diff --git a/frontend/src/app/components/overview/overview.component.scss b/frontend/src/app/components/overview/overview.component.scss index 8fa50ca098..06087ddd0a 100644 --- a/frontend/src/app/components/overview/overview.component.scss +++ b/frontend/src/app/components/overview/overview.component.scss @@ -1,8 +1,6 @@ -@import "../style/variables"; - .overview-container { overflow-x: hidden; - background-color: $overview-bg; + background: var(--okr-overview-background-color); transition: 0.5s; } @@ -13,3 +11,6 @@ .puzzle-logo { filter: invert(38%) sepia(31%) saturate(216%) hue-rotate(167deg) brightness(96%) contrast(85%); } +#no-team-text { + color: var(--okr-team-header-color); +} diff --git a/frontend/src/app/components/team/team.component.html b/frontend/src/app/components/team/team.component.html index dfd506b2a9..0a5d37f4cc 100644 --- a/frontend/src/app/components/team/team.component.html +++ b/frontend/src/app/components/team/team.component.html @@ -1,4 +1,4 @@ -
+

{{ OVEntity.team.name }}

@@ -6,13 +6,19 @@

{{ OVEntity.team.name }}

@@ -30,7 +36,6 @@

{{ OVEntity.team.name }}

class="col-xs-12 col-md-6 col-xl-4 pb-3 column-width" id="objective-column" > -
diff --git a/frontend/src/app/components/team/team.component.scss b/frontend/src/app/components/team/team.component.scss index a6c4cbadf4..f382215cc0 100644 --- a/frontend/src/app/components/team/team.component.scss +++ b/frontend/src/app/components/team/team.component.scss @@ -10,3 +10,16 @@ align-items: center; justify-content: center; } + +.team-title { + color: var(--okr-team-header-color); +} + +#add-objective-span { + color: var(--okr-add-objective-text-color); +} + +#add-objective:hover, +#add-objective:focus { + outline: 1px solid var(--okr-add-objective-outline-color); +} diff --git a/frontend/src/app/components/team/team.component.spec.ts b/frontend/src/app/components/team/team.component.spec.ts index c822303ec7..0ceb06657d 100644 --- a/frontend/src/app/components/team/team.component.spec.ts +++ b/frontend/src/app/components/team/team.component.spec.ts @@ -7,7 +7,6 @@ import { RouterTestingModule } from '@angular/router/testing'; import { MatMenuModule } from '@angular/material/menu'; import { KeyresultComponent } from '../keyresult/keyresult.component'; import { MatDialogModule } from '@angular/material/dialog'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; import { By } from '@angular/platform-browser'; import { RefreshDataService } from '../../services/refresh-data.service'; import { TranslateTestingModule } from 'ngx-translate-testing'; @@ -18,11 +17,30 @@ import { ConfidenceComponent } from '../confidence/confidence.component'; import { ScoringComponent } from '../../shared/custom/scoring/scoring.component'; import { ChangeDetectionStrategy } from '@angular/core'; import { DialogService } from '../../services/dialog.service'; +import { ConfigService } from '../../services/config.service'; +import { of } from 'rxjs'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; const dialogService = { open: jest.fn(), }; +const configServiceDefined = { + config$: of({ + customStyles: { + 'okr-add-objective-icon': 'new-icon-from-config-service.svg', + }, + }), +}; + +const configServiceUndefined = { + config$: of({ + customStyles: { + 'okr-add-objective-icon': undefined, + }, + }), +}; + const refreshDataServiceMock = { markDataRefresh: jest.fn(), }; @@ -37,7 +55,6 @@ describe('TeamComponent', () => { RouterTestingModule, MatMenuModule, MatDialogModule, - HttpClientTestingModule, MatTooltipModule, TranslateTestingModule.withTranslations({ de: de, @@ -50,10 +67,15 @@ describe('TeamComponent', () => { provide: DialogService, useValue: dialogService, }, + { + provide: ConfigService, + useValue: configServiceDefined, + }, { provide: RefreshDataService, useValue: refreshDataServiceMock, }, + provideHttpClient(withInterceptorsFromDi()), ], }) .overrideComponent(TeamComponent, { @@ -85,4 +107,49 @@ describe('TeamComponent', () => { const button = fixture.debugElement.query(By.css('[data-testId="add-objective"]')); expect(button).toBeFalsy(); }); + + it('should set value of addIconSrc if src from config service is defined', () => { + expect(component.addIconSrc.value).toBe('new-icon-from-config-service.svg'); + const addObjectiveIcon = fixture.debugElement.query(By.css('[data-testId="add-objective-icon"]')); + expect(addObjectiveIcon.attributes['src']).toBe('new-icon-from-config-service.svg'); + }); +}); + +describe('TeamComponent undefined values in config service', () => { + let component: TeamComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + MatMenuModule, + TranslateTestingModule.withTranslations({ + de: de, + }), + ], + declarations: [TeamComponent], + providers: [ + { + provide: ConfigService, + useValue: configServiceUndefined, + }, + provideHttpClient(withInterceptorsFromDi()), + ], + }) + .overrideComponent(TeamComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(TeamComponent); + component = fixture.componentInstance; + component.overviewEntity = overViewEntity1; + fixture.detectChanges(); + }); + + it('should keep default value of addIconSrc if src from config service is undefined', () => { + expect(component.addIconSrc.value).toBe('assets/icons/new-icon.svg'); + const addObjectiveIcon = fixture.debugElement.query(By.css('[data-testId="add-objective-icon"]')); + expect(addObjectiveIcon.attributes['src']).toBe('assets/icons/new-icon.svg'); + }); }); diff --git a/frontend/src/app/components/team/team.component.ts b/frontend/src/app/components/team/team.component.ts index ae17673f4b..2ea72495ca 100644 --- a/frontend/src/app/components/team/team.component.ts +++ b/frontend/src/app/components/team/team.component.ts @@ -6,6 +6,9 @@ import { Objective } from '../../shared/types/model/Objective'; import { KeyresultDialogComponent } from '../keyresult-dialog/keyresult-dialog.component'; import { ObjectiveMin } from '../../shared/types/model/ObjectiveMin'; import { DialogService } from '../../services/dialog.service'; +import { ConfigService } from '../../services/config.service'; +import { ClientConfig } from '../../shared/types/model/ClientConfig'; +import { BehaviorSubject, first } from 'rxjs'; @Component({ selector: 'app-team', @@ -13,19 +16,24 @@ import { DialogService } from '../../services/dialog.service'; styleUrls: ['./team.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class TeamComponent implements OnInit { +export class TeamComponent { @Input({ required: true }) public overviewEntity!: OverviewEntity; + addIconSrc: BehaviorSubject = new BehaviorSubject('assets/icons/new-icon.svg'); constructor( private dialogService: DialogService, private refreshDataService: RefreshDataService, - ) {} + private configService: ConfigService, + ) { + this.configService.config$.pipe(first()).subscribe((config: ClientConfig) => { + const configuredIconSrc = config.customStyles['okr-add-objective-icon']; + if (configuredIconSrc) this.addIconSrc.next(configuredIconSrc); + }); + } trackByObjectiveId: TrackByFunction = (index, objective) => objective.id; - ngOnInit(): void {} - createObjective() { const matDialogRef = this.dialogService.open(ObjectiveFormComponent, { data: { diff --git a/frontend/src/assets/icons/new-icon-demo.svg b/frontend/src/assets/icons/new-icon-demo.svg new file mode 100644 index 0000000000..0e915a6459 --- /dev/null +++ b/frontend/src/assets/icons/new-icon-demo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/images/okr-demo-logo.svg b/frontend/src/assets/images/okr-demo-logo.svg new file mode 100644 index 0000000000..da8db3e2a8 --- /dev/null +++ b/frontend/src/assets/images/okr-demo-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/style/_variables.scss b/frontend/src/style/_variables.scss index 3a0e3f75f6..708d48ea2b 100644 --- a/frontend/src/style/_variables.scss +++ b/frontend/src/style/_variables.scss @@ -11,8 +11,6 @@ $dark-grey-border: #5d6974; $top-bar-height: 48px; -$overview-bg: #ffffff; - $keyResult-attribute-bg: #ffffff; $keyResult-attribute-edit-border: #ddddddff; @@ -57,4 +55,8 @@ $pz-dark-blue-palette: ( --mat-dialog-with-actions-content-padding: #{$dialog-content-padding-y} 12px; --okr-topbar-background-color: #1e5a96; --okr-banner-background-color: #dcedf9; + --okr-overview-background-color: #ffffff; + --okr-team-header-color: #000000; + --okr-add-objective-text-color: #1e5a96; + --okr-add-objective-outline-color: none; } diff --git a/frontend/src/style/styles.scss b/frontend/src/style/styles.scss index 143795cce6..47fa44031b 100644 --- a/frontend/src/style/styles.scss +++ b/frontend/src/style/styles.scss @@ -9,7 +9,7 @@ body.okr { font-size: 100%; margin: 0; padding: 0; - background-color: $overview-bg; + background: var(--okr-overview-background-color); font-family: Roboto, "sans-serif"; --bs-body-font-family: Roboto; @@ -120,10 +120,6 @@ body.okr { @extend .py-1; } -.bg-grey { - background-color: $overview-bg; -} - body pzsh-backdrop { display: none; } @@ -319,7 +315,7 @@ img.add-cross-button { height: min-content; z-index: 100; transition: 0.5s; - background-color: var(--okr-banner-background-color); + background: var(--okr-banner-background-color); } app-team-management-banner > #okrBanner { @@ -331,6 +327,7 @@ table.okr-table { border-spacing: 0 0.5rem; border: none; box-shadow: none; + tr { $table-height: 40px; height: $table-height; @@ -340,6 +337,7 @@ table.okr-table { border: none; background-color: $light-grey; } + > th { font-family: Roboto, "sans-serif"; font-variation-settings: "wght" 700; @@ -349,6 +347,7 @@ table.okr-table { background-color: white; } } + tbody { tr:hover { box-shadow: 0 0 0 1px $dark-grey; @@ -359,6 +358,7 @@ table.okr-table { .search-form { button { border-radius: 0 4px 4px 0; + mat-icon { display: flex; align-items: center; @@ -366,11 +366,13 @@ table.okr-table { transform: scale(1.25); } } + .mdc-notched-outline__trailing { border-bottom-right-radius: 0 !important; border-top-right-radius: 0 !important; } } + .mat-mdc-autocomplete-panel { &.autocomplete-bigger { max-height: 480px;