Skip to content

Commit

Permalink
feat(admin-ui): Expose entity$ observable on action bar context
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Feb 16, 2024
1 parent 4d8bc74 commit 3f07179
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 156 deletions.
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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]',
Expand All @@ -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,
) {}

Expand Down Expand Up @@ -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),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/admin-ui/src/lib/core/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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);
Expand All @@ -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,
};
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}),
),
};
}
}
Loading

0 comments on commit 3f07179

Please sign in to comment.