Browse Source

Merge branch 'master' into minor

Michael Bromley 4 years ago
parent
commit
e1ffe3f7d3
35 changed files with 427 additions and 91 deletions
  1. 18 0
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 3 3
      packages/admin-ui-plugin/package.json
  4. 2 2
      packages/admin-ui/package.json
  5. 8 2
      packages/admin-ui/src/lib/catalog/src/components/collection-tree/collection-tree-node.component.html
  6. 14 0
      packages/admin-ui/src/lib/catalog/src/components/collection-tree/collection-tree-node.component.scss
  7. 17 7
      packages/admin-ui/src/lib/catalog/src/providers/product-detail/product-detail.service.ts
  8. 14 1
      packages/admin-ui/src/lib/core/src/app.component.ts
  9. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  10. 30 0
      packages/admin-ui/src/lib/core/src/data/data.module.ts
  11. 1 1
      packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.ts
  12. 2 0
      packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.html
  13. 18 7
      packages/admin-ui/src/lib/settings/src/components/global-settings/global-settings.component.ts
  14. 3 3
      packages/asset-server-plugin/package.json
  15. 1 1
      packages/common/package.json
  16. 2 2
      packages/core/e2e/fixtures/e2e-products-full.csv
  17. 43 0
      packages/core/e2e/product.e2e-spec.ts
  18. 57 14
      packages/core/e2e/shop-order.e2e-spec.ts
  19. 127 0
      packages/core/e2e/stock-control.e2e-spec.ts
  20. 2 2
      packages/core/package.json
  21. 8 5
      packages/core/src/api/middleware/validate-custom-fields-interceptor.ts
  22. 4 4
      packages/core/src/common/error/error-result.ts
  23. 2 2
      packages/core/src/config/vendure-config.ts
  24. 2 1
      packages/core/src/service/helpers/utils/translate-entity.ts
  25. 3 1
      packages/core/src/service/services/asset.service.ts
  26. 1 1
      packages/core/src/service/services/customer.service.ts
  27. 7 2
      packages/core/src/service/services/order.service.ts
  28. 10 2
      packages/core/src/service/services/stock-movement.service.ts
  29. 3 3
      packages/create/package.json
  30. 9 9
      packages/dev-server/package.json
  31. 3 3
      packages/elasticsearch-plugin/package.json
  32. 3 3
      packages/email-plugin/package.json
  33. 3 3
      packages/testing/package.json
  34. 4 4
      packages/ui-devkit/package.json
  35. 1 1
      packages/ui-devkit/src/compiler/types.ts

+ 18 - 0
CHANGELOG.md

