Browse Source

feat(admin-ui): Can add custom buttons to list/detail views

Relates to #55
Michael Bromley 6 years ago
parent
commit
ef47c62caf
38 changed files with 281 additions and 108 deletions
  1. 1 0
      packages/admin-ui/src/app/catalog/components/asset-list/asset-list.component.html
  2. 1 0
      packages/admin-ui/src/app/catalog/components/collection-detail/collection-detail.component.html
  3. 1 0
      packages/admin-ui/src/app/catalog/components/collection-list/collection-list.component.html
  4. 1 0
      packages/admin-ui/src/app/catalog/components/facet-detail/facet-detail.component.html
  5. 1 0
      packages/admin-ui/src/app/catalog/components/facet-list/facet-list.component.html
  6. 1 0
      packages/admin-ui/src/app/catalog/components/product-detail/product-detail.component.html
  7. 1 0
      packages/admin-ui/src/app/catalog/components/product-list/product-list.component.html
  8. 6 6
      packages/admin-ui/src/app/core/components/main-nav/main-nav.component.html
  9. 26 29
      packages/admin-ui/src/app/core/components/main-nav/main-nav.component.ts
  10. 46 0
      packages/admin-ui/src/app/core/providers/nav-builder/nav-builder-types.ts
  11. 72 68
      packages/admin-ui/src/app/core/providers/nav-builder/nav-builder.service.ts
  12. 1 0
      packages/admin-ui/src/app/customer/components/customer-detail/customer-detail.component.html
  13. 1 0
      packages/admin-ui/src/app/customer/components/customer-list/customer-list.component.html
  14. 1 0
      packages/admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.html
  15. 1 0
      packages/admin-ui/src/app/marketing/components/promotion-list/promotion-list.component.html
  16. 1 0
      packages/admin-ui/src/app/order/components/order-detail/order-detail.component.html
  17. 3 1
      packages/admin-ui/src/app/order/components/order-list/order-list.component.html
  18. 1 0
      packages/admin-ui/src/app/settings/components/admin-detail/admin-detail.component.html
  19. 1 0
      packages/admin-ui/src/app/settings/components/administrator-list/administrator-list.component.html
  20. 1 0
      packages/admin-ui/src/app/settings/components/channel-detail/channel-detail.component.html
  21. 1 0
      packages/admin-ui/src/app/settings/components/channel-list/channel-list.component.html
  22. 1 0
      packages/admin-ui/src/app/settings/components/country-detail/country-detail.component.html
  23. 1 0
      packages/admin-ui/src/app/settings/components/country-list/country-list.component.html
  24. 1 0
      packages/admin-ui/src/app/settings/components/global-settings/global-settings.component.html
  25. 2 1
      packages/admin-ui/src/app/settings/components/payment-method-detail/payment-method-detail.component.html
  26. 5 1
      packages/admin-ui/src/app/settings/components/payment-method-list/payment-method-list.component.html
  27. 1 0
      packages/admin-ui/src/app/settings/components/role-detail/role-detail.component.html
  28. 1 0
      packages/admin-ui/src/app/settings/components/role-list/role-list.component.html
  29. 1 0
      packages/admin-ui/src/app/settings/components/shipping-method-detail/shipping-method-detail.component.html
  30. 1 0
      packages/admin-ui/src/app/settings/components/shipping-method-list/shipping-method-list.component.html
  31. 1 0
      packages/admin-ui/src/app/settings/components/tax-category-detail/tax-category-detail.component.html
  32. 1 0
      packages/admin-ui/src/app/settings/components/tax-category-list/tax-category-list.component.html
  33. 1 0
      packages/admin-ui/src/app/settings/components/tax-rate-detail/tax-rate-detail.component.html
  34. 1 0
      packages/admin-ui/src/app/settings/components/tax-rate-list/tax-rate-list.component.html
  35. 10 0
      packages/admin-ui/src/app/shared/components/action-bar-items/action-bar-items.component.html
  36. 0 0
      packages/admin-ui/src/app/shared/components/action-bar-items/action-bar-items.component.scss
  37. 80 0
      packages/admin-ui/src/app/shared/components/action-bar-items/action-bar-items.component.ts
  38. 4 2
      packages/admin-ui/src/app/shared/shared.module.ts

+ 1 - 0
packages/admin-ui/src/app/catalog/components/asset-list/asset-list.component.html

@@ -9,6 +9,7 @@
         />
     </vdr-ab-left>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="asset-list"></vdr-action-bar-items>
         <vdr-asset-file-input
             (selectFiles)="filesSelected($event)"
             dropZoneTarget=".content-area"

