Browse Source

Merge branch 'fix-orderline-customfield-relations'

Michael Bromley 3 years ago
parent
commit
b4e89d3b8a

+ 96 - 3
packages/core/e2e/shop-order.e2e-spec.ts

@@ -130,6 +130,7 @@ describe('Shop orders', () => {
                     { name: 'notes', type: 'string' },
                     { name: 'notes', type: 'string' },
                     { name: 'privateField', type: 'string', public: false },
                     { name: 'privateField', type: 'string', public: false },
                     { name: 'lineImage', type: 'relation', entity: Asset },
                     { name: 'lineImage', type: 'relation', entity: Asset },
+                    { name: 'lineImages', type: 'relation', list: true, entity: Asset },
                     { name: 'dropShip', type: 'boolean', defaultValue: false },
                     { name: 'dropShip', type: 'boolean', defaultValue: false },
                 ],
                 ],
             },
             },
@@ -280,6 +281,9 @@ describe('Shop orders', () => {
                                 lineImage {
                                 lineImage {
                                     id
                                     id
                                 }
                                 }
+                                lineImages {
+                                    id
+                                }
                             }
                             }
                         }
                         }
                     }
                     }
@@ -403,11 +407,13 @@ describe('Shop orders', () => {
                 });
                 });
                 expect(adjustOrderLine.lines[1].customFields).toEqual({
                 expect(adjustOrderLine.lines[1].customFields).toEqual({
                     lineImage: null,
                     lineImage: null,
+                    lineImages: [],
                     notes: 'updated notes',
                     notes: 'updated notes',
                 });
                 });
                 const { activeOrder: ao1 } = await shopClient.query(GET_ORDER_WITH_ORDER_LINE_CUSTOM_FIELDS);
                 const { activeOrder: ao1 } = await shopClient.query(GET_ORDER_WITH_ORDER_LINE_CUSTOM_FIELDS);
                 expect(ao1.lines[1].customFields).toEqual({
                 expect(ao1.lines[1].customFields).toEqual({
                     lineImage: null,
                     lineImage: null,
+                    lineImages: [],
                     notes: 'updated notes',
                     notes: 'updated notes',
                 });
                 });
                 const updatedNotesLineId = ao1.lines[1].id;
                 const updatedNotesLineId = ao1.lines[1].id;
@@ -428,6 +434,7 @@ describe('Shop orders', () => {
                 expect(activeOrder.lines.find((l: any) => l.id === updatedNotesLineId)?.customFields).toEqual(
                 expect(activeOrder.lines.find((l: any) => l.id === updatedNotesLineId)?.customFields).toEqual(
                     {
                     {
                         lineImage: null,
                         lineImage: null,
+                        lineImages: [],
                         notes: 'updated notes',
                         notes: 'updated notes',
                     },
                     },
                 );
                 );
@@ -530,12 +537,95 @@ describe('Shop orders', () => {
                         orderLineId: activeOrder!.lines[2].id,
                         orderLineId: activeOrder!.lines[2].id,
                     },
                     },
                 );
                 );
