diff --git a/packages/admin-ui/src/lib/core/src/common/detail-breadcrumb.ts b/packages/admin-ui/src/lib/core/src/common/detail-breadcrumb.ts index 2deeb05e2f..a7b6c461d8 100644 --- a/packages/admin-ui/src/lib/core/src/common/detail-breadcrumb.ts +++ b/packages/admin-ui/src/lib/core/src/common/detail-breadcrumb.ts @@ -2,7 +2,7 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { Observable } from 'rxjs'; import { map, take } from 'rxjs/operators'; -import { BreadcrumbValue } from '../components/breadcrumb/breadcrumb.component'; +import { BreadcrumbValue } from '../providers/breadcrumb/breadcrumb.service'; /** * Creates an observable of breadcrumb links for use in the route config of a detail route. diff --git a/packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.html b/packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.html index 2cd37afe15..3cc60ba7cf 100644 --- a/packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.html +++ b/packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.html @@ -1,22 +1,44 @@ - - -
- vendure +
+
+ -
-
+
-
- - + +
+ +
+
+
-
-
- +
+
+
+
+ +
+ +
+
+ + +
+
- +
diff --git a/packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.scss b/packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.scss index 44ddc0b90d..50cd86f2df 100644 --- a/packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.scss +++ b/packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.scss @@ -1,31 +1,115 @@ -@import "variables"; +@import 'variables'; + +.app-container { + display: flex; + height: 100vh; + overflow: hidden; +} + +.left-nav { + background-color: var(--color-left-nav-bg); + color: var(--color-left-nav-text); + display: flex; + flex-direction: column; + height: 100%; + box-shadow: -3px 1px 10px 0px rgb(0 0 0 / 18%); + z-index: 1; +} + +.top-bar { + height: 48px; + width: 100%; + display: flex; + justify-content: space-between; + background-color: var(--color-top-bar-bg); + border-bottom: 1px solid var(--color-component-border-200); +} + +.main-nav-container { + overflow: auto; + flex: 1; + /* ===== Scrollbar CSS ===== */ + /* Firefox */ + scrollbar-width: auto; + scrollbar-color: var(--color-primary-700) #052731; + + /* Chrome, Edge, and Safari */ + &::-webkit-scrollbar { + width: 16px; + } + + &::-webkit-scrollbar-track { + background-color: #052731; + } + + &::-webkit-scrollbar-thumb { + background-color: var(--color-primary-700); + border-radius: var(--border-radius); + &:hover { + background-color: #0f5070; + } + } +} + +.surface { + display: flex; + flex-direction: column; + flex: 1; + background-color: var(--color-component-bg-100); +} + +.content-container { + overflow: auto; +} .branding { + display: flex; + align-items: center; + justify-content: center; min-width: 0; } + .logo { width: 40px; } + .wordmark { font-weight: bold; margin-left: 12px; font-size: 24px; color: var(--color-primary-500); + @media screen and (max-width: $breakpoint-medium) { + display: none; + } } vdr-breadcrumb { - @media screen and (min-width: $breakpoint-small){ - margin-left: $clr-sidenav-width; - } + width: 100%; + max-width: var(--layout-content-max-width); + background-color: var(--color-component-bg-100); + padding: var(--space-unit) 0; + z-index: 5; + margin: 0 auto; + position: sticky; + top: 0; } + .header-actions { align-items: center; } + .content-area { position: relative; + max-width: var(--layout-content-max-width); + margin: 0 auto; + padding: 0 var(--space-unit); } ::ng-deep { .header { - background-image: linear-gradient(to right, var(--color-header-gradient-from), var(--color-header-gradient-to)); + background-image: linear-gradient( + to right, + var(--color-header-gradient-from), + var(--color-header-gradient-to) + ); } } diff --git a/packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.ts b/packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.ts index 83bbeda905..815c7769b2 100644 --- a/packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.ts +++ b/packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.ts @@ -7,6 +7,7 @@ import { getAppConfig } from '../../app.config'; import { LanguageCode } from '../../common/generated-types'; import { DataService } from '../../data/providers/data.service'; import { AuthService } from '../../providers/auth/auth.service'; +import { BreadcrumbService } from '../../providers/breadcrumb/breadcrumb.service'; import { I18nService } from '../../providers/i18n/i18n.service'; import { LocalStorageService } from '../../providers/local-storage/local-storage.service'; import { ModalService } from '../../providers/modal/modal.service'; @@ -22,6 +23,7 @@ export class AppShellComponent implements OnInit { uiLanguageAndLocale$: Observable<[LanguageCode, string | undefined]>; availableLanguages: LanguageCode[] = []; hideVendureBranding = getAppConfig().hideVendureBranding; + pageTitle$: Observable; constructor( private authService: AuthService, @@ -30,6 +32,7 @@ export class AppShellComponent implements OnInit { private i18nService: I18nService, private modalService: ModalService, private localStorageService: LocalStorageService, + private breadcrumbService: BreadcrumbService, ) {} ngOnInit() { @@ -40,6 +43,9 @@ export class AppShellComponent implements OnInit { .uiState() .stream$.pipe(map(({ uiState }) => [uiState.language, uiState.locale ?? undefined])); this.availableLanguages = this.i18nService.availableLanguages; + this.pageTitle$ = this.breadcrumbService.breadcrumbs$.pipe( + map(breadcrumbs => breadcrumbs[breadcrumbs.length - 1].label), + ); } selectUiLanguage() { diff --git a/packages/admin-ui/src/lib/core/src/components/base-nav/base-nav.component.ts b/packages/admin-ui/src/lib/core/src/components/base-nav/base-nav.component.ts new file mode 100644 index 0000000000..283fb52892 --- /dev/null +++ b/packages/admin-ui/src/lib/core/src/components/base-nav/base-nav.component.ts @@ -0,0 +1,309 @@ +import { Component, Directive, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { Subscription } from 'rxjs'; +import { map, startWith } from 'rxjs/operators'; + +import { Permission } from '../../common/generated-types'; +import { DataService } from '../../data/providers/data.service'; +import { HealthCheckService } from '../../providers/health-check/health-check.service'; +import { JobQueueService } from '../../providers/job-queue/job-queue.service'; +import { NavMenuBadge, NavMenuItem } from '../../providers/nav-builder/nav-builder-types'; +import { NavBuilderService } from '../../providers/nav-builder/nav-builder.service'; + +@Directive({ + selector: '[vdrBaseNav]', +}) +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export class BaseNavComponent implements OnInit, OnDestroy { + constructor( + protected route: ActivatedRoute, + protected router: Router, + public navBuilderService: NavBuilderService, + protected healthCheckService: HealthCheckService, + protected jobQueueService: JobQueueService, + protected dataService: DataService, + ) {} + + private userPermissions: string[]; + private subscription: Subscription; + + shouldDisplayLink(menuItem: Pick) { + if (!this.userPermissions) { + return false; + } + if (!menuItem.requiresPermission) { + return true; + } + if (typeof menuItem.requiresPermission === 'string') { + return this.userPermissions.includes(menuItem.requiresPermission); + } + if (typeof menuItem.requiresPermission === 'function') { + return menuItem.requiresPermission(this.userPermissions); + } + } + + ngOnInit(): void { + this.defineNavMenu(); + this.subscription = this.dataService.client + .userStatus() + .mapStream(({ userStatus }) => { + this.userPermissions = userStatus.permissions; + }) + .subscribe(); + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + getRouterLink(item: NavMenuItem) { + return this.navBuilderService.getRouterLink(item, this.route); + } + + private defineNavMenu() { + function allow(...permissions: string[]): (userPermissions: string[]) => boolean { + return userPermissions => { + for (const permission of permissions) { + if (userPermissions.includes(permission)) { + return true; + } + } + return false; + }; + } + + this.navBuilderService.defineNavMenuSections([ + { + requiresPermission: allow( + Permission.ReadCatalog, + Permission.ReadProduct, + Permission.ReadFacet, + Permission.ReadCollection, + Permission.ReadAsset, + ), + id: 'catalog', + label: _('nav.catalog'), + items: [ + { + requiresPermission: allow(Permission.ReadCatalog, Permission.ReadProduct), + id: 'products', + label: _('nav.products'), + icon: 'library', + routerLink: ['/catalog', 'products'], + }, + { + requiresPermission: allow(Permission.ReadCatalog, Permission.ReadFacet), + id: 'facets', + label: _('nav.facets'), + icon: 'tag', + routerLink: ['/catalog', 'facets'], + }, + { + requiresPermission: allow(Permission.ReadCatalog, Permission.ReadCollection), + id: 'collections', + label: _('nav.collections'), + icon: 'folder-open', + routerLink: ['/catalog', 'collections'], + }, + { + requiresPermission: allow(Permission.ReadCatalog, Permission.ReadAsset), + id: 'assets', + label: _('nav.assets'), + icon: 'image-gallery', + routerLink: ['/catalog', 'assets'], + }, + ], + }, + { + id: 'sales', + label: _('nav.sales'), + requiresPermission: allow(Permission.ReadOrder), + items: [ + { + requiresPermission: allow(Permission.ReadOrder), + id: 'orders', + label: _('nav.orders'), + routerLink: ['/orders'], + icon: 'shopping-cart', + }, + ], + }, + { + id: 'customers', + label: _('nav.customers'), + requiresPermission: allow(Permission.ReadCustomer, Permission.ReadCustomerGroup), + items: [ + { + requiresPermission: allow(Permission.ReadCustomer), + id: 'customers', + label: _('nav.customers'), + routerLink: ['/customer', 'customers'], + icon: 'user', + }, + { + requiresPermission: allow(Permission.ReadCustomerGroup), + id: 'customer-groups', + label: _('nav.customer-groups'), + routerLink: ['/customer', 'groups'], + icon: 'users', + }, + ], + }, + { + id: 'marketing', + label: _('nav.marketing'), + requiresPermission: allow(Permission.ReadPromotion), + items: [ + { + requiresPermission: allow(Permission.ReadPromotion), + id: 'promotions', + label: _('nav.promotions'), + routerLink: ['/marketing', 'promotions'], + icon: 'asterisk', + }, + ], + }, + { + id: 'settings', + label: _('nav.settings'), + icon: 'cog', + displayMode: 'settings', + requiresPermission: allow( + Permission.ReadSettings, + Permission.ReadChannel, + Permission.ReadAdministrator, + Permission.ReadShippingMethod, + Permission.ReadPaymentMethod, + Permission.ReadTaxCategory, + Permission.ReadTaxRate, + Permission.ReadCountry, + Permission.ReadZone, + Permission.UpdateGlobalSettings, + ), + collapsible: true, + collapsedByDefault: true, + items: [ + { + requiresPermission: allow(Permission.ReadSeller), + id: 'sellers', + label: _('nav.sellers'), + routerLink: ['/settings', 'sellers'], + icon: 'store', + }, + { + requiresPermission: allow(Permission.ReadChannel), + id: 'channels', + label: _('nav.channels'), + routerLink: ['/settings', 'channels'], + icon: 'layers', + }, + { + requiresPermission: allow(Permission.ReadAdministrator), + id: 'administrators', + label: _('nav.administrators'), + routerLink: ['/settings', 'administrators'], + icon: 'administrator', + }, + { + requiresPermission: allow(Permission.ReadAdministrator), + id: 'roles', + label: _('nav.roles'), + routerLink: ['/settings', 'roles'], + icon: 'users', + }, + { + requiresPermission: allow(Permission.ReadShippingMethod), + id: 'shipping-methods', + label: _('nav.shipping-methods'), + routerLink: ['/settings', 'shipping-methods'], + icon: 'truck', + }, + { + requiresPermission: allow(Permission.ReadPaymentMethod), + id: 'payment-methods', + label: _('nav.payment-methods'), + routerLink: ['/settings', 'payment-methods'], + icon: 'credit-card', + }, + { + requiresPermission: allow(Permission.ReadTaxCategory), + id: 'tax-categories', + label: _('nav.tax-categories'), + routerLink: ['/settings', 'tax-categories'], + icon: 'view-list', + }, + { + requiresPermission: allow(Permission.ReadTaxRate), + id: 'tax-rates', + label: _('nav.tax-rates'), + routerLink: ['/settings', 'tax-rates'], + icon: 'calculator', + }, + { + requiresPermission: allow(Permission.ReadCountry), + id: 'countries', + label: _('nav.countries'), + routerLink: ['/settings', 'countries'], + icon: 'flag', + }, + { + requiresPermission: allow(Permission.ReadZone), + id: 'zones', + label: _('nav.zones'), + routerLink: ['/settings', 'zones'], + icon: 'world', + }, + { + requiresPermission: allow(Permission.UpdateGlobalSettings), + id: 'global-settings', + label: _('nav.global-settings'), + routerLink: ['/settings', 'global-settings'], + icon: 'cog', + }, + ], + }, + { + id: 'system', + label: _('nav.system'), + icon: 'computer', + displayMode: 'settings', + requiresPermission: Permission.ReadSystem, + collapsible: true, + collapsedByDefault: true, + items: [ + { + id: 'job-queue', + label: _('nav.job-queue'), + routerLink: ['/system', 'jobs'], + icon: 'tick-chart', + statusBadge: this.jobQueueService.activeJobs$.pipe( + startWith([]), + map( + jobs => + ({ + type: jobs.length === 0 ? 'none' : 'info', + propagateToSection: jobs.length > 0, + } as NavMenuBadge), + ), + ), + }, + { + id: 'system-status', + label: _('nav.system-status'), + routerLink: ['/system', 'system-status'], + icon: 'rack-server', + statusBadge: this.healthCheckService.status$.pipe( + map(status => ({ + type: status === 'ok' ? 'success' : 'error', + propagateToSection: status === 'error', + })), + ), + }, + ], + }, + ]); + } +} diff --git a/packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.scss b/packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.scss index 7ab708f83f..819600881e 100644 --- a/packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.scss +++ b/packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.scss @@ -2,9 +2,7 @@ :host { display: block; - @media screen and (min-width: $breakpoint-small) { - padding: 0 1rem; - } + padding: 0; } .breadcrumbs { list-style-type: none; @@ -12,11 +10,12 @@ overflow-x: auto; max-width: 100vw; padding: 0 3px; + margin: 0 var(--space-unit); + font-size: var(--font-size-sm); @media screen and (min-width: $breakpoint-small) { padding: 0; } li { - font-size: 16px; display: inline-block; margin-right: 10px; white-space: nowrap; diff --git a/packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.spec.ts b/packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.spec.ts index 59ba8ca613..19b0964028 100644 --- a/packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.spec.ts +++ b/packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.spec.ts @@ -8,8 +8,9 @@ import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; import { MockTranslatePipe } from '../../../../../testing/translate.pipe.mock'; import { DataService } from '../../data/providers/data.service'; +import { BreadcrumbLabelLinkPair } from '../../providers/breadcrumb/breadcrumb.service'; -import { BreadcrumbComponent, BreadcrumbLabelLinkPair } from './breadcrumb.component'; +import { BreadcrumbComponent } from './breadcrumb.component'; describe('BeadcrumbsComponent', () => { let baseRouteConfig: Routes; @@ -450,7 +451,9 @@ class TestParentComponent {} @Component({ // eslint-disable-next-line @angular-eslint/component-selector selector: 'test-child-component', - template: ` `, + template: ` + + `, }) class TestChildComponent {} diff --git a/packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.ts b/packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.ts index fc0089d6fc..9ed81f8190 100644 --- a/packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.ts +++ b/packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.ts @@ -1,22 +1,6 @@ -import { Component, OnDestroy } from '@angular/core'; -import { ActivatedRoute, Data, NavigationEnd, Params, PRIMARY_OUTLET, Router } from '@angular/router'; -import { flatten } from 'lodash'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subject } from 'rxjs'; -import { filter, map, startWith, switchMap, takeUntil } from 'rxjs/operators'; -import { DataService } from '../../data/providers/data.service'; - -export type BreadcrumbString = string; -export interface BreadcrumbLabelLinkPair { - label: string; - link: any[]; -} -export type BreadcrumbValue = BreadcrumbString | BreadcrumbLabelLinkPair | BreadcrumbLabelLinkPair[]; -export type BreadcrumbFunction = ( - data: Data, - params: Params, - dataService: DataService, -) => BreadcrumbValue | Observable; -export type BreadcrumbDefinition = BreadcrumbValue | BreadcrumbFunction | Observable; +import { Component } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { BreadcrumbService } from '../../providers/breadcrumb/breadcrumb.service'; /** * A breadcrumbs component which reads the route config and any route that has a `data.breadcrumb` property will @@ -32,126 +16,11 @@ export type BreadcrumbDefinition = BreadcrumbValue | BreadcrumbFunction | Observ templateUrl: './breadcrumb.component.html', styleUrls: ['./breadcrumb.component.scss'], }) -export class BreadcrumbComponent implements OnDestroy { +export class BreadcrumbComponent { breadcrumbs$: Observable>; private destroy$ = new Subject(); - constructor(private router: Router, private route: ActivatedRoute, private dataService: DataService) { - this.breadcrumbs$ = this.router.events.pipe( - filter(event => event instanceof NavigationEnd), - takeUntil(this.destroy$), - startWith(true), - switchMap(() => this.generateBreadcrumbs(this.route.root)), - ); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - private generateBreadcrumbs( - rootRoute: ActivatedRoute, - ): Observable; label: string }>> { - const breadcrumbParts = this.assembleBreadcrumbParts(rootRoute); - const breadcrumbObservables$ = breadcrumbParts.map( - ({ value$, path }) => - value$.pipe( - map(value => { - if (isBreadcrumbLabelLinkPair(value)) { - return { - label: value.label, - link: this.normalizeRelativeLinks(value.link, path), - }; - } else if (isBreadcrumbPairArray(value)) { - return value.map(val => ({ - label: val.label, - link: this.normalizeRelativeLinks(val.link, path), - })); - } else { - return { - label: value, - link: '/' + path.join('/'), - }; - } - }), - ) as Observable, - ); - - return observableCombineLatest(breadcrumbObservables$).pipe(map(links => flatten(links))); - } - - /** - * Walks the route definition tree to assemble an array from which the breadcrumbs can be derived. - */ - private assembleBreadcrumbParts( - rootRoute: ActivatedRoute, - ): Array<{ value$: Observable; path: string[] }> { - const breadcrumbParts: Array<{ value$: Observable; path: string[] }> = []; - const inferredUrl = ''; - const segmentPaths: string[] = []; - let currentRoute: ActivatedRoute | null = rootRoute; - do { - const childRoutes = currentRoute.children; - currentRoute = null; - childRoutes.forEach((route: ActivatedRoute) => { - if (route.outlet === PRIMARY_OUTLET) { - const routeSnapshot = route.snapshot; - let breadcrumbDef: BreadcrumbDefinition | undefined = - route.routeConfig && route.routeConfig.data && route.routeConfig.data['breadcrumb']; - segmentPaths.push(routeSnapshot.url.map(segment => segment.path).join('/')); - - if (breadcrumbDef) { - if (isBreadcrumbFunction(breadcrumbDef)) { - breadcrumbDef = breadcrumbDef( - routeSnapshot.data, - routeSnapshot.params, - this.dataService, - ); - } - const observableValue = isObservable(breadcrumbDef) - ? breadcrumbDef - : observableOf(breadcrumbDef); - breadcrumbParts.push({ value$: observableValue, path: segmentPaths.slice() }); - } - currentRoute = route; - } - }); - } while (currentRoute); - - return breadcrumbParts; + constructor(private breadcrumbService: BreadcrumbService) { + this.breadcrumbs$ = this.breadcrumbService.breadcrumbs$; } - - /** - * Accounts for relative routes in the link array, i.e. arrays whose first element is either: - * * `./` - this appends the rest of the link segments to the current active route - * * `../` - this removes the last segment of the current active route, and appends the link segments - * to the parent route. - */ - private normalizeRelativeLinks(link: any[], segmentPaths: string[]): any[] { - const clone = link.slice(); - if (clone[0] === './') { - clone[0] = segmentPaths.join('/'); - } - if (clone[0] === '../') { - clone[0] = segmentPaths.slice(0, -1).join('/'); - } - return clone.filter(segment => segment !== ''); - } -} - -function isBreadcrumbFunction(value: BreadcrumbDefinition): value is BreadcrumbFunction { - return typeof value === 'function'; -} - -function isObservable(value: BreadcrumbDefinition): value is Observable { - return value instanceof Observable; -} - -function isBreadcrumbLabelLinkPair(value: BreadcrumbValue): value is BreadcrumbLabelLinkPair { - return value.hasOwnProperty('label') && value.hasOwnProperty('link'); -} - -function isBreadcrumbPairArray(value: BreadcrumbValue): value is BreadcrumbLabelLinkPair[] { - return Array.isArray(value) && isBreadcrumbLabelLinkPair(value[0]); } diff --git a/packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.html b/packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.html index 9ccd6d8119..c5b713c1ae 100644 --- a/packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.html +++ b/packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.html @@ -1,11 +1,11 @@ - -
- +
diff --git a/packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.scss b/packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.scss index 22030d0f8f..d15332172d 100644 --- a/packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.scss +++ b/packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.scss @@ -1,34 +1,57 @@ @import 'variables'; :host { - // flex: 0 0 auto; - order: -1; background-color: var(--clr-nav-background-color); } -nav.sidenav { +nav.main-nav { height: 100%; border-right-color: var(--clr-sidenav-border-color); } -.sidenav .nav-group { +.main-nav .nav-group { + margin-bottom: calc(var(--space-unit) * 2); .nav-list { margin: 0; } .nav-group-header { margin: 0; line-height: 1.2; + font-size: var(--font-size-sm); + padding-left: calc(var(--space-unit) * 1); + //font-weight: bold; + color: var(--color-left-nav-text); + opacity: 0.7; } .nav-link { - display: inline-flex; + display: flex; line-height: 1rem; - padding-right: 0.6rem; + font-size: var(--font-size-sm); + padding: var(--space-unit); + border-radius: var(--border-radius); + &:link, + &:visited { + color: var(--color-text-600); + } + &:hover { + color: var(--color-primary-200); + } + + &.active { + background-color: var(--color-primary-600); + color: var(--color-text-inverse); + } + @media screen and (max-width: $breakpoint-medium) { + justify-content: center; + } } } .nav-list clr-icon { flex-shrink: 0; - margin-right: 12px; + @media screen and (min-width: $breakpoint-medium) { + margin-right: 12px; + } } .nav-group { diff --git a/packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.ts b/packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.ts index 5c2c1bc1d5..917846daab 100644 --- a/packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.ts +++ b/packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.ts @@ -1,306 +1,22 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; -import { Subscription } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; - -import { Permission } from '../../common/generated-types'; -import { DataService } from '../../data/providers/data.service'; -import { HealthCheckService } from '../../providers/health-check/health-check.service'; -import { JobQueueService } from '../../providers/job-queue/job-queue.service'; -import { NavMenuBadge, NavMenuItem } from '../../providers/nav-builder/nav-builder-types'; -import { NavBuilderService } from '../../providers/nav-builder/nav-builder.service'; +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { NavMenuSection } from '../../providers/nav-builder/nav-builder-types'; +import { BaseNavComponent } from '../base-nav/base-nav.component'; @Component({ selector: 'vdr-main-nav', templateUrl: './main-nav.component.html', styleUrls: ['./main-nav.component.scss'], }) -export class MainNavComponent implements OnInit, OnDestroy { - constructor( - private route: ActivatedRoute, - private router: Router, - public navBuilderService: NavBuilderService, - private healthCheckService: HealthCheckService, - private jobQueueService: JobQueueService, - private dataService: DataService, - ) {} - - private userPermissions: string[]; - private subscription: Subscription; - - shouldDisplayLink(menuItem: Pick) { - if (!this.userPermissions) { - return false; - } - if (!menuItem.requiresPermission) { - return true; - } - if (typeof menuItem.requiresPermission === 'string') { - return this.userPermissions.includes(menuItem.requiresPermission); - } - if (typeof menuItem.requiresPermission === 'function') { - return menuItem.requiresPermission(this.userPermissions); - } - } - - ngOnInit(): void { - this.defineNavMenu(); - this.subscription = this.dataService.client - .userStatus() - .mapStream(({ userStatus }) => { - this.userPermissions = userStatus.permissions; - }) - .subscribe(); - } - - ngOnDestroy() { - if (this.subscription) { - this.subscription.unsubscribe(); - } - } - - getRouterLink(item: NavMenuItem) { - return this.navBuilderService.getRouterLink(item, this.route); - } +export class MainNavComponent extends BaseNavComponent implements OnInit { + mainMenuConfig$: Observable; - private defineNavMenu() { - function allow(...permissions: string[]): (userPermissions: string[]) => boolean { - return userPermissions => { - for (const permission of permissions) { - if (userPermissions.includes(permission)) { - return true; - } - } - return false; - }; - } + override ngOnInit(): void { + super.ngOnInit(); - this.navBuilderService.defineNavMenuSections([ - { - requiresPermission: allow( - Permission.ReadCatalog, - Permission.ReadProduct, - Permission.ReadFacet, - Permission.ReadCollection, - Permission.ReadAsset, - ), - id: 'catalog', - label: _('nav.catalog'), - items: [ - { - requiresPermission: allow(Permission.ReadCatalog, Permission.ReadProduct), - id: 'products', - label: _('nav.products'), - icon: 'library', - routerLink: ['/catalog', 'products'], - }, - { - requiresPermission: allow(Permission.ReadCatalog, Permission.ReadFacet), - id: 'facets', - label: _('nav.facets'), - icon: 'tag', - routerLink: ['/catalog', 'facets'], - }, - { - requiresPermission: allow(Permission.ReadCatalog, Permission.ReadCollection), - id: 'collections', - label: _('nav.collections'), - icon: 'folder-open', - routerLink: ['/catalog', 'collections'], - }, - { - requiresPermission: allow(Permission.ReadCatalog, Permission.ReadAsset), - id: 'assets', - label: _('nav.assets'), - icon: 'image-gallery', - routerLink: ['/catalog', 'assets'], - }, - ], - }, - { - id: 'sales', - label: _('nav.sales'), - requiresPermission: allow(Permission.ReadOrder), - items: [ - { - requiresPermission: allow(Permission.ReadOrder), - id: 'orders', - label: _('nav.orders'), - routerLink: ['/orders'], - icon: 'shopping-cart', - }, - ], - }, - { - id: 'customers', - label: _('nav.customers'), - requiresPermission: allow(Permission.ReadCustomer, Permission.ReadCustomerGroup), - items: [ - { - requiresPermission: allow(Permission.ReadCustomer), - id: 'customers', - label: _('nav.customers'), - routerLink: ['/customer', 'customers'], - icon: 'user', - }, - { - requiresPermission: allow(Permission.ReadCustomerGroup), - id: 'customer-groups', - label: _('nav.customer-groups'), - routerLink: ['/customer', 'groups'], - icon: 'users', - }, - ], - }, - { - id: 'marketing', - label: _('nav.marketing'), - requiresPermission: allow(Permission.ReadPromotion), - items: [ - { - requiresPermission: allow(Permission.ReadPromotion), - id: 'promotions', - label: _('nav.promotions'), - routerLink: ['/marketing', 'promotions'], - icon: 'asterisk', - }, - ], - }, - { - id: 'settings', - label: _('nav.settings'), - requiresPermission: allow( - Permission.ReadSettings, - Permission.ReadChannel, - Permission.ReadAdministrator, - Permission.ReadShippingMethod, - Permission.ReadPaymentMethod, - Permission.ReadTaxCategory, - Permission.ReadTaxRate, - Permission.ReadCountry, - Permission.ReadZone, - Permission.UpdateGlobalSettings, - ), - collapsible: true, - collapsedByDefault: true, - items: [ - { - requiresPermission: allow(Permission.ReadSeller), - id: 'sellers', - label: _('nav.sellers'), - routerLink: ['/settings', 'sellers'], - icon: 'store', - }, - { - requiresPermission: allow(Permission.ReadChannel), - id: 'channels', - label: _('nav.channels'), - routerLink: ['/settings', 'channels'], - icon: 'layers', - }, - { - requiresPermission: allow(Permission.ReadAdministrator), - id: 'administrators', - label: _('nav.administrators'), - routerLink: ['/settings', 'administrators'], - icon: 'administrator', - }, - { - requiresPermission: allow(Permission.ReadAdministrator), - id: 'roles', - label: _('nav.roles'), - routerLink: ['/settings', 'roles'], - icon: 'users', - }, - { - requiresPermission: allow(Permission.ReadShippingMethod), - id: 'shipping-methods', - label: _('nav.shipping-methods'), - routerLink: ['/settings', 'shipping-methods'], - icon: 'truck', - }, - { - requiresPermission: allow(Permission.ReadPaymentMethod), - id: 'payment-methods', - label: _('nav.payment-methods'), - routerLink: ['/settings', 'payment-methods'], - icon: 'credit-card', - }, - { - requiresPermission: allow(Permission.ReadTaxCategory), - id: 'tax-categories', - label: _('nav.tax-categories'), - routerLink: ['/settings', 'tax-categories'], - icon: 'view-list', - }, - { - requiresPermission: allow(Permission.ReadTaxRate), - id: 'tax-rates', - label: _('nav.tax-rates'), - routerLink: ['/settings', 'tax-rates'], - icon: 'calculator', - }, - { - requiresPermission: allow(Permission.ReadCountry), - id: 'countries', - label: _('nav.countries'), - routerLink: ['/settings', 'countries'], - icon: 'flag', - }, - { - requiresPermission: allow(Permission.ReadZone), - id: 'zones', - label: _('nav.zones'), - routerLink: ['/settings', 'zones'], - icon: 'world', - }, - { - requiresPermission: allow(Permission.UpdateGlobalSettings), - id: 'global-settings', - label: _('nav.global-settings'), - routerLink: ['/settings', 'global-settings'], - icon: 'cog', - }, - ], - }, - { - id: 'system', - label: _('nav.system'), - requiresPermission: Permission.ReadSystem, - collapsible: true, - collapsedByDefault: true, - items: [ - { - id: 'job-queue', - label: _('nav.job-queue'), - routerLink: ['/system', 'jobs'], - icon: 'tick-chart', - statusBadge: this.jobQueueService.activeJobs$.pipe( - startWith([]), - map( - jobs => - ({ - type: jobs.length === 0 ? 'none' : 'info', - propagateToSection: jobs.length > 0, - } as NavMenuBadge), - ), - ), - }, - { - id: 'system-status', - label: _('nav.system-status'), - routerLink: ['/system', 'system-status'], - icon: 'rack-server', - statusBadge: this.healthCheckService.status$.pipe( - map(status => ({ - type: status === 'ok' ? 'success' : 'error', - propagateToSection: status === 'error', - })), - ), - }, - ], - }, - ]); + this.mainMenuConfig$ = this.navBuilderService.menuConfig$.pipe( + map(sections => sections.filter(s => s.displayMode === 'regular' || !s.displayMode)), + ); } } diff --git a/packages/admin-ui/src/lib/core/src/components/page-title/page-title.component.scss b/packages/admin-ui/src/lib/core/src/components/page-title/page-title.component.scss new file mode 100644 index 0000000000..54d8937380 --- /dev/null +++ b/packages/admin-ui/src/lib/core/src/components/page-title/page-title.component.scss @@ -0,0 +1,22 @@ +:host { + display: block; + width: 100%; + max-width: var(--layout-content-max-width); + padding: 0 var(--space-unit); + margin: 0 auto; +} + +.page-title { + h1 { + margin-top: 0; + color: var(--color-text-100); + // max-height: 48px; + //transition: all 0.5s; + opacity: 1; + overflow: hidden; + &.folded { + // max-height: 0; + opacity: 1; + } + } +} diff --git a/packages/admin-ui/src/lib/core/src/components/page-title/page-title.component.ts b/packages/admin-ui/src/lib/core/src/components/page-title/page-title.component.ts new file mode 100644 index 0000000000..47aa4e4966 --- /dev/null +++ b/packages/admin-ui/src/lib/core/src/components/page-title/page-title.component.ts @@ -0,0 +1,47 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + HostListener, + Input, +} from '@angular/core'; +import { fromEvent } from 'rxjs'; +import { debounceTime, throttleTime } from 'rxjs/operators'; + +@Component({ + selector: 'vdr-page-title', + template: ` +
+

{{ value | translate }}

+
+ `, + styleUrls: [`./page-title.component.scss`], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PageTitleComponent implements AfterViewInit { + @Input() value = ''; + isFolded = false; + private lastScrollTop = 0; + + constructor(private changeDetector: ChangeDetectorRef) {} + + ngAfterViewInit() { + const contentContainer = document.querySelector('.content-container'); + if (contentContainer) { + // fromEvent(contentContainer, 'scroll', { passive: true }).subscribe(() => { + // const scrollTop = contentContainer.scrollTop; + // const delta = this.lastScrollTop - scrollTop; + // this.lastScrollTop = scrollTop; + // if (100 < scrollTop && this.isFolded === false) { + // this.isFolded = true; + // this.changeDetector.markForCheck(); + // } + // if ((scrollTop === 0 || (scrollTop < 50 && 0 < delta)) && this.isFolded === true) { + // this.isFolded = false; + // this.changeDetector.markForCheck(); + // } + // }); + } + } +} diff --git a/packages/admin-ui/src/lib/core/src/components/settings-nav/settings-nav.component.html b/packages/admin-ui/src/lib/core/src/components/settings-nav/settings-nav.component.html new file mode 100644 index 0000000000..0264ad2cc3 --- /dev/null +++ b/packages/admin-ui/src/lib/core/src/components/settings-nav/settings-nav.component.html @@ -0,0 +1,57 @@ + diff --git a/packages/admin-ui/src/lib/core/src/components/settings-nav/settings-nav.component.scss b/packages/admin-ui/src/lib/core/src/components/settings-nav/settings-nav.component.scss new file mode 100644 index 0000000000..c5ba3ec5da --- /dev/null +++ b/packages/admin-ui/src/lib/core/src/components/settings-nav/settings-nav.component.scss @@ -0,0 +1,21 @@ +@import "variables"; + +.setting-link { + width: 100%; + border: none; + display: flex; + justify-content: space-between; + font-size: var(--font-size-xs); + align-items: center; + cursor: pointer; + background-color: transparent; + padding: var(--space-unit); + color: var(--color-left-nav-text); + &:hover { + color: var(--color-left-nav-text-hover); + } + + clr-icon { + margin-right: 6px; + } +} diff --git a/packages/admin-ui/src/lib/core/src/components/settings-nav/settings-nav.component.ts b/packages/admin-ui/src/lib/core/src/components/settings-nav/settings-nav.component.ts new file mode 100644 index 0000000000..413eb170af --- /dev/null +++ b/packages/admin-ui/src/lib/core/src/components/settings-nav/settings-nav.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { NavMenuSection } from '@vendure/admin-ui/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { BaseNavComponent } from '../base-nav/base-nav.component'; + +@Component({ + selector: 'vdr-settings-nav', + templateUrl: './settings-nav.component.html', + styleUrls: ['./settings-nav.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SettingsNavComponent extends BaseNavComponent implements OnInit { + settingsMenuConfig$: Observable; + + override ngOnInit(): void { + super.ngOnInit(); + + this.settingsMenuConfig$ = this.navBuilderService.menuConfig$.pipe( + map(sections => sections.filter(s => s.displayMode === 'settings')), + ); + } +} diff --git a/packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.html b/packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.html index ecc2f73680..b253b4c76e 100644 --- a/packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.html +++ b/packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.html @@ -1,8 +1,10 @@ - diff --git a/packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.scss b/packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.scss index 20cfdfe706..4ac068250f 100644 --- a/packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.scss +++ b/packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.scss @@ -1,20 +1,62 @@ -@import "variables"; +@import 'variables'; :host { display: flex; align-items: center; - margin: 0 0.5rem; height: 2.5rem; + width: 100%; + @media screen and (max-width: $breakpoint-medium) { + margin: 0; + } + width: 100%; + padding: var(--space-unit); + padding-left: 0; + vdr-dropdown { + width: 100%; + } } -.user-name { - color: var(--color-grey-200); - margin-right: 12px; - @media screen and (max-width: $breakpoint-small) { - display: none; +.user-menu-btn { + display: flex; + align-items: center; + justify-content: space-between; + border: none; + border-radius: var(--border-radius); + text-transform: uppercase; + font-size: var(--font-size-xs); + width: 100%; + background-color: transparent; + color: var(--color-text-200); + cursor: pointer; + &:hover { + color: var(--color-text-300); } } -.trigger clr-icon { - color: white; +.user-circle { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 100%; + background-color: var(--color-primary-600); + width: 24px; + height: 24px; + margin-right: 6px; + @media screen and (max-width: $breakpoint-medium) { + margin-right: 0; + } + + clr-icon { + color: var(--color-text-inverse); + } +} + +.user-name { + margin-right: var(--space-unit); + overflow: hidden; + max-width: 100px; + text-overflow: ellipsis; + @media screen and (max-width: $breakpoint-medium) { + display: none; + } } diff --git a/packages/admin-ui/src/lib/core/src/core.module.ts b/packages/admin-ui/src/lib/core/src/core.module.ts index 852f75feb9..87acf79015 100644 --- a/packages/admin-ui/src/lib/core/src/core.module.ts +++ b/packages/admin-ui/src/lib/core/src/core.module.ts @@ -8,11 +8,14 @@ import { TranslateCompiler, TranslateLoader, TranslateModule } from '@ngx-transl import { getAppConfig } from './app.config'; import { getDefaultUiLanguage } from './common/utilities/get-default-ui-language'; import { AppShellComponent } from './components/app-shell/app-shell.component'; +import { BaseNavComponent } from './components/base-nav/base-nav.component'; import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.component'; import { ChannelSwitcherComponent } from './components/channel-switcher/channel-switcher.component'; import { MainNavComponent } from './components/main-nav/main-nav.component'; import { NotificationComponent } from './components/notification/notification.component'; import { OverlayHostComponent } from './components/overlay-host/overlay-host.component'; +import { PageTitleComponent } from './components/page-title/page-title.component'; +import { SettingsNavComponent } from './components/settings-nav/settings-nav.component'; import { ThemeSwitcherComponent } from './components/theme-switcher/theme-switcher.component'; import { UiLanguageSwitcherDialogComponent } from './components/ui-language-switcher-dialog/ui-language-switcher-dialog.component'; import { UserMenuComponent } from './components/user-menu/user-menu.component'; @@ -45,13 +48,16 @@ import { SharedModule } from './shared/shared.module'; declarations: [ AppShellComponent, UserMenuComponent, + BaseNavComponent, MainNavComponent, + SettingsNavComponent, BreadcrumbComponent, OverlayHostComponent, NotificationComponent, UiLanguageSwitcherDialogComponent, ChannelSwitcherComponent, ThemeSwitcherComponent, + PageTitleComponent, ], }) export class CoreModule { diff --git a/packages/admin-ui/src/lib/core/src/providers/breadcrumb/breadcrumb.service.ts b/packages/admin-ui/src/lib/core/src/providers/breadcrumb/breadcrumb.service.ts new file mode 100644 index 0000000000..0934778840 --- /dev/null +++ b/packages/admin-ui/src/lib/core/src/providers/breadcrumb/breadcrumb.service.ts @@ -0,0 +1,147 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { ActivatedRoute, Data, NavigationEnd, Params, PRIMARY_OUTLET, Router } from '@angular/router'; +import { DataService } from '@vendure/admin-ui/core'; +import { flatten } from 'lodash'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subject } from 'rxjs'; +import { filter, map, startWith, switchMap, takeUntil } from 'rxjs/operators'; + +export type BreadcrumbString = string; + +export interface BreadcrumbLabelLinkPair { + label: string; + link: any[]; +} + +export type BreadcrumbValue = BreadcrumbString | BreadcrumbLabelLinkPair | BreadcrumbLabelLinkPair[]; +export type BreadcrumbFunction = ( + data: Data, + params: Params, + dataService: DataService, +) => BreadcrumbValue | Observable; +export type BreadcrumbDefinition = BreadcrumbValue | BreadcrumbFunction | Observable; + +@Injectable({ + providedIn: 'root', +}) +export class BreadcrumbService implements OnDestroy { + breadcrumbs$: Observable>; + private destroy$ = new Subject(); + + constructor(private router: Router, private route: ActivatedRoute, private dataService: DataService) { + this.breadcrumbs$ = this.router.events.pipe( + filter(event => event instanceof NavigationEnd), + takeUntil(this.destroy$), + startWith(true), + switchMap(() => this.generateBreadcrumbs(this.route.root)), + ); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + private generateBreadcrumbs( + rootRoute: ActivatedRoute, + ): Observable; label: string }>> { + const breadcrumbParts = this.assembleBreadcrumbParts(rootRoute); + const breadcrumbObservables$ = breadcrumbParts.map( + ({ value$, path }) => + value$.pipe( + map(value => { + if (isBreadcrumbLabelLinkPair(value)) { + return { + label: value.label, + link: this.normalizeRelativeLinks(value.link, path), + }; + } else if (isBreadcrumbPairArray(value)) { + return value.map(val => ({ + label: val.label, + link: this.normalizeRelativeLinks(val.link, path), + })); + } else { + return { + label: value, + link: '/' + path.join('/'), + }; + } + }), + ) as Observable, + ); + + return observableCombineLatest(breadcrumbObservables$).pipe(map(links => flatten(links))); + } + + /** + * Walks the route definition tree to assemble an array from which the breadcrumbs can be derived. + */ + private assembleBreadcrumbParts( + rootRoute: ActivatedRoute, + ): Array<{ value$: Observable; path: string[] }> { + const breadcrumbParts: Array<{ value$: Observable; path: string[] }> = []; + const segmentPaths: string[] = []; + let currentRoute: ActivatedRoute | null = rootRoute; + do { + const childRoutes = currentRoute.children; + currentRoute = null; + childRoutes.forEach((route: ActivatedRoute) => { + if (route.outlet === PRIMARY_OUTLET) { + const routeSnapshot = route.snapshot; + let breadcrumbDef: BreadcrumbDefinition | undefined = + route.routeConfig && route.routeConfig.data && route.routeConfig.data['breadcrumb']; + segmentPaths.push(routeSnapshot.url.map(segment => segment.path).join('/')); + + if (breadcrumbDef) { + if (isBreadcrumbFunction(breadcrumbDef)) { + breadcrumbDef = breadcrumbDef( + routeSnapshot.data, + routeSnapshot.params, + this.dataService, + ); + } + const observableValue = isObservable(breadcrumbDef) + ? breadcrumbDef + : observableOf(breadcrumbDef); + breadcrumbParts.push({ value$: observableValue, path: segmentPaths.slice() }); + } + currentRoute = route; + } + }); + } while (currentRoute); + + return breadcrumbParts; + } + + /** + * Accounts for relative routes in the link array, i.e. arrays whose first element is either: + * * `./` - this appends the rest of the link segments to the current active route + * * `../` - this removes the last segment of the current active route, and appends the link segments + * to the parent route. + */ + private normalizeRelativeLinks(link: any[], segmentPaths: string[]): any[] { + const clone = link.slice(); + if (clone[0] === './') { + clone[0] = segmentPaths.join('/'); + } + if (clone[0] === '../') { + clone[0] = segmentPaths.slice(0, -1).join('/'); + } + return clone.filter(segment => segment !== ''); + } +} + +function isBreadcrumbFunction(value: BreadcrumbDefinition): value is BreadcrumbFunction { + return typeof value === 'function'; +} + +function isObservable(value: BreadcrumbDefinition): value is Observable { + return value instanceof Observable; +} + +function isBreadcrumbLabelLinkPair(value: BreadcrumbValue): value is BreadcrumbLabelLinkPair { + return value.hasOwnProperty('label') && value.hasOwnProperty('link'); +} + +function isBreadcrumbPairArray(value: BreadcrumbValue): value is BreadcrumbLabelLinkPair[] { + return Array.isArray(value) && isBreadcrumbLabelLinkPair(value[0]); +} diff --git a/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder-types.ts b/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder-types.ts index 33d8e5b205..9e330ffe50 100644 --- a/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder-types.ts +++ b/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder-types.ts @@ -57,6 +57,8 @@ export interface NavMenuSection { id: string; label: string; items: NavMenuItem[]; + icon?: string; + displayMode?: 'regular' | 'settings'; /** * @description * Control the display of this item based on the user permissions. diff --git a/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder.service.spec.ts b/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder.service.spec.ts index 8b17cc73b7..a914f84950 100644 --- a/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder.service.spec.ts +++ b/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder.service.spec.ts @@ -16,7 +16,7 @@ describe('NavBuilderService', () => { it('defineNavMenuSections', done => { service.defineNavMenuSections(getBaseNav()); - service.navMenuConfig$.pipe(take(1)).subscribe(result => { + service.mainMenuConfig$.pipe(take(1)).subscribe(result => { expect(result).toEqual(getBaseNav()); done(); }); @@ -31,7 +31,7 @@ describe('NavBuilderService', () => { items: [], }); - service.navMenuConfig$.pipe(take(1)).subscribe(result => { + service.mainMenuConfig$.pipe(take(1)).subscribe(result => { expect(result.map(section => section.id)).toEqual(['catalog', 'sales', 'reports']); done(); }); @@ -48,7 +48,7 @@ describe('NavBuilderService', () => { 'sales', ); - service.navMenuConfig$.pipe(take(1)).subscribe(result => { + service.mainMenuConfig$.pipe(take(1)).subscribe(result => { expect(result.map(section => section.id)).toEqual(['catalog', 'reports', 'sales']); done(); }); @@ -62,7 +62,7 @@ describe('NavBuilderService', () => { items: [], }); - service.navMenuConfig$.pipe(take(1)).subscribe(result => { + service.mainMenuConfig$.pipe(take(1)).subscribe(result => { expect(result.map(section => section.id)).toEqual(['catalog', 'sales']); expect(result[1].label).toBe('Custom Sales'); done(); @@ -80,7 +80,7 @@ describe('NavBuilderService', () => { 'catalog', ); - service.navMenuConfig$.pipe(take(1)).subscribe(result => { + service.mainMenuConfig$.pipe(take(1)).subscribe(result => { expect(result.map(section => section.id)).toEqual(['sales', 'catalog']); expect(result[0].label).toBe('Custom Sales'); done(); @@ -101,7 +101,7 @@ describe('NavBuilderService', () => { 'farm-tools', ); - service.navMenuConfig$.pipe(take(1)).subscribe(result => { + service.mainMenuConfig$.pipe(take(1)).subscribe(result => { expect(console.error).toHaveBeenCalledWith( 'Could not add menu item "fulfillments", section "farm-tools" does not exist', ); @@ -120,7 +120,7 @@ describe('NavBuilderService', () => { 'sales', ); - service.navMenuConfig$.pipe(take(1)).subscribe(result => { + service.mainMenuConfig$.pipe(take(1)).subscribe(result => { const salesSection = result.find(r => r.id === 'sales')!; expect(salesSection.items.map(item => item.id)).toEqual(['orders', 'fulfillments']); @@ -140,7 +140,7 @@ describe('NavBuilderService', () => { 'orders', ); - service.navMenuConfig$.pipe(take(1)).subscribe(result => { + service.mainMenuConfig$.pipe(take(1)).subscribe(result => { const salesSection = result.find(r => r.id === 'sales')!; expect(salesSection.items.map(item => item.id)).toEqual(['fulfillments', 'orders']); @@ -159,7 +159,7 @@ describe('NavBuilderService', () => { 'catalog', ); - service.navMenuConfig$.pipe(take(1)).subscribe(result => { + service.mainMenuConfig$.pipe(take(1)).subscribe(result => { const catalogSection = result.find(r => r.id === 'catalog')!; expect(catalogSection.items.map(item => item.id)).toEqual(['products', 'facets']); @@ -180,7 +180,7 @@ describe('NavBuilderService', () => { 'products', ); - service.navMenuConfig$.pipe(take(1)).subscribe(result => { + service.mainMenuConfig$.pipe(take(1)).subscribe(result => { const catalogSection = result.find(r => r.id === 'catalog')!; expect(catalogSection.items.map(item => item.id)).toEqual(['facets', 'products']); diff --git a/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder.service.ts b/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder.service.ts index 79e74e5dcd..efcf3c6e35 100644 --- a/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder.service.ts +++ b/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder.service.ts @@ -135,7 +135,7 @@ export function addActionBarItem(config: ActionBarItem): Provider { providedIn: 'root', }) export class NavBuilderService { - navMenuConfig$: Observable; + menuConfig$: Observable; actionBarConfig$: Observable; sectionBadges: { [sectionId: string]: Observable } = {}; @@ -236,7 +236,7 @@ export class NavBuilderService { shareReplay(1), ); - this.navMenuConfig$ = combineLatest(combinedConfig$, itemAdditions$).pipe( + this.menuConfig$ = combineLatest(combinedConfig$, itemAdditions$).pipe( map(([sections, additionalItems]) => { for (const item of additionalItems) { const section = sections.find(s => s.id === item.sectionId); diff --git a/packages/admin-ui/src/lib/core/src/public_api.ts b/packages/admin-ui/src/lib/core/src/public_api.ts index c7dab26967..c9a20397cf 100644 --- a/packages/admin-ui/src/lib/core/src/public_api.ts +++ b/packages/admin-ui/src/lib/core/src/public_api.ts @@ -243,3 +243,8 @@ export * from './shared/pipes/time-ago.pipe'; export * from './shared/providers/routing/can-deactivate-detail-guard'; export * from './shared/shared.module'; export * from './validators/unicode-pattern.validator'; +export { BreadcrumbDefinition } from './providers/breadcrumb/breadcrumb.service'; +export { BreadcrumbFunction } from './providers/breadcrumb/breadcrumb.service'; +export { BreadcrumbValue } from './providers/breadcrumb/breadcrumb.service'; +export { BreadcrumbLabelLinkPair } from './providers/breadcrumb/breadcrumb.service'; +export { BreadcrumbString } from './providers/breadcrumb/breadcrumb.service'; diff --git a/packages/admin-ui/src/lib/core/src/shared/components/action-bar/action-bar.component.scss b/packages/admin-ui/src/lib/core/src/shared/components/action-bar/action-bar.component.scss index ffa278bee5..0c8dc8e689 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/action-bar/action-bar.component.scss +++ b/packages/admin-ui/src/lib/core/src/shared/components/action-bar/action-bar.component.scss @@ -6,7 +6,7 @@ align-items: baseline; background-color: var(--color-component-bg-100); position: sticky; - top: -24px; + top: 40px; z-index: 25; border-bottom: 1px solid var(--color-component-border-200); diff --git a/packages/admin-ui/src/lib/core/src/shared/components/channel-badge/channel-badge.component.scss b/packages/admin-ui/src/lib/core/src/shared/components/channel-badge/channel-badge.component.scss index a2172de1c3..afaae72388 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/channel-badge/channel-badge.component.scss +++ b/packages/admin-ui/src/lib/core/src/shared/components/channel-badge/channel-badge.component.scss @@ -1,12 +1,11 @@ +@import "variables"; :host { display: inline-block; - - button & { - margin-bottom: -1px; - } } clr-icon { - margin-right: 6px; + @media screen and (max-width: $breakpoint-medium) { + margin-right: 0; + } } diff --git a/packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.html b/packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.html index ca080a67a3..86b06f21a9 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.html +++ b/packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.html @@ -1,4 +1,4 @@ -
+