From 3f07179230519e08a50850432e13d6c4269cc2d4 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 16 Feb 2024 10:36:11 +0100 Subject: [PATCH] feat(admin-ui): Expose `entity$` observable on action bar context --- .../components/base-nav/base-nav.component.ts | 9 +- .../nav-builder/nav-builder-types.ts | 55 +++++++++++- .../admin-ui/src/lib/core/src/public_api.ts | 1 + .../action-bar-dropdown-menu.component.ts | 79 ++--------------- .../action-bar-base.component.ts | 87 +++++++++++++++++++ .../action-bar-items.component.ts | 86 ++---------------- 6 files changed, 161 insertions(+), 156 deletions(-) create mode 100644 packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-base.component.ts 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 index 9df0b3bf5c..61857670d9 100644 --- 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 @@ -1,7 +1,7 @@ import { Directive, Injector, 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 { of, Subscription } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; import { Permission } from '../../common/generated-types'; @@ -10,7 +10,7 @@ import { HealthCheckService } from '../../providers/health-check/health-check.se import { JobQueueService } from '../../providers/job-queue/job-queue.service'; import { ActionBarContext, NavMenuBadge, NavMenuItem } from '../../providers/nav-builder/nav-builder-types'; import { NavBuilderService } from '../../providers/nav-builder/nav-builder.service'; -// import { NotificationService } from '../../providers/notification/notification.service'; +import { NotificationService } from '../../providers/notification/notification.service'; @Directive({ selector: '[vdrBaseNav]', @@ -24,7 +24,7 @@ export class BaseNavComponent implements OnInit, OnDestroy { protected healthCheckService: HealthCheckService, protected jobQueueService: JobQueueService, protected dataService: DataService, - // protected notificationService: any, + protected notificationService: NotificationService, protected injector: Injector, ) {} @@ -325,7 +325,8 @@ export class BaseNavComponent implements OnInit, OnDestroy { route: this.route, injector: this.injector, dataService: this.dataService, - notificationService: {} as any, + notificationService: this.notificationService, + entity$: of(undefined), }; } } 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 6212959f0e..4d8926118a 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 @@ -82,15 +82,66 @@ export interface NavMenuSection { /** * @description - * Providers available to the onClick handler of an {@link ActionBarItem} or {@link NavMenuItem}. + * Providers & data available to the `onClick` & `buttonState` functions of an {@link ActionBarItem}, + * {@link ActionBarDropdownMenuItem} or {@link NavMenuItem}. * * @docsCategory action-bar */ export interface ActionBarContext { + /** + * @description + * The router's [ActivatedRoute](https://angular.dev/guide/routing/router-reference#activated-route) object for + * the current route. This object contains information about the route, its parameters, and additional data + * associated with the route. + */ route: ActivatedRoute; + /** + * @description + * The Angular [Injector](https://angular.dev/api/core/Injector) which can be used to get instances + * of services and other providers available in the application. + */ injector: Injector; + /** + * @description + * The [DataService](/reference/admin-ui-api/services/data-service), which provides methods for querying the + * server-side data. + */ dataService: DataService; + /** + * @description + * The [NotificationService](/reference/admin-ui-api/services/notification-service), which provides methods for + * displaying notifications to the user. + */ notificationService: NotificationService; + /** + * @description + * An observable of the current entity in a detail view. In a list view the observable will not emit any values. + * + * @example + * ```ts + * addActionBarDropdownMenuItem({ + * id: 'print-invoice', + * locationId: 'order-detail', + * label: 'Print Invoice', + * icon: 'printer', + * buttonState: context => { + * // highlight-start + * return context.entity$.pipe( + * map((order) => { + * return order?.state === 'PaymentSettled' + * ? { disabled: false, visible: true } + * : { disabled: true, visible: true }; + * }), + * ); + * // highlight-end + * }, + * requiresPermission: ['UpdateOrder'], + * }), + * ``` + * + * @since 2.2.0 + */ + entity$: Observable | undefined>; } export interface ActionBarButtonState { @@ -174,7 +225,7 @@ export interface ActionBarDropdownMenuItem { * A function which returns an observable of the button state, allowing you to * dynamically enable/disable or show/hide the button. */ - buttonState?: (context: ActionBarContext) => Observable; + buttonState?: (context: ActionBarContext) => Observable; onClick?: (event: MouseEvent, context: ActionBarContext) => void; routerLink?: RouterLinkDefinition; icon?: string; 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 d2301ab4e9..b7e7925954 100644 --- a/packages/admin-ui/src/lib/core/src/public_api.ts +++ b/packages/admin-ui/src/lib/core/src/public_api.ts @@ -126,6 +126,7 @@ export * from './providers/page/page.service'; export * from './providers/permissions/permissions.service'; export * from './shared/components/action-bar/action-bar.component'; export * from './shared/components/action-bar-dropdown-menu/action-bar-dropdown-menu.component'; +export * from './shared/components/action-bar-items/action-bar-base.component'; export * from './shared/components/action-bar-items/action-bar-items.component'; export * from './shared/components/address-form/address-form.component'; export * from './shared/components/affixed-input/affixed-input.component'; diff --git a/packages/admin-ui/src/lib/core/src/shared/components/action-bar-dropdown-menu/action-bar-dropdown-menu.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/action-bar-dropdown-menu/action-bar-dropdown-menu.component.ts index 1d45a538d7..cfca2deed9 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/action-bar-dropdown-menu/action-bar-dropdown-menu.component.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/action-bar-dropdown-menu/action-bar-dropdown-menu.component.ts @@ -2,29 +2,16 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, - HostBinding, - Injector, Input, - OnChanges, OnInit, Self, - SimpleChanges, ViewChild, } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs'; +import { combineLatest } from 'rxjs'; import { map, tap } from 'rxjs/operators'; - -import { ActionBarLocationId } from '../../../common/component-registry-types'; -import { DataService } from '../../../data/providers/data.service'; -import { - ActionBarButtonState, - ActionBarContext, - ActionBarDropdownMenuItem, -} from '../../../providers/nav-builder/nav-builder-types'; -import { NavBuilderService } from '../../../providers/nav-builder/nav-builder.service'; -import { NotificationService } from '../../../providers/notification/notification.service'; +import { ActionBarDropdownMenuItem } from '../../../providers/nav-builder/nav-builder-types'; +import { ActionBarBaseComponent } from '../action-bar-items/action-bar-base.component'; import { DropdownComponent } from '../dropdown/dropdown.component'; @Component({ @@ -47,55 +34,27 @@ import { DropdownComponent } from '../dropdown/dropdown.component'; }, ], }) -export class ActionBarDropdownMenuComponent implements OnInit, OnChanges, AfterViewInit { +export class ActionBarDropdownMenuComponent + extends ActionBarBaseComponent + implements OnInit, AfterViewInit +{ @ViewChild('dropdownComponent') dropdownComponent: DropdownComponent; @Input() alwaysShow = false; - @HostBinding('attr.data-location-id') - @Input() - locationId: ActionBarLocationId; - - items$: Observable; - buttonStates: { [id: string]: Observable } = {}; - private locationId$ = new BehaviorSubject(''); private onDropdownComponentResolvedFn: (dropdownComponent: DropdownComponent) => void; - constructor( - private navBuilderService: NavBuilderService, - private route: ActivatedRoute, - private dataService: DataService, - private notificationService: NotificationService, - private injector: Injector, - ) {} - ngOnInit() { this.items$ = combineLatest(this.navBuilderService.actionBarDropdownConfig$, this.locationId$).pipe( map(([items, locationId]) => items.filter(config => config.locationId === locationId)), tap(items => { - const context = this.createContext(); - for (const item of items) { - const buttonState$ = - typeof item.buttonState === 'function' - ? item.buttonState(context) - : of({ - disabled: false, - visible: true, - }); - this.buttonStates[item.id] = buttonState$; - } + this.buildButtonStates(items); }), ); } - ngOnChanges(changes: SimpleChanges): void { - if ('locationId' in changes) { - this.locationId$.next(changes['locationId'].currentValue); - } - } - ngAfterViewInit() { if (this.onDropdownComponentResolvedFn) { this.onDropdownComponentResolvedFn(this.dropdownComponent); @@ -105,26 +64,4 @@ export class ActionBarDropdownMenuComponent implements OnInit, OnChanges, AfterV onDropdownComponentResolved(fn: (dropdownComponent: DropdownComponent) => void) { this.onDropdownComponentResolvedFn = fn; } - - handleClick(event: MouseEvent, item: ActionBarDropdownMenuItem) { - if (typeof item.onClick === 'function') { - item.onClick(event, this.createContext()); - } - } - - getRouterLink(item: ActionBarDropdownMenuItem): any[] | null { - return this.navBuilderService.getRouterLink( - { routerLink: item.routerLink, context: this.createContext() }, - this.route, - ); - } - - private createContext(): ActionBarContext { - return { - route: this.route, - injector: this.injector, - dataService: this.dataService, - notificationService: this.notificationService, - }; - } } diff --git a/packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-base.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-base.component.ts new file mode 100644 index 0000000000..f130761f40 --- /dev/null +++ b/packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-base.component.ts @@ -0,0 +1,87 @@ +import { Directive, HostBinding, inject, Injector, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { BehaviorSubject, Observable, of, switchMap } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { ActionBarLocationId } from '../../../common/component-registry-types'; +import { DataService } from '../../../data/providers/data.service'; +import { + ActionBarButtonState, + ActionBarContext, + ActionBarDropdownMenuItem, + ActionBarItem, +} from '../../../providers/nav-builder/nav-builder-types'; +import { NavBuilderService } from '../../../providers/nav-builder/nav-builder.service'; +import { NotificationService } from '../../../providers/notification/notification.service'; + +@Directive() +export abstract class ActionBarBaseComponent + implements OnChanges +{ + @HostBinding('attr.data-location-id') + @Input() + locationId: ActionBarLocationId; + + items$: Observable; + buttonStates: { [id: string]: Observable } = {}; + protected locationId$ = new BehaviorSubject(''); + protected navBuilderService = inject(NavBuilderService); + protected route = inject(ActivatedRoute); + protected dataService = inject(DataService); + protected notificationService = inject(NotificationService); + protected injector = inject(Injector); + + ngOnChanges(changes: SimpleChanges): void { + if ('locationId' in changes) { + this.locationId$.next(changes['locationId'].currentValue); + } + } + + handleClick(event: MouseEvent, item: T) { + if (typeof item.onClick === 'function') { + item.onClick(event, this.createContext()); + } + } + + getRouterLink(item: T): any[] | null { + return this.navBuilderService.getRouterLink( + { routerLink: item.routerLink, context: this.createContext() }, + this.route, + ); + } + + protected buildButtonStates(items: T[]) { + const context = this.createContext(); + const defaultState = { + disabled: false, + visible: true, + }; + for (const item of items) { + const buttonState$ = + typeof item.buttonState === 'function' + ? item.buttonState(context).pipe( + map(result => result ?? defaultState), + catchError(() => of(defaultState)), + ) + : of(defaultState); + this.buttonStates[item.id] = buttonState$; + } + } + + protected createContext(): ActionBarContext { + return { + route: this.route, + injector: this.injector, + dataService: this.dataService, + notificationService: this.notificationService, + entity$: this.route.data.pipe( + switchMap(data => { + if (data.detail?.entity) { + return data.detail.entity as Observable>; + } else { + return of(undefined); + } + }), + ), + }; + } +} diff --git a/packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-items.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-items.component.ts index 64b0dbb858..727d87737b 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-items.component.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-items.component.ts @@ -1,27 +1,9 @@ -import { - ChangeDetectionStrategy, - Component, - HostBinding, - Injector, - Input, - OnChanges, - OnInit, - SimpleChanges, -} from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ChangeDetectionStrategy, Component, OnChanges, OnInit } from '@angular/core'; import { assertNever } from '@vendure/common/lib/shared-utils'; -import { BehaviorSubject, combineLatest, mergeAll, Observable, of } from 'rxjs'; +import { combineLatest } from 'rxjs'; import { map, tap } from 'rxjs/operators'; - -import { ActionBarLocationId } from '../../../common/component-registry-types'; -import { DataService } from '../../../data/providers/data.service'; -import { - ActionBarButtonState, - ActionBarContext, - ActionBarItem, -} from '../../../providers/nav-builder/nav-builder-types'; -import { NavBuilderService } from '../../../providers/nav-builder/nav-builder.service'; -import { NotificationService } from '../../../providers/notification/notification.service'; +import { ActionBarItem } from '../../../providers/nav-builder/nav-builder-types'; +import { ActionBarBaseComponent } from './action-bar-base.component'; @Component({ selector: 'vdr-action-bar-items', @@ -29,61 +11,16 @@ import { NotificationService } from '../../../providers/notification/notificatio styleUrls: ['./action-bar-items.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ActionBarItemsComponent implements OnInit, OnChanges { - @HostBinding('attr.data-location-id') - @Input() - locationId: ActionBarLocationId; - - items$: Observable; - buttonStates: { [id: string]: Observable } = {}; - private locationId$ = new BehaviorSubject(''); - - constructor( - private navBuilderService: NavBuilderService, - private route: ActivatedRoute, - private dataService: DataService, - private notificationService: NotificationService, - private injector: Injector, - ) {} - +export class ActionBarItemsComponent extends ActionBarBaseComponent implements OnInit { ngOnInit() { - this.items$ = combineLatest(this.navBuilderService.actionBarConfig$, this.locationId$).pipe( + this.items$ = combineLatest([this.navBuilderService.actionBarConfig$, this.locationId$]).pipe( map(([items, locationId]) => items.filter(config => config.locationId === locationId)), tap(items => { - const context = this.createContext(); - for (const item of items) { - const buttonState$ = - typeof item.buttonState === 'function' - ? item.buttonState(context) - : of({ - disabled: false, - visible: true, - }); - this.buttonStates[item.id] = buttonState$; - } + this.buildButtonStates(items); }), ); } - ngOnChanges(changes: SimpleChanges): void { - if ('locationId' in changes) { - this.locationId$.next(changes['locationId'].currentValue); - } - } - - handleClick(event: MouseEvent, item: ActionBarItem) { - if (typeof item.onClick === 'function') { - item.onClick(event, this.createContext()); - } - } - - getRouterLink(item: ActionBarItem): any[] | null { - return this.navBuilderService.getRouterLink( - { routerLink: item.routerLink, context: this.createContext() }, - this.route, - ); - } - getButtonStyles(item: ActionBarItem): string[] { const styles = ['button']; if (item.buttonStyle && item.buttonStyle === 'link') { @@ -109,13 +46,4 @@ export class ActionBarItemsComponent implements OnInit, OnChanges { return ''; } } - - private createContext(): ActionBarContext { - return { - route: this.route, - injector: this.injector, - dataService: this.dataService, - notificationService: this.notificationService, - }; - } }