Parcourir la source

feat(admin-ui): Display available UI extension points

Relates to #415
Michael Bromley il y a 4 ans
Parent
commit
0963745add
20 fichiers modifiés avec 305 ajouts et 45 suppressions
  1. 9 2
      packages/admin-ui/src/app/app.module.ts
  2. 28 1
      packages/admin-ui/src/lib/core/src/app.component.ts
  3. 20 1
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  4. 38 0
      packages/admin-ui/src/lib/core/src/common/ui-extension-types.ts
  5. 30 22
      packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.html
  6. 1 0
      packages/admin-ui/src/lib/core/src/data/client-state/client-defaults.ts
  7. 19 0
      packages/admin-ui/src/lib/core/src/data/client-state/client-resolvers.ts
  8. 2 0
      packages/admin-ui/src/lib/core/src/data/client-state/client-types.graphql
  9. 8 0
      packages/admin-ui/src/lib/core/src/data/definitions/client-definitions.ts
  10. 11 0
      packages/admin-ui/src/lib/core/src/data/providers/client-data.service.ts
  11. 23 1
      packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder-types.ts
  12. 9 9
      packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder.service.ts
  13. 11 7
      packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-items.component.html
  14. 4 0
      packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-items.component.scss
  15. 2 1
      packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-items.component.ts
  16. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/action-bar/action-bar.component.scss
  17. 29 0
      packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.html
  18. 31 0
      packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.scss
  19. 27 0
      packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts
  20. 2 0
      packages/admin-ui/src/lib/core/src/shared/shared.module.ts

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

@@ -1,6 +1,6 @@
 import { NgModule } from '@angular/core';
 import { RouterModule } from '@angular/router';
-import { AppComponent, AppComponentModule } from '@vendure/admin-ui/core';
+import { addActionBarItem, AppComponent, AppComponentModule } from '@vendure/admin-ui/core';
 
 import { routes } from './app.routes';
 
@@ -10,7 +10,14 @@ import { routes } from './app.routes';
         AppComponentModule,
         RouterModule.forRoot(routes, { useHash: false, relativeLinkResolution: 'legacy' }),
     ],
-    providers: [],
+    providers: [
+        addActionBarItem({
+            id: 'test',
+            locationId: 'product-detail',
+            buttonStyle: 'link',
+            label: 'Test Button',
+        }),
+    ],
     bootstrap: [AppComponent],
 })
 export class AppModule {}

+ 28 - 1
packages/admin-ui/src/lib/core/src/app.component.ts

@@ -1,5 +1,5 @@
 import { DOCUMENT } from '@angular/common';
-import { Component, Inject, OnInit } from '@angular/core';
+import { Component, HostListener, Inject, isDevMode, OnInit } from '@angular/core';
 import { Observable } from 'rxjs';
 import { filter, map, switchMap } from 'rxjs/operators';
 
@@ -62,5 +62,32 @@ export class AppComponent implements OnInit {
                     }
                 },
             });
+
+        if (isDevMode()) {
+            // tslint:disable-next-line:no-console
+            console.log(
+                `%cVendure Admin UI: Press "ctrl/cmd + u" to view UI extension points`,
+                `color: #17C1FF; font-weight: bold;`,
+            );
+        }
+    }
+
+    @HostListener('window:keydown', ['$event'])
+    handleGlobalHotkeys(event: KeyboardEvent) {
+        if (event.ctrlKey === true && event.key === 'u') {
+            event.preventDefault();
+            if (isDevMode()) {
+                this.dataService.client
+                    .uiState()
+                    .single$.pipe(
+                        switchMap(({ uiState }) =>
+                            this.dataService.client.setDisplayUiExtensionPoints(
+                                !uiState.displayUiExtensionPoints,
+                            ),
+                        ),
+                    )
+                    .subscribe();
+            }
+        }
     }
 }

