Browse Source

refactor(admin-ui): Extract timeline entry into shared component

Michael Bromley 5 years ago
parent
commit
679d4af62b

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

@@ -104,6 +104,7 @@ export * from './shared/components/dropdown/dropdown-item.directive';
 export * from './shared/components/dropdown/dropdown-menu.component';
 export * from './shared/components/dropdown/dropdown-trigger.directive';
 export * from './shared/components/dropdown/dropdown.component';
+export * from './shared/components/empty-placeholder/empty-placeholder.component';
 export * from './shared/components/entity-info/entity-info.component';
 export * from './shared/components/extension-host/extension-host-config';
 export * from './shared/components/extension-host/extension-host.component';
@@ -142,6 +143,7 @@ export * from './shared/components/rich-text-editor/rich-text-editor.component';
 export * from './shared/components/select-toggle/select-toggle.component';
 export * from './shared/components/simple-dialog/simple-dialog.component';
 export * from './shared/components/table-row-action/table-row-action.component';
+export * from './shared/components/timeline-entry/timeline-entry.component';
 export * from './shared/components/title-input/title-input.component';
 export * from './shared/directives/disabled.directive';
 export * from './shared/directives/if-default-channel-active.directive';

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

@@ -0,0 +1,25 @@
+<div [ngClass]="displayType" [class.has-custom-icon]="!!iconShape" class="entry" [class.last]="isLast === true">
+    <div class="timeline">
+        <div class="custom-icon">
+            <clr-icon
+                *ngIf="iconShape"
+                [attr.shape]="getIconShape()"
+                [ngClass]="getIconClass()"
+                size="24"
+            ></clr-icon>
+        </div>
+    </div>
+    <div class="entry-body">
+        <div class="detail">
+            <div class="time">
+                {{ createdAt | date: 'short' }}
+            </div>
+            <div class="name">
+                {{ name }}
+            </div>
+        </div>
+        <div [class.featured-entry]="featured">
+            <ng-content></ng-content>
+        </div>
+    </div>
+</div>

+ 126 - 0
packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.scss

@@ -0,0 +1,126 @@
+@import "variables";
+
+.entry {
+    display: flex;
+}
+.timeline {
+    border-left: 2px solid $color-primary-100;
+    padding-bottom: 24px;
+    position: relative;
+
+    &:before {
+        content: '';
+        position: absolute;
+        width: 2px;
+        height: 10px;
+        left: -2px;
+        border-left: 2px solid $color-primary-100;
+    }
+    &:after {
+        content: '';
+        display: block;
+        border-radius: 50%;
+        width: 8px;
+        height: 8px;
+        background-color: $color-grey-200;
+        border: 1px solid $color-grey-400;
+        position: absolute;
+        left: -5px;
+        top: 4px;
+    }
+
+    .custom-icon {
+        position: absolute;
+        width: 32px;
+        height: 32px;
+        left: -17px;
+        top: -4px;
+        align-items: center;
+        justify-content: center;
+        border-radius: 50%;
+        color: $color-primary-600;
+        background-color: $color-grey-100;
+        border: 1px solid $color-grey-300;
+        display: none;
+    }
+}
+.entry.has-custom-icon {
+    .timeline:after {
+        display: none;
+    }
+    .custom-icon {
+        display: flex;
+    }
+}
+.entry.last {
+    .timeline {
+        border-left-color: transparent;
+    }
+}
+
+.entry-body {
+    flex: 1;
+    padding-bottom: 24px;
+    padding-left: 12px;
+    line-height: 16px;
+    margin-left: 12px;
+    color: $color-grey-500;
+}
+
+.featured-entry {
+    .title {
+        color: $color-grey-800;
+        font-size: 16px;
+        line-height: 26px;
+        background-color: hotpink;
+    }
+    .note-text {
+        color: $color-grey-800;
+        white-space: pre-wrap;
+    }
+}
+
+.detail {
+    display: flex;
+    color: $color-grey-400;
+    font-size: 12px;
+    .time {
+    }
+    .name {
+        margin-left: 12px;
+    }
+}
+
+
+.muted {
+    .timeline, .timeline .custom-icon {
+        color: $color-grey-400;
+    }
+}
+
+.success {
+    .timeline, .timeline .custom-icon {
+        color: $color-success-400;
+    }
+    .timeline:after {
+        background-color: $color-success-200;
+        border: 1px solid $color-success-400;
+    }
+}
+
+.error {
+    .timeline, .timeline .custom-icon {
+        color: $color-error-400;
+    }
+    .timeline:after {
+        background-color: $color-error-200;
+        border: 1px solid $color-error-400;
+    }
+}
+
+.warning {
+    .timeline:after {
+        background-color: $color-warning-200;
+        border: 1px solid $color-warning-400;
+    }
+}

