Browse Source

feat(admin-ui): Allow custom components for Customer history timeline

Relates to #1694, relates to #432
Michael Bromley 3 years ago
parent
commit
eeba323617

+ 8 - 8
packages/admin-ui/src/lib/core/src/providers/custom-history-entry-component/history-entry-component-types.ts

@@ -16,7 +16,7 @@ export type TimelineHistoryEntry = NonNullable<GetOrderHistoryQuery['order']>['h
 export interface HistoryEntryComponent {
     /**
      * @description
-     * The HistoryEntry item itself.
+     * The HistoryEntry data.
      */
     entry: TimelineHistoryEntry;
     /**
@@ -24,23 +24,23 @@ export interface HistoryEntryComponent {
      * Defines whether this entry is highlighted with a "success", "error" etc. color.
      */
     getDisplayType: (entry: TimelineHistoryEntry) => TimelineDisplayType;
-    /**
-     * @description
-     * Returns the name of the entry, as displayed in the timeline next to the date. Can be
-     * a translation token which will get passed through the `translate` pipe.
-     */
-    getName: (entry: TimelineHistoryEntry) => string;
     /**
      * @description
      * Featured entries are always expanded. Non-featured entries start of collapsed and can be clicked
      * to expand.
      */
     isFeatured: (entry: TimelineHistoryEntry) => boolean;
+    /**
+     * @description
+     * Returns the name of the person who did this action. For example, it could be the Customer's name
+     * or "Administrator".
+     */
+    getName?: (entry: TimelineHistoryEntry) => string | undefined;
     /**
      * @description
      * Optional Clarity icon shape to display with the entry. Examples: `'note'`, `['success-standard', 'is-solid']`
      */
-    getIconShape?: (entry: TimelineHistoryEntry) => string | readonly [string, string] | undefined;
+    getIconShape?: (entry: TimelineHistoryEntry) => string | string[] | undefined;
 }
 
 /**

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.html

@@ -21,7 +21,7 @@
                 {{ createdAt | localeDate: 'short' }}
             </div>
             <div class="name">
-                {{ name | translate }}
+                {{ name || '' }}
             </div>
         </div>
         <div [class.featured-entry]="featured">

+ 66 - 0
packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history-entry-host.component.ts

@@ -0,0 +1,66 @@
+import {
+    Component,
+    ComponentFactoryResolver,
+    ComponentRef,
+    EventEmitter,
+    Input,
+    OnDestroy,
+    OnInit,
+    Output,
+    Type,
+    ViewChild,
+    ViewContainerRef,
+} from '@angular/core';
+import {
+    CustomerFragment,
+    CustomerHistoryEntryComponent,
+    HistoryEntryComponentService,
+    TimelineHistoryEntry,
+} from '@vendure/admin-ui/core';
+
+@Component({
+    selector: 'vdr-customer-history-entry-host',
+    template: ` <vdr-timeline-entry
+        [displayType]="instance.getDisplayType(entry)"
+        [iconShape]="instance.getIconShape && instance.getIconShape(entry)"
+        [createdAt]="entry.createdAt"
+        [name]="instance.getName && instance.getName(entry)"
+        [featured]="instance.isFeatured(entry)"
+        [collapsed]="!expanded && !instance.isFeatured(entry)"
+        (expandClick)="expandClick.emit()"
+    >
+        <div #portal></div>
+    </vdr-timeline-entry>`,
+    exportAs: 'historyEntry',
+})
+export class CustomerHistoryEntryHostComponent implements OnInit, OnDestroy {
+    @Input() entry: TimelineHistoryEntry;
+    @Input() customer: CustomerFragment;
+    @Input() expanded: boolean;
+    @Output() expandClick = new EventEmitter<void>();
+    @ViewChild('portal', { static: true, read: ViewContainerRef }) portalRef: ViewContainerRef;
+    instance: CustomerHistoryEntryComponent;
+    private componentRef: ComponentRef<CustomerHistoryEntryComponent>;
+
+    constructor(
+        private componentFactoryResolver: ComponentFactoryResolver,
+        private historyEntryComponentService: HistoryEntryComponentService,
+    ) {}
+
+    ngOnInit(): void {
+        const componentType = this.historyEntryComponentService.getComponent(
+            this.entry.type,
+        ) as Type<CustomerHistoryEntryComponent>;
+
+        const factory = this.componentFactoryResolver.resolveComponentFactory(componentType);
+        const componentRef = this.portalRef.createComponent(factory);
+        componentRef.instance.entry = this.entry;
+        componentRef.instance.customer = this.customer;
+        this.instance = componentRef.instance;
+        this.componentRef = componentRef;
+    }
+
+    ngOnDestroy() {
+        this.componentRef?.destroy();
+    }
+}

+ 164 - 154
packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.html

@@ -8,160 +8,170 @@
             </button>
         </div>
     </vdr-timeline-entry>
-    <vdr-timeline-entry
-        *ngFor="let entry of history"
-        [displayType]="getDisplayType(entry)"
-        [iconShape]="getTimelineIcon(entry)"
-        [createdAt]="entry.createdAt"
-        [name]="getName(entry)"
-        [featured]="isFeatured(entry)"
-    >
-        <ng-container [ngSwitch]="entry.type">
-            <ng-container *ngSwitchCase="type.CUSTOMER_REGISTERED">
-                <div class="title">
-                    {{ 'customer.history-customer-registered' | translate }}
-                </div>
-                <ng-container *ngIf="entry.data.strategy === 'native'; else namedStrategy">
-                    {{ 'customer.history-using-native-auth-strategy' | translate }}
+    <ng-container *ngFor="let entry of history">
+        <vdr-customer-history-entry-host
+            *ngIf="hasCustomComponent(entry.type); else defaultComponents"
+            [customer]="customer"
+            [entry]="entry"
+            [expanded]="expanded"
+            (expandClick)="expanded = !expanded"
+        ></vdr-customer-history-entry-host>
+        <ng-template #defaultComponents>
+            <vdr-timeline-entry
+                [displayType]="getDisplayType(entry)"
+                [iconShape]="getTimelineIcon(entry)"
+                [createdAt]="entry.createdAt"
+                [name]="getName(entry)"
+                [featured]="isFeatured(entry)"
+            >
+                <ng-container [ngSwitch]="entry.type">
+                    <ng-container *ngSwitchCase="type.CUSTOMER_REGISTERED">
+                        <div class="title">
+                            {{ 'customer.history-customer-registered' | translate }}
+                        </div>
+                        <ng-container *ngIf="entry.data.strategy === 'native'; else namedStrategy">
+                            {{ 'customer.history-using-native-auth-strategy' | translate }}
+                        </ng-container>
+                        <ng-template #namedStrategy>
+                            {{
+                                'customer.history-using-external-auth-strategy'
+                                    | translate: { strategy: entry.data.strategy }
+                            }}
+                        </ng-template>
+                    </ng-container>
+                    <ng-container *ngSwitchCase="type.CUSTOMER_VERIFIED">
+                        <div class="title">
+                            {{ 'customer.history-customer-verified' | translate }}
+                        </div>
+                        <ng-container *ngIf="entry.data.strategy === 'native'; else namedStrategy">
+                            {{ 'customer.history-using-native-auth-strategy' | translate }}
+                        </ng-container>
+                        <ng-template #namedStrategy>
+                            {{
+                                'customer.history-using-external-auth-strategy'
+                                    | translate: { strategy: entry.data.strategy }
+                            }}
+                        </ng-template>
+                    </ng-container>
+                    <ng-container *ngSwitchCase="type.CUSTOMER_DETAIL_UPDATED">
+                        <div class="flex">
+                            {{ 'customer.history-customer-detail-updated' | translate }}
+                            <vdr-history-entry-detail>
+                                <vdr-object-tree [value]="entry.data.input"></vdr-object-tree>
+                            </vdr-history-entry-detail>
+                        </div>
+                    </ng-container>
+                    <ng-container *ngSwitchCase="type.CUSTOMER_ADDED_TO_GROUP">
+                        {{
+                            'customer.history-customer-added-to-group'
+                                | translate: { groupName: entry.data.groupName }
+                        }}
+                    </ng-container>
+                    <ng-container *ngSwitchCase="type.CUSTOMER_REMOVED_FROM_GROUP">
+                        {{
+                            'customer.history-customer-removed-from-group'
+                                | translate: { groupName: entry.data.groupName }
+                        }}
+                    </ng-container>
+                    <ng-container *ngSwitchCase="type.CUSTOMER_ADDRESS_CREATED">
+                        {{ 'customer.history-customer-address-created' | translate }}
+                        <div class="flex">
+                            <div class="address-string">{{ entry.data.address }}</div>
+                        </div>
+                    </ng-container>
+                    <ng-container *ngSwitchCase="type.CUSTOMER_ADDRESS_UPDATED">
+                        {{ 'customer.history-customer-address-updated' | translate }}
+                        <div class="flex">
+                            <div class="address-string">{{ entry.data.address }}</div>
+                            <vdr-history-entry-detail>
+                                <vdr-object-tree [value]="entry.data.input"></vdr-object-tree>
+                            </vdr-history-entry-detail>
+                        </div>
+                    </ng-container>
+                    <ng-container *ngSwitchCase="type.CUSTOMER_ADDRESS_DELETED">
+                        {{ 'customer.history-customer-address-deleted' | translate }}
+                        <div class="address-string">{{ entry.data.address }}</div>
+                    </ng-container>
+                    <ng-container *ngSwitchCase="type.CUSTOMER_PASSWORD_UPDATED">
+                        {{ 'customer.history-customer-password-updated' | translate }}
+                    </ng-container>
+                    <ng-container *ngSwitchCase="type.CUSTOMER_PASSWORD_RESET_REQUESTED">
+                        {{ 'customer.history-customer-password-reset-requested' | translate }}
+                    </ng-container>
+                    <ng-container *ngSwitchCase="type.CUSTOMER_PASSWORD_RESET_VERIFIED">
+                        {{ 'customer.history-customer-password-reset-verified' | translate }}
+                    </ng-container>
+                    <ng-container *ngSwitchCase="type.CUSTOMER_EMAIL_UPDATE_REQUESTED">
+                        <div class="flex">
+                            {{ 'customer.history-customer-email-update-requested' | translate }}
+                            <vdr-history-entry-detail>
+                                <vdr-labeled-data [label]="'customer.old-email-address' | translate">{{
+                                    entry.data.oldEmailAddress
+                                }}</vdr-labeled-data>
+                                <vdr-labeled-data [label]="'customer.new-email-address' | translate">{{
+                                    entry.data.newEmailAddress
+                                }}</vdr-labeled-data>
+                            </vdr-history-entry-detail>
+                        </div>
+                    </ng-container>
+                    <ng-container *ngSwitchCase="type.CUSTOMER_EMAIL_UPDATE_VERIFIED">
+                        <div class="flex">
+                            {{ 'customer.history-customer-email-update-verified' | translate }}
+                            <vdr-history-entry-detail>
+                                <vdr-labeled-data [label]="'customer.old-email-address' | translate">{{
+                                    entry.data.oldEmailAddress
+                                }}</vdr-labeled-data>
+                                <vdr-labeled-data [label]="'customer.new-email-address' | translate">{{
+                                    entry.data.newEmailAddress
+                                }}</vdr-labeled-data>
+                            </vdr-history-entry-detail>
+                        </div>
+                    </ng-container>
+                    <ng-container *ngSwitchCase="type.CUSTOMER_NOTE">
+                        <div class="flex">
+                            <div class="note-text">
+                                {{ entry.data.note }}
+                            </div>
+                            <div class="flex-spacer"></div>
+                            <vdr-dropdown>
+                                <button class="icon-button" vdrDropdownTrigger>
+                                    <clr-icon shape="ellipsis-vertical"></clr-icon>
+                                </button>
+                                <vdr-dropdown-menu vdrPosition="bottom-right">
+                                    <button
+                                        class="button"
+                                        vdrDropdownItem
+                                        (click)="updateNote.emit(entry)"
+                                        [disabled]="!('UpdateCustomer' | hasPermission)"
+                                    >
+                                        <clr-icon shape="edit"></clr-icon>
+                                        {{ 'common.edit' | translate }}
+                                    </button>
+                                    <div class="dropdown-divider"></div>
+                                    <button
+                                        class="button"
+                                        vdrDropdownItem
+                                        (click)="deleteNote.emit(entry)"
+                                        [disabled]="!('UpdateCustomer' | hasPermission)"
+                                    >
+                                        <clr-icon shape="trash" class="is-danger"></clr-icon>
+                                        {{ 'common.delete' | translate }}
+                                    </button>
+                                </vdr-dropdown-menu>
+                            </vdr-dropdown>
+                        </div>
+                    </ng-container>
+                    <ng-container *ngSwitchDefault>
+                        <div class="title">
+                            {{ entry.type | translate }}
+                        </div>
+                        <vdr-history-entry-detail *ngIf="entry.data">
+                            <vdr-object-tree [value]="entry.data"></vdr-object-tree>
+                        </vdr-history-entry-detail>
+                    </ng-container>
                 </ng-container>
-                <ng-template #namedStrategy>
-                    {{
-                        'customer.history-using-external-auth-strategy'
-                            | translate: { strategy: entry.data.strategy }
-                    }}
-                </ng-template>
-            </ng-container>
-            <ng-container *ngSwitchCase="type.CUSTOMER_VERIFIED">
-                <div class="title">
-                    {{ 'customer.history-customer-verified' | translate }}
-                </div>
-                <ng-container *ngIf="entry.data.strategy === 'native'; else namedStrategy">
-                    {{ 'customer.history-using-native-auth-strategy' | translate }}
-                </ng-container>
-                <ng-template #namedStrategy>
-                    {{
-                        'customer.history-using-external-auth-strategy'
-                            | translate: { strategy: entry.data.strategy }
-                    }}
-                </ng-template>
-            </ng-container>
-            <ng-container *ngSwitchCase="type.CUSTOMER_DETAIL_UPDATED">
-                <div class="flex">
-                    {{ 'customer.history-customer-detail-updated' | translate }}
-                    <vdr-history-entry-detail>
-                        <vdr-object-tree [value]="entry.data.input"></vdr-object-tree>
-                    </vdr-history-entry-detail>
-                </div>
-            </ng-container>
-            <ng-container *ngSwitchCase="type.CUSTOMER_ADDED_TO_GROUP">
-                {{
-                    'customer.history-customer-added-to-group'
-                        | translate: { groupName: entry.data.groupName }
-                }}
-            </ng-container>
-            <ng-container *ngSwitchCase="type.CUSTOMER_REMOVED_FROM_GROUP">
-                {{
-                    'customer.history-customer-removed-from-group'
-                        | translate: { groupName: entry.data.groupName }
-                }}
-            </ng-container>
-            <ng-container *ngSwitchCase="type.CUSTOMER_ADDRESS_CREATED">
-                {{ 'customer.history-customer-address-created' | translate }}
-                <div class="flex">
-                    <div class="address-string">{{ entry.data.address }}</div>
-                </div>
-            </ng-container>
-            <ng-container *ngSwitchCase="type.CUSTOMER_ADDRESS_UPDATED">
-                {{ 'customer.history-customer-address-updated' | translate }}
-                <div class="flex">
-                    <div class="address-string">{{ entry.data.address }}</div>
-                    <vdr-history-entry-detail>
-                        <vdr-object-tree [value]="entry.data.input"></vdr-object-tree>
-                    </vdr-history-entry-detail>
-                </div>
-            </ng-container>
-            <ng-container *ngSwitchCase="type.CUSTOMER_ADDRESS_DELETED">
-                {{ 'customer.history-customer-address-deleted' | translate }}
-                <div class="address-string">{{ entry.data.address }}</div>
-            </ng-container>
-            <ng-container *ngSwitchCase="type.CUSTOMER_PASSWORD_UPDATED">
-                {{ 'customer.history-customer-password-updated' | translate }}
-            </ng-container>
-            <ng-container *ngSwitchCase="type.CUSTOMER_PASSWORD_RESET_REQUESTED">
-                {{ 'customer.history-customer-password-reset-requested' | translate }}
-            </ng-container>
-            <ng-container *ngSwitchCase="type.CUSTOMER_PASSWORD_RESET_VERIFIED">
-                {{ 'customer.history-customer-password-reset-verified' | translate }}
-            </ng-container>
-            <ng-container *ngSwitchCase="type.CUSTOMER_EMAIL_UPDATE_REQUESTED">
-                <div class="flex">
-                    {{ 'customer.history-customer-email-update-requested' | translate }}
-                    <vdr-history-entry-detail>
-                        <vdr-labeled-data [label]="'customer.old-email-address' | translate">{{
-                            entry.data.oldEmailAddress
-                        }}</vdr-labeled-data>
-                        <vdr-labeled-data [label]="'customer.new-email-address' | translate">{{
-                            entry.data.newEmailAddress
-                        }}</vdr-labeled-data>
-                    </vdr-history-entry-detail>
-                </div>
-            </ng-container>
-            <ng-container *ngSwitchCase="type.CUSTOMER_EMAIL_UPDATE_VERIFIED">
-                <div class="flex">
-                    {{ 'customer.history-customer-email-update-verified' | translate }}
-                    <vdr-history-entry-detail>
-                        <vdr-labeled-data [label]="'customer.old-email-address' | translate">{{
-                            entry.data.oldEmailAddress
-                        }}</vdr-labeled-data>
-                        <vdr-labeled-data [label]="'customer.new-email-address' | translate">{{
-                            entry.data.newEmailAddress
-                        }}</vdr-labeled-data>
-                    </vdr-history-entry-detail>
-                </div>
-            </ng-container>
-            <ng-container *ngSwitchCase="type.CUSTOMER_NOTE">
-                <div class="flex">
-                    <div class="note-text">
-                        {{ entry.data.note }}
-                    </div>
-                    <div class="flex-spacer"></div>
-                    <vdr-dropdown>
-                        <button class="icon-button" vdrDropdownTrigger>
-                            <clr-icon shape="ellipsis-vertical"></clr-icon>
-                        </button>
-                        <vdr-dropdown-menu vdrPosition="bottom-right">
-                            <button
-                                class="button"
-                                vdrDropdownItem
-                                (click)="updateNote.emit(entry)"
-                                [disabled]="!('UpdateCustomer' | hasPermission)"
-                            >
-                                <clr-icon shape="edit"></clr-icon>
-                                {{ 'common.edit' | translate }}
-                            </button>
-                            <div class="dropdown-divider"></div>
-                            <button
-                                class="button"
-                                vdrDropdownItem
-                                (click)="deleteNote.emit(entry)"
-                                [disabled]="!('UpdateCustomer' | hasPermission)"
-                            >
-                                <clr-icon shape="trash" class="is-danger"></clr-icon>
-                                {{ 'common.delete' | translate }}
-                            </button>
-                        </vdr-dropdown-menu>
-                    </vdr-dropdown>
-                </div>
-            </ng-container>
-            <ng-container *ngSwitchDefault>
-                <div class="title">
-                    {{ entry.type | translate }}
-                </div>
-                <vdr-history-entry-detail *ngIf="entry.data">
-                    <vdr-object-tree [value]="entry.data"></vdr-object-tree>
-                </vdr-history-entry-detail>
-            </ng-container>
-        </ng-container>
-    </vdr-timeline-entry>
+            </vdr-timeline-entry>
+        </ng-template>
+    </ng-container>
     <vdr-timeline-entry [isLast]="true"></vdr-timeline-entry>
 </div>

+ 8 - 0
packages/admin-ui/src/lib/customer/src/components/customer-history/customer-history.component.ts

@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from
 import {
     Customer,
     GetCustomerHistory,
+    HistoryEntryComponentService,
     HistoryEntryType,
     TimelineDisplayType,
     TimelineHistoryEntry,
@@ -20,8 +21,15 @@ export class CustomerHistoryComponent {
     @Output() updateNote = new EventEmitter<TimelineHistoryEntry>();
     @Output() deleteNote = new EventEmitter<TimelineHistoryEntry>();
     note = '';
+    expanded = false;
     readonly type = HistoryEntryType;
 
+    constructor(private historyEntryComponentService: HistoryEntryComponentService) {}
+
+    hasCustomComponent(type: string): boolean {
+        return !!this.historyEntryComponentService.getComponent(type);
+    }
+
     getDisplayType(entry: GetCustomerHistory.Items): TimelineDisplayType {
         switch (entry.type) {
             case HistoryEntryType.CUSTOMER_VERIFIED:

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

@@ -9,6 +9,7 @@ import { CustomerDetailComponent } from './components/customer-detail/customer-d
 import { CustomerGroupDetailDialogComponent } from './components/customer-group-detail-dialog/customer-group-detail-dialog.component';
 import { CustomerGroupListComponent } from './components/customer-group-list/customer-group-list.component';
 import { CustomerGroupMemberListComponent } from './components/customer-group-member-list/customer-group-member-list.component';
+import { CustomerHistoryEntryHostComponent } from './components/customer-history/customer-history-entry-host.component';
 import { CustomerHistoryComponent } from './components/customer-history/customer-history.component';
 import { CustomerListComponent } from './components/customer-list/customer-list.component';
 import { CustomerStatusLabelComponent } from './components/customer-status-label/customer-status-label.component';
@@ -29,6 +30,7 @@ import { customerRoutes } from './customer.routes';
         SelectCustomerGroupDialogComponent,
         CustomerHistoryComponent,
         AddressDetailDialogComponent,
+        CustomerHistoryEntryHostComponent,
     ],
     exports: [AddressCardComponent],
 })

+ 1 - 0
packages/admin-ui/src/lib/customer/src/public_api.ts

@@ -6,6 +6,7 @@ export * from './components/customer-detail/customer-detail.component';
 export * from './components/customer-group-detail-dialog/customer-group-detail-dialog.component';
 export * from './components/customer-group-list/customer-group-list.component';
 export * from './components/customer-group-member-list/customer-group-member-list.component';
+export * from './components/customer-history/customer-history-entry-host.component';
 export * from './components/customer-history/customer-history.component';
 export * from './components/customer-list/customer-list.component';
 export * from './components/customer-status-label/customer-status-label.component';

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/order-history/order-history-entry-host.component.ts

@@ -24,7 +24,7 @@ import {
         [displayType]="instance.getDisplayType(entry)"
         [iconShape]="instance.getIconShape && instance.getIconShape(entry)"
         [createdAt]="entry.createdAt"
-        [name]="instance.getName(entry)"
+        [name]="instance.getName && instance.getName(entry)"
         [featured]="instance.isFeatured(entry)"
         [collapsed]="!expanded && !instance.isFeatured(entry)"
         (expandClick)="expandClick.emit()"