Browse Source

feat(admin-ui): Support custom state transitions from Order detail view

Michael Bromley 5 years ago
parent
commit
1d2ba31d3c

+ 10 - 10
packages/admin-ui/i18n-coverage.json

@@ -1,36 +1,36 @@
 {
-  "generatedOn": "2020-06-30T13:29:58.358Z",
-  "lastCommit": "dc82b2c2392004d08f4a1aea4b126360f733b296",
+  "generatedOn": "2020-07-10T12:40:06.107Z",
+  "lastCommit": "3196b525dfbb31edc2bbe8c5f50b0349b8386f15",
   "translationStatus": {
     "de": {
-      "tokenCount": 656,
+      "tokenCount": 658,
       "translatedCount": 609,
       "percentage": 93
     },
     "en": {
-      "tokenCount": 656,
-      "translatedCount": 656,
+      "tokenCount": 658,
+      "translatedCount": 658,
       "percentage": 100
     },
     "es": {
-      "tokenCount": 656,
+      "tokenCount": 658,
       "translatedCount": 467,
       "percentage": 71
     },
     "pl": {
-      "tokenCount": 656,
+      "tokenCount": 658,
       "translatedCount": 566,
       "percentage": 86
     },
     "zh_Hans": {
-      "tokenCount": 656,
+      "tokenCount": 658,
       "translatedCount": 550,
       "percentage": 84
     },
     "zh_Hant": {
-      "tokenCount": 656,
+      "tokenCount": 658,
       "translatedCount": 550,
       "percentage": 84
     }
   }
-}
+}

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