+ 30 - 0
packages/admin-ui/src/lib/core/src/shared/components/timeline-entry/timeline-entry.component.ts

@@ -0,0 +1,30 @@
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
+
+export type TimelineDisplayType = 'success' | 'error' | 'warning' | 'default' | 'muted';
+
+@Component({
+    selector: 'vdr-timeline-entry',
+    templateUrl: './timeline-entry.component.html',
+    styleUrls: ['./timeline-entry.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class TimelineEntryComponent {
+    @Input() displayType: TimelineDisplayType;
+    @Input() createdAt: string;
+    @Input() name: string;
+    @Input() featured: boolean;
+    @Input() iconShape?: string | [string, string];
+    @Input() isLast?: boolean;
+
+    getIconShape() {
+        if (this.iconShape) {
+            return typeof this.iconShape === 'string' ? this.iconShape : this.iconShape[0];
+        }
+    }
+
+    getIconClass() {
+        if (this.iconShape) {
+            return typeof this.iconShape === 'string' ? '' : this.iconShape[1];
+        }
+    }
+}

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

@@ -67,6 +67,7 @@ import { RichTextEditorComponent } from './components/rich-text-editor/rich-text
 import { SelectToggleComponent } from './components/select-toggle/select-toggle.component';
 import { SimpleDialogComponent } from './components/simple-dialog/simple-dialog.component';
 import { TableRowActionComponent } from './components/table-row-action/table-row-action.component';
+import { TimelineEntryComponent } from './components/timeline-entry/timeline-entry.component';
 import { TitleInputComponent } from './components/title-input/title-input.component';
 import { DisabledDirective } from './directives/disabled.directive';
 import { IfDefaultChannelActiveDirective } from './directives/if-default-channel-active.directive';
@@ -166,6 +167,7 @@ const DECLARATIONS = [
     TimeAgoPipe,
     DurationPipe,
     EmptyPlaceholderComponent,
+    TimelineEntryComponent,
 ];
 
 @NgModule({

+ 2 - 2
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.ts

@@ -33,7 +33,7 @@ import { SettleRefundDialogComponent } from '../settle-refund-dialog/settle-refu
 export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragment>
     implements OnInit, OnDestroy {
     detailForm = new FormGroup({});
-    history$: Observable<GetOrderHistory.Items[] | null | undefined>;
+    history$: Observable<GetOrderHistory.Items[] | undefined>;
     fetchHistory = new Subject<void>();
     customFields: CustomFieldConfig[];
     orderLineCustomFields: CustomFieldConfig[];
@@ -72,7 +72,7 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                             createdAt: SortOrder.DESC,
                         },
                     })
-                    .mapStream((data) => data.order && data.order.history.items);
+                    .mapStream((data) => data.order?.history.items);
             }),
         );
     }

+ 60 - 102
packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.html

@@ -1,93 +1,50 @@
 <h4>{{ 'order.order-history' | translate }}</h4>
 <div class="entry-list">
-    <div class="entry has-custom-icon">
-        <div class="timeline">
-            <div class="custom-icon">
-                <clr-icon shape="note" size="24" class="compose-note"></clr-icon>
-            </div>
+    <vdr-timeline-entry iconShape="note" displayType="muted">
+        <div class="note-entry">
+            <textarea [(ngModel)]="note" name="note" class="note"></textarea>
+            <button class="btn btn-secondary" [disabled]="!note" (click)="addNoteToOrder()">
+                {{ 'order.add-note' | translate }}
+            </button>
         </div>