+                const { removeOrderLine } = await shopClient.query<
+                    RemoveItemFromOrder.Mutation,
+                    RemoveItemFromOrder.Variables
+                >(REMOVE_ITEM_FROM_ORDER, {
+                    orderLineId: activeOrder!.lines[1].id,
+                });
+                orderResultGuard.assertSuccess(removeOrderLine);
+                expect(removeOrderLine.lines.length).toBe(1);
+            });
+
+            it('addItemToOrder with list relation customField', async () => {
+                const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation>(
+                    ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                    {
+                        productVariantId: 'T_3',
+                        quantity: 1,
+                        customFields: {
+                            lineImagesIds: ['T_1', 'T_2'],
+                        },
+                    },
+                );
+
+                orderResultGuard.assertSuccess(addItemToOrder);
+                expect(addItemToOrder!.lines.length).toBe(2);
+                expect(addItemToOrder!.lines[1].quantity).toBe(1);
+
+                const { activeOrder } = await shopClient.query(GET_ORDER_WITH_ORDER_LINE_CUSTOM_FIELDS);
+                expect(activeOrder.lines[1].customFields.lineImages.length).toBe(2);
+                expect(activeOrder.lines[1].customFields.lineImages).toContainEqual({ id: 'T_1' });
+                expect(activeOrder.lines[1].customFields.lineImages).toContainEqual({ id: 'T_2' });
+            });
+
+            it('addItemToOrder with equal list relation customField adds to quantity', async () => {
+                const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation>(
+                    ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                    {
+                        productVariantId: 'T_3',
+                        quantity: 1,
+                        customFields: {
+                            lineImagesIds: ['T_1', 'T_2'],
+                        },
+                    },
+                );
+
+                orderResultGuard.assertSuccess(addItemToOrder);
+                expect(addItemToOrder!.lines.length).toBe(2);
+                expect(addItemToOrder!.lines[1].quantity).toBe(2);
+
+                const { activeOrder } = await shopClient.query(GET_ORDER_WITH_ORDER_LINE_CUSTOM_FIELDS);
+
+                expect(activeOrder.lines[1].customFields.lineImages.length).toBe(2);
+                expect(activeOrder.lines[1].customFields.lineImages).toContainEqual({ id: 'T_1' });
+                expect(activeOrder.lines[1].customFields.lineImages).toContainEqual({ id: 'T_2' });
+            });
+
+            it('addItemToOrder with different list relation customField adds new line', async () => {
+                const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation>(
+                    ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
+                    {
+                        productVariantId: 'T_3',
+                        quantity: 1,
+                        customFields: {
+                            lineImagesIds: ['T_1'],
+                        },
+                    },
+                );
+
+                orderResultGuard.assertSuccess(addItemToOrder);
+                expect(addItemToOrder!.lines.length).toBe(3);
+                expect(addItemToOrder!.lines[2].quantity).toBe(1);
+
+                const { activeOrder } = await shopClient.query(GET_ORDER_WITH_ORDER_LINE_CUSTOM_FIELDS);
+
+                expect(activeOrder.lines[2].customFields.lineImages).toEqual([{ id: 'T_1' }]);
+
                 await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
                 await shopClient.query<RemoveItemFromOrder.Mutation, RemoveItemFromOrder.Variables>(
                     REMOVE_ITEM_FROM_ORDER,
                     REMOVE_ITEM_FROM_ORDER,
                     {
                     {
-                        orderLineId: activeOrder!.lines[1].id,
+                        orderLineId: activeOrder!.lines[2].id,
                     },
                     },
                 );
                 );
+                const { removeOrderLine } = await shopClient.query<
+                    RemoveItemFromOrder.Mutation,
+                    RemoveItemFromOrder.Variables
+                >(REMOVE_ITEM_FROM_ORDER, {
+                    orderLineId: activeOrder!.lines[1].id,
+                });
+                orderResultGuard.assertSuccess(removeOrderLine);
+                expect(removeOrderLine.lines.length).toBe(1);
             });
             });
         });
         });
 
 
