From aa32a8248016202989b909fec437f5a304213d8d Mon Sep 17 00:00:00 2001 From: palajsamuel Date: Wed, 30 Sep 2026 15:10:45 +0200 Subject: [PATCH] [NAE-2468] UI/UX fixes & improvements - text overflow fixes in double navigation-double-drawer.component.html, dashboard.component.html and gridster.component.html - double drawer nested path highlighting fix - table view mat-menu styling changes - added option to change dashboard toolbar color --- .../dashboard/abstract-dashboard.component.ts | 12 ++- .../abstract-navigation-double-drawer.ts | 80 ++++++++++++++++++- .../util/double-drawer-utils.ts | 8 +- .../lib/toolbar/abstract-toolbar.component.ts | 15 ++++ .../src/lib/toolbar/toolbar-config.ts | 1 + .../gridster-data-field.component.html | 3 +- .../gridster-data-field.component.ts | 10 ++- .../gridster/gridster.component.html | 3 +- .../gridster/gridster.component.ts | 18 +++++ .../src/lib/header/header.component.html | 12 +-- .../src/lib/header/header.component.scss | 41 +++++----- .../dashboard/dashboard.component.html | 5 +- .../dashboard/dashboard.component.scss | 6 ++ .../navigation-double-drawer.component.html | 23 +++--- .../navigation-double-drawer.component.scss | 28 +++++++ .../src/lib/navigation/navigation.theme.scss | 10 ++- .../language-selector.component.html | 2 +- .../language-selector.component.scss | 9 +++ .../src/lib/toolbar/toolbar.component.html | 2 +- 19 files changed, 239 insertions(+), 49 deletions(-) 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 a9bb57116..909a5dcd6 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 @@ -22,7 +22,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[] { @@ -48,7 +52,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 bfa711b65..105250418 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">Placeholder for {{dataField.dataVariable.type}} with id '{{dataField.dataVariable?.id}}' + [additionalFieldProperties]="{taskId: ''}" [dataField]="engineField" [offset]="0" fxFlex + [ngClass]="{'date-field-fix': isDateField()}">
diff --git a/projects/netgrif-components/src/lib/builder/form-builder/gridster/gridster-datafield/gridster-data-field.component.ts b/projects/netgrif-components/src/lib/builder/form-builder/gridster/gridster-datafield/gridster-data-field.component.ts index 1d87fc83a..a279eece0 100644 --- a/projects/netgrif-components/src/lib/builder/form-builder/gridster/gridster-datafield/gridster-data-field.component.ts +++ b/projects/netgrif-components/src/lib/builder/form-builder/gridster/gridster-datafield/gridster-data-field.component.ts @@ -1,5 +1,5 @@ import {Component, Input, OnDestroy, OnInit, QueryList, TemplateRef, ViewChildren} from '@angular/core'; -import {DataField, EnumerationField, MultichoiceField} from '@netgrif/components-core'; +import {DataField, EnumerationField, MultichoiceField, DateField, DateTimeField} from '@netgrif/components-core'; import {DataRefBehavior, DataType, I18nWithDynamic} from '@netgrif/petriflow'; import moment from 'moment'; import {Subscription} from 'rxjs'; @@ -76,6 +76,14 @@ export class GridsterDataFieldComponent implements OnInit, OnDestroy { return this.dataField.dataRef.logic.behavior === DataRefBehavior.HIDDEN; } + /** + * Mirrors the `date-field-fix` wrapper class applied by `FieldComponentResolverComponent` + * in the regular task view, so the datepicker toggle does not overlap the field title. + */ + public isDateField(): boolean { + return this.engineField instanceof DateField || this.engineField instanceof DateTimeField; + } + public name(): string { return `${this.dataField?.dataVariable?.title?.value} [${this.dataField?.dataVariable?.id}]`; } diff --git a/projects/netgrif-components/src/lib/builder/form-builder/gridster/gridster.component.html b/projects/netgrif-components/src/lib/builder/form-builder/gridster/gridster.component.html index 143a68ba7..cb876549d 100644 --- a/projects/netgrif-components/src/lib/builder/form-builder/gridster/gridster.component.html +++ b/projects/netgrif-components/src/lib/builder/form-builder/gridster/gridster.component.html @@ -1,10 +1,11 @@
- +
-
+ +
{{ 'headers.overflowMode' | translate}}
-
- + {{ 'headers.columnWidth' | translate}} {{getErrorMessageWidth()}}
-
- + {{ 'headers.columnCount' | translate}} {{getErrorMessageCount()}} diff --git a/projects/netgrif-components/src/lib/header/header.component.scss b/projects/netgrif-components/src/lib/header/header.component.scss index 9727b13d9..0461e148a 100644 --- a/projects/netgrif-components/src/lib/header/header.component.scss +++ b/projects/netgrif-components/src/lib/header/header.component.scss @@ -19,27 +19,30 @@ flex: 1 1 5%; } -.mat-mdc-menu-item-look { - user-select: none; - cursor: pointer; - outline: none; - border: none; - -webkit-tap-highlight-color: transparent; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: block; - line-height: 48px; - height: 48px; +.header-menu-row { + box-sizing: border-box; + display: flex; + align-items: center; padding: 0 16px; - text-align: left; - text-decoration: none; - max-width: 100%; - position: relative; font-size: 14px; + user-select: none; } -.mat-mdc-menu-item-height { - min-height: 64px; - height: auto; +.header-menu-toggle { + min-height: 48px; + cursor: pointer; +} + +.header-menu-field { + // fixed, compact width so the fields don't stretch the whole menu + width: 100%; + min-width: 200px; + max-width: 280px; + padding-top: 12px; + padding-bottom: 4px; + cursor: default; + + mat-form-field { + width: 100%; + } } diff --git a/projects/netgrif-components/src/lib/navigation/dashboard/dashboard.component.html b/projects/netgrif-components/src/lib/navigation/dashboard/dashboard.component.html index 886c9dc27..b40fe82bc 100644 --- a/projects/netgrif-components/src/lib/navigation/dashboard/dashboard.component.html +++ b/projects/netgrif-components/src/lib/navigation/dashboard/dashboard.component.html @@ -10,7 +10,10 @@ {{ getItemIcon(item) }} {{getItemName(item)}} -
{{ getItemName(item) }}
+
{{ getItemName(item) }}
diff --git a/projects/netgrif-components/src/lib/navigation/dashboard/dashboard.component.scss b/projects/netgrif-components/src/lib/navigation/dashboard/dashboard.component.scss index 5804e3f9a..6084b9c2e 100644 --- a/projects/netgrif-components/src/lib/navigation/dashboard/dashboard.component.scss +++ b/projects/netgrif-components/src/lib/navigation/dashboard/dashboard.component.scss @@ -27,6 +27,8 @@ .card-content { text-align: center; margin-bottom: 0 !important; + max-width: 100%; + box-sizing: border-box; } .card-icon { @@ -46,4 +48,8 @@ text-align: center; margin-bottom: 20px !important; margin-top: 10px !important; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/projects/netgrif-components/src/lib/navigation/navigation-double-drawer/navigation-double-drawer.component.html b/projects/netgrif-components/src/lib/navigation/navigation-double-drawer/navigation-double-drawer.component.html index 5bff28e28..b674a5ec0 100644 --- a/projects/netgrif-components/src/lib/navigation/navigation-double-drawer/navigation-double-drawer.component.html +++ b/projects/netgrif-components/src/lib/navigation/navigation-double-drawer/navigation-double-drawer.component.html @@ -36,13 +36,15 @@ -