-        <div class="entry-body">
-            <div class="note-entry">
-                <textarea [(ngModel)]="note" name="note" class="note"></textarea>
-                <button class="btn btn-secondary" [disabled]="!note" (click)="addNoteToOrder()">
-                    {{ 'order.add-note' | translate }}
-                </button>
-            </div>
-            <div class="visibility-select">
-                <clr-checkbox-wrapper>
-                    <input type="checkbox" clrCheckbox [(ngModel)]="noteIsPrivate" />
-                    <label>{{ 'order.note-is-private' | translate }}</label>
-                </clr-checkbox-wrapper>
-                <span *ngIf="noteIsPrivate" class="private">
-                    {{ 'order.note-only-visible-to-administrators' | translate }}
-                </span>
-                <span *ngIf="!noteIsPrivate" class="public">
-                    {{ 'order.note-visible-to-customer' | translate }}
-                </span>
-            </div>
+        <div class="visibility-select">
+            <clr-checkbox-wrapper>
+                <input type="checkbox" clrCheckbox [(ngModel)]="noteIsPrivate" />
+                <label>{{ 'order.note-is-private' | translate }}</label>
+            </clr-checkbox-wrapper>
+            <span *ngIf="noteIsPrivate" class="private">
+                {{ 'order.note-only-visible-to-administrators' | translate }}
+            </span>
+            <span *ngIf="!noteIsPrivate" class="public">
+                {{ 'order.note-visible-to-customer' | translate }}
+            </span>
         </div>
-    </div>
-    <div
+    </vdr-timeline-entry>
+    <vdr-timeline-entry
         *ngFor="let entry of history"
-        [ngClass]="getClassForEntry(entry)"
-        [class.has-custom-icon]="!!getTimelineIcon(entry)"
-        class="entry"
+        [displayType]="getDisplayType(entry)"
+        [iconShape]="getTimelineIcon(entry)"
+        [createdAt]="entry.createdAt"
+        [name]="getName(entry)"
+        [featured]="isFeatured(entry)"
     >
-        <div class="timeline">
-            <div class="custom-icon">
-                <clr-icon
-                    *ngIf="getTimelineIcon(entry) === 'complete'"
-                    shape="success-standard"
-                    size="24"
-                    class="complete is-solid"
-                ></clr-icon>
-                <clr-icon *ngIf="getTimelineIcon(entry) === 'fulfillment'" shape="truck" size="24"></clr-icon>
-                <clr-icon
-                    *ngIf="getTimelineIcon(entry) === 'payment'"
-                    shape="credit-card"
-                    size="24"
-                ></clr-icon>
-                <clr-icon
-                    *ngIf="getTimelineIcon(entry) === 'cancelled'"
-                    shape="ban"
-                    size="24"
-                    class="cancelled"
-                ></clr-icon>
-                <clr-icon *ngIf="getTimelineIcon(entry) === 'note'" shape="note" size="24"></clr-icon>
-            </div>
-        </div>
-        <div class="entry-body" [ngSwitch]="entry.type">
-            <div class="detail">
-                <div class="time">
-                    {{ entry.createdAt | date: 'short' }}
-                </div>
-                <div class="name">
-                    {{ getName(entry) }}
-                </div>
-            </div>
+        <ng-container [ngSwitch]="entry.type">
             <ng-container *ngSwitchCase="type.ORDER_STATE_TRANSITION">
-                <div class="featured-entry" *ngIf="entry.data.to === 'Fulfilled'">
-                    <div class="title">
-                        {{ 'order.history-order-fulfilled' | translate }}
-                    </div>
+                <div class="title" *ngIf="entry.data.to === 'Fulfilled'">
+                    {{ 'order.history-order-fulfilled' | translate }}
                 </div>