+ 20 - 1
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -2390,6 +2390,7 @@ export type Mutation = {
   setAsLoggedIn: UserStatus;
   setAsLoggedOut: UserStatus;
   setContentLanguage: LanguageCode;
+  setDisplayUiExtensionPoints: Scalars['Boolean'];
   setOrderCustomFields?: Maybe<Order>;
   setUiLanguage: LanguageCode;
   setUiTheme: Scalars['String'];
@@ -2846,6 +2847,11 @@ export type MutationSetContentLanguageArgs = {
 };
 
 
+export type MutationSetDisplayUiExtensionPointsArgs = {
+  display: Scalars['Boolean'];
+};
+
+
 export type MutationSetOrderCustomFieldsArgs = {
   input: UpdateOrderInput;
 };
@@ -4924,6 +4930,7 @@ export type UiState = {
   language: LanguageCode;
   contentLanguage: LanguageCode;
   theme: Scalars['String'];
+  displayUiExtensionPoints: Scalars['Boolean'];
 };
 
 export type UpdateActiveAdministratorInput = {
@@ -5481,6 +5488,13 @@ export type SetUiLanguageMutationVariables = Exact<{
 
 export type SetUiLanguageMutation = Pick<Mutation, 'setUiLanguage'>;
 
+export type SetDisplayUiExtensionPointsMutationVariables = Exact<{
+  display: Scalars['Boolean'];
+}>;
+
+
+export type SetDisplayUiExtensionPointsMutation = Pick<Mutation, 'setDisplayUiExtensionPoints'>;
+
 export type SetContentLanguageMutationVariables = Exact<{
   languageCode: LanguageCode;
 }>;
@@ -5516,7 +5530,7 @@ export type GetUiStateQueryVariables = Exact<{ [key: string]: never; }>;
 
 export type GetUiStateQuery = { uiState: (
     { __typename?: 'UiState' }
-    & Pick<UiState, 'language' | 'contentLanguage' | 'theme'>
+    & Pick<UiState, 'language' | 'contentLanguage' | 'theme' | 'displayUiExtensionPoints'>
   ) };
 
 export type GetClientStateQueryVariables = Exact<{ [key: string]: never; }>;
@@ -9068,6 +9082,11 @@ export namespace SetUiLanguage {
   export type Mutation = SetUiLanguageMutation;
 }
 
+export namespace SetDisplayUiExtensionPoints {
+  export type Variables = SetDisplayUiExtensionPointsMutationVariables;
+  export type Mutation = SetDisplayUiExtensionPointsMutation;
+}
+
 export namespace SetContentLanguage {
   export type Variables = SetContentLanguageMutationVariables;
   export type Mutation = SetContentLanguageMutation;

+ 38 - 0
packages/admin-ui/src/lib/core/src/common/ui-extension-types.ts

@@ -0,0 +1,38 @@
+export type ActionBarLocationId =
+    | 'administrator-detail'
+    | 'administrator-list'
+    | 'asset-detail'
+    | 'asset-list'
+    | 'channel-detail'
+    | 'channel-list'
+    | 'collection-detail'
+    | 'collection-list'
+    | 'country-detail'
+    | 'country-list'
+    | 'customer-detail'
+    | 'customer-list'
+    | 'customer-group-list'
+    | 'facet-detail'
+    | 'facet-list'
+    | 'global-setting-detail'
+    | 'system-status'
+    | 'job-list'
+    | 'order-detail'
+    | 'order-list'
+    | 'payment-method-detail'
+    | 'payment-method-list'
+    | 'product-detail'
+    | 'product-list'
+    | 'promotion-detail'
+    | 'promotion-list'
+    | 'role-detail'
+    | 'role-list'
+    | 'shipping-method-detail'
+    | 'shipping-method-list'
+    | 'tax-category-detail'
+    | 'tax-category-list'
+    | 'tax-rate-detail'
+    | 'tax-rate-list'
+    | 'zone-list';
+
+export type UIExtensionLocationId = ActionBarLocationId;

+ 30 - 22
packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.html

@@ -7,29 +7,37 @@
                 [class.collapsible]="section.collapsible"
                 *ngIf="shouldDisplayLink(section)"
             >
-                <ng-container *ngIf="navBuilderService.sectionBadges[section.id] | async as sectionBadge">
-                    <vdr-status-badge *ngIf="sectionBadge !== 'none'" [type]="sectionBadge"></vdr-status-badge>
-                </ng-container>
-                <input [id]="section.id" type="checkbox" [checked]="section.collapsedByDefault" />
-                <label [for]="section.id">{{ section.label | translate }}</label>
-                <ul class="nav-list">
-                    <ng-container *ngFor="let item of section.items">
-                        <li *ngIf="shouldDisplayLink(item)">
-                            <a
-                                class="nav-link"
-                                [attr.data-item-id]="section.id"
-                                [routerLink]="getRouterLink(item)"
-                                routerLinkActive="active"
-                            >
-                                <ng-container *ngIf="item.statusBadge | async as itemBadge">
-                                    <vdr-status-badge *ngIf="itemBadge.type !== 'none'" [type]="itemBadge.type"></vdr-status-badge>
-                                </ng-container>
-                                <clr-icon [attr.shape]="item.icon || 'block'" size="20"></clr-icon>
-                                {{ item.label | translate }}
-                            </a>
-                        </li>
+                <vdr-ui-extension-point [locationId]="section.id" api="navMenu" [topPx]="-6" [leftPx]="8">
+                    <ng-container *ngIf="navBuilderService.sectionBadges[section.id] | async as sectionBadge">
+                        <vdr-status-badge
+                            *ngIf="sectionBadge !== 'none'"
+                            [type]="sectionBadge"
+                        ></vdr-status-badge>
                     </ng-container>
-                </ul>
+                    <input [id]="section.id" type="checkbox" [checked]="section.collapsedByDefault" />
+                    <label [for]="section.id">{{ section.label | translate }}</label>
+                    <ul class="nav-list">
+                        <ng-container *ngFor="let item of section.items">
+                            <li *ngIf="shouldDisplayLink(item)">
+                                <a
+                                    class="nav-link"
+                                    [attr.data-item-id]="section.id"
+                                    [routerLink]="getRouterLink(item)"
+                                    routerLinkActive="active"
+                                >
+                                    <ng-container *ngIf="item.statusBadge | async as itemBadge">
+                                        <vdr-status-badge
+                                            *ngIf="itemBadge.type !== 'none'"
+                                            [type]="itemBadge.type"
+                                        ></vdr-status-badge>
+                                    </ng-container>
+                                    <clr-icon [attr.shape]="item.icon || 'block'" size="20"></clr-icon>
+                                    {{ item.label | translate }}
+                                </a>
+                            </li>
+                        </ng-container>
+                    </ul>
+                </vdr-ui-extension-point>
             </section>
         </ng-container>
     </section>

+ 1 - 0
packages/admin-ui/src/lib/core/src/data/client-state/client-defaults.ts

@@ -25,6 +25,7 @@ export function getClientDefaults(localStorageService: LocalStorageService) {
             language: currentLanguage,
             contentLanguage: currentContentLanguage,
             theme: activeTheme,
+            displayUiExtensionPoints: false,
             __typename: 'UiState',
         } as GetUiState.UiState,
     };

+ 19 - 0
packages/admin-ui/src/lib/core/src/data/client-state/client-resolvers.ts

@@ -8,6 +8,7 @@ import {
     SetActiveChannel,
     SetAsLoggedIn,
     SetContentLanguage,
+    SetDisplayUiExtensionPoints,
     SetUiLanguage,
     SetUiTheme,
     UpdateUserChannels,
@@ -79,6 +80,7 @@ export const clientResolvers: ResolverDefinition = {
                     language: args.languageCode,
                     contentLanguage: previous.uiState.contentLanguage,
                     theme: previous.uiState.theme,
+                    displayUiExtensionPoints: previous.uiState.displayUiExtensionPoints,
                 },
             };
             cache.writeQuery({ query: GET_UI_STATE, data });
@@ -93,6 +95,7 @@ export const clientResolvers: ResolverDefinition = {
                     language: previous.uiState.language,
                     contentLanguage: args.languageCode,
                     theme: previous.uiState.theme,
+                    displayUiExtensionPoints: previous.uiState.displayUiExtensionPoints,
                 },
             };
             cache.writeQuery({ query: GET_UI_STATE, data });
@@ -107,11 +110,27 @@ export const clientResolvers: ResolverDefinition = {
                     language: previous.uiState.language,
                     contentLanguage: previous.uiState.contentLanguage,
                     theme: args.theme,
+                    displayUiExtensionPoints: previous.uiState.displayUiExtensionPoints,
                 },
             };
             cache.writeQuery({ query: GET_UI_STATE, data });
             return args.theme;
         },
