diff --git a/projects/netgrif-components-core/src/lib/navigation/dashboard/abstract-dashboard.component.ts b/projects/netgrif-components-core/src/lib/navigation/dashboard/abstract-dashboard.component.ts
index 0c3be28ad..6b5bf88f1 100644
--- a/projects/netgrif-components-core/src/lib/navigation/dashboard/abstract-dashboard.component.ts
+++ b/projects/netgrif-components-core/src/lib/navigation/dashboard/abstract-dashboard.component.ts
@@ -33,6 +33,7 @@ export abstract class AbstractDashboardComponent {
public static readonly DASHBOARD_MANAGEMENT_LOGOUT_TOOLBAR_DATAFIELD = 'logout_dashboard_toolbar';
public static readonly DASHBOARD_MANAGEMENT_NAME_DATAFIELD = 'dashboard_name';
public static readonly DASHBOARD_MANAGEMENT_LOGO_DATAFIELD = 'dashboard_logo';
+ public static readonly DASHBOARD_MANAGEMENT_COLOR_DATAFIELD = 'toolbar_color';
public static readonly DASHBOARD_MANAGEMENT_ITEMS_ORDER_DATAFIELD = "items_order";
public static readonly DASHBOARD_MANAGEMENT_PROFILE_URL_DATAFIELD = "profile_url";
public static readonly DASHBOARD_MANAGEMENT_LOGIN_URL_DATAFIELD = "login_url";
@@ -105,7 +106,8 @@ export abstract class AbstractDashboardComponent {
toolbarName: this.getManagementName(this.dashboardCase),
toolbarLogo: this.getManagementLogo(this.dashboardCase),
profileUrl: this.getManagementProfileUrl(this.dashboardCase),
- loginUrl: this.getManagementLoginUrl(this.dashboardCase)
+ loginUrl: this.getManagementLoginUrl(this.dashboardCase),
+ color: this.getManagementColor(this.dashboardCase),
}
const dashboardItemsOptions = this.dashboardCase.immediateData
@@ -201,7 +203,7 @@ export abstract class AbstractDashboardComponent {
public getItemFontColor(itemCase: Case): string {
const fontColor = this.getFieldValue(itemCase, AbstractDashboardComponent.DASHBOARD_ITEM_FONT_COLOR_DATAFIELD) as string;
- return !!fontColor && fontColor !== '' ? fontColor : 'black';
+ return !!fontColor && fontColor !== '' ? fontColor : null;
}
public getItemFontWeight(itemCase: Case): string {
@@ -211,7 +213,7 @@ export abstract class AbstractDashboardComponent {
public getItemIconColor(itemCase: Case): string {
const iconColor = this.getFieldValue(itemCase, AbstractDashboardComponent.DASHBOARD_ITEM_ICON_COLOR_DATAFIELD);
- return !!iconColor && iconColor !== '' ? iconColor : 'black';
+ return !!iconColor && iconColor !== '' ? iconColor : null;
}
// GET management fields
@@ -267,4 +269,8 @@ export abstract class AbstractDashboardComponent {
window.open(this.getItemURL(itemCase), "_blank");
}
}
+
+ private getManagementColor(itemCase: Case): string {
+ return this.getFieldValue(itemCase, AbstractDashboardComponent.DASHBOARD_MANAGEMENT_COLOR_DATAFIELD) as string;
+ }
}
diff --git a/projects/netgrif-components-core/src/lib/navigation/navigation-double-drawer/abstract-navigation-double-drawer.ts b/projects/netgrif-components-core/src/lib/navigation/navigation-double-drawer/abstract-navigation-double-drawer.ts
index 1cdc0a22d..e7f92d121 100644
--- a/projects/netgrif-components-core/src/lib/navigation/navigation-double-drawer/abstract-navigation-double-drawer.ts
+++ b/projects/netgrif-components-core/src/lib/navigation/navigation-double-drawer/abstract-navigation-double-drawer.ts
@@ -1,10 +1,10 @@
import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
import {Component, EventEmitter, Inject, Input, OnDestroy, OnInit, Output, TemplateRef} from '@angular/core';
-import {ActivatedRoute, Router} from '@angular/router';
+import {ActivatedRoute, IsActiveMatchOptions, NavigationEnd, Router} from '@angular/router';
import {TranslateService} from '@ngx-translate/core';
import {ResizeEvent} from 'angular-resizable-element';
-import {Observable, Subscription} from 'rxjs';
-import {filter, take, switchMap} from 'rxjs/operators';
+import {combineLatest, Observable, Subscription} from 'rxjs';
+import {filter, take, switchMap, startWith} from 'rxjs/operators';
import {AccessService} from '../../authorization/permission/access.service';
import {ConfigurationService} from '../../configuration/configuration.service';
import {ImpersonationUserSelectService} from '../../impersonation/services/impersonation-user-select.service';
@@ -96,6 +96,25 @@ export abstract class AbstractNavigationDoubleDrawerComponent implements OnInit,
protected itemClickedSub: Subscription;
protected itemLoadedSub: Subscription;
protected rightItemsSub: Subscription;
+ protected activeParentSub: Subscription;
+
+ /**
+ * Id of the left side item (parent folder), whose child view is currently rendered by the router.
+ * Undefined when none of the displayed child views matches the active route.
+ * */
+ protected _activeParentItemId: string;
+
+ /**
+ * Route matching rules used to resolve the active child view. They mirror the defaults of
+ * `routerLinkActive` (without `[routerLinkActiveOptions]="{exact: true}"`), so that the parent
+ * folder is highlighted for exactly the same routes as its child item.
+ * */
+ protected readonly activeMatchOptions: IsActiveMatchOptions = {
+ paths: 'subset',
+ queryParams: 'subset',
+ fragment: 'ignored',
+ matrixParams: 'ignored',
+ };
protected constructor(protected _router: Router,
protected _activatedRoute: ActivatedRoute,
@@ -140,6 +159,15 @@ export abstract class AbstractNavigationDoubleDrawerComponent implements OnInit,
}
});
+ this.activeParentSub = combineLatest([
+ this._router.events.pipe(filter(event => event instanceof NavigationEnd), startWith(null)),
+ this.leftItems$,
+ this.rightItems$,
+ this.moreItems$,
+ ]).subscribe(() => {
+ this._activeParentItemId = this.resolveActiveParentItemId();
+ });
+
if (this.canApplyAutoSelect()) {
this.rightItemsSub = this.rightItems$.pipe(
filter(rightItems => rightItems.length > 0),
@@ -166,6 +194,7 @@ export abstract class AbstractNavigationDoubleDrawerComponent implements OnInit,
this.itemClickedSub.unsubscribe();
this.itemLoadedSub.unsubscribe();
this.rightItemsSub?.unsubscribe();
+ this.activeParentSub?.unsubscribe();
}
public get currentNode(): UriNodeResource {
@@ -349,6 +378,51 @@ export abstract class AbstractNavigationDoubleDrawerComponent implements OnInit,
return DoubleDrawerUtils.isItemAndNodeEqual(item, node);
}
+ /**
+ * Resolves whether the given left side item is the parent folder of the view that is currently
+ * rendered by the router. Used to highlight the parent of the selected view the same way the
+ * selected view itself is highlighted by `routerLinkActive`.
+ *
+ * @param item an item of the left side menu
+ * @returns true if one of the currently displayed children of the item is the active route
+ * */
+ public isParentOfActiveView(item: NavigationItem): boolean {
+ return !!item && !!this._activeParentItemId && item.id === this._activeParentItemId;
+ }
+
+ /**
+ * Resolves whether the item's view is the one currently rendered by the router.
+ *
+ * @param item any navigation item holding a routing path
+ * */
+ public isItemRouteActive(item: NavigationItem): boolean {
+ const path = item?.routing?.path;
+ if (!path) {
+ return false;
+ }
+ // the url tree is built the same way `routerLink` builds it in the template,
+ // so both absolute and route relative paths are resolved identically
+ return this._router.isActive(
+ this._router.createUrlTree([path], {relativeTo: this._activatedRoute}),
+ this.activeMatchOptions,
+ );
+ }
+
+ /**
+ * The right side items (including the ones hidden behind the "load more" button) are the children of
+ * [currentNode]{@link AbstractNavigationDoubleDrawerComponent#currentNode}. Their parent folder on the
+ * left side is therefore the left item that corresponds to the current node.
+ *
+ * @returns the id of the left side item whose child view is active, or undefined if no child view is active
+ * */
+ protected resolveActiveParentItemId(): string | undefined {
+ const children = [...(this.rightItems ?? []), ...(this.moreItems ?? [])];
+ if (!children.some(child => this.isItemRouteActive(child))) {
+ return undefined;
+ }
+ return this.leftItems?.find(item => this.isItemAndNodeEqual(item, this.currentNode))?.id;
+ }
+
public setMenuEditMode(newVal: boolean): void {
this.editModeEnabled = newVal;
}
diff --git a/projects/netgrif-components-core/src/lib/navigation/navigation-double-drawer/util/double-drawer-utils.ts b/projects/netgrif-components-core/src/lib/navigation/navigation-double-drawer/util/double-drawer-utils.ts
index f3c529eee..6268adf37 100644
--- a/projects/netgrif-components-core/src/lib/navigation/navigation-double-drawer/util/double-drawer-utils.ts
+++ b/projects/netgrif-components-core/src/lib/navigation/navigation-double-drawer/util/double-drawer-utils.ts
@@ -23,7 +23,11 @@ export class DoubleDrawerUtils {
}
public static isItemAndNodeEqual(item: NavigationItem, node: UriNodeResource): boolean {
- return item.resource?.immediateData.find(f => f.stringId === GroupNavigationConstants.ITEM_FIELD_ID_NODE_PATH)?.value === node.uriPath;
+ if (!item || !node) {
+ return false;
+ }
+ const itemNodePath = item.resource?.immediateData?.find(f => f.stringId === GroupNavigationConstants.ITEM_FIELD_ID_NODE_PATH)?.value;
+ return !!itemNodePath && itemNodePath === node.uriPath;
}
public static extractChildCaseIds(item: Case): string[] {
@@ -49,7 +53,7 @@ export class DoubleDrawerUtils {
}
public static isNodeCorrespondingToItem(node: UriNodeResource, item: NavigationItem): boolean {
- return item.resource?.immediateData.find(f => f.stringId === GroupNavigationConstants.ITEM_FIELD_ID_NODE_PATH)?.value === node.uriPath
+ return DoubleDrawerUtils.isItemAndNodeEqual(item, node);
}
public static findTaskIdInCase(useCase: Case, transId: string): string {
diff --git a/projects/netgrif-components-core/src/lib/toolbar/abstract-toolbar.component.ts b/projects/netgrif-components-core/src/lib/toolbar/abstract-toolbar.component.ts
index b7f08b387..3d2365bf6 100644
--- a/projects/netgrif-components-core/src/lib/toolbar/abstract-toolbar.component.ts
+++ b/projects/netgrif-components-core/src/lib/toolbar/abstract-toolbar.component.ts
@@ -4,6 +4,7 @@ import {LanguageService} from '../translate/language.service';
import {ToolbarConfig} from './toolbar-config';
import {UserService} from '../user/services/user.service';
import {Router} from '@angular/router';
+import {ThemePalette} from '@angular/material/core';
import {User} from '../user/models/user';
import {RedirectService} from '../routing/redirect-service/redirect.service';
@@ -62,6 +63,20 @@ export abstract class AbstractToolbarComponent {
return this.loggedUser.isImpersonating();
}
+ /**
+ * @returns the background color defined in `toolbarConfig.color`, or `null` if it is not defined
+ */
+ public get toolbarCustomColor(): string | null {
+ return this.toolbarConfig?.color?.trim() || null;
+ }
+
+ /**
+ * @returns `primary` theme palette as a fallback, only when `toolbarConfig.color` is not defined
+ */
+ public get toolbarThemeColor(): ThemePalette {
+ return this.toolbarCustomColor ? undefined : 'primary';
+ }
+
public getToolbarTitle(): string {
return this.selectLangService.getLanguage() in this.toolbarConfig.toolbarName.translations
? this.toolbarConfig.toolbarName.translations[this.selectLangService.getLanguage()]
diff --git a/projects/netgrif-components-core/src/lib/toolbar/toolbar-config.ts b/projects/netgrif-components-core/src/lib/toolbar/toolbar-config.ts
index 432bd37b9..1f5f44e35 100644
--- a/projects/netgrif-components-core/src/lib/toolbar/toolbar-config.ts
+++ b/projects/netgrif-components-core/src/lib/toolbar/toolbar-config.ts
@@ -9,4 +9,5 @@ export interface ToolbarConfig {
toolbarLogo?: string;
profileUrl?: string;
loginUrl?: string;
+ color?: string;
}
diff --git a/projects/netgrif-components/src/lib/builder/form-builder/gridster/gridster-datafield/gridster-data-field.component.html b/projects/netgrif-components/src/lib/builder/form-builder/gridster/gridster-datafield/gridster-data-field.component.html
index c1776c0ce..eeaf125b6 100644
--- a/projects/netgrif-components/src/lib/builder/form-builder/gridster/gridster-datafield/gridster-data-field.component.html
+++ b/projects/netgrif-components/src/lib/builder/form-builder/gridster/gridster-datafield/gridster-data-field.component.html
@@ -3,7 +3,8 @@
class="placeholder-text">{{ 'builder.form-builder.gridster.placeholderFor' | translate }} {{dataField.dataVariable.type}} {{ 'builder.form-builder.gridster.withId' | translate }} '{{dataField.dataVariable?.id}}'