-                <div class="featured-entry" *ngIf="entry.data.to === 'Cancelled'">
-                    <div class="title">
-                        {{ 'order.history-order-cancelled' | translate }}
-                    </div>
+                <div class="title" *ngIf="entry.data.to === 'Cancelled'">
+                    {{ 'order.history-order-cancelled' | translate }}
                 </div>
                 <ng-template [ngIf]="entry.data.to !== 'Cancelled' && entry.data.to !== 'Fulfilled'">
                     {{
-                    'order.history-order-transition'
-                        | translate: { from: entry.data.from, to: entry.data.to }
+                        'order.history-order-transition'
+                            | translate: { from: entry.data.from, to: entry.data.to }
                     }}
                 </ng-template>
             </ng-container>
             <ng-container *ngSwitchCase="type.ORDER_PAYMENT_TRANSITION">
-                <div
-                    class="featured-entry"
-                    *ngIf="entry.data.to === 'Settled'; else regularPaymentTransition"
-                >
+                <ng-container *ngIf="entry.data.to === 'Settled'; else regularPaymentTransition">
                     <div class="title">
                         {{ 'order.history-payment-settled' | translate }}
                     </div>
@@ -98,19 +55,19 @@
                             [currencyCode]="order.currencyCode"
                         ></vdr-payment-detail>
                     </vdr-history-entry-detail>
-                </div>
+                </ng-container>
                 <ng-template #regularPaymentTransition>
                     {{
-                    'order.history-payment-transition'
-                        | translate
-                        : { from: entry.data.from, to: entry.data.to, id: entry.data.paymentId }
+                        'order.history-payment-transition'
+                            | translate
+                                : { from: entry.data.from, to: entry.data.to, id: entry.data.paymentId }
                     }}
                 </ng-template>
             </ng-container>
             <ng-container *ngSwitchCase="type.ORDER_REFUND_TRANSITION">
                 {{
-                'order.history-refund-transition'
-                    | translate: { from: entry.data.from, to: entry.data.to, id: entry.data.refundId }
+                    'order.history-refund-transition'
+                        | translate: { from: entry.data.from, to: entry.data.to, id: entry.data.refundId }
                 }}
             </ng-container>
             <ng-container *ngSwitchCase="type.ORDER_CANCELLATION">
@@ -125,26 +82,26 @@
                 </vdr-history-entry-detail>
             </ng-container>
             <ng-container *ngSwitchCase="type.ORDER_FULLFILLMENT">
-                <div class="featured-entry">
-                    <div class="title">
-                        {{ 'order.history-fulfillment-created' | translate }}
-                    </div>
-                    {{ 'order.tracking-code' | translate }}: {{ getFullfillment(entry)?.trackingCode }}
-                    <vdr-history-entry-detail *ngIf="getFullfillment(entry) as fulfillment">
-                        <vdr-fulfillment-detail
-                            [fulfillmentId]="fulfillment.id"
-                            [order]="order"
-                        ></vdr-fulfillment-detail>
-                    </vdr-history-entry-detail>
+                <div class="title">
+                    {{ 'order.history-fulfillment-created' | translate }}
                 </div>
+                {{ 'order.tracking-code' | translate }}: {{ getFullfillment(entry)?.trackingCode }}
+                <vdr-history-entry-detail *ngIf="getFullfillment(entry) as fulfillment">
+                    <vdr-fulfillment-detail
+                        [fulfillmentId]="fulfillment.id"
+                        [order]="order"
+                    ></vdr-fulfillment-detail>
+                </vdr-history-entry-detail>
             </ng-container>
             <ng-container *ngSwitchCase="type.ORDER_NOTE">
-                <div class="featured-entry">
-                    <div class="note-text">
-                        <span *ngIf="entry.isPublic" class="note-visibility public">{{ 'common.public' | translate }}</span>
-                        <span *ngIf="!entry.isPublic" class="note-visibility private">{{ 'common.private' | translate }}</span>
-                        {{ entry.data.note }}
-                    </div>
+                <div class="note-text">
+                    <span *ngIf="entry.isPublic" class="note-visibility public">{{
+                        'common.public' | translate
+                    }}</span>
+                    <span *ngIf="!entry.isPublic" class="note-visibility private">{{
+                        'common.private' | translate
+                    }}</span>
+                    {{ entry.data.note }}
                 </div>
             </ng-container>
             <ng-container *ngSwitchCase="type.ORDER_COUPON_APPLIED">