+        setDisplayUiExtensionPoints: (_, args: SetDisplayUiExtensionPoints.Variables, { cache }): boolean => {
+            // tslint:disable-next-line:no-non-null-assertion
+            const previous = cache.readQuery<GetUiState.Query>({ query: GET_UI_STATE })!;
+            const data: GetUiState.Query = {
+                uiState: {
+                    __typename: 'UiState',
+                    language: previous.uiState.language,
+                    contentLanguage: previous.uiState.contentLanguage,
+                    displayUiExtensionPoints: args.display,
+                    theme: previous.uiState.theme,
+                },
+            };
+            cache.writeQuery({ query: GET_UI_STATE, data });
+            return args.display;
+        },
         setActiveChannel: (_, args: SetActiveChannel.Variables, { cache }): UserStatus => {
             // tslint:disable-next-line:no-non-null-assertion
             const previous = cache.readQuery<GetUserStatus.Query>({ query: GET_USER_STATUS })!;

+ 2 - 0
packages/admin-ui/src/lib/core/src/data/client-state/client-types.graphql

@@ -13,6 +13,7 @@ type Mutation {
     setContentLanguage(languageCode: LanguageCode!): LanguageCode!
     setUiTheme(theme: String!): String!
     setActiveChannel(channelId: ID!): UserStatus!
+    setDisplayUiExtensionPoints(display: Boolean!): Boolean!
     updateUserChannels(channels: [CurrentUserChannelInput!]!): UserStatus!
 }
 
@@ -33,6 +34,7 @@ type UiState {
     language: LanguageCode!
     contentLanguage: LanguageCode!
     theme: String!
+    displayUiExtensionPoints: Boolean!
 }
 
 input CurrentUserChannelInput {

+ 8 - 0
packages/admin-ui/src/lib/core/src/data/definitions/client-definitions.ts

@@ -52,6 +52,12 @@ export const SET_UI_LANGUAGE = gql`
     }
 `;
 
+export const SET_DISPLAY_UI_EXTENSION_POINTS = gql`
+    mutation SetDisplayUiExtensionPoints($display: Boolean!) {
+        setDisplayUiExtensionPoints(display: $display) @client
+    }
+`;
+
 export const SET_CONTENT_LANGUAGE = gql`
     mutation SetContentLanguage($languageCode: LanguageCode!) {
         setContentLanguage(languageCode: $languageCode) @client
@@ -87,6 +93,7 @@ export const GET_UI_STATE = gql`
             language
             contentLanguage
             theme
+            displayUiExtensionPoints
         }
     }
 `;
@@ -103,6 +110,7 @@ export const GET_CLIENT_STATE = gql`
             language
             contentLanguage
             theme