+ 1 - 0
packages/admin-ui/src/app/catalog/components/collection-detail/collection-detail.component.html

@@ -9,6 +9,7 @@
     </vdr-ab-left>
 
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="collection-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             *ngIf="isNew$ | async; else updateButton"

+ 1 - 0
packages/admin-ui/src/app/catalog/components/collection-list/collection-list.component.html

@@ -1,5 +1,6 @@
 <vdr-action-bar>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="collection-list"></vdr-action-bar-items>
         <a class="btn btn-primary" *vdrIfPermissions="'CreateCatalog'" [routerLink]="['./create']">
             <clr-icon shape="plus"></clr-icon>
             {{ 'catalog.create-new-collection' | translate }}

+ 1 - 0
packages/admin-ui/src/app/catalog/components/facet-detail/facet-detail.component.html

@@ -9,6 +9,7 @@
     </vdr-ab-left>
 
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="facet-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             *ngIf="isNew$ | async; else updateButton"

+ 1 - 0
packages/admin-ui/src/app/catalog/components/facet-list/facet-list.component.html

@@ -1,5 +1,6 @@
 <vdr-action-bar>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="facet-list"></vdr-action-bar-items>
         <a class="btn btn-primary"
            *vdrIfPermissions="'CreateCatalog'"
            [routerLink]="['./create']">

+ 1 - 0
packages/admin-ui/src/app/catalog/components/product-detail/product-detail.component.html

@@ -18,6 +18,7 @@
     </vdr-ab-left>
 
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="product-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             *ngIf="isNew$ | async; else updateButton"

+ 1 - 0
packages/admin-ui/src/app/catalog/components/product-list/product-list.component.html

@@ -29,6 +29,7 @@
         </clr-checkbox-wrapper>
     </vdr-ab-left>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="product-list"></vdr-action-bar-items>
         <a class="btn btn-primary" [routerLink]="['./create']" *vdrIfPermissions="'CreateCatalog'">
             <clr-icon shape="plus"></clr-icon>
             <span class="full-label">{{ 'catalog.create-new-product' | translate }}</span>

+ 6 - 6
packages/admin-ui/src/app/core/components/main-nav/main-nav.component.html

@@ -1,20 +1,20 @@
 <nav class="sidenav" [clr-nav-level]="2">
     <section class="sidenav-content">
-        <ng-container *ngFor="let section of menuBuilderService.navMenuConfig$ | async">
+        <ng-container *ngFor="let section of navBuilderService.navMenuConfig$ | async">
             <section
                 class="nav-group"
-                [attr.data-section-name]="section.name"
+                [attr.data-section-id]="section.id"
                 [class.collapsible]="section.collapsible"
                 *vdrIfPermissions="section.requiresPermission"
             >
-                <input [id]="section.name" type="checkbox" [checked]="section.collapsedByDefault" />
-                <label [for]="section.name">{{ section.label | translate }}</label>
+                <input [id]="section.id" type="checkbox" [checked]="section.collapsedByDefault" />
+                <label [for]="section.id">{{ section.label | translate }}</label>
                 <ul class="nav-list">
                     <li *ngFor="let item of section.items">
                         <a
                             class="nav-link"
-                            [attr.data-item-name]="section.name"
-                            [routerLink]="item.routerLink"
+                            [attr.data-item-id]="section.id"
+                            [routerLink]="getRouterLink(item)"
                             routerLinkActive="active"
                         >
                             <clr-icon [attr.shape]="item.icon" size="20"></clr-icon>

+ 26 - 29
packages/admin-ui/src/app/core/components/main-nav/main-nav.component.ts

@@ -1,6 +1,7 @@
 import { Component, OnInit } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
 
