Browse Source

feat(admin-ui): Expose `entity$` observable on action bar context

Michael Bromley 1 year ago
parent
commit
3f07179230

+ 5 - 4
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),
         };
     }
 }

+ 53 - 2
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<Record<string, any> | 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<ActionBarButtonState>;
+    buttonState?: (context: ActionBarContext) => Observable<ActionBarButtonState | undefined>;
     onClick?: (event: MouseEvent, context: ActionBarContext) => void;
     routerLink?: RouterLinkDefinition;
     icon?: string;

+ 1 - 0
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';

+ 8 - 71
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<ActionBarDropdownMenuItem>
+    implements OnInit, AfterViewInit
+{
     @ViewChild('dropdownComponent')
     dropdownComponent: DropdownComponent;
 
     @Input()
     alwaysShow = false;
 
-    @HostBinding('attr.data-location-id')
-    @Input()
-    locationId: ActionBarLocationId;
-
-    items$: Observable<ActionBarDropdownMenuItem[]>;
-    buttonStates: { [id: string]: Observable<ActionBarButtonState> } = {};
-    private locationId$ = new BehaviorSubject<string>('');
     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,
-        };
-    }
 }

+ 87 - 0
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<T extends ActionBarItem | ActionBarDropdownMenuItem>
+    implements OnChanges
+{
+    @HostBinding('attr.data-location-id')
+    @Input()
+    locationId: ActionBarLocationId;
+
+    items$: Observable<T[]>;
+    buttonStates: { [id: string]: Observable<ActionBarButtonState> } = {};
+    protected locationId$ = new BehaviorSubject<string>('');
+    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<Record<string, any>>;
+                    } else {
+                        return of(undefined);
+                    }
+                }),
+            ),
+        };
+    }
+}

+ 7 - 79
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<ActionBarItem[]>;
-    buttonStates: { [id: string]: Observable<ActionBarButtonState> } = {};
-    private locationId$ = new BehaviorSubject<string>('');
-
-    constructor(
-        private navBuilderService: NavBuilderService,
-        private route: ActivatedRoute,
-        private dataService: DataService,
-        private notificationService: NotificationService,
-        private injector: Injector,
-    ) {}
-
+export class ActionBarItemsComponent extends ActionBarBaseComponent<ActionBarItem> 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,
-        };
-    }
 }