@@ -611,7 +701,7 @@ describe('Shop orders', () => {
                 AdjustItemQuantity.Mutation,
                 AdjustItemQuantity.Mutation,
                 AdjustItemQuantity.Variables
                 AdjustItemQuantity.Variables
             >(ADJUST_ITEM_QUANTITY, {
             >(ADJUST_ITEM_QUANTITY, {
-                orderLineId: 'T_10',
+                orderLineId: addItemToOrder!.order.lines[1].id,
                 quantity: 101,
                 quantity: 101,
             });
             });
             orderResultGuard.assertErrorResult(adjustOrderLine);
             orderResultGuard.assertErrorResult(adjustOrderLine);
@@ -627,7 +717,7 @@ describe('Shop orders', () => {
                 AdjustItemQuantity.Mutation,
                 AdjustItemQuantity.Mutation,
                 AdjustItemQuantity.Variables
                 AdjustItemQuantity.Variables
             >(ADJUST_ITEM_QUANTITY, {
             >(ADJUST_ITEM_QUANTITY, {
-                orderLineId: 'T_10',
+                orderLineId: addItemToOrder!.order.lines[1].id,
                 quantity: 0,
                 quantity: 0,
             });
             });
             orderResultGuard.assertSuccess(adjustLine2);
             orderResultGuard.assertSuccess(adjustLine2);
@@ -2182,6 +2272,9 @@ const ADJUST_ORDER_LINE_WITH_CUSTOM_FIELDS = gql`
                         lineImage {
                         lineImage {
                             id
                             id
                         }
                         }
+                        lineImages {
+                            id
+                        }
                     }
                     }
                 }
                 }
             }
             }

+ 5 - 2
packages/core/src/config/order/order-item-price-calculation-strategy.ts

@@ -5,9 +5,9 @@ import { ProductVariant } from '../../entity/product-variant/product-variant.ent
 
 
 /**
 /**
  * @description
  * @description
- * The OrderItemPriceCalculationStrategy defines the price of an OrderItem. By default the 
+ * The OrderItemPriceCalculationStrategy defines the price of an OrderItem. By default the
  * {@link DefaultOrderItemPriceCalculationStrategy} is used.
  * {@link DefaultOrderItemPriceCalculationStrategy} is used.
- * 
+ *
  * ### When is the strategy invoked ?
  * ### When is the strategy invoked ?
  * * addItemToOrder (only on the new order line)
  * * addItemToOrder (only on the new order line)
  * * adjustOrderLine  (only on the adjusted order line)
  * * adjustOrderLine  (only on the adjusted order line)
@@ -48,6 +48,9 @@ export interface OrderItemPriceCalculationStrategy extends InjectableStrategy {
      * @description
      * @description
      * Receives the ProductVariant to be added to the Order as well as any OrderLine custom fields and returns
      * Receives the ProductVariant to be added to the Order as well as any OrderLine custom fields and returns
      * the price for a single unit.
      * the price for a single unit.
+     *
+     * Note: if you have any `relation` type custom fields defined on the OrderLine entity, they will only be
+     * passed in to this method if they are set to `eager: true`.
      */
      */
     calculateUnitPrice(
     calculateUnitPrice(
         ctx: RequestContext,
         ctx: RequestContext,

+ 30 - 14
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -6,7 +6,7 @@ import {
     RefundOrderInput,
     RefundOrderInput,
 } from '@vendure/common/lib/generated-types';
 } from '@vendure/common/lib/generated-types';
 import { ID } from '@vendure/common/lib/shared-types';
 import { ID } from '@vendure/common/lib/shared-types';
-import { summate } from '@vendure/common/lib/shared-utils';
+import { getGraphQlInputName, summate } from '@vendure/common/lib/shared-utils';
 
 
 import { RequestContext } from '../../../api/common/request-context';
 import { RequestContext } from '../../../api/common/request-context';
 import { isGraphQlErrorResult, JustErrorResults } from '../../../common/error/error-result';
 import { isGraphQlErrorResult, JustErrorResults } from '../../../common/error/error-result';
@@ -27,7 +27,9 @@ import {
 import { AdjustmentSource } from '../../../common/types/adjustment-source';
 import { AdjustmentSource } from '../../../common/types/adjustment-source';
 import { idsAreEqual } from '../../../common/utils';
 import { idsAreEqual } from '../../../common/utils';
 import { ConfigService } from '../../../config/config.service';
 import { ConfigService } from '../../../config/config.service';
+import { CustomFieldConfig } from '../../../config/custom-field/custom-field-types';
 import { TransactionalConnection } from '../../../connection/transactional-connection';
 import { TransactionalConnection } from '../../../connection/transactional-connection';
+import { VendureEntity } from '../../../entity/base/base.entity';
 import { OrderItem } from '../../../entity/order-item/order-item.entity';
 import { OrderItem } from '../../../entity/order-item/order-item.entity';
 import { OrderLine } from '../../../entity/order-line/order-line.entity';
 import { OrderLine } from '../../../entity/order-line/order-line.entity';
 import { OrderModification } from '../../../entity/order-modification/order-modification.entity';
 import { OrderModification } from '../../../entity/order-modification/order-modification.entity';
@@ -591,8 +593,8 @@ export class OrderModifier {
             // or equal to the defaultValue
             // or equal to the defaultValue
             for (const def of customFieldDefs) {
             for (const def of customFieldDefs) {
                 const key = def.name;
                 const key = def.name;
-                const existingValue = existingCustomFields?.[key];
-                if (existingValue != null) {
+                const existingValue = this.coerceValue(def, existingCustomFields);
+                if (existingValue != null && (!def.list || existingValue?.length !== 0)) {
                     if (def.defaultValue != null) {
                     if (def.defaultValue != null) {
                         if (existingValue !== def.defaultValue) {
                         if (existingValue !== def.defaultValue) {
                             return false;
                             return false;
@@ -619,14 +621,8 @@ export class OrderModifier {
 
 
         for (const def of customFieldDefs) {
         for (const def of customFieldDefs) {
             const key = def.name;
             const key = def.name;
-            // This ternary is there because with the MySQL driver, boolean customFields with a default
-            // of `false` were being rep-resented as `0`, thus causing the equality check to fail.
-            // So if it's a boolean, we'll explicitly coerce the value to a boolean.
-            const existingValue =
-                def.type === 'boolean' && typeof existingCustomFields?.[key] === 'number'
-                    ? !!existingCustomFields?.[key]
-                    : existingCustomFields?.[key];
-            if (existingValue !== undefined) {
+            const existingValue = this.coerceValue(def, existingCustomFields);
+            if (def.type !== 'relation' && existingValue !== undefined) {
                 const valuesMatch =
                 const valuesMatch =
                     JSON.stringify(inputCustomFields?.[key]) === JSON.stringify(existingValue);
                     JSON.stringify(inputCustomFields?.[key]) === JSON.stringify(existingValue);
                 const undefinedMatchesNull = existingValue === null && inputCustomFields?.[key] === undefined;
                 const undefinedMatchesNull = existingValue === null && inputCustomFields?.[key] === undefined;
@@ -636,18 +632,38 @@ export class OrderModifier {
                     return false;
                     return false;
                 }
                 }
             } else if (def.type === 'relation') {
             } else if (def.type === 'relation') {
-                const inputId = `${key}Id`;
+                const inputId = getGraphQlInputName(def);
                 const inputValue = inputCustomFields?.[inputId];
                 const inputValue = inputCustomFields?.[inputId];
                 // tslint:disable-next-line:no-non-null-assertion
                 // tslint:disable-next-line:no-non-null-assertion
                 const existingRelation = (lineWithCustomFieldRelations!.customFields as any)[key];
                 const existingRelation = (lineWithCustomFieldRelations!.customFields as any)[key];
-                if (inputValue && inputValue !== existingRelation?.id) {
-                    return false;
+                if (inputValue) {
+                    const customFieldNotEqual = def.list
+                        ? JSON.stringify((inputValue as ID[]).sort()) !==
+                          JSON.stringify(
+                              existingRelation?.map((relation: VendureEntity) => relation.id).sort(),
+                          )
+                        : inputValue !== existingRelation?.id;
+                    if (customFieldNotEqual) {
+                        return false;
+                    }
                 }
                 }
             }
             }
         }
         }
         return true;
         return true;
     }
     }
 
 
+    /**
+     * This function is required because with the MySQL driver, boolean customFields with a default
+     * of `false` were being represented as `0`, thus causing the equality check to fail.
+     * So if it's a boolean, we'll explicitly coerce the value to a boolean.
+     */
+    private coerceValue(def: CustomFieldConfig, existingCustomFields: { [p: string]: any } | undefined) {
+        const key = def.name;
+        return def.type === 'boolean' && typeof existingCustomFields?.[key] === 'number'
+            ? !!existingCustomFields?.[key]
+            : existingCustomFields?.[key];
+    }
+
     private async getProductVariantOrThrow(
     private async getProductVariantOrThrow(
         ctx: RequestContext,
         ctx: RequestContext,
         productVariantId: ID,
         productVariantId: ID,