diff --git a/packages/admin-ui/src/lib/catalog/src/components/asset-detail/asset-detail.component.html b/packages/admin-ui/src/lib/catalog/src/components/asset-detail/asset-detail.component.html index f7858a972c..4fc2e3b9ca 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/asset-detail/asset-detail.component.html +++ b/packages/admin-ui/src/lib/catalog/src/components/asset-detail/asset-detail.component.html @@ -3,7 +3,7 @@ - + + diff --git a/packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.html b/packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.html index 94cba06157..c3a2ba922e 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.html +++ b/packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.html @@ -9,7 +9,7 @@ > - + - + + +
@@ -138,10 +139,7 @@ > - + - + {{ 'catalog.create-new-collection' | translate }} + diff --git a/packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.html b/packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.html index a24b52e385..79dbac4928 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.html +++ b/packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.html @@ -10,7 +10,7 @@ - + + diff --git a/packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html b/packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html index 3d99d7f678..e6e7b242ad 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html +++ b/packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html @@ -17,17 +17,12 @@ {{ 'catalog.create-new-product' | translate }} - - - - - - + diff --git a/packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.html b/packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.html index ad9c11974a..931a1868fd 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.html +++ b/packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.html @@ -32,6 +32,7 @@ > {{ 'common.update' | translate }} + diff --git a/packages/admin-ui/src/lib/core/src/extension/add-action-bar-dropdown-menu-item.ts b/packages/admin-ui/src/lib/core/src/extension/add-action-bar-dropdown-menu-item.ts new file mode 100644 index 0000000000..cc3cabdeac --- /dev/null +++ b/packages/admin-ui/src/lib/core/src/extension/add-action-bar-dropdown-menu-item.ts @@ -0,0 +1,34 @@ +import { APP_INITIALIZER, Provider } from '@angular/core'; +import { ActionBarDropdownMenuItem } from '../providers/nav-builder/nav-builder-types'; +import { NavBuilderService } from '../providers/nav-builder/nav-builder.service'; + +/** + * @description + * Adds a dropdown menu item to the ActionBar at the top right of each list or detail view. The locationId can + * be determined by pressing `ctrl + u` when running the Admin UI in dev mode. + * + * @example + * ```ts title="providers.ts" + * import { addActionBarDropdownMenuItem } from '\@vendure/admin-ui/core'; + * + * export default [ + * addActionBarDropdownMenuItem({ + * id: 'print-invoice', + * label: 'Print Invoice', + * locationId: 'order-detail', + * routerLink: ['/extensions/invoicing'], + * }), + * ]; + * ``` + * @docsCategory action-bar + */ +export function addActionBarDropdownMenuItem(config: ActionBarDropdownMenuItem): Provider { + return { + provide: APP_INITIALIZER, + multi: true, + useFactory: (navBuilderService: NavBuilderService) => () => { + navBuilderService.addActionBarDropdownMenuItem(config); + }, + deps: [NavBuilderService], + }; +} 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 2a30f00b19..6212959f0e 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 @@ -105,6 +105,10 @@ export interface ActionBarButtonState { * @docsCategory action-bar */ export interface ActionBarItem { + /** + * @description + * A unique identifier for the item. + */ id: string; label: string; locationId: ActionBarLocationId; @@ -145,6 +149,53 @@ export interface ActionBarItem { requiresPermission?: string | string[]; } +/** + * @description + * A dropdown menu item in the ActionBar area at the top of one of the list or detail views. + * + * @docsCategory action-bar + * @since 2.2.0 + */ +export interface ActionBarDropdownMenuItem { + /** + * @description + * A unique identifier for the item. + */ + id: string; + label: string; + locationId: ActionBarLocationId; + /** + * @description + * Whether to render a divider above this item. + */ + hasDivider?: boolean; + /** + * @description + * 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; + onClick?: (event: MouseEvent, context: ActionBarContext) => void; + routerLink?: RouterLinkDefinition; + icon?: string; + /** + * @description + * Control the display of this item based on the user permissions. Note: if you attempt to pass a + * {@link PermissionDefinition} object, you will get a compilation error. Instead, pass the plain + * string version. For example, if the permission is defined as: + * ```ts + * export const MyPermission = new PermissionDefinition('ProductReview'); + * ``` + * then the generated permission strings will be: + * + * - `CreateProductReview` + * - `ReadProductReview` + * - `UpdateProductReview` + * - `DeleteProductReview` + */ + requiresPermission?: string | string[]; +} + /** * @description * A function which returns the router link for an {@link ActionBarItem} or {@link NavMenuItem}. 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 ea6189b39c..0517fdbc4e 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 @@ -8,6 +8,7 @@ import { Permission } from '../../common/generated-types'; import { ActionBarContext, + ActionBarDropdownMenuItem, ActionBarItem, NavMenuBadgeType, NavMenuItem, @@ -24,6 +25,7 @@ import { export class NavBuilderService { menuConfig$: Observable; actionBarConfig$: Observable; + actionBarDropdownConfig$: Observable; sectionBadges: { [sectionId: string]: Observable } = {}; private initialNavMenuConfig$ = new BehaviorSubject([]); @@ -34,6 +36,7 @@ export class NavBuilderService { before?: string; }> = []; private addedActionBarItems: ActionBarItem[] = []; + private addedActionBarDropdownMenuItems: ActionBarDropdownMenuItem[] = []; constructor() { this.setupStreams(); @@ -86,6 +89,20 @@ export class NavBuilderService { } } + /** + * Adds a dropdown menu to the ActionBar at the top right of each list or detail view. The locationId can + * be determined by inspecting the DOM and finding the `` element and its + * `data-location-id` attribute. + */ + addActionBarDropdownMenuItem(config: ActionBarDropdownMenuItem) { + if (!this.addedActionBarDropdownMenuItems.find(item => item.id === config.id)) { + this.addedActionBarDropdownMenuItems.push({ + requiresPermission: Permission.Authenticated, + ...config, + }); + } + } + getRouterLink( config: { routerLink?: RouterLinkDefinition; context: ActionBarContext }, route: ActivatedRoute, @@ -185,5 +202,6 @@ export class NavBuilderService { ); this.actionBarConfig$ = of(this.addedActionBarItems); + this.actionBarDropdownConfig$ = of(this.addedActionBarDropdownMenuItems); } } 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 c93f716a77..d2301ab4e9 100644 --- a/packages/admin-ui/src/lib/core/src/public_api.ts +++ b/packages/admin-ui/src/lib/core/src/public_api.ts @@ -74,6 +74,7 @@ export * from './data/utils/add-custom-fields'; export * from './data/utils/get-server-location'; export * from './data/utils/remove-readonly-custom-fields'; export * from './data/utils/transform-relation-custom-field-inputs'; +export * from './extension/add-action-bar-dropdown-menu-item'; export * from './extension/add-action-bar-item'; export * from './extension/add-nav-menu-item'; export * from './extension/components/angular-route.component'; @@ -124,6 +125,7 @@ export * from './providers/overlay-host/overlay-host.service'; 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-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.css b/packages/admin-ui/src/lib/core/src/shared/components/action-bar-dropdown-menu/action-bar-dropdown-menu.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/admin-ui/src/lib/core/src/shared/components/action-bar-dropdown-menu/action-bar-dropdown-menu.component.html b/packages/admin-ui/src/lib/core/src/shared/components/action-bar-dropdown-menu/action-bar-dropdown-menu.component.html new file mode 100644 index 0000000000..ab01ee20a6 --- /dev/null +++ b/packages/admin-ui/src/lib/core/src/shared/components/action-bar-dropdown-menu/action-bar-dropdown-menu.component.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + 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 new file mode 100644 index 0000000000..1d45a538d7 --- /dev/null +++ b/packages/admin-ui/src/lib/core/src/shared/components/action-bar-dropdown-menu/action-bar-dropdown-menu.component.ts @@ -0,0 +1,130 @@ +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 { 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 { DropdownComponent } from '../dropdown/dropdown.component'; + +@Component({ + selector: 'vdr-action-bar-dropdown-menu', + templateUrl: './action-bar-dropdown-menu.component.html', + styleUrls: ['./action-bar-dropdown-menu.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + // This is a rather involved work-around to allow the {@link DropdownItemDirective} to + // be able to access the DropdownComponent instance even when it is not a direct parent, + // as is the case when this component is used. + { + provide: DropdownComponent, + useFactory: (actionBarDropdownMenuComponent: ActionBarDropdownMenuComponent) => { + return new Promise(resolve => + actionBarDropdownMenuComponent.onDropdownComponentResolved(cmp => resolve(cmp)), + ); + }, + deps: [[new Self(), ActionBarDropdownMenuComponent]], + }, + ], +}) +export class ActionBarDropdownMenuComponent implements OnInit, OnChanges, 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$; + } + }), + ); + } + + ngOnChanges(changes: SimpleChanges): void { + if ('locationId' in changes) { + this.locationId$.next(changes['locationId'].currentValue); + } + } + + ngAfterViewInit() { + if (this.onDropdownComponentResolvedFn) { + this.onDropdownComponentResolvedFn(this.dropdownComponent); + } + } + + 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/dropdown/dropdown-item.directive.ts b/packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-item.directive.ts index 8809ddb682..b8caaeb5dc 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-item.directive.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-item.directive.ts @@ -1,4 +1,4 @@ -import { Directive, HostListener } from '@angular/core'; +import { Directive, HostListener, Inject } from '@angular/core'; import { DropdownComponent } from './dropdown.component'; @@ -8,10 +8,12 @@ import { DropdownComponent } from './dropdown.component'; host: { '[class.dropdown-item]': 'true' }, }) export class DropdownItemDirective { - constructor(private dropdown: DropdownComponent) {} + constructor( + @Inject(DropdownComponent) private dropdown: DropdownComponent | Promise, + ) {} @HostListener('click', ['$event']) - onDropdownItemClick(event: any): void { - this.dropdown.onClick(); + async onDropdownItemClick() { + (await this.dropdown).onClick(); } } diff --git a/packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts index 5b789f81d1..febfd971c3 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts @@ -17,7 +17,7 @@ import { UIExtensionLocationId } from '../../../common/component-registry-types' import { DataService } from '../../../data/providers/data.service'; import { DropdownComponent } from '../dropdown/dropdown.component'; -type UiExtensionType = 'actionBar' | 'navMenu' | 'detailComponent' | 'dataTable'; +type UiExtensionType = 'actionBar' | 'actionBarDropdown' | 'navMenu' | 'detailComponent' | 'dataTable'; @Component({ selector: 'vdr-ui-extension-point', @@ -25,7 +25,7 @@ type UiExtensionType = 'actionBar' | 'navMenu' | 'detailComponent' | 'dataTable' styleUrls: ['./ui-extension-point.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class UiExtensionPointComponent implements OnInit, AfterViewInit { +export class UiExtensionPointComponent implements OnInit { @Input() locationId: UIExtensionLocationId; @Input() metadata?: any; @Input() topPx: number; @@ -66,13 +66,6 @@ export class UiExtensionPointComponent implements OnInit, AfterViewInit { }), ); } - - ngAfterViewInit() { - // this.dropdownComponent.onOpenChange(isOpen => { - // if (isOpen) { - // } - // }); - } } function highlightTsCode(tsCode: string) { @@ -108,6 +101,16 @@ export default [ label: 'My Action', locationId: '${locationId}', }), +];`, + actionBarDropdown: locationId => ` +import { addActionBarDropdownMenuItem } from '@vendure/admin-ui/core'; + +export default [ + addActionBarDropdownMenuItem({ + id: 'my-dropdown-item', + label: 'My Action', + locationId: '${locationId}', + }), ];`, navMenu: locationId => ` import { addNavMenuSection } from '@vendure/admin-ui/core'; diff --git a/packages/admin-ui/src/lib/core/src/shared/shared.module.ts b/packages/admin-ui/src/lib/core/src/shared/shared.module.ts index 9ca91622c4..339fc16544 100644 --- a/packages/admin-ui/src/lib/core/src/shared/shared.module.ts +++ b/packages/admin-ui/src/lib/core/src/shared/shared.module.ts @@ -173,6 +173,7 @@ import { LanguageCodeSelectorComponent } from './components/language-code-select import { DataTableFilterPresetsComponent } from './components/data-table-filter-presets/data-table-filter-presets.component'; import { AddFilterPresetButtonComponent } from './components/data-table-filter-presets/add-filter-preset-button.component'; import { RenameFilterPresetDialogComponent } from './components/data-table-filter-presets/rename-filter-preset-dialog.component'; +import { ActionBarDropdownMenuComponent } from './components/action-bar-dropdown-menu/action-bar-dropdown-menu.component'; const IMPORTS = [ ClarityModule, @@ -192,6 +193,7 @@ const DECLARATIONS = [ ActionBarComponent, ActionBarLeftComponent, ActionBarRightComponent, + ActionBarDropdownMenuComponent, AssetPreviewComponent, AssetPreviewDialogComponent, AssetSearchInputComponent, diff --git a/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.html b/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.html index b21dc417ec..2538e7792c 100644 --- a/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.html +++ b/packages/admin-ui/src/lib/customer/src/components/customer-detail/customer-detail.component.html @@ -22,6 +22,7 @@ {{ 'common.update' | translate }} + diff --git a/packages/admin-ui/src/lib/customer/src/components/customer-group-detail/customer-group-detail.component.html b/packages/admin-ui/src/lib/customer/src/components/customer-group-detail/customer-group-detail.component.html index 8450d0e75d..465fd13aa9 100644 --- a/packages/admin-ui/src/lib/customer/src/components/customer-group-detail/customer-group-detail.component.html +++ b/packages/admin-ui/src/lib/customer/src/components/customer-group-detail/customer-group-detail.component.html @@ -22,6 +22,7 @@ {{ 'common.update' | translate }} + diff --git a/packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.html b/packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.html index 32793cfe83..ce72b125a8 100644 --- a/packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.html +++ b/packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.html @@ -7,6 +7,7 @@ {{ 'customer.create-new-customer-group' | translate }} + diff --git a/packages/admin-ui/src/lib/customer/src/components/customer-list/customer-list.component.html b/packages/admin-ui/src/lib/customer/src/components/customer-list/customer-list.component.html index 719f0fa16f..4717ad9804 100644 --- a/packages/admin-ui/src/lib/customer/src/components/customer-list/customer-list.component.html +++ b/packages/admin-ui/src/lib/customer/src/components/customer-list/customer-list.component.html @@ -7,6 +7,7 @@ {{ 'customer.create-new-customer' | translate }} + diff --git a/packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.html b/packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.html index 534b4d563a..e92f8b53ba 100644 --- a/packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.html +++ b/packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.html @@ -12,7 +12,7 @@ - + - - - - - - + diff --git a/packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html b/packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html index e669078652..13e06cf23a 100644 --- a/packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html +++ b/packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html @@ -37,57 +37,50 @@ > {{ 'order.fulfill-order' | translate }} - - - - - - + + + + + + + + + - - - - - - - - - + + + + diff --git a/packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html b/packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html index cdf8edba13..40a4729a9f 100644 --- a/packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html +++ b/packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html @@ -1,13 +1,14 @@ - + {{ 'catalog.create-draft-order' | translate }} + diff --git a/packages/admin-ui/src/lib/settings/src/components/admin-detail/admin-detail.component.html b/packages/admin-ui/src/lib/settings/src/components/admin-detail/admin-detail.component.html index 1f5871d7e5..64641b7f59 100644 --- a/packages/admin-ui/src/lib/settings/src/components/admin-detail/admin-detail.component.html +++ b/packages/admin-ui/src/lib/settings/src/components/admin-detail/admin-detail.component.html @@ -2,7 +2,7 @@ - + + diff --git a/packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.html b/packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.html index d826bd14aa..8b287c6ad5 100644 --- a/packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.html +++ b/packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.html @@ -6,11 +6,11 @@ [availableLanguageCodes]="availableLanguages$ | async" [currentLanguageCode]="languageCode$ | async" (languageCodeChange)="setLanguage($event)" - > + /> - +