@@ -1,3 +1,21 @@
+## <small>1.1.4 (2021-08-19)</small>
+
+
+#### Fixes
+
+* **admin-ui** Apply variant name auto-generation for new translations ([df3d3f4](https://github.com/vendure-ecommerce/vendure/commit/df3d3f4)), closes [#600](https://github.com/vendure-ecommerce/vendure/issues/600)
+* **admin-ui** Correctly display OrderLine custom field values ([496ce5e](https://github.com/vendure-ecommerce/vendure/commit/496ce5e)), closes [#1031](https://github.com/vendure-ecommerce/vendure/issues/1031)
+* **admin-ui** Correctly set content lang based on available langs ([d9531fd](https://github.com/vendure-ecommerce/vendure/commit/d9531fd)), closes [#1033](https://github.com/vendure-ecommerce/vendure/issues/1033)
+* **admin-ui** Fix Channel dropdown auto-select in Safari (#1040) ([aee8416](https://github.com/vendure-ecommerce/vendure/commit/aee8416)), closes [#1040](https://github.com/vendure-ecommerce/vendure/issues/1040) [#1036](https://github.com/vendure-ecommerce/vendure/issues/1036)
+* **admin-ui** Improve display of long Collection paths in dropdown ([4d7032b](https://github.com/vendure-ecommerce/vendure/commit/4d7032b)), closes [#1042](https://github.com/vendure-ecommerce/vendure/issues/1042)
+* **core** Allow custom host id when creating new entity with orderable assets (#1035) ([aeaf308](https://github.com/vendure-ecommerce/vendure/commit/aeaf308)), closes [#1035](https://github.com/vendure-ecommerce/vendure/issues/1035) [#1034](https://github.com/vendure-ecommerce/vendure/issues/1034)
+* **core** Fix custom field validation when updating ProductVariants ([372b4af](https://github.com/vendure-ecommerce/vendure/commit/372b4af)), closes [#1014](https://github.com/vendure-ecommerce/vendure/issues/1014)
+* **core** Fix incorrect quantity adjustment (#983) ([2441ce7](https://github.com/vendure-ecommerce/vendure/commit/2441ce7)), closes [#983](https://github.com/vendure-ecommerce/vendure/issues/983) [#931](https://github.com/vendure-ecommerce/vendure/issues/931)
+* **core** Fix publishing CustomerEvent without customer ID ([03cd5d7](https://github.com/vendure-ecommerce/vendure/commit/03cd5d7))
+* **core** Fix stock movements when multiple OrderLines have same ProductVariant ([1b05f38](https://github.com/vendure-ecommerce/vendure/commit/1b05f38)), closes [#1028](https://github.com/vendure-ecommerce/vendure/issues/1028)
+* **core** Improve def of Translated<T> to allow customField typings ([3911059](https://github.com/vendure-ecommerce/vendure/commit/3911059)), closes [#1021](https://github.com/vendure-ecommerce/vendure/issues/1021)
+* **core** Loosen type def for ErrorResultUnion ([43ce722](https://github.com/vendure-ecommerce/vendure/commit/43ce722))
+
 ## <small>1.1.3 (2021-07-29)</small>
 
 

+ 1 - 1
lerna.json

@@ -2,7 +2,7 @@
   "packages": [
     "packages/*"
   ],
-  "version": "1.1.3",
+  "version": "1.1.4",
   "npmClient": "yarn",
   "useWorkspaces": true,
   "command": {

+ 3 - 3
packages/admin-ui-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui-plugin",
-  "version": "1.1.3",
+  "version": "1.1.4",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "files": [
@@ -19,8 +19,8 @@
   "devDependencies": {
     "@types/express": "^4.17.8",
     "@types/fs-extra": "^9.0.1",
-    "@vendure/common": "^1.1.3",
-    "@vendure/core": "^1.1.3",
+    "@vendure/common": "^1.1.4",
+    "@vendure/core": "^1.1.4",
     "express": "^4.17.1",
     "rimraf": "^3.0.2",
     "typescript": "4.1.5"

+ 2 - 2
packages/admin-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui",
-  "version": "1.1.3",
+  "version": "1.1.4",
   "license": "MIT",
   "scripts": {
     "ng": "ng",
@@ -37,7 +37,7 @@
     "@ng-select/ng-select": "^6.1.0",
     "@ngx-translate/core": "^13.0.0",
     "@ngx-translate/http-loader": "^6.0.0",
-    "@vendure/common": "^1.1.3",
+    "@vendure/common": "^1.1.4",
     "@webcomponents/custom-elements": "^1.4.3",
     "apollo-angular": "^2.4.0",
     "apollo-upload-client": "^14.1.3",

+ 8 - 2
packages/admin-ui/src/lib/catalog/src/components/collection-tree/collection-tree-node.component.html

@@ -92,8 +92,14 @@
                         (click)="move(collection, item.id)"
                         [disabled]="!(hasUpdatePermission$ | async)"
                     >
-                        <clr-icon shape="child-arrow"></clr-icon>
-                        {{ item.path }}
+                        <div class="move-to-item">
+                            <div class="move-icon">
+                                <clr-icon shape="child-arrow"></clr-icon>
+                            </div>
+                            <div class="path">
+                                {{ item.path }}
+                            </div>
+                        </div>
                     </button>
                     <div class="dropdown-divider"></div>
                     <button

+ 14 - 0
packages/admin-ui/src/lib/catalog/src/components/collection-tree/collection-tree-node.component.scss

@@ -73,3 +73,17 @@
 .example-list.cdk-drop-list-dragging .tree-node:not(.cdk-drag-placeholder) {
     transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
 }
+
+.move-to-item {
+    display: flex;
+    white-space: normal;
+    align-items: baseline;
+
+    .move-icon {
+        flex: none;
+        margin-right: 3px;
+    }
+    .path {
+        line-height: 18px;
+    }
+}

+ 17 - 7
packages/admin-ui/src/lib/catalog/src/providers/product-detail/product-detail.service.ts

@@ -163,9 +163,9 @@ export class ProductDetailService {
         if (productInput) {
             updateOperations.push(this.dataService.product.updateProduct(productInput));
 
-            const productOldName = findTranslation(product, languageCode)?.name;
+            const productOldName = findTranslation(product, languageCode)?.name ?? '';
             const productNewName = findTranslation(productInput, languageCode)?.name;
-            if (productOldName && productNewName && productOldName !== productNewName && autoUpdate) {
+            if (productNewName && productOldName !== productNewName && autoUpdate) {
                 for (const variant of product.variants) {
                     const currentVariantName = findTranslation(variant, languageCode)?.name || '';
                     let variantInput: UpdateProductVariantInput;
@@ -181,11 +181,21 @@ export class ProductDetailService {
                     }
                     const variantTranslation = findTranslation(variantInput, languageCode);
                     if (variantTranslation) {
-                        variantTranslation.name = replaceLast(
-                            variantTranslation.name,
-                            productOldName,
-                            productNewName,
-                        );
+                        if (variantTranslation.name) {
+                            variantTranslation.name = replaceLast(
+                                variantTranslation.name,
+                                productOldName,
+                                productNewName,
+                            );
+                        } else {
+                            // The variant translation was falsy, which occurs
+                            // when defining the product name for a new translation
+                            // language that had not yet been defined.
+                            variantTranslation.name = [
+                                productNewName,
+                                ...variant.options.map(o => o.name),
+                            ].join(' ');
+                        }
                     }
                 }
             }

+ 14 - 1
packages/admin-ui/src/lib/core/src/app.component.ts

@@ -1,9 +1,11 @@
 import { DOCUMENT } from '@angular/common';
 import { Component, HostBinding, Inject, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
 import { Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
 
 import { DataService } from './data/providers/data.service';
+import { LocalStorageService } from './providers/local-storage/local-storage.service';
 
 @Component({
     selector: 'vdr-root',
@@ -14,7 +16,11 @@ export class AppComponent implements OnInit {
     loading$: Observable<boolean>;
     private _document?: Document;
 
-    constructor(private dataService: DataService, @Inject(DOCUMENT) private document?: any) {
+    constructor(
+        private dataService: DataService,
+        private localStorageService: LocalStorageService,
+        @Inject(DOCUMENT) private document?: any,
+    ) {
         this._document = document;
     }
 
@@ -29,5 +35,12 @@ export class AppComponent implements OnInit {
             .subscribe(theme => {
                 this._document?.body.setAttribute('data-theme', theme);
             });
+
+        this.dataService.client
+            .uiState()
+            .mapStream(data => data.uiState.contentLanguage)
+            .subscribe(code => {
+                this.localStorageService.set('contentLanguageCode', code);
+            });
     }
 }

+ 1 - 1
packages/admin-ui/src/lib/core/src/common/version.ts

@@ -1,2 +1,2 @@
 // Auto-generated by the set-version.js script.
-export const ADMIN_UI_VERSION = '1.1.3';
+export const ADMIN_UI_VERSION = '1.1.4';

+ 30 - 0
packages/admin-ui/src/lib/core/src/data/data.module.ts

@@ -8,6 +8,7 @@ import { createUploadLink } from 'apollo-upload-client';
 
 import { getAppConfig } from '../app.config';
 import { introspectionResult } from '../common/introspection-result-wrapper';
+import { getDefaultUiLanguage } from '../common/utilities/get-default-ui-language';
 import { LocalStorageService } from '../providers/local-storage/local-storage.service';
 
 import { CheckJobsLink } from './check-jobs-link';
@@ -79,6 +80,29 @@ export function createApollo(
     };
 }
 
+/**
+ * On bootstrap, this function will fetch the available languages from the GlobalSettings and compare it
+ * to the currently-configured content language to ensure that the content language is actually one
+ * of the available languages.
+ */
+export function initContentLanguage(
+    serverConfigService: ServerConfigService,
+    localStorageService: LocalStorageService,
+    dataService: DataService,
+): () => Promise<any> {
+    // Why store in a intermediate variable? https://github.com/angular/angular/issues/23629
+    const result = async () => {
+        const availableLanguages = await serverConfigService.getAvailableLanguages().toPromise();
+        const contentLang = localStorageService.get('contentLanguageCode') || getDefaultUiLanguage();
+        if (availableLanguages.length && !availableLanguages.includes(contentLang)) {
+            dataService.client.setContentLanguage(availableLanguages[0]).subscribe(() => {
+                localStorageService.set('contentLanguageCode', availableLanguages[0]);
+            });
+        }
+    };
+    return result;
+}
+
 /**
  * The DataModule is responsible for all API calls *and* serves as the source of truth for global app
  * state via the apollo-link-state package.
@@ -104,6 +128,12 @@ export function createApollo(
             useFactory: initializeServerConfigService,
             deps: [ServerConfigService],
         },
+        {
+            provide: APP_INITIALIZER,
+            multi: true,
+            useFactory: initContentLanguage,
+            deps: [ServerConfigService, LocalStorageService, DataService],
+        },
     ],
 })
 export class DataModule {}

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/order-table/order-table.component.ts

@@ -38,8 +38,8 @@ export class OrderTableComponent implements OnInit {
     }
 
     private getLineCustomFields() {
-        const formGroup = new FormGroup({});
         for (const line of this.order.lines) {
+            const formGroup = new FormGroup({});
             const result = this.orderLineCustomFields
                 .map(config => {
                     const value = (line as any).customFields[config.name];

+ 2 - 0
packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.html

@@ -73,6 +73,7 @@
             formControlName="defaultTaxZoneId"
             [vdrDisabled]="!(updatePermission | hasPermission)"
         >
+            <option selected value style="display: none"></option>
             <option *ngFor="let zone of zones$ | async" [value]="zone.id">{{ zone.name }}</option>
         </select>
     </vdr-form-field>
@@ -95,6 +96,7 @@
             formControlName="defaultShippingZoneId"
             [vdrDisabled]="!(updatePermission | hasPermission)"
         >
+            <option selected value style="display: none"></option>            
             <option *ngFor="let zone of zones$ | async" [value]="zone.id">{{ zone.name }}</option>
         </select>
     </vdr-form-field>

+ 18 - 7
packages/admin-ui/src/lib/settings/src/components/global-settings/global-settings.component.ts

@@ -2,12 +2,17 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@
 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { BaseDetailComponent } from '@vendure/admin-ui/core';
-import { CustomFieldConfig, GlobalSettings, LanguageCode, Permission } from '@vendure/admin-ui/core';
-import { NotificationService } from '@vendure/admin-ui/core';
-import { DataService } from '@vendure/admin-ui/core';
-import { ServerConfigService } from '@vendure/admin-ui/core';
-import { switchMap, tap } from 'rxjs/operators';
+import {
+    BaseDetailComponent,
+    CustomFieldConfig,
+    DataService,
+    GlobalSettings,
+    LanguageCode,
+    NotificationService,
+    Permission,
+    ServerConfigService,
+} from '@vendure/admin-ui/core';
+import { switchMap, tap, withLatestFrom } from 'rxjs/operators';
 
 @Component({
     selector: 'vdr-global-settings',
@@ -80,8 +85,14 @@ export class GlobalSettingsComponent extends BaseDetailComponent<GlobalSettings>
                     }
                 }),
                 switchMap(() => this.serverConfigService.refreshGlobalSettings()),
+                withLatestFrom(this.dataService.client.uiState().single$),
             )
-            .subscribe();
+            .subscribe(([{ globalSettings }, { uiState }]) => {
+                const availableLangs = globalSettings.availableLanguages;
+                if (availableLangs.length && !availableLangs.includes(uiState.contentLanguage)) {
+                    this.dataService.client.setContentLanguage(availableLangs[0]).subscribe();
+                }
+            });
     }
 
     protected setFormValues(entity: GlobalSettings, languageCode: LanguageCode): void {

+ 3 - 3
packages/asset-server-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/asset-server-plugin",
-  "version": "1.1.3",
+  "version": "1.1.4",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "files": [
@@ -22,8 +22,8 @@
     "@types/fs-extra": "^9.0.8",
     "@types/node-fetch": "^2.5.8",
     "@types/sharp": "^0.27.1",
-    "@vendure/common": "^1.1.3",
-    "@vendure/core": "^1.1.3",
+    "@vendure/common": "^1.1.4",
+    "@vendure/core": "^1.1.4",
     "aws-sdk": "^2.856.0",
     "express": "^4.17.1",
     "node-fetch": "^2.6.1",

+ 1 - 1
packages/common/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/common",
-  "version": "1.1.3",
+  "version": "1.1.4",
   "main": "index.js",
   "license": "MIT",
   "scripts": {

+ 2 - 2
packages/core/e2e/fixtures/e2e-products-full.csv

@@ -1,7 +1,7 @@
 name              ,slug              ,description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  ,assets                            ,facets                                 ,optionGroups   ,optionValues     ,sku         ,price  ,taxCategory,stockOnHand,trackInventory,variantAssets,variantFacets
 Laptop            ,laptop            ,"Now equipped with seventh-generation Intel Core processors, Laptop is snappier than ever. From daily tasks like launching apps and opening files to more advanced computing, you can power through your day thanks to faster SSDs and Turbo Boost processing up to 3.6GHz."                                                                                                                                                                                                                 ,derick-david-409858-unsplash.jpg  ,category:electronics|category:computers,screen size|RAM,13 inch|8GB      ,L2201308    ,1299.00,standard   ,100        ,false         ,             ,
-                  ,                  ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             ,                                  ,                                       ,               ,15 inch|8GB      ,L2201508    ,1399.00,standard   ,100        ,false         ,             ,
-                  ,                  ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             ,                                  ,                                       ,               ,13 inch|16GB     ,L2201316    ,2199.00,standard   ,100        ,false         ,             ,
+                  ,                  ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             ,                                  ,                                       ,               ,15 inch|8GB      ,L2201508    ,1399.00,standard   ,100        ,false          ,             ,
+                  ,                  ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             ,                                  ,                                       ,               ,13 inch|16GB     ,L2201316    ,2199.00,standard   ,100        ,true         ,             ,
                   ,                  ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             ,                                  ,                                       ,               ,15 inch|16GB     ,L2201516    ,2299.00,standard   ,100        ,false         ,             ,
 Curvy Monitor     ,curvy-monitor     ,"Discover a truly immersive viewing experience with this monitor curved more deeply than any other. Wrapping around your field of vision the 1,800 R screencreates a wider field of view, enhances depth perception, and minimises peripheral distractions to draw you deeper in to your content."                                                                                                                                                                                           ,alexandru-acea-686569-unsplash.jpg,category:electronics|category:computers,monitor size   ,24 inch          ,C24F390     ,143.74 ,standard   ,100        ,false         ,             ,
                   ,                  ,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             ,                                  ,                                       ,               ,27 inch          ,C27F390     ,169.94 ,standard   ,100        ,false         ,             ,

+ 43 - 0
packages/core/e2e/product.e2e-spec.ts

@@ -501,6 +501,49 @@ describe('Product resolver', () => {
                 expect(product.slug).toBe(en_translation.slug);
             });
         });
+
+        describe('product.variants', () => {
+            it('returns product variants', async () => {
+                const { product } = await adminClient.query<
+                    GetProductWithVariants.Query,
+                    GetProductWithVariants.Variables
+                >(GET_PRODUCT_WITH_VARIANTS, {
+                    id: 'T_1',
+                });
+
+                expect(product?.variants.length).toBe(4);
+            });
+
+            it('returns product variants in existing language', async () => {
+                const { product } = await adminClient.query<
+                    GetProductWithVariants.Query,
+                    GetProductWithVariants.Variables
+                >(
+                    GET_PRODUCT_WITH_VARIANTS,
+                    {
+                        id: 'T_1',
+                    },
+                    { languageCode: LanguageCode.en },
+                );
+
+                expect(product?.variants.length).toBe(4);
+            });
+
+            it('returns product variants in non-existing language', async () => {
+                const { product } = await adminClient.query<
+                    GetProductWithVariants.Query,
+                    GetProductWithVariants.Variables
+                >(
+                    GET_PRODUCT_WITH_VARIANTS,
+                    {
+                        id: 'T_1',
+                    },
+                    { languageCode: LanguageCode.ru },
+                );
+
+                expect(product?.variants.length).toBe(4);
+            });
+        });
     });
 
     describe('productVariants list query', () => {

+ 57 - 14
packages/core/e2e/shop-order.e2e-spec.ts

@@ -64,6 +64,7 @@ import {
 } from './graphql/shared-definitions';
 import {
     ADD_ITEM_TO_ORDER,
+    ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
     ADD_PAYMENT,
     ADJUST_ITEM_QUANTITY,
     GET_ACTIVE_ORDER,
@@ -81,7 +82,6 @@ import {
     SET_CUSTOMER,
     SET_SHIPPING_ADDRESS,
     SET_SHIPPING_METHOD,
-    TEST_ORDER_FRAGMENT,
     TRANSITION_TO_STATE,
     UPDATED_ORDER_FRAGMENT,
 } from './graphql/shop-definitions';
@@ -109,7 +109,7 @@ describe('Shop orders', () => {
                 ],
             },
             orderOptions: {
-                orderItemsLimit: 99,
+                orderItemsLimit: 199,
             },
         }),
     );
@@ -473,12 +473,12 @@ describe('Shop orders', () => {
                 AddItemToOrder.Variables
             >(ADD_ITEM_TO_ORDER, {
                 productVariantId: 'T_1',
-                quantity: 100,
+                quantity: 200,
             });
 
             orderResultGuard.assertErrorResult(addItemToOrder);
             expect(addItemToOrder.message).toBe(
-                'Cannot add items. An order may consist of a maximum of 99 items',
+                'Cannot add items. An order may consist of a maximum of 199 items',
             );
             expect(addItemToOrder.errorCode).toBe(ErrorCode.ORDER_LIMIT_ERROR);
         });
@@ -520,17 +520,60 @@ describe('Shop orders', () => {
             expect(adjustOrderLine!.lines.map(i => i.productVariant.id)).toEqual(['T_1']);
         });
 
+        it('adjustOrderLine with quantity > stockOnHand only allows user to have stock on hand', async () => {
+            const { addItemToOrder } = await shopClient.query<
+                AddItemToOrder.Mutation,
+                AddItemToOrder.Variables
+            >(ADD_ITEM_TO_ORDER, {
+                productVariantId: 'T_3',
+                quantity: 111,
+            });
+            orderResultGuard.assertErrorResult(addItemToOrder);
+            // Insufficient stock error should return because there are only 100 available
+            expect(addItemToOrder.errorCode).toBe('INSUFFICIENT_STOCK_ERROR');
+
+            // But it should still add the item to the order
+            expect(addItemToOrder!.order.lines[1].quantity).toBe(100);
+
+            const { adjustOrderLine } = await shopClient.query<
+                AdjustItemQuantity.Mutation,
+                AdjustItemQuantity.Variables
+            >(ADJUST_ITEM_QUANTITY, {
+                orderLineId: 'T_8',
+                quantity: 101,
+            });
+            orderResultGuard.assertErrorResult(adjustOrderLine);
+            expect(adjustOrderLine.errorCode).toBe('INSUFFICIENT_STOCK_ERROR');
+            expect(adjustOrderLine.message).toBe(
+                'Only 100 items were added to the order due to insufficient stock',
+            );
+
+            const order = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
+            expect(order.activeOrder?.lines[1].quantity).toBe(100);
+
+            const { adjustOrderLine: adjustLine2 } = await shopClient.query<
+                AdjustItemQuantity.Mutation,
+                AdjustItemQuantity.Variables
+            >(ADJUST_ITEM_QUANTITY, {
+                orderLineId: 'T_8',
+                quantity: 0,
+            });
+            orderResultGuard.assertSuccess(adjustLine2);
+            expect(adjustLine2!.lines.length).toBe(1);
+            expect(adjustLine2!.lines.map(i => i.productVariant.id)).toEqual(['T_1']);
+        });
+
         it('adjustOrderLine errors when going beyond orderItemsLimit', async () => {
             const { adjustOrderLine } = await shopClient.query<
                 AdjustItemQuantity.Mutation,
                 AdjustItemQuantity.Variables
             >(ADJUST_ITEM_QUANTITY, {
                 orderLineId: firstOrderLineId,
-                quantity: 100,
+                quantity: 200,
             });
             orderResultGuard.assertErrorResult(adjustOrderLine);
             expect(adjustOrderLine.message).toBe(
-                'Cannot add items. An order may consist of a maximum of 99 items',
+                'Cannot add items. An order may consist of a maximum of 199 items',
             );
             expect(adjustOrderLine.errorCode).toBe(ErrorCode.ORDER_LIMIT_ERROR);
         });
@@ -1811,6 +1854,14 @@ const SET_ORDER_CUSTOM_FIELDS = gql`
     }
 `;
 
+export const LOG_OUT = gql`
+    mutation LogOut {
+        logout {
+            success
+        }
+    }
+`;
+
 export const ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS = gql`
     mutation AddItemToOrderWithCustomFields(
         $productVariantId: ID!
@@ -1831,11 +1882,3 @@ export const ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS = gql`
     }
     ${UPDATED_ORDER_FRAGMENT}
 `;
-
-export const LOG_OUT = gql`
-    mutation LogOut {
-        logout {
-            success
-        }
-    }
-`;

+ 127 - 0
packages/core/e2e/stock-control.e2e-spec.ts

@@ -63,6 +63,9 @@ describe('Stock control', () => {
             paymentOptions: {
                 paymentMethodHandlers: [testSuccessfulPaymentMethod, twoStagePaymentMethod],
             },
+            customFields: {
+                OrderLine: [{ name: 'customization', type: 'string', nullable: true }],
+            },
         }),
     );
 
@@ -924,6 +927,130 @@ describe('Stock control', () => {
             });
         });
     });
+
+    // https://github.com/vendure-ecommerce/vendure/issues/1028
+    describe('OrderLines with same variant but different custom fields', () => {
+        let orderId: string;
+
+        const ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS = `
+            mutation AddItemToOrderWithCustomFields(
+                $productVariantId: ID!
+                $quantity: Int!
+                $customFields: OrderLineCustomFieldsInput
+            ) {
+                addItemToOrder(
+                    productVariantId: $productVariantId
+                    quantity: $quantity
+                    customFields: $customFields
+                ) {
+                    ... on Order {
+                        id
+                        lines { id }
+                    }
+                    ... on ErrorResult {
+                        errorCode
+                        message
+                    }
+                }
+            }
+        `;
+
+        it('correctly allocates stock', async () => {
+            await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
+
+            const product = await getProductWithStockMovement('T_2');
+            const [variant1, variant2, variant3] = product!.variants;
+
+            expect(variant2.stockAllocated).toBe(0);
+
+            await shopClient.query<AddItemToOrder.Mutation, any>(gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS), {
+                productVariantId: variant2.id,
+                quantity: 1,
+                customFields: {
+                    customization: 'foo',
+                },
+            });
+            const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation, any>(
+                gql(ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS),
+                {
+                    productVariantId: variant2.id,
+                    quantity: 1,
+                    customFields: {
+                        customization: 'bar',
+                    },
+                },
+            );
+
+            orderGuard.assertSuccess(addItemToOrder);
+            orderId = addItemToOrder.id;
+            // Assert that separate order lines have been created
+            expect(addItemToOrder.lines.length).toBe(2);
+
+            await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(
+                SET_SHIPPING_ADDRESS,
+                {
+                    input: {
+                        streetLine1: '1 Test Street',
+                        countryCode: 'GB',
+                    } as CreateAddressInput,
+                },
+            );
+            await shopClient.query<TransitionToState.Mutation, TransitionToState.Variables>(
+                TRANSITION_TO_STATE,
+                {
+                    state: 'ArrangingPayment',
+                },
+            );
+            const { addPaymentToOrder: order } = await shopClient.query<
+                AddPaymentToOrder.Mutation,
+                AddPaymentToOrder.Variables
+            >(ADD_PAYMENT, {
+                input: {
+                    method: testSuccessfulPaymentMethod.code,
+                    metadata: {},
+                } as PaymentInput,
+            });
+            orderGuard.assertSuccess(order);
+
+            const product2 = await getProductWithStockMovement('T_2');
+            const [variant1_2, variant2_2, variant3_2] = product2!.variants;
+
+            expect(variant2_2.stockAllocated).toBe(2);
+        });
+
+        it('correctly creates Sales', async () => {
+            const product = await getProductWithStockMovement('T_2');
+            const [variant1, variant2, variant3] = product!.variants;
+
+            expect(variant2.stockOnHand).toBe(3);
+
+            const { order } = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
+                id: orderId,
+            });
+
+            await adminClient.query<CreateFulfillment.Mutation, CreateFulfillment.Variables>(
+                CREATE_FULFILLMENT,
+                {
+                    input: {
+                        lines: order?.lines.map(l => ({ orderLineId: l.id, quantity: l.quantity })) ?? [],
+                        handler: {
+                            code: manualFulfillmentHandler.code,
+                            arguments: [
+                                { name: 'method', value: 'test method' },
+                                { name: 'trackingCode', value: 'ABC123' },
+                            ],
+                        },
+                    },
+                },
+            );
+
+            const product2 = await getProductWithStockMovement('T_2');
+            const [variant1_2, variant2_2, variant3_2] = product2!.variants;
+
+            expect(variant2_2.stockAllocated).toBe(0);
+            expect(variant2_2.stockOnHand).toBe(1);
+        });
+    });
 });
 
 const UPDATE_STOCK_ON_HAND = gql`

+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/core",
-  "version": "1.1.3",
+  "version": "1.1.4",
   "description": "A modern, headless ecommerce framework",
   "repository": {
     "type": "git",
@@ -47,7 +47,7 @@
     "@nestjs/testing": "7.6.17",
     "@nestjs/typeorm": "7.1.5",
     "@types/fs-extra": "^9.0.1",
-    "@vendure/common": "^1.1.3",
+    "@vendure/common": "^1.1.4",
     "apollo-server-express": "2.24.1",
     "bcrypt": "^5.0.0",
     "body-parser": "^1.19.0",

+ 8 - 5
packages/core/src/api/middleware/validate-custom-fields-interceptor.ts

@@ -51,12 +51,15 @@ export class ValidateCustomFieldsInterceptor implements NestInterceptor {
                 for (const [inputName, typeName] of Object.entries(inputTypeNames)) {
                     if (this.inputsWithCustomFields.has(typeName)) {
                         if (variables[inputName]) {
-                            await this.validateInput(
-                                typeName,
-                                ctx.languageCode,
-                                injector,
+                            const inputVariables: Array<Record<string, any>> = Array.isArray(
                                 variables[inputName],
-                            );
+                            )
+                                ? variables[inputName]
+                                : [variables[inputName]];
+
+                            for (const inputVariable of inputVariables) {
+                                await this.validateInput(typeName, ctx.languageCode, injector, inputVariable);
+                            }
                         }
                     }
                 }

+ 4 - 4
packages/core/src/common/error/error-result.ts

@@ -1,9 +1,9 @@
-import { ErrorResult as GraphQLErrorResultShop } from '@vendure/common/lib/generated-shop-types';
-import { ErrorResult, ErrorResult as GraphQLErrorResultAdmin } from '@vendure/common/lib/generated-types';
-
 import { VendureEntity } from '../../entity/base/base.entity';
 
-export type GraphQLErrorResult = GraphQLErrorResultShop | GraphQLErrorResultAdmin;
+export type GraphQLErrorResult = {
+    errorCode: string;
+    message: string;
+};
 
 /**
  * @description

+ 2 - 2
packages/core/src/config/vendure-config.ts

@@ -72,7 +72,7 @@ export interface ApiOptions {
     adminApiPath?: string;
     /**
      * @description
-     * The path to the admin GraphQL API.
+     * The path to the shop GraphQL API.
      *
      * @default 'shop-api'
      */
@@ -350,7 +350,7 @@ export interface AuthOptions {
      * `password` property - doing so will result in an error. Instead, the password is set at a later stage
      * (once the email with the verification token has been opened) via the `verifyCustomerAccount` mutation.
      *
-     * @defaut true
+     * @default true
      */
     requireVerification?: boolean;
     /**

+ 2 - 1
packages/core/src/service/helpers/utils/translate-entity.ts

@@ -15,7 +15,8 @@ export type TranslatableRelationsKeys<T> = {
     T[K] extends string[] ? never :
     T[K] extends number[] ? never :
     T[K] extends boolean[] ? never :
-    K extends 'translations' ? never : K
+    K extends 'translations' ? never :
+    K extends 'customFields' ? never : K
 }[keyof T];
 
 // prettier-ignore

+ 3 - 1
packages/core/src/service/services/asset.service.ts

@@ -19,6 +19,7 @@ import { ReadStream } from 'fs-extra';
 import mime from 'mime-types';
 import path from 'path';
 import { Readable, Stream } from 'stream';
+import { camelCase } from 'typeorm/util/StringUtils';
 
 import { RequestContext } from '../../api/common/request-context';
 import { isGraphQlErrorResult } from '../../common/error/error-result';
@@ -45,6 +46,7 @@ import { TransactionalConnection } from '../transaction/transactional-connection
 import { ChannelService } from './channel.service';
 import { RoleService } from './role.service';
 import { TagService } from './tag.service';
+
 // tslint:disable-next-line:no-var-requires
 const sizeOf = require('image-size');
 
@@ -565,7 +567,7 @@ export class AssetService {
             case 'Collection':
                 return 'collectionId';
             default:
-                throw new InternalServerError('error.could-not-find-matching-orderable-asset');
+                return `${camelCase(entityName, true)}Id`;
         }
     }
 

+ 1 - 1
packages/core/src/service/services/customer.service.ts

@@ -544,7 +544,7 @@ export class CustomerService {
             customer = patchEntity(existing, input);
             customer.channels.push(await this.connection.getEntityOrThrow(ctx, Channel, ctx.channelId));
         } else {
-            customer = new Customer(input);
+            customer = await this.connection.getRepository(ctx, Customer).save(new Customer(input));
             this.channelService.assignToCurrentChannel(customer, ctx);
             this.eventBus.publish(new CustomerEvent(ctx, customer, 'created'));
         }

+ 7 - 2
packages/core/src/service/services/order.service.ts

@@ -409,7 +409,12 @@ export class OrderService {
             productVariantId,
             customFields,
         );
-        await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, correctedQuantity, order);
+        if (correctedQuantity < quantity) {
+            const newQuantity = (existingOrderLine ? existingOrderLine?.quantity : 0) + correctedQuantity;
+            await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, newQuantity, order);
+        } else {
+            await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, correctedQuantity, order);
+        }
         const quantityWasAdjustedDown = correctedQuantity < quantity;
         const updatedOrder = await this.applyPriceAdjustments(ctx, order, orderLine);
         if (quantityWasAdjustedDown) {
@@ -457,7 +462,7 @@ export class OrderService {
             order.lines = order.lines.filter(l => !idsAreEqual(l.id, orderLine.id));
             await this.connection.getRepository(ctx, OrderLine).remove(orderLine);
         } else {
-            await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, quantity, order);
+            await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, correctedQuantity, order);
         }
         const quantityWasAdjustedDown = correctedQuantity < quantity;
         const updatedOrder = await this.applyPriceAdjustments(ctx, order, orderLine);

+ 10 - 2
packages/core/src/service/services/stock-movement.service.ts

@@ -83,7 +83,11 @@ export class StockMovementService {
         const allocations: Allocation[] = [];
         const globalTrackInventory = (await this.globalSettingsService.getSettings(ctx)).trackInventory;
         for (const line of order.lines) {
-            const { productVariant } = line;
+            const productVariant = await this.connection.getEntityOrThrow(
+                ctx,
+                ProductVariant,
+                line.productVariant.id,
+            );
             const allocation = new Allocation({
                 productVariant,
                 quantity: line.quantity,
@@ -125,7 +129,11 @@ export class StockMovementService {
             value.items.push(orderItem);
         }
         for (const lineRow of orderLinesMap.values()) {
-            const { productVariant } = lineRow.line;
+            const productVariant = await this.connection.getEntityOrThrow(
+                ctx,
+                ProductVariant,
+                lineRow.line.productVariant.id,
+            );
             const sale = new Sale({
                 productVariant,
                 quantity: lineRow.items.length * -1,

+ 3 - 3
packages/create/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/create",
-  "version": "1.1.3",
+  "version": "1.1.4",
   "license": "MIT",
   "bin": {
     "create": "./index.js"
@@ -26,13 +26,13 @@
     "@types/handlebars": "^4.1.0",
     "@types/listr": "^0.14.2",
     "@types/semver": "^6.2.2",
-    "@vendure/core": "^1.1.3",
+    "@vendure/core": "^1.1.4",
     "rimraf": "^3.0.2",
     "ts-node": "^9.0.0",
     "typescript": "4.1.5"
   },
   "dependencies": {
-    "@vendure/common": "^1.1.3",
+    "@vendure/common": "^1.1.4",
     "chalk": "^4.1.0",
     "commander": "^7.1.0",
     "cross-spawn": "^7.0.3",

+ 9 - 9
packages/dev-server/package.json

@@ -1,6 +1,6 @@
 {
   "name": "dev-server",
-  "version": "1.1.3",
+  "version": "1.1.4",
   "main": "index.js",
   "license": "MIT",
   "private": true,
@@ -14,18 +14,18 @@
     "load-test:100k": "node -r ts-node/register load-testing/run-load-test.ts 100000"
   },
   "dependencies": {
-    "@vendure/admin-ui-plugin": "^1.1.3",
-    "@vendure/asset-server-plugin": "^1.1.3",
-    "@vendure/common": "^1.1.3",
-    "@vendure/core": "^1.1.3",
-    "@vendure/elasticsearch-plugin": "^1.1.3",
-    "@vendure/email-plugin": "^1.1.3",
+    "@vendure/admin-ui-plugin": "^1.1.4",
+    "@vendure/asset-server-plugin": "^1.1.4",
+    "@vendure/common": "^1.1.4",
+    "@vendure/core": "^1.1.4",
+    "@vendure/elasticsearch-plugin": "^1.1.4",
+    "@vendure/email-plugin": "^1.1.4",
     "typescript": "4.1.5"
   },
   "devDependencies": {
     "@types/csv-stringify": "^3.1.0",
-    "@vendure/testing": "^1.1.3",
-    "@vendure/ui-devkit": "^1.1.3",
+    "@vendure/testing": "^1.1.4",
+    "@vendure/ui-devkit": "^1.1.4",
     "concurrently": "^5.0.0",
     "csv-stringify": "^5.3.3"
   }

+ 3 - 3
packages/elasticsearch-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/elasticsearch-plugin",
-  "version": "1.1.3",
+  "version": "1.1.4",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -23,8 +23,8 @@
     "fast-deep-equal": "^3.1.3"
   },
   "devDependencies": {
-    "@vendure/common": "^1.1.3",
-    "@vendure/core": "^1.1.3",
+    "@vendure/common": "^1.1.4",
+    "@vendure/core": "^1.1.4",
     "rimraf": "^3.0.2",
     "typescript": "4.1.5"
   }

+ 3 - 3
packages/email-plugin/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/email-plugin",
-  "version": "1.1.3",
+  "version": "1.1.4",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
@@ -33,8 +33,8 @@
     "@types/fs-extra": "^9.0.1",
     "@types/handlebars": "^4.1.0",
     "@types/mjml": "^4.0.4",
-    "@vendure/common": "^1.1.3",
-    "@vendure/core": "^1.1.3",
+    "@vendure/common": "^1.1.4",
+    "@vendure/core": "^1.1.4",
     "rimraf": "^3.0.2",
     "typescript": "4.1.5"
   }

+ 3 - 3
packages/testing/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/testing",
-  "version": "1.1.3",
+  "version": "1.1.4",
   "description": "End-to-end testing tools for Vendure projects",
   "keywords": [
     "vendure",
@@ -33,7 +33,7 @@
   },
   "dependencies": {
     "@types/node-fetch": "^2.5.4",
-    "@vendure/common": "^1.1.3",
+    "@vendure/common": "^1.1.4",
     "faker": "^4.1.0",
     "form-data": "^3.0.0",
     "graphql": "15.5.0",
@@ -44,7 +44,7 @@
   "devDependencies": {
     "@types/mysql": "^2.15.15",
     "@types/pg": "^7.14.5",
-    "@vendure/core": "^1.1.3",
+    "@vendure/core": "^1.1.4",
     "mysql": "^2.18.1",
     "pg": "^8.4.0",
     "rimraf": "^3.0.0",

+ 4 - 4
packages/ui-devkit/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/ui-devkit",
-  "version": "1.1.3",
+  "version": "1.1.4",
   "description": "A library for authoring Vendure Admin UI extensions",
   "keywords": [
     "vendure",
@@ -39,8 +39,8 @@
     "@angular/cli": "11.2.4",
     "@angular/compiler": "11.2.5",
     "@angular/compiler-cli": "11.2.5",
-    "@vendure/admin-ui": "^1.1.3",
-    "@vendure/common": "^1.1.3",
+    "@vendure/admin-ui": "^1.1.4",
+    "@vendure/common": "^1.1.4",
     "chalk": "^4.1.0",
     "chokidar": "^3.5.1",
     "fs-extra": "^9.1.0",
@@ -51,7 +51,7 @@
     "@rollup/plugin-node-resolve": "^11.2.0",
     "@types/fs-extra": "^9.0.8",
     "@types/glob": "^7.1.3",
-    "@vendure/core": "^1.1.3",
+    "@vendure/core": "^1.1.4",
     "rimraf": "^3.0.2",
     "rollup": "^2.40.0",
     "rollup-plugin-terser": "^7.0.2",

+ 1 - 1
packages/ui-devkit/src/compiler/types.ts

@@ -71,7 +71,7 @@ export interface GlobalStylesExtension {
  * Angular [NgModules](https://angular.io/guide/ngmodules) which are compiled
  * into the application.
  *
- * See [Extending the Admin UI](/docs/developer-guide/plugins/extending-the-admin-ui/) for
+ * See [Extending the Admin UI](/docs/plugins/extending-the-admin-ui/) for
  * detailed instructions.
  *
  * @docsCategory UiDevkit