Skip to content

Commit

Permalink
feat(core): configurable styles for whole platform (#1953)
Browse files Browse the repository at this point in the history
this enables user-configured "white-label" design of the system in custom colors

MIGRATION NECESSARY: see PR #1953

closes #1949
---------

Co-authored-by: Sebastian <sebastian@aam-digital.com>
  • Loading branch information
TheSlimvReal and sleidig authored Sep 8, 2023
1 parent d5ddd29 commit 5bc0b08
Show file tree
Hide file tree
Showing 63 changed files with 1,219 additions and 741 deletions.
498 changes: 201 additions & 297 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@angular/platform-browser-dynamic": "^16.2.1",
"@angular/router": "^16.2.1",
"@angular/service-worker": "^16.2.1",
"@aytek/material-color-picker": "^1.0.4",
"@casl/ability": "^6.5.0",
"@casl/angular": "^8.2.1",
"@faker-js/faker": "^8.0.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
@use "src/styles/mixins/grid-layout";
@use "src/styles/variables/sizes";
@use "@angular/material" as mat;
@use "src/styles/variables/ndb-light-theme" as theme;
@use "src/styles/variables/colors";

.top-control {
position: sticky;
Expand All @@ -19,7 +18,7 @@
padding-right: sizes.$margin-main-view-right;
margin-top: - sizes.$margin-main-view-top;
padding-top: sizes.$margin-main-view-top;
background-color: mat.get-color-from-palette(theme.$primary, 50);
background-color: colors.$background;
}

.cards-list {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,9 @@
[class.inactive]="!entity.isActive"
class="truncate-text container"
>
<app-fa-dynamic-icon
*ngIf="!imgPath"
[icon]="icon"
class="margin-right-small"
/>
<img *ngIf="imgPath" [src]="imgPath" class="child-pic" alt="" />
<app-display-img [entity]="entity" imgProperty="photo" [defaultIcon]="icon" class="child-pic"></app-display-img>
<span class="font-size-rel">{{ entity?.toString() }}</span>
<span class="subnote" *ngIf="entity?.projectNumber">
({{ entity?.projectNumber }})</span
>
<span class="subnote" *ngIf="entity?.projectNumber"> ({{ entity?.projectNumber }})</span>
</span>

<ng-template #tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@
object-fit: cover;
margin-right: 4px;
vertical-align: middle;
}

.child-pic-large {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
overflow: hidden;
}

.inactive {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,25 @@ import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { ChildBlockComponent } from "./child-block.component";
import { ChildrenService } from "../children.service";
import { Child } from "../model/child";
import { FileService } from "app/features/file/file.service";
import { of } from "rxjs";
import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing";
import { FileService } from "../../../features/file/file.service";

describe("ChildBlockComponent", () => {
let component: ChildBlockComponent;
let fixture: ComponentFixture<ChildBlockComponent>;
let mockChildrenService: jasmine.SpyObj<ChildrenService>;
let mockFileService: jasmine.SpyObj<FileService>;

beforeEach(waitForAsync(() => {
mockChildrenService = jasmine.createSpyObj("mockChildrenService", [
"getChild",
]);
mockChildrenService.getChild.and.resolveTo(new Child(""));
mockFileService = jasmine.createSpyObj(["loadFile"]);
mockFileService.loadFile.and.returnValue(of("success"));

TestBed.configureTestingModule({
imports: [ChildBlockComponent, FontAwesomeTestingModule],
providers: [
{ provide: ChildrenService, useValue: mockChildrenService },
{ provide: FileService, useValue: mockFileService },
{ provide: FileService, useValue: undefined },
],
}).compileComponents();
}));
Expand All @@ -40,24 +36,4 @@ describe("ChildBlockComponent", () => {
it("should create", () => {
expect(component).toBeTruthy();
});

it("should reset picture if child has none", async () => {
const withPicture = new Child();
withPicture.photo = "some-picture";
component.entity = withPicture;

await component.ngOnChanges({ entity: undefined });

expect(mockFileService.loadFile).toHaveBeenCalled();
expect(component.imgPath).toBeDefined();

mockFileService.loadFile.calls.reset();
// without picture
component.entity = new Child();

await component.ngOnChanges({ entity: undefined });

expect(mockFileService.loadFile).not.toHaveBeenCalled();
expect(component.imgPath).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import { DynamicComponent } from "../../../core/config/dynamic-components/dynami
import { NgIf } from "@angular/common";
import { TemplateTooltipDirective } from "../../../core/common-components/template-tooltip/template-tooltip.directive";
import { ChildBlockTooltipComponent } from "./child-block-tooltip/child-block-tooltip.component";
import { SafeUrl } from "@angular/platform-browser";
import { FileService } from "../../../features/file/file.service";
import { FaDynamicIconComponent } from "../../../core/common-components/fa-dynamic-icon/fa-dynamic-icon.component";
import { DisplayImgComponent } from "../../../features/file/display-img/display-img.component";

@DynamicComponent("ChildBlock")
@Component({
Expand All @@ -24,7 +23,7 @@ import { FaDynamicIconComponent } from "../../../core/common-components/fa-dynam
NgIf,
TemplateTooltipDirective,
ChildBlockTooltipComponent,
FaDynamicIconComponent,
DisplayImgComponent,
],
standalone: true,
})
Expand All @@ -38,7 +37,6 @@ export class ChildBlockComponent implements OnChanges {
/** prevent additional details to be displayed in a tooltip on mouse over */
@Input() tooltipDisabled: boolean;

imgPath: SafeUrl;
icon = Child.icon;

constructor(
Expand All @@ -50,11 +48,5 @@ export class ChildBlockComponent implements OnChanges {
if (changes.hasOwnProperty("entityId")) {
this.entity = await this.childrenService.getChild(this.entityId);
}
this.imgPath = undefined;
if (this.entity?.photo) {
this.fileService
.loadFile(this.entity, "photo")
.subscribe((res) => (this.imgPath = res));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import { ConfigurableEnumValue } from "../../../core/basic-datatypes/configurabl
* providing special context for {@link Note} categories.
*/
export interface InteractionType extends ConfigurableEnumValue {
/** color highlighting the individual category */
color?: string;

/** whether the Note is a group type category that stores attendance details for each related person */
isMeeting?: boolean;
}
Expand Down
2 changes: 0 additions & 2 deletions src/app/core/alerts/_alert-style-classes.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
@use "@angular/material" as mat;
@use "src/styles/variables/ndb-light-theme" as theme;
@use "src/styles/variables/colors";

.ndb-alert {
Expand Down
25 changes: 24 additions & 1 deletion src/app/core/analytics/analytics.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { TestBed } from "@angular/core/testing";

import { AnalyticsService } from "./analytics.service";
import { Angulartics2Matomo, Angulartics2Module } from "angulartics2";
import {
Angulartics2,
Angulartics2Matomo,
Angulartics2Module,
} from "angulartics2";
import { RouterTestingModule } from "@angular/router/testing";
import { ConfigService } from "../config/config.service";
import { UsageAnalyticsConfig } from "./usage-analytics-config";
import { Subject } from "rxjs";
import { Config } from "../config/config";
import { SiteSettingsService } from "../site-settings/site-settings.service";

describe("AnalyticsService", () => {
let service: AnalyticsService;

let mockConfigService: jasmine.SpyObj<ConfigService>;
const configUpdates = new Subject();
let mockMatomo: jasmine.SpyObj<Angulartics2Matomo>;
let mockAngulartics: jasmine.SpyObj<Angulartics2>;
let siteNameSubject = new Subject();

beforeEach(() => {
mockConfigService = jasmine.createSpyObj(
Expand All @@ -25,13 +32,21 @@ describe("AnalyticsService", () => {
"setUsername",
"startTracking",
]);
mockAngulartics = jasmine.createSpyObj([], {
setUserProperties: { next: jasmine.createSpy() },
});

TestBed.configureTestingModule({
imports: [Angulartics2Module.forRoot(), RouterTestingModule],
providers: [
AnalyticsService,
{ provide: ConfigService, useValue: mockConfigService },
{ provide: Angulartics2Matomo, useValue: mockMatomo },
{ provide: Angulartics2, useValue: mockAngulartics },
{
provide: SiteSettingsService,
useValue: { siteName: siteNameSubject },
},
],
});
service = TestBed.inject(AnalyticsService);
Expand Down Expand Up @@ -108,4 +123,12 @@ describe("AnalyticsService", () => {
testAnalyticsConfig2.url + "matomo.php",
]);
});

it("should set the hostname as the organisation", () => {
service.init();

expect(mockAngulartics.setUserProperties.next).toHaveBeenCalledWith({
dimension2: location.hostname,
});
});
});
7 changes: 1 addition & 6 deletions src/app/core/analytics/analytics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
} from "./usage-analytics-config";
import { Angulartics2, Angulartics2Matomo } from "angulartics2";
import md5 from "md5";
import { UiConfig } from "../ui/ui-config";

/**
* Track usage analytics data and report it to a backend server like Matomo.
Expand Down Expand Up @@ -48,6 +47,7 @@ export class AnalyticsService {
window["_paq"].push(["trackPageView"]);
window["_paq"].push(["enableLinkTracking"]);
this.setVersion();
this.setOrganization(location.hostname);
this.setUser(undefined);
this.configService.configUpdates.subscribe(() => this.setConfigValues());
}
Expand Down Expand Up @@ -97,11 +97,6 @@ export class AnalyticsService {
if (site_id) {
window["_paq"].push(["setSiteId", site_id]);
}
const { site_name } =
this.configService.getConfig<UiConfig>("appConfig") || {};
if (site_name) {
this.setOrganization(site_name);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ describe("Schema data type: configurable-enum", () => {
const TEST_CONFIG: ConfigurableEnumConfig = [
{ id: "NONE", label: "" },
{ id: "TEST_1", label: "Category 1" },
{ id: "TEST_3", label: "Category 3", color: "#FFFFFF", isMeeting: true },
{
id: "TEST_3",
label: "Category 3",
color: "#FFFFFF",
isMeeting: true,
} as ConfigurableEnumValue,
];

@DatabaseEntity("ConfigurableEnumDatatypeTestEntity")
Expand All @@ -53,7 +58,11 @@ describe("Schema data type: configurable-enum", () => {
let enumService: jasmine.SpyObj<ConfigurableEnumService>;

beforeEach(waitForAsync(() => {
enumService = jasmine.createSpyObj(["getEnumValues", "preLoadEnums"]);
enumService = jasmine.createSpyObj([
"getEnumValues",
"preLoadEnums",
"cacheEnum",
]);
enumService.getEnumValues.and.returnValue(TEST_CONFIG);

TestBed.configureTestingModule({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Directive, Input, TemplateRef, ViewContainerRef } from "@angular/core";
import { ConfigurableEnumService } from "../configurable-enum.service";
import { ConfigurableEnumValue } from "../configurable-enum.interface";

/**
* Enumerate over all {@link ConfigurableEnumConfig} values for the given enum config id.
Expand Down Expand Up @@ -36,4 +37,19 @@ export class ConfigurableEnumDirective {
private viewContainerRef: ViewContainerRef,
private enumService: ConfigurableEnumService,
) {}

/**
* Make sure the template checker knows the type of the context with which the
* template of this directive will be rendered
* See {@link https://angular.io/guide/structural-directives#typing-the-directives-context}
* @param directive
* @param context
*/

static ngTemplateContextGuard(
directive: ConfigurableEnumDirective,
context: unknown,
): context is { $implicit: ConfigurableEnumValue } {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export namespace Ordering {
* of 'greater' or 'less' than is dependent on the concrete enum.
*/
export interface HasOrdinal {
_ordinal: number;
_ordinal?: number;
}

export function hasOrdinalValue(value: any): value is HasOrdinal {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Ordering } from "./configurable-enum-ordering";
import HasOrdinal = Ordering.HasOrdinal;

/**
* Interface specifying overall object representing an enum with all its options
* as stored in the config database
Expand All @@ -10,7 +13,7 @@ export type ConfigurableEnumConfig<
* Mandatory properties of each option of an configurable enum
* the actual object can contain additional properties in the specific context of that enum (e.g. a `color` property)
*/
export interface ConfigurableEnumValue {
export interface ConfigurableEnumValue extends HasOrdinal {
/**
* identifier that is unique among all values of the same enum and does not change even when label or other things are edited
*/
Expand All @@ -33,19 +36,12 @@ export interface ConfigurableEnumValue {
isInvalidOption?: boolean;

/**
* Optionally any number of additional properties specific to a certain enum collection.
* optional styling class that should be applied when displaying this value
*/
[x: string]: any;
style?: string;
}

export const EMPTY: ConfigurableEnumValue = {
id: "",
label: "",
};

/**
* The prefix of all enum collection entries in the config database.
*
* This prefix is concatenated with the individual enum collection's id, resulting in the full config object id.
*/
export const CONFIGURABLE_ENUM_CONFIG_PREFIX = "enum:";
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { EntityAbility } from "../../permissions/ability/entity-ability";

@Injectable({ providedIn: "root" })
export class ConfigurableEnumService {
private enums: Map<string, ConfigurableEnum>;
private enums = new Map<string, ConfigurableEnum>();

constructor(
private entityMapper: EntityMapperService,
Expand All @@ -20,7 +20,6 @@ export class ConfigurableEnumService {

async preLoadEnums() {
const allEnums = await this.entityMapper.loadType(ConfigurableEnum);
this.enums = new Map();
allEnums.forEach((entity) => this.cacheEnum(entity));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ import { DatabaseField } from "../../entity/database-field.decorator";
@DatabaseEntity("ConfigurableEnum")
export class ConfigurableEnum extends Entity {
@DatabaseField() values: ConfigurableEnumValue[] = [];
constructor(id?: string, values: ConfigurableEnumValue[] = []) {
super(id);
this.values = values;
}
}
Loading

0 comments on commit 5bc0b08

Please sign in to comment.