+import { NavMenuItem } from '../../providers/nav-builder/nav-builder-types';
 import { NavBuilderService } from '../../providers/nav-builder/nav-builder.service';
 
 @Component({
@@ -12,36 +13,36 @@ export class MainNavComponent implements OnInit {
     constructor(
         private route: ActivatedRoute,
         private router: Router,
-        public menuBuilderService: NavBuilderService,
+        public navBuilderService: NavBuilderService,
     ) {}
 
     ngOnInit(): void {
-        this.menuBuilderService.defineNavMenuSections([
+        this.navBuilderService.defineNavMenuSections([
             {
                 requiresPermission: 'ReadCatalog',
-                name: 'catalog',
+                id: 'catalog',
                 label: 'nav.catalog',
                 items: [
                     {
-                        name: 'products',
+                        id: 'products',
                         label: 'nav.products',
                         icon: 'library',
                         routerLink: ['/catalog', 'products'],
                     },
                     {
-                        name: 'facets',
+                        id: 'facets',
                         label: 'nav.facets',
                         icon: 'tag',
                         routerLink: ['/catalog', 'facets'],
                     },
                     {
-                        name: 'collections',
+                        id: 'collections',
                         label: 'nav.collections',
                         icon: 'folder-open',
                         routerLink: ['/catalog', 'collections'],
                     },
                     {
-                        name: 'assets',
+                        id: 'assets',
                         label: 'nav.assets',
                         icon: 'image-gallery',
                         routerLink: ['/catalog', 'assets'],
@@ -49,12 +50,12 @@ export class MainNavComponent implements OnInit {
                 ],
             },
             {
-                name: 'sales',
+                id: 'sales',
                 label: 'nav.sales',
                 requiresPermission: 'ReadOrder',
                 items: [
                     {
-                        name: 'orders',
+                        id: 'orders',
                         label: 'nav.orders',
                         routerLink: ['/orders'],
                         icon: 'shopping-cart',
@@ -62,12 +63,12 @@ export class MainNavComponent implements OnInit {
                 ],
             },
             {
-                name: 'customers',
+                id: 'customers',
                 label: 'nav.customers',
                 requiresPermission: 'ReadCustomer',
                 items: [
                     {
-                        name: 'customers',
+                        id: 'customers',
                         label: 'nav.customers',
                         routerLink: ['/customer', 'customers'],
                         icon: 'user',
@@ -75,12 +76,12 @@ export class MainNavComponent implements OnInit {
                 ],
             },
             {
-                name: 'marketing',
+                id: 'marketing',
                 label: 'nav.marketing',
                 requiresPermission: 'ReadPromotion',
                 items: [
                     {
-                        name: 'promotions',
+                        id: 'promotions',
                         label: 'nav.promotions',
                         routerLink: ['/marketing', 'promotions'],
                         icon: 'asterisk',
@@ -88,64 +89,64 @@ export class MainNavComponent implements OnInit {
                 ],
             },
             {
-                name: 'settings',
+                id: 'settings',
                 label: 'nav.settings',
                 requiresPermission: 'ReadSettings',
                 collapsible: true,
                 collapsedByDefault: true,
                 items: [
                     {
-                        name: 'channels',
+                        id: 'channels',
                         label: 'nav.channels',
                         routerLink: ['/settings', 'channels'],
                         icon: 'layers',
                     },
                     {
-                        name: 'administrators',
+                        id: 'administrators',
                         label: 'nav.administrators',
                         requiresPermission: 'ReadAdministrator',
                         routerLink: ['/settings', 'administrators'],
                         icon: 'administrator',
                     },
                     {
-                        name: 'roles',
+                        id: 'roles',
                         label: 'nav.roles',
                         requiresPermission: 'ReadAdministrator',
                         routerLink: ['/settings', 'roles'],
                         icon: 'users',
                     },
                     {
-                        name: 'shipping-methods',
+                        id: 'shipping-methods',
                         label: 'nav.shipping-methods',
                         routerLink: ['/settings', 'shipping-methods'],
                         icon: 'truck',
                     },
                     {
-                        name: 'payment-methods',
+                        id: 'payment-methods',
                         label: 'nav.payment-methods',
                         routerLink: ['/settings', 'payment-methods'],
                         icon: 'credit-card',
                     },
                     {
-                        name: 'tax-categories',
+                        id: 'tax-categories',
                         label: 'nav.tax-categories',
                         routerLink: ['/settings', 'tax-categories'],
                         icon: 'view-list',
                     },
                     {
-                        name: 'tax-rates',
+                        id: 'tax-rates',
                         label: 'nav.tax-rates',
                         routerLink: ['/settings', 'tax-rates'],
                         icon: 'calculator',
                     },
                     {
-                        name: 'countries',
+                        id: 'countries',
                         label: 'nav.countries',
                         routerLink: ['/settings', 'countries'],
                         icon: 'world',
                     },
                     {
-                        name: 'global-settings',
+                        id: 'global-settings',
                         label: 'nav.global-settings',
                         routerLink: ['/settings', 'global-settings'],
                         icon: 'cog',
@@ -155,11 +156,7 @@ export class MainNavComponent implements OnInit {
         ]);
     }
 
-    /**
-     * Work-around for routerLinkActive on links which include queryParams.
-     * See https://github.com/angular/angular/issues/13205
-     */
-    isLinkActive(route: string): boolean {
-        return this.router.url.startsWith(route);
+    getRouterLink(item: NavMenuItem) {
+        return this.navBuilderService.getRouterLink(item, this.route);
     }
 }

+ 46 - 0
packages/admin-ui/src/app/core/providers/nav-builder/nav-builder-types.ts

@@ -0,0 +1,46 @@
+import { ActivatedRoute } from '@angular/router';
+import { Observable } from 'rxjs';
+
+/**
+ * A NavMenuItem is a menu item in the main (left-hand side) nav
+ * bar.
+ */
+export interface NavMenuItem {
+    id: string;
+    label: string;
+    routerLink: RouterLinkDefinition;
+    onClick?: (event: MouseEvent) => void;
+    icon?: string;
+    requiresPermission?: string;
+}
+
+/**
+ * A NavMenuSection is a grouping of links in the main
+ * (left-hand side) nav bar.
+ */
+export interface NavMenuSection {
+    id: string;
+    label: string;
+    items: NavMenuItem[];
+    requiresPermission?: string;
+    collapsible?: boolean;
+    collapsedByDefault?: boolean;
+}
+
+/**
+ * A button in the ActionBar area at the top of one of the list or detail views.
+ */
+export interface ActionBarItem {
+    id: string;
+    label: string;
+    locationId: string;
+    disabled?: Observable<boolean>;
+    onClick?: (event: MouseEvent, route: ActivatedRoute) => void;
+    routerLink?: RouterLinkDefinition;
+    buttonColor?: 'primary' | 'success' | 'warning';
+    buttonStyle?: 'solid' | 'outline' | 'link';
+    icon?: string;
+    requiresPermission?: string;
+}
+
+export type RouterLinkDefinition = ((route: ActivatedRoute) => any[]) | any[];

+ 72 - 68
packages/admin-ui/src/app/core/providers/nav-builder/nav-builder.service.ts

@@ -1,31 +1,9 @@
 import { Injectable } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
 import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
 import { map, scan, shareReplay } from 'rxjs/operators';
 
-/**
- * A NavMenuItem is a menu item in the main (left-hand side) nav
- * bar.
- */
-export interface NavMenuItem {
-    name: string;
-    label: string;
-    routerLink: any[];
-    icon?: string;
-    requiresPermission?: string;
-}
-
-/**
- * A NavMenuSection is a grouping of links in the main
- * (left-hand side) nav bar.
- */
-export interface NavMenuSection {
-    name: string;
-    label: string;
-    items: NavMenuItem[];
-    requiresPermission?: string;
-    collapsible?: boolean;
-    collapsedByDefault?: boolean;
-}
+import { ActionBarItem, NavMenuItem, NavMenuSection, RouterLinkDefinition } from './nav-builder-types';
 
 /**
  * This service is used to define the contents of configurable menus in the application.
@@ -33,6 +11,7 @@ export interface NavMenuSection {
 @Injectable()
 export class NavBuilderService {
     navMenuConfig$: Observable<NavMenuSection[]>;
+    actionBarConfig$: Observable<ActionBarItem[]>;
 
     private initialNavMenuConfig$ = new BehaviorSubject<NavMenuSection[]>([]);
     private addNavMenuSection$ = new BehaviorSubject<{ config: NavMenuSection; before?: string } | null>(
@@ -40,33 +19,82 @@ export class NavBuilderService {
     );
     private addNavMenuItem$ = new BehaviorSubject<{
         config: NavMenuItem;
-        section: string;
+        sectionId: string;
         before?: string;
     } | null>(null);
+    private addActionBarItem$ = new BehaviorSubject<ActionBarItem | null>(null);
 
     constructor() {
+        this.setupStreams();
+    }
+
+    /**
+     * Used to define the initial sections and items of the main nav menu.
+     */
+    defineNavMenuSections(config: NavMenuSection[]) {
+        this.initialNavMenuConfig$.next(config);
+    }
+
+    /**
+     * Add a section to the main nav menu. Providing the `before` argument will
+     * move the section before any existing section with the specified id. If
+     * omitted (or if the id is not found) the section will be appended to the
+     * existing set of sections.
+     */
+    addNavMenuSection(config: NavMenuSection, before?: string) {
+        this.addNavMenuSection$.next({ config, before });
+    }
+
+    /**
+     * Add a menu item to an existing section specified by `sectionId`. The id of the section
+     * can be found by inspecting the DOM and finding the `data-section-id` attribute.
+     * Providing the `before` argument will move the item before any existing item with the specified id.
+     * If omitted (or if the name is not found) the item will be appended to the
+     * end of the section.
+     */
+    addNavMenuItem(config: NavMenuItem, sectionId: string, before?: string) {
+        this.addNavMenuItem$.next({ config, sectionId, before });
+    }
+
+    /**
+     * Adds a button 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 <vdr-action-bar> element and its
+     * `data-location-id` attribute.
+     */
+    addActionBarItem(config: ActionBarItem) {
+        this.addActionBarItem$.next(config);
+    }
+
+    getRouterLink(config: { routerLink?: RouterLinkDefinition }, route: ActivatedRoute): string[] | null {
+        if (typeof config.routerLink === 'function') {
+            return config.routerLink(route);
+        }
+        if (Array.isArray(config.routerLink)) {
+            return config.routerLink;
+        }
+        return null;
+    }
+
+    private setupStreams() {
         const sectionAdditions$ = this.addNavMenuSection$.pipe(
-            scan(
-                (acc, value) => {
-                    return value ? [...acc, value] : acc;
-                },
-                [] as Array<{ config: NavMenuSection; before?: string }>,
-            ),
+            scan((acc, value) => (value ? [...acc, value] : acc), [] as Array<{
+                config: NavMenuSection;
+                before?: string;
+            }>),
         );
 
         const itemAdditions$ = this.addNavMenuItem$.pipe(
-            scan(
-                (acc, value) => {
-                    return value ? [...acc, value] : acc;
-                },
-                [] as Array<{ config: NavMenuItem; section: string; before?: string }>,
-            ),
+            scan((acc, value) => (value ? [...acc, value] : acc), [] as Array<{
+                config: NavMenuItem;
+                sectionId: string;
+                before?: string;
+            }>),
         );
 
         const combinedConfig$ = combineLatest(this.initialNavMenuConfig$, sectionAdditions$).pipe(
             map(([initalConfig, additions]) => {
                 for (const { config, before } of additions) {
-                    const index = initalConfig.findIndex(c => c.name === before);
+                    const index = initalConfig.findIndex(c => c.id === before);
                     if (-1 < index) {
                         initalConfig.splice(index, 0, config);
                     } else {
@@ -81,14 +109,14 @@ export class NavBuilderService {
         this.navMenuConfig$ = combineLatest(combinedConfig$, itemAdditions$).pipe(
             map(([sections, additionalItems]) => {
                 for (const item of additionalItems) {
-                    const section = sections.find(s => s.name === item.section);
+                    const section = sections.find(s => s.id === item.sectionId);
                     if (!section) {
                         // tslint:disable-next-line:no-console
                         console.error(
-                            `Could not add menu item "${item.config.name}", section "${item.section}" does not exist`,
+                            `Could not add menu item "${item.config.id}", section "${item.sectionId}" does not exist`,
                         );
                     } else {
-                        const index = section.items.findIndex(i => i.name === item.before);
+                        const index = section.items.findIndex(i => i.id === item.before);
                         if (-1 < index) {
                             section.items.splice(index, 0, item.config);
                         } else {
@@ -99,33 +127,9 @@ export class NavBuilderService {
                 return sections;
             }),
         );
-    }
-
-    /**
-     * Used to define the initial sections and items of the main nav menu.
-     */
-    defineNavMenuSections(config: NavMenuSection[]) {
-        this.initialNavMenuConfig$.next(config);
-    }
-
-    /**
-     * Add a section to the main nav menu. Providing the `before` argument will
-     * move the section before any existing section with the specified name. If
-     * omitted (or if the name is not found) the section will be appended to the
-     * existing set of sections.
-     */
-    addNavMenuSection(config: NavMenuSection, before?: string) {
-        this.addNavMenuSection$.next({ config, before });
-    }
 
-    /**
-     * Add a menu item to an existing section specified by `section`. The name of the section
-     * can be found by inspecting the DOM and finding the `data-section-name` attribute.
-     * Providing the `before` argument will move the item before any existing item with the specified name.
-     * If omitted (or if the name is not found) the item will be appended to the
-     * end of the section.
-     */
-    addNavMenuItem(config: NavMenuItem, section: string, before?: string) {
-        this.addNavMenuItem$.next({ config, section, before });
+        this.actionBarConfig$ = this.addActionBarItem$.pipe(
+            scan((acc, value) => (value ? [...acc, value] : acc), [] as ActionBarItem[]),
+        );
     }
 }

+ 1 - 0
packages/admin-ui/src/app/customer/components/customer-detail/customer-detail.component.html

@@ -4,6 +4,7 @@
     </vdr-ab-left>
 
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="customer-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             *ngIf="(isNew$ | async); else updateButton"

+ 1 - 0
packages/admin-ui/src/app/customer/components/customer-list/customer-list.component.html

@@ -9,6 +9,7 @@
         />
     </vdr-ab-left>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="customer-list"></vdr-action-bar-items>
         <a class="btn btn-primary" [routerLink]="['./create']">
             <clr-icon shape="plus"></clr-icon>
             {{ 'customer.create-new-customer' | translate }}

+ 1 - 0
packages/admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.html

@@ -12,6 +12,7 @@
     </vdr-ab-left>
 
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="promotion-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             *ngIf="isNew$ | async; else updateButton"

+ 1 - 0
packages/admin-ui/src/app/marketing/components/promotion-list/promotion-list.component.html

@@ -1,5 +1,6 @@
 <vdr-action-bar>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="promotion-list"></vdr-action-bar-items>
         <a class="btn btn-primary"
            *vdrIfPermissions="'CreatePromotion'"
            [routerLink]="['./create']">

+ 1 - 0
packages/admin-ui/src/app/order/components/order-detail/order-detail.component.html

@@ -10,6 +10,7 @@
     </vdr-ab-left>
 
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="order-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             (click)="fulfillOrder()"

+ 3 - 1
packages/admin-ui/src/app/order/components/order-list/order-list.component.html

@@ -23,7 +23,9 @@
             />
         </div>
     </vdr-ab-left>
-    <vdr-ab-right></vdr-ab-right>
+    <vdr-ab-right>
+        <vdr-action-bar-items locationId="order-list"></vdr-action-bar-items>
+    </vdr-ab-right>
 </vdr-action-bar>
 
 <vdr-data-table

+ 1 - 0
packages/admin-ui/src/app/settings/components/admin-detail/admin-detail.component.html

@@ -1,6 +1,7 @@
 <vdr-action-bar>
     <vdr-ab-left></vdr-ab-left>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="administrator-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             *ngIf="(isNew$ | async); else updateButton"

+ 1 - 0
packages/admin-ui/src/app/settings/components/administrator-list/administrator-list.component.html

@@ -1,5 +1,6 @@
 <vdr-action-bar>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="administrator-list"></vdr-action-bar-items>
         <a class="btn btn-primary" [routerLink]="['./create']" *vdrIfPermissions="'CreateAdministrator'">
             <clr-icon shape="plus"></clr-icon>
             {{ 'admin.create-new-administrator' | translate }}

+ 1 - 0
packages/admin-ui/src/app/settings/components/channel-detail/channel-detail.component.html

@@ -2,6 +2,7 @@
     <vdr-ab-left></vdr-ab-left>
 
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="channel-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             *ngIf="isNew$ | async; else updateButton"

+ 1 - 0
packages/admin-ui/src/app/settings/components/channel-list/channel-list.component.html

@@ -1,5 +1,6 @@
 <vdr-action-bar>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="channel-list"></vdr-action-bar-items>
         <a class="btn btn-primary" [routerLink]="['./create']" *vdrIfPermissions="'SuperAdmin'">
             <clr-icon shape="plus"></clr-icon>
             {{ 'settings.create-new-channel' | translate }}

+ 1 - 0
packages/admin-ui/src/app/settings/components/country-detail/country-detail.component.html

@@ -8,6 +8,7 @@
         ></vdr-language-selector>
     </vdr-ab-left>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="country-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             *ngIf="isNew$ | async; else updateButton"

+ 1 - 0
packages/admin-ui/src/app/settings/components/country-list/country-list.component.html

@@ -18,6 +18,7 @@
     </vdr-ab-left>
 
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="country-list"></vdr-action-bar-items>
         <a class="btn btn-primary" [routerLink]="['./create']" *vdrIfPermissions="'CreateSettings'">
             <clr-icon shape="plus"></clr-icon>
             {{ 'settings.create-new-country' | translate }}

+ 1 - 0
packages/admin-ui/src/app/settings/components/global-settings/global-settings.component.html

@@ -1,5 +1,6 @@
 <vdr-action-bar>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="global-settings-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             (click)="save()"

+ 2 - 1
packages/admin-ui/src/app/settings/components/payment-method-detail/payment-method-detail.component.html

@@ -2,6 +2,7 @@
     <vdr-ab-left></vdr-ab-left>
 
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="payment-method-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             *ngIf="isNew$ | async; else updateButton"
@@ -11,7 +12,7 @@
         </button>
         <ng-template #updateButton>
             <button
-                *vdrIfPermissions="'UpdateSettings' | hasPermission"
+                *vdrIfPermissions="'UpdateSettings'"
                 class="btn btn-primary"
                 (click)="save()"
                 [disabled]="detailForm.pristine || detailForm.invalid"

+ 5 - 1
packages/admin-ui/src/app/settings/components/payment-method-list/payment-method-list.component.html

@@ -1,4 +1,8 @@
-<vdr-action-bar><vdr-ab-right></vdr-ab-right></vdr-action-bar>
+<vdr-action-bar>
+    <vdr-ab-right>
+        <vdr-action-bar-items locationId="payment-method-list"></vdr-action-bar-items>
+    </vdr-ab-right>
+</vdr-action-bar>
 
 <vdr-data-table
     [items]="items$ | async"

+ 1 - 0
packages/admin-ui/src/app/settings/components/role-detail/role-detail.component.html

@@ -1,6 +1,7 @@
 <vdr-action-bar>
     <vdr-ab-left></vdr-ab-left>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="role-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             *ngIf="(isNew$ | async); else updateButton"

+ 1 - 0
packages/admin-ui/src/app/settings/components/role-list/role-list.component.html

@@ -1,5 +1,6 @@
 <vdr-action-bar>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="role-list"></vdr-action-bar-items>
         <a class="btn btn-primary" [routerLink]="['./create']">
             <clr-icon shape="plus"></clr-icon>
             {{ 'settings.create-new-role' | translate }}

+ 1 - 0
packages/admin-ui/src/app/settings/components/shipping-method-detail/shipping-method-detail.component.html

@@ -2,6 +2,7 @@
     <vdr-ab-left></vdr-ab-left>
 
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="shipping-method-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             *ngIf="isNew$ | async; else updateButton"

+ 1 - 0
packages/admin-ui/src/app/settings/components/shipping-method-list/shipping-method-list.component.html

@@ -1,5 +1,6 @@
 <vdr-action-bar>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="shipping-method-list"></vdr-action-bar-items>
         <a class="btn btn-primary" [routerLink]="['./create']" *vdrIfPermissions="'CreateSettings'">
             <clr-icon shape="plus"></clr-icon>
             {{ 'settings.create-new-shipping-method' | translate }}

+ 1 - 0
packages/admin-ui/src/app/settings/components/tax-category-detail/tax-category-detail.component.html

@@ -2,6 +2,7 @@
     <vdr-ab-left></vdr-ab-left>
 
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="tax-category-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             *ngIf="isNew$ | async; else updateButton"

+ 1 - 0
packages/admin-ui/src/app/settings/components/tax-category-list/tax-category-list.component.html

@@ -1,5 +1,6 @@
 <vdr-action-bar>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="tax-category-list"></vdr-action-bar-items>
         <a class="btn btn-primary" [routerLink]="['./create']" *vdrIfPermissions="'CreateSettings'">
             <clr-icon shape="plus"></clr-icon>
             {{ 'settings.create-new-tax-category' | translate }}

+ 1 - 0
packages/admin-ui/src/app/settings/components/tax-rate-detail/tax-rate-detail.component.html

@@ -2,6 +2,7 @@
     <vdr-ab-left></vdr-ab-left>
 
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="tax-rate-detail"></vdr-action-bar-items>
         <button
             class="btn btn-primary"
             *ngIf="isNew$ | async; else updateButton"

+ 1 - 0
packages/admin-ui/src/app/settings/components/tax-rate-list/tax-rate-list.component.html

@@ -1,5 +1,6 @@
 <vdr-action-bar>
     <vdr-ab-right>
+        <vdr-action-bar-items locationId="tax-rate-list"></vdr-action-bar-items>
         <a class="btn btn-primary" [routerLink]="['./create']" *vdrIfPermissions="'CreateSettings'">
             <clr-icon shape="plus"></clr-icon>
             {{ 'settings.create-new-tax-rate' | translate }}

+ 10 - 0
packages/admin-ui/src/app/shared/components/action-bar-items/action-bar-items.component.html

@@ -0,0 +1,10 @@
+<ng-container *ngFor="let item of items$ | async">
+    <button *vdrIfPermissions="item.requiresPermission"
+            [routerLink]="getRouterLink(item)"
+            [disabled]="item.disabled ? (item.disabled | async) : false"
+            (click)="handleClick($event, item)"
+            [ngClass]="getButtonStyles(item)">
+        <clr-icon *ngIf="item.icon" [attr.shape]="item.icon"></clr-icon>
+        {{ item.label | translate }}
+    </button>
+</ng-container>

+ 0 - 0
packages/admin-ui/src/app/shared/components/action-bar-items/action-bar-items.component.scss


+ 80 - 0
packages/admin-ui/src/app/shared/components/action-bar-items/action-bar-items.component.ts

@@ -0,0 +1,80 @@
+import {
+    ChangeDetectionStrategy,
+    Component,
+    HostBinding,
+    Input,
+    OnChanges,
+    OnInit,
+    SimpleChanges,
+} from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { ActionBarItem } from '@vendure/admin-ui/src/app/core/providers/nav-builder/nav-builder-types';
+import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
+import { filter, map } from 'rxjs/operators';
+import { assertNever } from 'shared/shared-utils';
+
+import { NavBuilderService } from '../../../core/providers/nav-builder/nav-builder.service';
+
+@Component({
+    selector: 'vdr-action-bar-items',
+    templateUrl: './action-bar-items.component.html',
+    styleUrls: ['./action-bar-items.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ActionBarItemsComponent implements OnInit, OnChanges {
+    @HostBinding('attr.data-location-id')
+    @Input()
+    locationId: string;
+
+    items$: Observable<ActionBarItem[]>;
+    private locationId$ = new BehaviorSubject<string>('');
+
+    constructor(private navBuilderService: NavBuilderService, private route: ActivatedRoute) {}
+
+    ngOnInit() {
+        this.items$ = combineLatest(this.navBuilderService.actionBarConfig$, this.locationId$).pipe(
+            map(([items, locationId]) => items.filter(config => config.locationId === locationId)),
+        );
+    }
+
+    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.route);
+        }
+    }
+
+    getRouterLink(item: ActionBarItem): any[] | null {
+        return this.navBuilderService.getRouterLink(item, this.route);
+    }
+
+    getButtonStyles(item: ActionBarItem): string[] {
+        const styles = ['btn'];
+        if (item.buttonStyle && item.buttonStyle === 'link') {
+            styles.push('btn-link');
+            return styles;
+        }
+        styles.push(this.getButtonColorClass(item));
+        return styles;
+    }
+
+    private getButtonColorClass(item: ActionBarItem): string {
+        switch (item.buttonColor) {
+            case undefined:
+            case 'primary':
+                return item.buttonStyle === 'outline' ? 'btn-outline' : 'btn-primary';
+            case 'success':
+                return item.buttonStyle === 'outline' ? 'btn-success-outline' : 'btn-success';
+            case 'warning':
+                return item.buttonStyle === 'outline' ? 'btn-warning-outline' : 'btn-warning';
+            default:
+                assertNever(item.buttonColor);
+                return '';
+        }
+    }
+}

+ 4 - 2
packages/admin-ui/src/app/shared/shared.module.ts

@@ -8,6 +8,7 @@ import { NgSelectModule } from '@ng-select/ng-select';
 import { TranslateModule } from '@ngx-translate/core';
 import { NgxPaginationModule } from 'ngx-pagination';
 
+import { ActionBarItemsComponent } from './components/action-bar-items/action-bar-items.component';
 import {
     ActionBarComponent,
     ActionBarLeftComponent,
@@ -17,7 +18,6 @@ import { IfPermissionsDirective } from './directives/if-permissions.directive';
 import { HasPermissionPipe } from './pipes/has-permission.pipe';
 import { ModalService } from './providers/modal/modal.service';
 import { CanDeactivateDetailGuard } from './providers/routing/can-deactivate-detail-guard';
-import { AffixedInputComponent } from './shared-declarations';
 import { PercentageSuffixInputComponent } from './shared-declarations';
 import { ChipComponent } from './shared-declarations';
 import { ConfigurableInputComponent } from './shared-declarations';
@@ -35,7 +35,7 @@ import { FacetValueSelectorComponent } from './shared-declarations';
 import { FormFieldControlDirective } from './shared-declarations';
 import { FormFieldComponent } from './shared-declarations';
 import { FormItemComponent } from './shared-declarations';
-import { FormattedAddressComponent } from './shared-declarations';
+import { AffixedInputComponent } from './shared-declarations';
 import { ItemsPerPageControlsComponent } from './shared-declarations';
 import { LabeledDataComponent } from './shared-declarations';
 import { LanguageSelectorComponent } from './shared-declarations';
@@ -56,6 +56,7 @@ import { FileSizePipe } from './shared-declarations';
 import { SentenceCasePipe } from './shared-declarations';
 import { SortPipe } from './shared-declarations';
 import { StringToColorPipe } from './shared-declarations';
+import { FormattedAddressComponent } from './shared-declarations';
 
 const IMPORTS = [
     ClarityModule,
@@ -114,6 +115,7 @@ const DECLARATIONS = [
     ObjectTreeComponent,
     IfPermissionsDirective,
     HasPermissionPipe,
+    ActionBarItemsComponent,
 ];
 
 @NgModule({