@@ -152,15 +109,16 @@
                 <vdr-chip>
                     <a [routerLink]="['/marketing', 'promotions', entry.data.promotionId]">{{
                         entry.data.couponCode
-                        }}</a>
+                    }}</a>
                 </vdr-chip>
             </ng-container>
             <ng-container *ngSwitchCase="type.ORDER_COUPON_REMOVED">
                 {{ 'order.history-coupon-code-removed' | translate }}:
                 <vdr-chip
-                ><span class="cancelled-coupon-code">{{ entry.data.couponCode }}</span></vdr-chip
+                    ><span class="cancelled-coupon-code">{{ entry.data.couponCode }}</span></vdr-chip
                 >
             </ng-container>
-        </div>
-    </div>
+        </ng-container>
+    </vdr-timeline-entry>
+    <vdr-timeline-entry [isLast]="true"></vdr-timeline-entry>
 </div>

+ 7 - 121
packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.scss

@@ -1,134 +1,15 @@
-@import "variables";
+@import 'variables';
 
 :host {
     margin-top: 48px;
     display: block;
 }
+
 .entry-list {
     margin-top: 24px;
     margin-left: 24px;
     margin-right: 12px;
 }
-.entry {
-    display: flex;
-}
-.timeline {
-    border-left: 2px solid $color-primary-100;
-    padding-bottom: 24px;
-    position: relative;
-
-    &:before {
-        content: '';
-        position: absolute;
-        width: 2px;
-        height: 10px;
-        left: -2px;
-        border-left: 2px solid $color-primary-100;
-    }
-    &:after {
-        content: '';
-        display: block;
-        border-radius: 50%;
-        width: 8px;
-        height: 8px;
-        background-color: $color-grey-200;
-        border: 1px solid $color-grey-400;
-        position: absolute;
-        left: -5px;
-        top: 4px;
-    }
-
-    .custom-icon {
-        position: absolute;
-        width: 32px;
-        height: 32px;
-        left: -17px;
-        top: -4px;
-        align-items: center;
-        justify-content: center;
-        border-radius: 50%;
-        color: $color-primary-600;
-        background-color: $color-grey-100;
-        border: 1px solid $color-grey-300;
-        display: none;
-    }
-
-    .complete {
-        color: $color-success-400;
-    }
-    .cancelled {
-        color: $color-error-400;
-    }
-    .compose-note {
-        color: $color-grey-400;
-    }
-}
-.entry.has-custom-icon {
-    .timeline:after {
-        display: none;
-    }
-    .custom-icon {
-        display: flex;
-    }
-}
-.entry:last-of-type {
-    .timeline {
-        border-left-color: transparent;
-    }
-}
-
-.entry-body {
-    flex: 1;
-    padding-bottom: 24px;
-    padding-left: 12px;
-    line-height: 16px;
-    margin-left: 12px;
-    color: $color-grey-500;
-}
-
-.featured-entry {
-    .title {
-        color: $color-grey-800;
-        font-size: 16px;
-        line-height: 26px;
-    }
-    .note-text {
-        color: $color-grey-800;
-        white-space: pre-wrap;
-    }
-}
-
-.detail {
-    display: flex;
-    color: $color-grey-400;
-    font-size: 12px;
-    .time {
-    }
-    .name {
-        margin-left: 12px;
-    }
-}
-
-.success {
-    .timeline:after {
-        background-color: $color-success-200;
-        border: 1px solid $color-success-400;
-    }
-}
-
-.error {
-    .timeline:after {
-        background-color: $color-error-200;
-        border: 1px solid $color-error-400;
-    }
-}
-
-.warning {
-    .timeline:after {
-        background-color: $color-warning-200;
-        border: 1px solid $color-warning-400;
-    }
-}
 
 .note-entry {
     display: flex;
@@ -158,6 +39,11 @@ textarea.note {
     border-radius: 3px;
     margin-right: 6px;
 }
+.note-text {
+    color: $color-grey-800;
+    white-space: pre-wrap;
+}
+
 .cancelled-coupon-code {
     text-decoration: line-through;
 }

+ 38 - 14
packages/admin-ui/src/lib/order/src/components/order-history/order-history.component.ts

@@ -1,10 +1,10 @@
 import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
-
 import {
     GetOrderHistory,
     HistoryEntryType,
     OrderDetail,
     OrderDetailFragment,
+    TimelineDisplayType,
 } from '@vendure/admin-ui/core';
 
 @Component({
@@ -21,11 +21,17 @@ export class OrderHistoryComponent {
     noteIsPrivate = true;
     readonly type = HistoryEntryType;
 
-    getClassForEntry(entry: GetOrderHistory.Items): 'success' | 'error' | 'warning' | undefined {
-        if (entry.type === HistoryEntryType.ORDER_PAYMENT_TRANSITION) {
-            if (entry.data.to === 'Settled') {
+    getDisplayType(entry: GetOrderHistory.Items): TimelineDisplayType {
+        if (entry.type === HistoryEntryType.ORDER_STATE_TRANSITION) {
+            if (entry.data.to === 'Fulfilled') {
                 return 'success';
-            } else if (entry.data.to === 'Declined') {
+            }
+            if (entry.data.to === 'Cancelled') {
+                return 'error';
+            }
+        }
+        if (entry.type === HistoryEntryType.ORDER_PAYMENT_TRANSITION) {
+            if (entry.data.to === 'Declined') {
                 return 'error';
             }
         }
@@ -35,39 +41,57 @@ export class OrderHistoryComponent {
         if (entry.type === HistoryEntryType.ORDER_REFUND_TRANSITION) {
             return 'warning';
         }
+        return 'default';
     }
 
-    getTimelineIcon(
-        entry: GetOrderHistory.Items,
-    ): 'complete' | 'note' | 'fulfillment' | 'payment' | 'cancelled' | undefined {
+    getTimelineIcon(entry: GetOrderHistory.Items) {
         if (entry.type === HistoryEntryType.ORDER_STATE_TRANSITION) {
             if (entry.data.to === 'Fulfilled') {
-                return 'complete';
+                return ['success-standard', 'is-solid'];
             }
             if (entry.data.to === 'Cancelled') {
-                return 'cancelled';
+                return 'ban';
             }
         }
         if (entry.type === HistoryEntryType.ORDER_PAYMENT_TRANSITION && entry.data.to === 'Settled') {
-            return 'payment';
+            return 'credit-card';
         }
         if (entry.type === HistoryEntryType.ORDER_NOTE) {
             return 'note';
         }
         if (entry.type === HistoryEntryType.ORDER_FULLFILLMENT) {
-            return 'fulfillment';
+            return 'truck';
+        }
+    }
+
+    isFeatured(entry: GetOrderHistory.Items): boolean {
+        switch (entry.type) {
+            case HistoryEntryType.ORDER_STATE_TRANSITION: {
+                return (
+                    entry.data.to === 'Fulfilled' ||
+                    entry.data.to === 'Cancelled' ||
+                    entry.data.to === 'Settled'
+                );
+            }
+            case HistoryEntryType.ORDER_PAYMENT_TRANSITION:
+                return entry.data.to === 'Settled';
+            case HistoryEntryType.ORDER_FULLFILLMENT:
+            case HistoryEntryType.ORDER_NOTE:
+                return true;
+            default:
+                return false;
         }
     }
 
     getFullfillment(entry: GetOrderHistory.Items): OrderDetail.Fulfillments | undefined {
         if (entry.type === HistoryEntryType.ORDER_FULLFILLMENT && this.order.fulfillments) {
-            return this.order.fulfillments.find(f => f.id === entry.data.fulfillmentId);
+            return this.order.fulfillments.find((f) => f.id === entry.data.fulfillmentId);
         }
     }
 
     getPayment(entry: GetOrderHistory.Items): OrderDetail.Payments | undefined {
         if (entry.type === HistoryEntryType.ORDER_PAYMENT_TRANSITION && this.order.payments) {
-            return this.order.payments.find(p => p.id === entry.data.paymentId);
+            return this.order.payments.find((p) => p.id === entry.data.paymentId);
         }
     }