Browse Source

perf(core): Optimize OrderDetail view

Relates to #1727. This commit makes use of the new Fulfillment
API extensions to massively reduce the amount of data needed to
render the Order detail view. In benchmarks of an order with
5000 OrderItems, query time went from 2.6s to 1.9s, and payload from 1.14MB to 367kb.
Michael Bromley 3 years ago
parent
commit
987355cfdb

+ 23 - 11
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -1613,12 +1613,19 @@ export type Fulfillment = Node & {
   createdAt: Scalars['DateTime'];
   updatedAt: Scalars['DateTime'];
   orderItems: Array<OrderItem>;
+  summary: Array<FulfillmentLineSummary>;
   state: Scalars['String'];
   method: Scalars['String'];
   trackingCode?: Maybe<Scalars['String']>;
   customFields?: Maybe<Scalars['JSON']>;
 };
 
+export type FulfillmentLineSummary = {
+  __typename?: 'FulfillmentLineSummary';
+  orderLine: OrderLine;
+  quantity: Scalars['Int'];
+};
+
 /** Returned when there is an error in transitioning the Fulfillment state */
 export type FulfillmentStateTransitionError = ErrorResult & {
   __typename?: 'FulfillmentStateTransitionError';
@@ -3395,6 +3402,7 @@ export type OrderLine = Node & {
   discounts: Array<Discount>;
   taxLines: Array<TaxLine>;
   order: Order;
+  fulfillments?: Maybe<Array<Fulfillment>>;
   customFields?: Maybe<Scalars['JSON']>;
 };
 
@@ -6299,9 +6307,13 @@ export type OrderFragment = (
 export type FulfillmentFragment = (
   { __typename?: 'Fulfillment' }
   & Pick<Fulfillment, 'id' | 'state' | 'nextStates' | 'createdAt' | 'updatedAt' | 'method' | 'trackingCode'>
-  & { orderItems: Array<(
-    { __typename?: 'OrderItem' }
-    & Pick<OrderItem, 'id'>
+  & { summary: Array<(
+    { __typename?: 'FulfillmentLineSummary' }
+    & Pick<FulfillmentLineSummary, 'quantity'>
+    & { orderLine: (
+      { __typename?: 'OrderLine' }
+      & Pick<OrderLine, 'id'>
+    ) }
   )> }
 );
 
@@ -6317,13 +6329,12 @@ export type OrderLineFragment = (
   ), discounts: Array<(
     { __typename?: 'Discount' }
     & DiscountFragment
-  )>, items: Array<(
+  )>, fulfillments?: Maybe<Array<(
+    { __typename?: 'Fulfillment' }
+    & FulfillmentFragment
+  )>>, items: Array<(
     { __typename?: 'OrderItem' }
-    & Pick<OrderItem, 'id' | 'unitPrice' | 'unitPriceWithTax' | 'taxRate' | 'refundId' | 'cancelled'>
-    & { fulfillment?: Maybe<(
-      { __typename?: 'Fulfillment' }
-      & FulfillmentFragment
-    )> }
+    & Pick<OrderItem, 'id' | 'refundId' | 'cancelled'>
   )> }
 );
 
@@ -9719,7 +9730,8 @@ export namespace Order {
 
 export namespace Fulfillment {
   export type Fragment = FulfillmentFragment;
-  export type OrderItems = NonNullable<(NonNullable<FulfillmentFragment['orderItems']>)[number]>;
+  export type Summary = NonNullable<(NonNullable<FulfillmentFragment['summary']>)[number]>;
+  export type OrderLine = (NonNullable<NonNullable<(NonNullable<FulfillmentFragment['summary']>)[number]>['orderLine']>);
 }
 
 export namespace OrderLine {
@@ -9727,8 +9739,8 @@ export namespace OrderLine {
   export type FeaturedAsset = (NonNullable<OrderLineFragment['featuredAsset']>);
   export type ProductVariant = (NonNullable<OrderLineFragment['productVariant']>);
   export type Discounts = NonNullable<(NonNullable<OrderLineFragment['discounts']>)[number]>;
+  export type Fulfillments = NonNullable<(NonNullable<OrderLineFragment['fulfillments']>)[number]>;
   export type Items = NonNullable<(NonNullable<OrderLineFragment['items']>)[number]>;
-  export type Fulfillment = (NonNullable<NonNullable<(NonNullable<OrderLineFragment['items']>)[number]>['fulfillment']>);
 }
 
 export namespace OrderDetail {

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

@@ -83,8 +83,11 @@ export const FULFILLMENT_FRAGMENT = gql`
         createdAt
         updatedAt
         method
-        orderItems {
-            id
+        summary {
+            orderLine {
+                id
+            }
+            quantity
         }
         trackingCode
     }
@@ -106,6 +109,9 @@ export const ORDER_LINE_FRAGMENT = gql`
         discounts {
             ...Discount
         }
+        fulfillments {
+            ...Fulfillment
+        }
         unitPrice
         unitPriceWithTax
         proratedUnitPrice
@@ -113,14 +119,8 @@ export const ORDER_LINE_FRAGMENT = gql`
         quantity
         items {
             id
-            unitPrice
-            unitPriceWithTax
-            taxRate
             refundId
             cancelled
-            fulfillment {
-                ...Fulfillment
-            }
         }
         linePrice
         lineTax

+ 6 - 1
packages/admin-ui/src/lib/order/src/components/fulfill-order-dialog/fulfill-order-dialog.component.ts

@@ -68,7 +68,12 @@ export class FulfillOrderDialogComponent implements Dialog<FulfillOrderInput>, O
     }
 
     getUnfulfilledCount(line: OrderDetail.Lines): number {
-        const fulfilled = line.items.reduce((sum, item) => sum + (item.fulfillment ? 1 : 0), 0);
+        const fulfilled =
+            line.fulfillments
+                ?.map(f => f.summary)
+                .flat()
+                .filter(row => row.orderLine.id === line.id)
+                .reduce((sum, row) => sum + row.quantity, 0) ?? 0;
         return line.quantity - fulfilled;
     }
 

+ 10 - 15
packages/admin-ui/src/lib/order/src/components/fulfillment-detail/fulfillment-detail.component.ts

@@ -31,21 +31,16 @@ export class FulfillmentDetailComponent implements OnInit, OnChanges {
     }
 
     get items(): Array<{ name: string; quantity: number }> {
-        const itemMap = new Map<string, number>();
-        const fulfillmentItemIds = this.fulfillment?.orderItems.map(i => i.id);
-        for (const line of this.order.lines) {
-            for (const item of line.items) {
-                if (fulfillmentItemIds?.includes(item.id)) {
-                    const count = itemMap.get(line.productVariant.name);
-                    if (count != null) {
-                        itemMap.set(line.productVariant.name, count + 1);
-                    } else {
-                        itemMap.set(line.productVariant.name, 1);
-                    }
-                }
-            }
-        }
-        return Array.from(itemMap.entries()).map(([name, quantity]) => ({ name, quantity }));
+        return (
+            this.fulfillment?.summary.map(row => {
+                return {
+                    name:
+                        this.order.lines.find(line => line.id === row.orderLine.id)?.productVariant.name ??
+                        '',
+                    quantity: row.quantity,
+                };
+            }) ?? []
+        );
     }
 
     buildCustomFieldsFormGroup() {

+ 22 - 26
packages/admin-ui/src/lib/order/src/components/line-fulfillment/line-fulfillment.component.ts

@@ -1,5 +1,5 @@
 import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core';
-import { OrderDetail } from '@vendure/admin-ui/core';
+import { OrderDetail, OrderDetailFragment } from '@vendure/admin-ui/core';
 import { unique } from '@vendure/common/lib/unique';
 
 export type FulfillmentStatus = 'full' | 'partial' | 'none';
@@ -15,7 +15,10 @@ export class LineFulfillmentComponent implements OnChanges {
     @Input() orderState: string;
     fulfilledCount = 0;
     fulfillmentStatus: FulfillmentStatus;
-    fulfillments: Array<{ count: number; fulfillment: OrderDetail.Fulfillments }> = [];
+    fulfillments: Array<{
+        count: number;
+        fulfillment: NonNullable<OrderDetailFragment['fulfillments']>[number];
+    }> = [];
 
     ngOnChanges(changes: SimpleChanges): void {
         if (this.line) {
@@ -29,7 +32,13 @@ export class LineFulfillmentComponent implements OnChanges {
      * Returns the number of items in an OrderLine which are fulfilled.
      */
     private getDeliveredCount(line: OrderDetail.Lines): number {
-        return line.items.reduce((sum, item) => sum + (item.fulfillment ? 1 : 0), 0);
+        return (
+            line.fulfillments?.reduce(
+                (sum, fulfillment) =>
+                    sum + (fulfillment.summary.find(s => s.orderLine.id === line.id)?.quantity ?? 0),
+                0,
+            ) ?? 0
+        );
     }
 
     private getFulfillmentStatus(fulfilledCount: number, lineQuantity: number): FulfillmentStatus {
@@ -44,28 +53,15 @@ export class LineFulfillmentComponent implements OnChanges {
 
     private getFulfillments(
         line: OrderDetail.Lines,
-    ): Array<{ count: number; fulfillment: OrderDetail.Fulfillments }> {
-        const counts: { [fulfillmentId: string]: number } = {};
-
-        for (const item of line.items) {
-            if (item.fulfillment) {
-                if (counts[item.fulfillment.id] === undefined) {
-                    counts[item.fulfillment.id] = 1;
-                } else {
-                    counts[item.fulfillment.id]++;
-                }
-            }
-        }
-        const all = line.items.reduce((fulfillments, item) => {
-            return item.fulfillment ? [...fulfillments, item.fulfillment] : fulfillments;
-        }, [] as OrderDetail.Fulfillments[]);
-
-        return Object.entries(counts).map(([id, count]) => {
-            return {
-                count,
-                // tslint:disable-next-line:no-non-null-assertion
-                fulfillment: all.find(f => f.id === id)!,
-            };
-        });
+    ): Array<{ count: number; fulfillment: NonNullable<OrderDetailFragment['fulfillments']>[number] }> {
+        return (
+            line.fulfillments?.map(fulfillment => {
+                const summaryLine = fulfillment.summary.find(s => s.orderLine.id === line.id);
+                return {
+                    count: summaryLine?.quantity ?? 0,
+                    fulfillment,
+                };
+            }) ?? []
+        );
     }
 }

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

@@ -8,6 +8,8 @@ import {
     CustomFieldConfig,
     DataService,
     EditNoteDialogComponent,
+    FulfillmentFragment,
+    FulfillmentLineSummary,
     GetOrderHistory,
     GetOrderQuery,
     HistoryEntry,
@@ -252,9 +254,19 @@ export class OrderDetailComponent
     }
 
     canAddFulfillment(order: OrderDetail.Fragment): boolean {
-        const allItemsFulfilled = order.lines
-            .reduce((items, line) => [...items, ...line.items], [] as OrderLineFragment['items'])
-            .every(item => !!item.fulfillment || item.cancelled);
+        const allFulfillmentSummaryRows: FulfillmentFragment['summary'] = (order.fulfillments ?? []).reduce(
+            (all, fulfillment) => [...all, ...fulfillment.summary],
+            [] as FulfillmentFragment['summary'],
+        );
+        let allItemsFulfilled = true;
+        for (const line of order.lines) {
+            const totalFulfilledCount = allFulfillmentSummaryRows
+                .filter(row => row.orderLine.id === line.id)
+                .reduce((sum, row) => sum + row.quantity, 0);
+            if (totalFulfilledCount < line.quantity) {
+                allItemsFulfilled = false;
+            }
+        }
         return (
             !allItemsFulfilled &&
             !this.hasUnsettledModifications(order) &&