+            displayUiExtensionPoints
         }
     }
     ${USER_STATUS_FRAGMENT}

+ 11 - 0
packages/admin-ui/src/lib/core/src/data/providers/client-data.service.ts

@@ -10,6 +10,7 @@ import {
     SetActiveChannel,
     SetAsLoggedIn,
     SetContentLanguage,
+    SetDisplayUiExtensionPoints,
     SetUiLanguage,
     SetUiTheme,
     UpdateUserChannels,
@@ -24,6 +25,7 @@ import {
     SET_AS_LOGGED_IN,
     SET_AS_LOGGED_OUT,
     SET_CONTENT_LANGUAGE,
+    SET_DISPLAY_UI_EXTENSION_POINTS,
     SET_UI_LANGUAGE,
     SET_UI_THEME,
     UPDATE_USER_CHANNELS,
@@ -97,6 +99,15 @@ export class ClientDataService {
         });
     }
 
+    setDisplayUiExtensionPoints(display: boolean) {
+        return this.baseDataService.mutate<
+            SetDisplayUiExtensionPoints.Mutation,
+            SetDisplayUiExtensionPoints.Variables
+        >(SET_DISPLAY_UI_EXTENSION_POINTS, {
+            display,
+        });
+    }
+
     setActiveChannel(channelId: string) {
         return this.baseDataService.mutate<SetActiveChannel.Mutation, SetActiveChannel.Variables>(
             SET_ACTIVE_CHANNEL,

+ 23 - 1
packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder-types.ts

@@ -1,18 +1,24 @@
 import { ActivatedRoute } from '@angular/router';
 import { Observable } from 'rxjs';
 
+import { UIExtensionLocationId } from '../../common/ui-extension-types';
 import { DataService } from '../../data/providers/data.service';
 import { NotificationService } from '../notification/notification.service';
 
 export type NavMenuBadgeType = 'none' | 'info' | 'success' | 'warning' | 'error';
 
 /**
+ * @description
  * A color-coded notification badge which will be displayed by the
  * NavMenuItem's icon.
+ *
+ * @docsCategory navigation
+ * @docsPage navigation-types
  */
 export interface NavMenuBadge {
     type: NavMenuBadgeType;
     /**
+     * @description
      * If true, the badge will propagate to the NavMenuItem's
      * parent section, displaying a notification badge next
      * to the section name.
@@ -21,8 +27,12 @@ export interface NavMenuBadge {
 }
 
 /**
+ * @description
  * A NavMenuItem is a menu item in the main (left-hand side) nav
  * bar.
+ *
+ * @docsCategory navigation
+ * @docsPage navigation-types
  */
 export interface NavMenuItem {
     id: string;
@@ -38,8 +48,12 @@ export interface NavMenuItem {
 }
 
 /**
+ * @description
  * A NavMenuSection is a grouping of links in the main
  * (left-hand side) nav bar.
+ *
+ * @docsCategory navigation
+ * @docsPage navigation-types
  */
 export interface NavMenuSection {
     id: string;
@@ -54,7 +68,11 @@ export interface NavMenuSection {
 }
 
 /**
+ * @description
  * Utilities available to the onClick handler of an ActionBarItem.
+ *
+ * @docsCategory navigation
+ * @docsPage navigation-types
  */
 export interface OnClickContext {
     route: ActivatedRoute;
@@ -63,12 +81,16 @@ export interface OnClickContext {
 }
 
 /**
+ * @description
  * A button in the ActionBar area at the top of one of the list or detail views.
+ *
+ * @docsCategory navigation
+ * @docsPage navigation-types
  */
 export interface ActionBarItem {
     id: string;
     label: string;
-    locationId: string;
+    locationId: UIExtensionLocationId;
     disabled?: Observable<boolean>;
     onClick?: (event: MouseEvent, context: OnClickContext) => void;
     routerLink?: RouterLinkDefinition;

+ 9 - 9
packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder.service.ts

@@ -28,10 +28,11 @@ import {
  *   imports: [SharedModule],
  *   providers: [
  *     addNavMenuSection({
- *       id: 'reviews',
- *       label: 'Product Reviews',
- *       routerLink: ['/extensions/reviews'],
- *       icon: 'star',
+ *       id: 'reports',
+ *       label: 'Reports',
+ *       items: [{
+ *           // ...
+ *       }],
  *     },
  *     'settings'),
  *   ],
@@ -67,11 +68,10 @@ export function addNavMenuSection(config: NavMenuSection, before?: string): Prov
  *   imports: [SharedModule],
  *   providers: [
  *     addNavMenuItem({
- *       id: 'reports',
- *       label: 'Reports',
- *       items: [{
- *           // ...
- *       }],
+ *       id: 'reviews',
+ *       label: 'Product Reviews',
+ *       routerLink: ['/extensions/reviews'],
+ *       icon: 'star',
  *     },
  *     'marketing'),
  *   ],

+ 11 - 7
packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-items.component.html

@@ -1,10 +1,14 @@
-<ng-container *ngFor="let item of items$ | async">
-    <button *vdrIfPermissions="item.requiresPermission"
+<vdr-ui-extension-point [locationId]="locationId" api="actionBar" [leftPx]="-24" [topPx]="-6">
+    <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>
+            [ngClass]="getButtonStyles(item)"
+        >
+            <clr-icon *ngIf="item.icon" [attr.shape]="item.icon"></clr-icon>
+            {{ item.label | translate }}
+        </button>
+    </ng-container>
+</vdr-ui-extension-point>

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

@@ -0,0 +1,4 @@
+:host {
+    display: inline-block;
+    min-height: 36px;
+}

+ 2 - 1
packages/admin-ui/src/lib/core/src/shared/components/action-bar-items/action-bar-items.component.ts

@@ -12,6 +12,7 @@ import { assertNever } from '@vendure/common/lib/shared-utils';
 import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
 import { filter, map } from 'rxjs/operators';
 
+import { ActionBarLocationId } from '../../../common/ui-extension-types';
 import { DataService } from '../../../data/providers/data.service';
 import { ActionBarItem } from '../../../providers/nav-builder/nav-builder-types';
 import { NavBuilderService } from '../../../providers/nav-builder/nav-builder.service';
@@ -26,7 +27,7 @@ import { NotificationService } from '../../../providers/notification/notificatio
 export class ActionBarItemsComponent implements OnInit, OnChanges {
     @HostBinding('attr.data-location-id')
     @Input()
-    locationId: string;
+    locationId: ActionBarLocationId;
 
     items$: Observable<ActionBarItem[]>;
     private locationId$ = new BehaviorSubject<string>('');

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/action-bar/action-bar.component.scss

@@ -2,7 +2,7 @@
 :host {
     display: flex;
     justify-content: space-between;
-    align-items: flex-start;
+    align-items: baseline;
     background-color: var(--color-component-bg-100);
     position: sticky;
     top: -24px;

+ 29 - 0
packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.html

@@ -0,0 +1,29 @@
+<div [class.highlight]="isDevMode && (display$ | async)" class="wrapper">
+    <vdr-dropdown *ngIf="isDevMode && (display$ | async)">
+        <button class="btn btn-icon btn-link extension-point-info-trigger"
+                [style.top.px]="topPx ?? 0"
+                [style.left.px]="leftPx ?? 0"
+                vdrDropdownTrigger>
+            <clr-icon shape="plugin" class="" size="16"></clr-icon>
+        </button>
+        <vdr-dropdown-menu>
+            <div class="extension-info">
+                <pre *ngIf="api === 'actionBar'">
+addActionBarItem({{ '{' }}
+  id: 'my-button',
+  label: 'My Action',
+  locationId: '{{ locationId }}',
+{{ '}' }})</pre>
+                <pre *ngIf="api === 'navMenu'">
+addNavMenuItem({{ '{' }}
+  id: 'my-menu-item',
+  label: 'My Menu Item',
+  routerLink: ['/extensions/my-plugin'],
+  {{ '}' }},
+  '{{ locationId }}'
+)</pre>
+            </div>
+        </vdr-dropdown-menu>
+    </vdr-dropdown>
+    <ng-content></ng-content>
+</div>

+ 31 - 0
packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.scss

@@ -0,0 +1,31 @@
+@import "variables";
+
+:host {
+    position: relative;
+    display: inline-block;
+}
+
+.wrapper {
+    display: inline-block;
+    height: 100%;
+}
+
+.extension-point-info-trigger {
+    position: absolute;
+    margin: 0;
+    padding: 0;
+    z-index: 100;
+    clr-icon {
+        color: var(--color-success-500);
+    }
+}
+
+.extension-info {
+    padding: 12px;
+}
+
+pre {
+    padding: 6px;
+    font-family: 'Source Code Pro', 'Lucida Console', Monaco, monospace;
+    background-color: var(--color-grey-200);
+}

+ 27 - 0
packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts

@@ -0,0 +1,27 @@
+import { ChangeDetectionStrategy, Component, Input, isDevMode, OnInit } from '@angular/core';
+import { DataService } from '@vendure/admin-ui/core';
+import { Observable } from 'rxjs';
+
+import { UIExtensionLocationId } from '../../../common/ui-extension-types';
+
+@Component({
+    selector: 'vdr-ui-extension-point',
+    templateUrl: './ui-extension-point.component.html',
+    styleUrls: ['./ui-extension-point.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class UiExtensionPointComponent implements OnInit {
+    @Input() locationId: UIExtensionLocationId | string;
+    @Input() topPx: number;
+    @Input() leftPx: number;
+    @Input() api: 'actionBar' | 'navMenu';
+    display$: Observable<boolean>;
+    readonly isDevMode = isDevMode();
+    constructor(private dataService: DataService) {}
+
+    ngOnInit(): void {
+        this.display$ = this.dataService.client
+            .uiState()
+            .mapStream(({ uiState }) => uiState.displayUiExtensionPoints);
+    }
+}

+ 2 - 0
packages/admin-ui/src/lib/core/src/shared/shared.module.ts

@@ -79,6 +79,7 @@ import { TableRowActionComponent } from './components/table-row-action/table-row
 import { TagSelectorComponent } from './components/tag-selector/tag-selector.component';
 import { TimelineEntryComponent } from './components/timeline-entry/timeline-entry.component';
 import { TitleInputComponent } from './components/title-input/title-input.component';
+import { UiExtensionPointComponent } from './components/ui-extension-point/ui-extension-point.component';
 import { DisabledDirective } from './directives/disabled.directive';
 import { IfDefaultChannelActiveDirective } from './directives/if-default-channel-active.directive';
 import { IfMultichannelDirective } from './directives/if-multichannel.directive';
@@ -225,6 +226,7 @@ const DECLARATIONS = [
     RelationCardComponent,
     StatusBadgeComponent,
     TabbedCustomFieldsComponent,
+    UiExtensionPointComponent,
 ];
 
 const DYNAMIC_FORM_INPUTS = [