@@ -1899,6 +1899,7 @@ export type Mutation = {
   setUiLanguage?: Maybe<LanguageCode>;
   settlePayment: Payment;
   settleRefund: Refund;
+  transitionOrderToState?: Maybe<Order>;
   /** Update an existing Administrator */
   updateAdministrator: Administrator;
   /** Update an existing Asset */
@@ -2289,6 +2290,12 @@ export type MutationSettleRefundArgs = {
 };
 
 
+export type MutationTransitionOrderToStateArgs = {
+  id: Scalars['ID'];
+  state: Scalars['String'];
+};
+
+
 export type MutationUpdateAdministratorArgs = {
   input: UpdateAdministratorInput;
 };
@@ -4780,7 +4787,7 @@ export type OrderAddressFragment = (
 
 export type OrderFragment = (
   { __typename?: 'Order' }
-  & Pick<Order, 'id' | 'createdAt' | 'updatedAt' | 'code' | 'state' | 'total' | 'currencyCode'>
+  & Pick<Order, 'id' | 'createdAt' | 'updatedAt' | 'code' | 'state' | 'nextStates' | 'total' | 'currencyCode'>
   & { customer?: Maybe<(
     { __typename?: 'Customer' }
     & Pick<Customer, 'id' | 'firstName' | 'lastName'>
@@ -5015,6 +5022,20 @@ export type DeleteOrderNoteMutation = (
   ) }
 );
 
+export type TransitionOrderToStateMutationVariables = {
+  id: Scalars['ID'];
+  state: Scalars['String'];
+};
+
+
+export type TransitionOrderToStateMutation = (
+  { __typename?: 'Mutation' }
+  & { transitionOrderToState?: Maybe<(
+    { __typename?: 'Order' }
+    & OrderFragment
+  )> }
+);
+
 export type AssetFragment = (
   { __typename?: 'Asset' }
   & Pick<Asset, 'id' | 'createdAt' | 'updatedAt' | 'name' | 'fileSize' | 'mimeType' | 'type' | 'preview' | 'source' | 'width' | 'height'>
@@ -7322,6 +7343,12 @@ export namespace DeleteOrderNote {
   export type DeleteOrderNote = DeleteOrderNoteMutation['deleteOrderNote'];
 }
 
+export namespace TransitionOrderToState {
+  export type Variables = TransitionOrderToStateMutationVariables;
+  export type Mutation = TransitionOrderToStateMutation;
+  export type TransitionOrderToState = OrderFragment;
+}
+
 export namespace Asset {
   export type Fragment = AssetFragment;
   export type FocalPoint = (NonNullable<AssetFragment['focalPoint']>);

+ 10 - 0
packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts

@@ -42,6 +42,7 @@ export const ORDER_FRAGMENT = gql`
         updatedAt
         code
         state
+        nextStates
         total
         currencyCode
         customer {
@@ -291,3 +292,12 @@ export const DELETE_ORDER_NOTE = gql`
         }
     }
 `;
+
+export const TRANSITION_ORDER_TO_STATE = gql`
+    mutation TransitionOrderToState($id: ID!, $state: String!) {
+        transitionOrderToState(id: $id, state: $state) {
+            ...Order
+        }
+    }
+    ${ORDER_FRAGMENT}
+`;

+ 12 - 0
packages/admin-ui/src/lib/core/src/data/providers/order-data.service.ts

@@ -15,6 +15,7 @@ import {
     SettlePayment,
     SettleRefund,
     SettleRefundInput,
+    TransitionOrderToState,
     UpdateOrderNote,
     UpdateOrderNoteInput,
 } from '../../common/generated-types';
@@ -29,6 +30,7 @@ import {
     REFUND_ORDER,
     SETTLE_PAYMENT,
     SETTLE_REFUND,
+    TRANSITION_ORDER_TO_STATE,
     UPDATE_ORDER_NOTE,
 } from '../definitions/order-definitions';
 
@@ -119,4 +121,14 @@ export class OrderDataService {
             },
         );
     }
+
+    transitionToState(id: string, state: string) {
+        return this.baseDataService.mutate<TransitionOrderToState.Mutation, TransitionOrderToState.Variables>(
+            TRANSITION_ORDER_TO_STATE,
+            {
+                id,
+                state,
+            },
+        );
+    }
 }

+ 55 - 48
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html

@@ -24,7 +24,7 @@
                     type="button"
                     class="btn"
                     vdrDropdownItem
-                    *ngIf="order.state !== 'Cancelled'"
+                    *ngIf="order.nextStates.includes('Cancelled')"
                     (click)="cancelOrRefund(order)"
                 >
                     <clr-icon shape="error-standard" class="is-error"></clr-icon>
@@ -37,6 +37,13 @@
                         {{ 'order.cancel-order' | translate }}
                     </ng-template>
                 </button>
+                <ng-container *ngFor="let nextState of nextStates$ | async">
+                    <div class="dropdown-divider"></div>
+                    <button type="button" class="btn" vdrDropdownItem (click)="transitionToState(nextState)">
+                        <clr-icon shape="step-forward-2"></clr-icon>
+                        {{ 'order.transition-to-state' | translate: { state: nextState } }}
+                    </button>
+                </ng-container>
             </vdr-dropdown-menu>
         </vdr-dropdown>
     </vdr-ab-right>
@@ -47,39 +54,39 @@
         <div class="clr-col-lg-8">
             <table class="order-lines table">
                 <thead>
-                <tr>
-                    <th></th>
-                    <th>{{ 'order.product-name' | translate }}</th>
-                    <th>{{ 'order.product-sku' | translate }}</th>
-                    <th>{{ 'order.unit-price' | translate }}</th>
-                    <th>{{ 'order.quantity' | translate }}</th>
-                    <ng-container *ngFor="let customField of visibileOrderLineCustomFields">
-                        <th class="order-line-custom-field">
-                            <button
-                                class="custom-field-header-button"
-                                (click)="toggleOrderLineCustomFields()"
-                                [title]="'common.hide-custom-fields' | translate"
-                            >
-                                {{ customField | customFieldLabel }}
-                            </button>
-                        </th>
-                    </ng-container>
-                    <ng-container *ngIf="showElided">
-                        <th>
-                            <button
-                                class="custom-field-header-button"
-                                (click)="toggleOrderLineCustomFields()"
-                                [title]="'common.display-custom-fields' | translate"
-                            >
-                                <clr-icon
-                                    shape="ellipsis-horizontal"
-                                    class="custom-field-ellipsis"
-                                ></clr-icon>
-                            </button>
-                        </th>
-                    </ng-container>
-                    <th>{{ 'order.total' | translate }}</th>
-                </tr>
+                    <tr>
+                        <th></th>
+                        <th>{{ 'order.product-name' | translate }}</th>
+                        <th>{{ 'order.product-sku' | translate }}</th>
+                        <th>{{ 'order.unit-price' | translate }}</th>
+                        <th>{{ 'order.quantity' | translate }}</th>
+                        <ng-container *ngFor="let customField of visibleOrderLineCustomFields">
+                            <th class="order-line-custom-field">
+                                <button
+                                    class="custom-field-header-button"
+                                    (click)="toggleOrderLineCustomFields()"
+                                    [title]="'common.hide-custom-fields' | translate"
+                                >
+                                    {{ customField | customFieldLabel }}
+                                </button>
+                            </th>
+                        </ng-container>
+                        <ng-container *ngIf="showElided">
+                            <th>
+                                <button
+                                    class="custom-field-header-button"
+                                    (click)="toggleOrderLineCustomFields()"
+                                    [title]="'common.display-custom-fields' | translate"
+                                >
+                                    <clr-icon
+                                        shape="ellipsis-horizontal"
+                                        class="custom-field-ellipsis"
+                                    ></clr-icon>
+                                </button>
+                            </th>
+                        </ng-container>
+                        <th>{{ 'order.total' | translate }}</th>
+                    </tr>
                 </thead>
                 <tr
                     *ngFor="let line of order.lines"
@@ -87,7 +94,7 @@
                     [class.is-cancelled]="line.quantity === 0"
                 >
                     <td class="align-middle thumb">
-                        <img *ngIf="line.featuredAsset" [src]="line.featuredAsset | assetPreview:'tiny'" />
+                        <img *ngIf="line.featuredAsset" [src]="line.featuredAsset | assetPreview: 'tiny'" />
                     </td>
                     <td class="align-middle name">{{ line.productVariant.name }}</td>
                     <td class="align-middle sku">{{ line.productVariant.sku }}</td>
@@ -102,13 +109,13 @@
                         <vdr-line-refunds [line]="line"></vdr-line-refunds>
                         <vdr-line-fulfillment [line]="line" [orderState]="order.state"></vdr-line-fulfillment>
                     </td>
-                    <ng-container *ngFor="let customField of visibileOrderLineCustomFields">
+                    <ng-container *ngFor="let customField of visibleOrderLineCustomFields">
                         <td class="order-line-custom-field align-middle">
                             <ng-container [ngSwitch]="customField.type">
                                 <ng-template [ngSwitchCase]="'datetime'">
                                     <span [title]="line.customFields[customField.name]">{{
                                         line.customFields[customField.name] | date: 'short'
-                                        }}</span>
+                                    }}</span>
                                 </ng-template>
                                 <ng-template [ngSwitchCase]="'boolean'">
                                     <ng-template [ngIf]="line.customFields[customField.name] === true">
@@ -125,11 +132,11 @@
                         </td>
                     </ng-container>
                     <ng-container *ngIf="showElided"
-                    ><td class="order-line-custom-field align-middle">
-                        <clr-icon
-                            shape="ellipsis-horizontal"
-                            class="custom-field-ellipsis"
-                        ></clr-icon></td
+                        ><td class="order-line-custom-field align-middle">
+                            <clr-icon
+                                shape="ellipsis-horizontal"
+                                class="custom-field-ellipsis"
+                            ></clr-icon></td
                     ></ng-container>
                     <td class="align-middle total">
                         {{ line.totalPrice / 100 | currency: order.currencyCode }}
@@ -150,7 +157,7 @@
                                         <a
                                             class="promotion-name"
                                             [routerLink]="getPromotionLink(promotion)"
-                                        >{{ promotion.description }}</a
+                                            >{{ promotion.description }}</a
                                         >
                                         <div class="promotion-amount">
                                             {{ promotion.amount / 100 | currency: order.currencyCode }}
@@ -164,7 +171,7 @@
                 <tr class="sub-total">
                     <td class="left clr-align-middle">{{ 'order.sub-total' | translate }}</td>
                     <td></td>
-                    <td [attr.colspan]="3 + visibileOrderLineCustomFields.length"></td>
+                    <td [attr.colspan]="3 + visibleOrderLineCustomFields.length"></td>
                     <ng-container *ngIf="showElided"><td></td></ng-container>
                     <td class="clr-align-middle">
                         {{ order.subTotal / 100 | currency: order.currencyCode }}
@@ -175,13 +182,13 @@
                 </tr>
                 <tr class="order-ajustment" *ngFor="let adjustment of order.adjustments">
                     <td
-                        [attr.colspan]="5 + visibileOrderLineCustomFields.length"
+                        [attr.colspan]="5 + visibleOrderLineCustomFields.length"
                         class="left clr-align-middle"
                     >
                         <a [routerLink]="getPromotionLink(adjustment)">{{ adjustment.description }}</a>
                         <vdr-chip *ngIf="getCouponCodeForAdjustment(order, adjustment) as couponCode">{{
                             couponCode
-                            }}</vdr-chip>
+                        }}</vdr-chip>
                     </td>
                     <ng-container *ngIf="showElided"><td></td></ng-container>
                     <td class="clr-align-middle">
@@ -191,7 +198,7 @@
                 <tr class="shipping">
                     <td class="left clr-align-middle">{{ 'order.shipping' | translate }}</td>
                     <td class="clr-align-middle">{{ order.shippingMethod?.description }}</td>
-                    <td [attr.colspan]="3 + visibileOrderLineCustomFields.length"></td>
+                    <td [attr.colspan]="3 + visibleOrderLineCustomFields.length"></td>
                     <ng-container *ngIf="showElided"><td></td></ng-container>
                     <td class="clr-align-middle">
                         {{ order.shippingWithTax / 100 | currency: order.currencyCode }}
@@ -203,7 +210,7 @@
                 <tr class="total">
                     <td class="left clr-align-middle">{{ 'order.total' | translate }}</td>
                     <td></td>
-                    <td [attr.colspan]="3 + visibileOrderLineCustomFields.length"></td>
+                    <td [attr.colspan]="3 + visibleOrderLineCustomFields.length"></td>
                     <ng-container *ngIf="showElided"><td></td></ng-container>
                     <td class="clr-align-middle">
                         {{ order.total / 100 | currency: order.currencyCode }}

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

@@ -19,7 +19,7 @@ import {
 } from '@vendure/admin-ui/core';
 import { omit } from '@vendure/common/lib/omit';
 import { EMPTY, Observable, of, Subject } from 'rxjs';
-import { startWith, switchMap, take } from 'rxjs/operators';
+import { map, startWith, switchMap, take } from 'rxjs/operators';
 
 import { CancelOrderDialogComponent } from '../cancel-order-dialog/cancel-order-dialog.component';
 import { FulfillOrderDialogComponent } from '../fulfill-order-dialog/fulfill-order-dialog.component';
@@ -36,10 +36,20 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
     implements OnInit, OnDestroy {
     detailForm = new FormGroup({});
     history$: Observable<GetOrderHistory.Items[] | undefined>;
+    nextStates$: Observable<string[]>;
     fetchHistory = new Subject<void>();
     customFields: CustomFieldConfig[];
     orderLineCustomFields: CustomFieldConfig[];
     orderLineCustomFieldsVisible = false;
+    private readonly defaultStates = [
+        'AddingItems',
+        'ArrangingPayment',
+        'PaymentAuthorized',
+        'PaymentSettled',
+        'PartiallyFulfilled',
+        'Fulfilled',
+        'Cancelled',
+    ];
     constructor(
         router: Router,
         route: ActivatedRoute,
@@ -52,7 +62,7 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
         super(route, router, serverConfigService, dataService);
     }
 
-    get visibileOrderLineCustomFields(): CustomFieldConfig[] {
+    get visibleOrderLineCustomFields(): CustomFieldConfig[] {
         return this.orderLineCustomFieldsVisible ? this.orderLineCustomFields : [];
     }
 
@@ -77,6 +87,14 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
                     .mapStream((data) => data.order?.history.items);
             }),
         );
+        this.nextStates$ = this.entity$.pipe(
+            map((order) => {
+                const isInCustomState = !this.defaultStates.includes(order.state);
+                return isInCustomState
+                    ? order.nextStates
+                    : order.nextStates.filter((s) => !this.defaultStates.includes(s));
+            }),
+        );
     }
 
     ngOnDestroy() {
@@ -96,6 +114,13 @@ export class OrderDetailComponent extends BaseDetailComponent<OrderDetail.Fragme
         return ['/marketing', 'promotions', id];
     }
 
+    transitionToState(state: string) {
+        this.dataService.order.transitionToState(this.id, state).subscribe((val) => {
+            this.notificationService.success(_('order.transitioned-to-state-success'), { state });
+            this.fetchHistory.next();
+        });
+    }
+
     getCouponCodeForAdjustment(
         order: OrderDetail.Fragment,
         promotionAdjustment: OrderDetail.Adjustments,

+ 4 - 3
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -60,9 +60,8 @@
     "collection-contents": "Inhalt der Sammlung",
     "confirm-adding-options-delete-default-body": "Das Hinzufügen von Optionen zu diesem Produkt führt dazu, dass die vorhandene Standardvariante gelöscht wird. Möchten Sie fortfahren?",
     "confirm-adding-options-delete-default-title": "Standardvariante löschen?",
-    "confirm-delete-assets": "{count} {count, plural, one {Asset} other {Assets}} löschen?",
     "confirm-delete-administrator": "",
-    "confirm-delete-asset": "Asset löschen?",
+    "confirm-delete-assets": "{count} {count, plural, one {Asset} other {Assets}} löschen?",
     "confirm-delete-channel": "Kanal löschen?",
     "confirm-delete-collection": "Sammlung löschen?",
     "confirm-delete-collection-and-children-body": "Wenn Sie diese Sammlung löschen, werden auch alle untergeordneten Sammlungen gelöscht.",
@@ -602,6 +601,8 @@
     "total": "Gesamtsumme",
     "tracking-code": "Sendungsverfolgungscode",
     "transaction-id": "Transaktions-ID",
+    "transition-to-state": "",
+    "transitioned-to-state-success": "",
     "unfulfilled": "Nicht ausgeführt",
     "unit-price": "Einzelpreis"
   },
@@ -686,4 +687,4 @@
     "job-result": "Job-Ergebnis",
     "job-state": "Job-Status"
   }
-}
+}

+ 3 - 1
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -601,6 +601,8 @@
     "total": "Total",
     "tracking-code": "Tracking code",
     "transaction-id": "Transaction ID",
+    "transition-to-state": "Transition to { state } state",
+    "transitioned-to-state-success": "Successfully transitioned to { state }",
     "unfulfilled": "Unfulfilled",
     "unit-price": "Unit price"
   },
@@ -685,4 +687,4 @@
     "job-result": "Job result",
     "job-state": "Job state"
   }
-}
+}

+ 3 - 1
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -601,6 +601,8 @@
     "total": "Total",
     "tracking-code": "",
     "transaction-id": "ID de transacción",
+    "transition-to-state": "",
+    "transitioned-to-state-success": "",
     "unfulfilled": "",
     "unit-price": "Precio unitario"
   },
@@ -685,4 +687,4 @@
     "job-result": "Resultado",
     "job-state": "Estado"
   }
-}
+}

+ 3 - 2
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -61,7 +61,6 @@
     "confirm-adding-options-delete-default-body": "Dodawanie opcji spowoduje, że obecna domyślna opcja zostanie usunięta. Czy chcesz kontynuować?",
     "confirm-adding-options-delete-default-title": "Usunąć domyślny tytuł?",
     "confirm-delete-administrator": "",
-    "confirm-delete-asset": "",
     "confirm-delete-assets": "",
     "confirm-delete-channel": "Usunąć kanał?",
     "confirm-delete-collection": "Usunąć kolekcje?",
@@ -602,6 +601,8 @@
     "total": "Total",
     "tracking-code": "Numer przesyłki",
     "transaction-id": "Numer transakcji",
+    "transition-to-state": "",
+    "transitioned-to-state-success": "",
     "unfulfilled": "Unfulfilled",
     "unit-price": "Cena jednostkowa"
   },
@@ -686,4 +687,4 @@
     "job-result": "Rezultat zlecenia",
     "job-state": "Status zlecenia"
   }
-}
+}

+ 3 - 2
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -61,7 +61,6 @@
     "confirm-adding-options-delete-default-body": "添加新规格到此产品会导致含此规格的产品被删除,确认继续吗?",
     "confirm-adding-options-delete-default-title": "确认删除产品规格么?",
     "confirm-delete-administrator": "",
-    "confirm-delete-asset": "",
     "confirm-delete-assets": "",
     "confirm-delete-channel": "确认删除销售渠道?",
     "confirm-delete-collection": "确认删除商品系列吗?",
@@ -602,6 +601,8 @@
     "total": "总计金额",
     "tracking-code": "物流码",
     "transaction-id": "交易ID",
+    "transition-to-state": "",
+    "transitioned-to-state-success": "",
     "unfulfilled": "未配货",
     "unit-price": "单价"
   },
@@ -686,4 +687,4 @@
     "job-result": "",
     "job-state": ""
   }
-}
+}

+ 3 - 2
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -61,7 +61,6 @@
     "confirm-adding-options-delete-default-body": "新增規格到此產品會引致包含此規格的產品被移除,確認繼續吗?",
     "confirm-adding-options-delete-default-title": "確認移除產品規格嗎?",
     "confirm-delete-administrator": "",
-    "confirm-delete-asset": "",
     "confirm-delete-assets": "",
     "confirm-delete-channel": "確認移除渠道?",
     "confirm-delete-collection": "確認移除商品系列吗?",
@@ -602,6 +601,8 @@
     "total": "總計金額",
     "tracking-code": "物流碼",
     "transaction-id": "交易編號",
+    "transition-to-state": "",
+    "transitioned-to-state-success": "",
     "unfulfilled": "未配貨",
     "unit-price": "單價"
   },
@@ -686,4 +687,4 @@
     "job-result": "",
     "job-state": ""
   }
-}
+}