Explorar o código

Merge branch 'master' into minor

Michael Bromley %!s(int64=2) %!d(string=hai) anos
pai
achega
1330d68cfe
Modificáronse 64 ficheiros con 782 adicións e 449 borrados
  1. 10 4
      .github/workflows/publish_and_install.yml
  2. 1 1
      .github/workflows/verdaccio/config.yaml
  3. 19 0
      CHANGELOG.md
  4. 1 1
      docs/docs/guides/developer-guide/custom-permissions/index.md
  5. 4 3
      docs/docs/guides/developer-guide/importing-data/index.md
  6. 1 1
      docs/docs/guides/extending-the-admin-ui/creating-detail-views/index.md
  7. 1 1
      docs/docs/guides/extending-the-admin-ui/custom-detail-components/index.md
  8. 1 1
      docs/docs/guides/extending-the-admin-ui/custom-form-inputs/index.md
  9. 13 0
      docs/docs/guides/extending-the-admin-ui/ui-library/index.md
  10. 1 1
      e2e-common/vitest.config.ts
  11. 1 1
      lerna.json
  12. 3 3
      packages/admin-ui-plugin/package.json
  13. 1 1
      packages/admin-ui/package-lock.json
  14. 2 2
      packages/admin-ui/package.json
  15. 12 5
      packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts
  16. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  17. 27 1
      packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder-types.ts
  18. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/asset-picker-dialog/asset-picker-dialog.component.scss
  19. 0 4
      packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.ts
  20. 5 2
      packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.service.ts
  21. 23 0
      packages/admin-ui/src/lib/core/src/shared/components/modal-dialog/modal-dialog.component.scss
  22. 10 10
      packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts
  23. 1 1
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/code-editor-form-input/base-code-editor.scss
  24. 6 1
      packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.ts
  25. 1 0
      packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.ts
  26. 2 1
      packages/admin-ui/src/lib/static/styles/theme/dark.scss
  27. 3 3
      packages/asset-server-plugin/package.json
  28. 2 2
      packages/cli/package.json
  29. 1 1
      packages/common/package.json
  30. 358 281
      packages/core/e2e/default-search-plugin.e2e-spec.ts
  31. 45 1
      packages/core/e2e/entity-hydrator.e2e-spec.ts
  32. 17 11
      packages/core/e2e/money-strategy.e2e-spec.ts
  33. 10 11
      packages/core/e2e/utils/await-running-jobs.ts
  34. 2 2
      packages/core/package.json
  35. 4 1
      packages/core/src/bootstrap.ts
  36. 1 1
      packages/core/src/common/finite-state-machine/validate-transition-definition.spec.ts
  37. 11 13
      packages/core/src/common/finite-state-machine/validate-transition-definition.ts
  38. 59 0
      packages/core/src/entity/register-custom-entity-fields.ts
  39. 0 1
      packages/core/src/event-bus/vendure-entity-event.ts
  40. 2 1
      packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts
  41. 3 1
      packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts
  42. 0 8
      packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-common.ts
  43. 15 14
      packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts
  44. 3 2
      packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts
  45. 21 0
      packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts
  46. 3 0
      packages/core/src/service/helpers/fulfillment-state-machine/fulfillment-state-machine.ts
  47. 3 0
      packages/core/src/service/helpers/order-state-machine/order-state-machine.ts
  48. 3 0
      packages/core/src/service/helpers/payment-state-machine/payment-state-machine.ts
  49. 1 0
      packages/core/src/service/services/order.service.ts
  50. 3 3
      packages/create/package.json
  51. 1 1
      packages/dev-server/load-testing/graphql/shop/complete-order.graphql
  52. 15 1
      packages/dev-server/load-testing/init-load-test.ts
  53. 1 1
      packages/dev-server/load-testing/load-test-config.ts
  54. 2 2
      packages/dev-server/load-testing/run-load-test.ts
  55. 6 4
      packages/dev-server/load-testing/scripts/search-and-checkout.js
  56. 7 4
      packages/dev-server/load-testing/utils/api-request.js
  57. 9 9
      packages/dev-server/package.json
  58. 3 3
      packages/elasticsearch-plugin/package.json
  59. 3 3
      packages/email-plugin/package.json
  60. 3 3
      packages/harden-plugin/package.json
  61. 3 3
      packages/job-queue-plugin/package.json
  62. 4 4
      packages/payments-plugin/package.json
  63. 3 3
      packages/testing/package.json
  64. 4 4
      packages/ui-devkit/package.json

+ 10 - 4
.github/workflows/publish_and_install.yml

@@ -33,32 +33,38 @@ jobs:
         npm install -g verdaccio
         npm install -g wait-on
         tmp_registry_log=`mktemp`
-        nohup verdaccio &>$tmp_registry_log &
+        mkdir -p $HOME/.config/verdaccio
+        cp -v ./.github/workflows/verdaccio/config.yaml $HOME/.config/verdaccio/config.yaml
+        nohup verdaccio --config $HOME/.config/verdaccio/config.yaml &
         wait-on http://localhost:4873
         TOKEN_RES=$(curl -XPUT \
           -H "Content-type: application/json" \
           -d '{ "name": "test", "password": "test" }' \
           'http://localhost:4873/-/user/org.couchdb.user:test')
         TOKEN=$(echo "$TOKEN_RES" | jq -r '.token')
-        npm set registry "http://0.0.0.0:4873"
-        npm set //0.0.0.0:4873/:_authToken $TOKEN
+        npm set //localhost:4873/:_authToken $TOKEN
     - name: Windows dependencies
       if: matrix.os == 'windows-latest'
       run: npm install -g @angular/cli
     - name: Yarn install
       run: |
+        npm i -g yarn
         yarn config set unsafe-perm true
         yarn install --network-timeout 1000000 --prefer-offline
       env:
         CI: true
     - name: Publish to Verdaccio
       run: |
+        nohup verdaccio --config $HOME/.config/verdaccio/config.yaml &
+        wait-on http://localhost:4873
         yarn lerna publish prepatch --preid ci --no-push --no-git-tag-version --no-commit-hooks --force-publish "*" --yes --dist-tag ci --registry http://localhost:4873
     - name: Install via @vendure/create
       run: |
         mkdir -p $HOME/install
         cd $HOME/install
-        npm set registry=http://0.0.0.0:4873
+        nohup verdaccio --config $HOME/.config/verdaccio/config.yaml &
+        wait-on http://localhost:4873
+        npm set registry=http://localhost:4873
         npm dist-tag ls @vendure/create
         npx @vendure/create@ci test-app --ci --use-npm --log-level info
     - name: Server smoke tests

+ 1 - 1
.github/workflows/verdaccio/config.yaml

@@ -10,7 +10,7 @@ plugins: ./plugins
 max_body_size: 1000mb
 web:
   # WebUI is enabled as default, if you want disable it, just uncomment this line
-  enable: false
+  enable: true
   title: Verdaccio
 
 auth:

+ 19 - 0
CHANGELOG.md

@@ -1,3 +1,22 @@
+## <small>2.1.4 (2023-11-24)</small>
+
+
+#### Fixes
+
+* **admin-ui** Fix admin ui code templates (#2545) ([a9e67fe](https://github.com/vendure-ecommerce/vendure/commit/a9e67fe)), closes [#2545](https://github.com/vendure-ecommerce/vendure/issues/2545)
+* **admin-ui** Fix card component colors in dark theme ([c077e15](https://github.com/vendure-ecommerce/vendure/commit/c077e15))
+* **admin-ui** Fix code editor border color for dark mode ([9eb9d9d](https://github.com/vendure-ecommerce/vendure/commit/9eb9d9d))
+* **admin-ui** Fix encoding of configurable arg values ([84764b1](https://github.com/vendure-ecommerce/vendure/commit/84764b1)), closes [#2539](https://github.com/vendure-ecommerce/vendure/issues/2539)
+* **admin-ui** Fix localized custom fields in Promotion & PaymentMethod ([d665ec6](https://github.com/vendure-ecommerce/vendure/commit/d665ec6))
+* **admin-ui** Fix responsive layout of modal dialog for assets ([5176017](https://github.com/vendure-ecommerce/vendure/commit/5176017)), closes [#2537](https://github.com/vendure-ecommerce/vendure/issues/2537)
+* **admin-ui** Fix stack overflow when datetime picker inside a list ([f7b4f46](https://github.com/vendure-ecommerce/vendure/commit/f7b4f46))
+* **core** Fix custom MoneyStrategy handling from plugins ([a09c2b2](https://github.com/vendure-ecommerce/vendure/commit/a09c2b2)), closes [#2527](https://github.com/vendure-ecommerce/vendure/issues/2527)
+* **core** Fix DefaultSearchPlugin for non-default languages (#2515) ([fb0ea13](https://github.com/vendure-ecommerce/vendure/commit/fb0ea13)), closes [#2515](https://github.com/vendure-ecommerce/vendure/issues/2515) [#2197](https://github.com/vendure-ecommerce/vendure/issues/2197)
+* **core** Fix entity hydration postgres edge-case ([9546d1b](https://github.com/vendure-ecommerce/vendure/commit/9546d1b)), closes [#2546](https://github.com/vendure-ecommerce/vendure/issues/2546)
+* **core** Fix i18n custom fields in Promotion & PaymentMethod ([3d6edb5](https://github.com/vendure-ecommerce/vendure/commit/3d6edb5))
+* **core** Log error on misconfigured localized custom fields ([5775447](https://github.com/vendure-ecommerce/vendure/commit/5775447))
+* **core** Relax validation of custom process states ([cf301eb](https://github.com/vendure-ecommerce/vendure/commit/cf301eb))
+
 ## <small>2.1.3 (2023-11-17)</small>
 
 #### Security

+ 1 - 1
docs/docs/guides/developer-guide/custom-permissions/index.md

@@ -81,7 +81,7 @@ For example, let's imagine we are creating a plugin which adds a new entity call
 ```ts title="src/plugins/product-review/constants.ts"
 import { CrudPermissionDefinition } from '@vendure/core';
 
-export const productReview = new CrudPermissionDefinition(ProductReview);
+export const productReview = new CrudPermissionDefinition('ProductReview');
 ```
 
 These permissions can then be used in our resolver:

+ 4 - 3
docs/docs/guides/developer-guide/importing-data/index.md

@@ -182,9 +182,10 @@ The `@vendure/core` package exposes a [`populate()` function](/reference/typescr
 ```ts title="src/my-populate-script.ts"
 import { bootstrap, DefaultJobQueuePlugin } from '@vendure/core';
 import { populate } from '@vendure/core/cli';
+import path from "path";
 
-import { config } from './vendure-config.ts';
-import { initialData } from './my-initial-data.ts';
+import { config } from './vendure-config';
+import { initialData } from './my-initial-data';
 
 const productsCsvFile = path.join(__dirname, 'path/to/products.csv')
 
@@ -198,7 +199,7 @@ const populateConfig = {
 }
 
 populate(
-    () => bootstrap(config),
+    () => bootstrap(populateConfig),
     initialData,
     productsCsvFile,
     'my-channel-token' // optional - used to assign imported 

+ 1 - 1
docs/docs/guides/extending-the-admin-ui/creating-detail-views/index.md

@@ -117,7 +117,7 @@ export class ReviewDetailComponent extends TypedBaseDetailComponent<typeof getRe
     update() {
         const { title, rating, authorName } = this.detailForm.value;
         this.dataService
-            .mutate(updateOrganizationDocument, {
+            .mutate(updateReviewDocument, {
                 input: { id: this.id, title, rating, authorName },
             })
             .subscribe(() => {

+ 1 - 1
docs/docs/guides/extending-the-admin-ui/custom-detail-components/index.md

@@ -23,7 +23,7 @@ Let's imagine that your project has an external content management system (CMS)
 
 ```ts title="src/plugins/cms/ui/components/product-info/product-info.component.ts"
 import { Component, OnInit } from '@angular/core';
-import { switchMap } from 'rxjs';
+import { Observable, switchMap } from 'rxjs';
 import { FormGroup } from '@angular/forms';
 import { DataService, CustomDetailComponent, SharedModule } from '@vendure/admin-ui/core';
 import { CmsDataService } from '../../providers/cms-data.service';

+ 1 - 1
docs/docs/guides/extending-the-admin-ui/custom-form-inputs/index.md

@@ -201,7 +201,7 @@ import { Component, OnInit } from '@angular/core';
 import { FormControl } from '@angular/forms';
 import { ActivatedRoute } from '@angular/router';
 import { RelationCustomFieldConfig } from '@vendure/common/lib/generated-types';
-import { CustomFieldControl, DataService, SharedModule } from '@vendure/admin-ui/core';
+import { FormInputComponent, DataService, SharedModule } from '@vendure/admin-ui/core';
 import { Observable } from 'rxjs';
 import { switchMap } from 'rxjs/operators';
 

+ 13 - 0
docs/docs/guides/extending-the-admin-ui/ui-library/index.md

@@ -223,6 +223,19 @@ export function DemoComponent() {
 </TabItem>
 </Tabs>
 
+The `form-grid` class is used to lay out the form fields into a 2-column grid on larger screens, and a single column on smaller screens.
+If you want to force a particular field to always take up the full width (i.e. to span 2 columns at all screen sizes), you can add the
+`form-grid-span` class to that form field.
+
+```html
+<div class="form-grid">
+    // highlight-next-line
+    <vdr-form-field label="Page title" class="form-grid-span">
+        <input type="text" />
+    </vdr-form-field>
+</div>
+```
+
 ## Cards
 
 Cards are used as a general-purpose container for page content, as a way to visually group related sets of components.

+ 1 - 1
e2e-common/vitest.config.ts

@@ -4,7 +4,7 @@ import { defineConfig } from 'vitest/config';
 
 export default defineConfig({
     test: {
-        include: '**/*.e2e-spec.ts',
+        include: ['**/*.e2e-spec.ts'],
         /**
          * For local debugging of the e2e tests, we set a very long timeout value otherwise tests will
          * automatically fail for going over the 5 second default timeout.

+ 1 - 1
lerna.json

@@ -1,6 +1,6 @@
 {
     "packages": ["packages/*"],
-    "version": "2.1.3",
+    "version": "2.1.4",
     "npmClient": "yarn",
     "command": {
         "version": {

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

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

+ 1 - 1
packages/admin-ui/package-lock.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/admin-ui",
-    "version": "2.1.3",
+    "version": "2.1.4",
     "lockfileVersion": 1,
     "requires": true,
     "dependencies": {

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/admin-ui",
-    "version": "2.1.3",
+    "version": "2.1.4",
     "license": "MIT",
     "scripts": {
         "ng": "ng",
@@ -49,7 +49,7 @@
         "@ng-select/ng-select": "^11.1.1",
         "@ngx-translate/core": "^15.0.0",
         "@ngx-translate/http-loader": "^8.0.0",
-        "@vendure/common": "^2.1.3",
+        "@vendure/common": "^2.1.4",
         "@webcomponents/custom-elements": "^1.6.0",
         "apollo-angular": "^5.0.0",
         "apollo-upload-client": "^17.0.0",

+ 12 - 5
packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts

@@ -1,4 +1,4 @@
-import { ConfigArgType, CustomFieldType } from '@vendure/common/lib/shared-types';
+import { ConfigArgType } from '@vendure/common/lib/shared-types';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 
 import {
@@ -15,7 +15,14 @@ import {
  */
 export function getConfigArgValue(value: any) {
     try {
-        return value != null ? JSON.parse(value) : undefined;
+        const result = value != null ? JSON.parse(value) : undefined;
+        if (result && typeof result === 'object' && !Array.isArray(result)) {
+            // There is an edge-case where the value is a valid JSON-encoded string and
+            // will get parsed as an object, but we actually want it to be a string.
+            return JSON.stringify(result);
+        } else {
+            return result;
+        }
     } catch (e: any) {
         return value;
     }
@@ -34,9 +41,9 @@ export function configurableDefinitionToInstance(
     return {
         ...def,
         args: def.args.map(arg => ({
-                ...arg,
-                value: getDefaultConfigArgValue(arg),
-            })),
+            ...arg,
+            value: getDefaultConfigArgValue(arg),
+        })),
     } as ConfigurableOperation;
 }
 

+ 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 = '2.1.3';
+export const ADMIN_UI_VERSION = '2.1.4';

+ 27 - 1
packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder-types.ts

@@ -62,7 +62,18 @@ export interface NavMenuSection {
     displayMode?: 'regular' | 'settings';
     /**
      * @description
-     * Control the display of this item based on the user permissions.
+     * Control the display of this item based on the user permissions. Note: if you attempt to pass a
+     * {@link PermissionDefinition} object, you will get a compilation error. Instead, pass the plain
+     * string version. For example, if the permission is defined as:
+     * ```ts
+     * export const MyPermission = new PermissionDefinition('ProductReview');
+     * ```
+     * then the generated permission strings will be:
+     *
+     * - `CreateProductReview`
+     * - `ReadProductReview`
+     * - `UpdateProductReview`
+     * - `DeleteProductReview`
      */
     requiresPermission?: string | ((userPermissions: string[]) => boolean);
     collapsible?: boolean;
@@ -116,6 +127,21 @@ export interface ActionBarItem {
     buttonColor?: 'primary' | 'success' | 'warning';
     buttonStyle?: 'solid' | 'outline' | 'link';
     icon?: string;
+    /**
+     * @description
+     * Control the display of this item based on the user permissions. Note: if you attempt to pass a
+     * {@link PermissionDefinition} object, you will get a compilation error. Instead, pass the plain
+     * string version. For example, if the permission is defined as:
+     * ```ts
+     * export const MyPermission = new PermissionDefinition('ProductReview');
+     * ```
+     * then the generated permission strings will be:
+     *
+     * - `CreateProductReview`
+     * - `ReadProductReview`
+     * - `UpdateProductReview`
+     * - `DeleteProductReview`
+     */
     requiresPermission?: string | string[];
 }
 

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/asset-picker-dialog/asset-picker-dialog.component.scss

@@ -2,7 +2,7 @@
 :host {
     display: flex;
     flex-direction: column;
-    height: 70vh;
+    //height: 70vh;
     overflow-y: auto;
 }
 

+ 0 - 4
packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.ts

@@ -21,11 +21,7 @@ import {
     Validator,
     Validators,
 } from '@angular/forms';
-import { ConfigArgType } from '@vendure/common/lib/shared-types';
-import { assertNever } from '@vendure/common/lib/shared-utils';
 import { BehaviorSubject, Observable, Subscription } from 'rxjs';
-
-import { InputComponentConfig } from '../../../common/component-registry-types';
 import {
     ConfigArg,
     ConfigArgDefinition,

+ 5 - 2
packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.service.ts

@@ -1,7 +1,7 @@
 import { Injectable } from '@angular/core';
 import dayjs from 'dayjs';
 import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
-import { map } from 'rxjs/operators';
+import { distinctUntilChanged, map } from 'rxjs/operators';
 
 import { dayOfWeekIndex } from './constants';
 import { CalendarView, DayCell, DayOfWeek } from './types';
@@ -19,7 +19,10 @@ export class DatetimePickerService {
     private jumping = false;
 
     constructor() {
-        this.selected$ = this.selectedDatetime$.pipe(map(value => value && value.toDate()));
+        this.selected$ = this.selectedDatetime$.pipe(
+            map(value => value && value.toDate()),
+            distinctUntilChanged((a, b) => a?.getTime() === b?.getTime()),
+        );
         this.viewing$ = this.viewingDatetime$.pipe(map(value => value.toDate()));
         this.weekStartDayIndex = dayOfWeekIndex['mon'];
         this.calendarView$ = combineLatest(this.viewingDatetime$, this.selectedDatetime$).pipe(

+ 23 - 0
packages/admin-ui/src/lib/core/src/shared/components/modal-dialog/modal-dialog.component.scss

@@ -5,6 +5,29 @@
     &.modal-valign-bottom .modal {
         justify-content: flex-end;
     }
+    .modal-dialog {
+        display: flex;
+    }
+    .modal-content-wrapper {
+        flex: 1;
+        display: flex;
+    }
+    .modal-dialog .modal-content {
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+    }
+    @media screen and (max-height: 700px) {
+        .modal-dialog .modal-content {
+            padding: 0.8rem;
+        }
+        .modal-header, .modal-header--accessible {
+            padding-bottom: 0.8rem;
+        }
+        .modal-footer {
+            padding-top: 0.8rem;
+        }
+    }
 }
 
 .modal-body {

+ 10 - 10
packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts

@@ -107,8 +107,8 @@ export default [
     id: 'my-button',
     label: 'My Action',
     locationId: '${locationId}',
-  });
-]`,
+  }),
+];`,
     navMenu: locationId => `
 import { addNavMenuSection } from '@vendure/admin-ui/core';
 
@@ -117,10 +117,10 @@ export default [
       id: 'my-menu-item',
       label: 'My Menu Item',
       routerLink: ['/extensions/my-plugin'],
-    }
-    '${locationId}'
-  );
-]`,
+    },
+    '${locationId}',
+  ),
+];`,
     detailComponent: locationId => `
 import { registerCustomDetailComponent } from '@vendure/admin-ui/core';
 
@@ -128,8 +128,8 @@ export default [
   registerCustomDetailComponent({
     locationId: '${locationId}',
     component: MyCustomComponent,
-  });
-]`,
+  }),
+];`,
     dataTable: (locationId, metadata) => `
 import { registerDataTableComponent } from '@vendure/admin-ui/core';
 
@@ -138,6 +138,6 @@ export default [
     tableId: '${locationId}',
     columnId: '${metadata}',
     component: MyCustomComponent,
-  });
-]`,
+  }),
+];`,
 };

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/code-editor-form-input/base-code-editor.scss

@@ -3,7 +3,7 @@
         min-height: 6rem;
         background-color: var(--color-json-editor-background-color);
         color: var(--color-json-editor-text);
-        border: 1px solid var(--color-component-border-200);
+        border: 1px solid var(--color-weight-200);
         border-radius: 3px;
         padding: 6px;
         tab-size: 4;

+ 6 - 1
packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.ts

@@ -266,7 +266,12 @@ export class PromotionDetailComponent
         });
         entity.actions.forEach(o => this.addOperation('actions', o));
         if (this.customFields.length) {
-            this.setCustomFieldFormValues(this.customFields, this.detailForm.get('customFields'), entity);
+            this.setCustomFieldFormValues(
+                this.customFields,
+                this.detailForm.get('customFields'),
+                entity,
+                currentTranslation,
+            );
         }
     }
 

+ 1 - 0
packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.ts

@@ -273,6 +273,7 @@ export class PaymentMethodDetailComponent
                 this.customFields,
                 this.detailForm.get('customFields'),
                 paymentMethod,
+                currentTranslation,
             );
         }
     }

+ 2 - 1
packages/admin-ui/src/lib/static/styles/theme/dark.scss

@@ -250,8 +250,9 @@
     /**********
     * Card
     */
+    --clr-card-header-title-color: var(--color-text-200);
     --clr-card-bg-color: hsl(198, 28%, 18%);
-    --clr-card-border-color: hsl(203, 30%, 8%);
+    --clr-card-border-color: hsl(203, 30%, 13%);
     --clr-card-title-color: hsl(210, 16%, 93%);
     --clr-card-box-shadow-color: var(--clr-card-border-color);
     --clr-card-box-shadow: 0 0.15rem 0 0 var(--clr-card-border-color);

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/asset-server-plugin",
-    "version": "2.1.3",
+    "version": "2.1.4",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
     "files": [
@@ -27,8 +27,8 @@
         "@types/fs-extra": "^11.0.1",
         "@types/node-fetch": "^2.5.8",
         "@types/sharp": "^0.30.4",
-        "@vendure/common": "^2.1.3",
-        "@vendure/core": "^2.1.3",
+        "@vendure/common": "^2.1.4",
+        "@vendure/core": "^2.1.4",
         "express": "^4.17.1",
         "node-fetch": "^2.6.7",
         "rimraf": "^3.0.2",

+ 2 - 2
packages/cli/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/cli",
-    "version": "2.1.3",
+    "version": "2.1.4",
     "description": "A modern, headless ecommerce framework",
     "repository": {
         "type": "git",
@@ -34,7 +34,7 @@
     ],
     "dependencies": {
         "@clack/prompts": "^0.7.0",
-        "@vendure/common": "^2.1.3",
+        "@vendure/common": "^2.1.4",
         "change-case": "^4.1.2",
         "commander": "^11.0.0",
         "fs-extra": "^11.1.1",

+ 1 - 1
packages/common/package.json

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

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 358 - 281
packages/core/e2e/default-search-plugin.e2e-spec.ts


+ 45 - 1
packages/core/e2e/entity-hydrator.e2e-spec.ts

@@ -8,6 +8,10 @@ import {
     ProductVariant,
     RequestContext,
     ActiveOrderService,
+    OrderService,
+    TransactionalConnection,
+    OrderLine,
+    RequestContextService,
 } from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import gql from 'graphql-tag';
@@ -43,7 +47,7 @@ describe('Entity hydration', () => {
         await server.init({
             initialData,
             productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
-            customerCount: 1,
+            customerCount: 2,
         });
         await adminClient.asSuperAdmin();
     }, TEST_SETUP_TIMEOUT_MS);
@@ -290,6 +294,46 @@ describe('Entity hydration', () => {
             expect(order!.lines[1].productVariant.priceWithTax).toBeGreaterThan(0);
         });
     });
+
+    // https://github.com/vendure-ecommerce/vendure/issues/2546
+    it('Preserves ordering when merging arrays of relations', async () => {
+        await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test');
+        await shopClient.query(AddItemToOrderDocument, {
+            productVariantId: '1',
+            quantity: 1,
+        });
+        const { addItemToOrder } = await shopClient.query(AddItemToOrderDocument, {
+            productVariantId: '2',
+            quantity: 2,
+        });
+        orderResultGuard.assertSuccess(addItemToOrder);
+        const internalOrderId = +addItemToOrder.id.replace(/^\D+/g, '');
+        const ctx = await server.app.get(RequestContextService).create({ apiType: 'admin' });
+        const order = await server.app
+            .get(OrderService)
+            .findOne(ctx, internalOrderId, ['lines.productVariant']);
+
+        for (const line of order?.lines ?? []) {
+            // Assert that things are as we expect before hydrating
+            expect(line.productVariantId).toBe(line.productVariant.id);
+        }
+
+        // modify the first order line to make postgres tend to return the lines in the wrong order
+        await server.app
+            .get(TransactionalConnection)
+            .getRepository(ctx, OrderLine)
+            .update(order!.lines[0].id, {
+                sellerChannelId: 1,
+            });
+
+        await server.app.get(EntityHydrator).hydrate(ctx, order!, {
+            relations: ['lines.sellerChannel'],
+        });
+
+        for (const line of order?.lines ?? []) {
+            expect(line.productVariantId).toBe(line.productVariant.id);
+        }
+    });
 });
 
 function getVariantWithName(product: Product, name: string) {

+ 17 - 11
packages/core/e2e/money-strategy.e2e-spec.ts

@@ -1,8 +1,8 @@
-import { DefaultMoneyStrategy, Logger, mergeConfig, MoneyStrategy } from '@vendure/core';
+import { DefaultMoneyStrategy, Logger, mergeConfig, MoneyStrategy, VendurePlugin } from '@vendure/core';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
 import path from 'path';
 import { ColumnOptions } from 'typeorm';
-import { afterAll, beforeAll, describe, expect, it } from 'vitest';
+import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
@@ -21,6 +21,7 @@ const orderGuard: ErrorResultGuard<CodegenShop.UpdatedOrderFragment> = createErr
 );
 
 class CustomMoneyStrategy implements MoneyStrategy {
+    static transformerFromSpy = vi.fn();
     readonly moneyColumnOptions: ColumnOptions = {
         type: 'bigint',
         transformer: {
@@ -28,6 +29,7 @@ class CustomMoneyStrategy implements MoneyStrategy {
                 return entityValue;
             },
             from: (databaseValue: string): number => {
+                CustomMoneyStrategy.transformerFromSpy(databaseValue);
                 if (databaseValue == null) {
                     return databaseValue;
                 }
@@ -48,12 +50,18 @@ class CustomMoneyStrategy implements MoneyStrategy {
     }
 }
 
+@VendurePlugin({
+    configuration: config => {
+        config.entityOptions.moneyStrategy = new CustomMoneyStrategy();
+        return config;
+    },
+})
+class MyPlugin {}
+
 describe('Custom MoneyStrategy', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(
         mergeConfig(testConfig(), {
-            entityOptions: {
-                moneyStrategy: new CustomMoneyStrategy(),
-            },
+            plugins: [MyPlugin],
         }),
     );
 
@@ -74,6 +82,8 @@ describe('Custom MoneyStrategy', () => {
     });
 
     it('check initial prices', async () => {
+        expect(CustomMoneyStrategy.transformerFromSpy).toHaveBeenCalledTimes(0);
+
         const { productVariants } = await adminClient.query<
             Codegen.GetProductVariantListQuery,
             Codegen.GetProductVariantListQueryVariables
@@ -91,6 +101,8 @@ describe('Custom MoneyStrategy', () => {
 
         cheapVariantId = productVariants.items[0].id;
         expensiveVariantId = productVariants.items[1].id;
+
+        expect(CustomMoneyStrategy.transformerFromSpy).toHaveBeenCalledTimes(6);
     });
 
     // https://github.com/vendure-ecommerce/vendure/issues/838
@@ -127,9 +139,3 @@ describe('Custom MoneyStrategy', () => {
         expect(addItemToOrder.lines[0].linePriceWithTax).toBe(372);
     });
 });
-
-class CustomRoundingStrategy extends DefaultMoneyStrategy {
-    round(value: number): number {
-        return value;
-    }
-}

+ 10 - 11
packages/core/e2e/utils/await-running-jobs.ts

@@ -1,7 +1,6 @@
 import { SimpleGraphQLClient } from '@vendure/testing';
-import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 
-import { GetRunningJobs, JobState } from '../graphql/generated-e2e-admin-types';
+import { GetRunningJobsQuery, GetRunningJobsQueryVariables } from '../graphql/generated-e2e-admin-types';
 import { GET_RUNNING_JOBS } from '../graphql/shared-definitions';
 
 /**
@@ -20,18 +19,18 @@ export async function awaitRunningJobs(
     // e.g. event debouncing is used before triggering the job.
     await new Promise(resolve => setTimeout(resolve, delay));
     do {
-        const { jobs } = await adminClient.query<
-            Codegen.GetRunningJobsQuery,
-            Codegen.GetRunningJobsQueryVariables
-        >(GET_RUNNING_JOBS, {
-            options: {
-                filter: {
-                    isSettled: {
-                        eq: false,
+        const { jobs } = await adminClient.query<GetRunningJobsQuery, GetRunningJobsQueryVariables>(
+            GET_RUNNING_JOBS,
+            {
+                options: {
+                    filter: {
+                        isSettled: {
+                            eq: false,
+                        },
                     },
                 },
             },
-        });
+        );
         runningJobs = jobs.totalItems;
         timedOut = timeout < +new Date() - startTime;
     } while (runningJobs > 0 && !timedOut);

+ 2 - 2
packages/core/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/core",
-    "version": "2.1.3",
+    "version": "2.1.4",
     "description": "A modern, headless ecommerce framework",
     "repository": {
         "type": "git",
@@ -50,7 +50,7 @@
         "@nestjs/testing": "10.2.1",
         "@nestjs/typeorm": "10.0.0",
         "@types/fs-extra": "^9.0.1",
-        "@vendure/common": "^2.1.3",
+        "@vendure/common": "^2.1.4",
         "bcrypt": "^5.1.1",
         "body-parser": "^1.20.2",
         "chalk": "^4.1.2",

+ 4 - 1
packages/core/src/bootstrap.ts

@@ -147,6 +147,10 @@ export async function preBootstrapConfig(
     });
 
     let config = getConfig();
+    // The logger is set here so that we are able to log any messages prior to the final
+    // logger (which may depend on config coming from a plugin) being set.
+    Logger.useLogger(config.logger);
+    config = await runPluginConfigurations(config);
     const entityIdStrategy = config.entityOptions.entityIdStrategy ?? config.entityIdStrategy;
     setEntityIdStrategy(entityIdStrategy, entities);
     const moneyStrategy = config.entityOptions.moneyStrategy;
@@ -156,7 +160,6 @@ export async function preBootstrapConfig(
         process.exitCode = 1;
         throw new Error('CustomFields config error:\n- ' + customFieldValidationResult.errors.join('\n- '));
     }
-    config = await runPluginConfigurations(config);
     registerCustomEntityFields(config);
     await runEntityMetadataModifiers(config);
     setExposedHeaders(config);

+ 1 - 1
packages/core/src/common/finite-state-machine/validate-transition-definition.spec.ts

@@ -74,7 +74,7 @@ describe('FSM validateTransitionDefinition()', () => {
 
         const result = validateTransitionDefinition(valid, 'Start');
 
-        expect(result.valid).toBe(false);
+        expect(result.valid).toBe(true);
         expect(result.error).toBe('The following states are unreachable: Unreachable');
     });
 

+ 11 - 13
packages/core/src/common/finite-state-machine/validate-transition-definition.ts

@@ -53,17 +53,15 @@ export function validateTransitionDefinition<T extends string>(
         };
     }
 
-    if (!allStatesReached()) {
-        return {
-            valid: false,
-            error: `The following states are unreachable: ${Object.entries(result)
-                .filter(([s, v]) => !(v as ValidationResult).reachable)
-                .map(([s]) => s)
-                .join(', ')}`,
-        };
-    } else {
-        return {
-            valid: true,
-        };
-    }
+    const error = !allStatesReached()
+        ? `The following states are unreachable: ${Object.entries(result)
+              .filter(([s, v]) => !(v as ValidationResult).reachable)
+              .map(([s]) => s)
+              .join(', ')}`
+        : undefined;
+
+    return {
+        valid: true,
+        error,
+    };
 }

+ 59 - 0
packages/core/src/entity/register-custom-entity-fields.ts

@@ -35,6 +35,7 @@ import {
     CustomOrderFields,
     CustomOrderLineFields,
     CustomPaymentMethodFields,
+    CustomPaymentMethodFieldsTranslation,
     CustomProductFields,
     CustomProductFieldsTranslation,
     CustomProductOptionFields,
@@ -44,6 +45,7 @@ import {
     CustomProductVariantFields,
     CustomProductVariantFieldsTranslation,
     CustomPromotionFields,
+    CustomPromotionFieldsTranslation,
     CustomRegionFields,
     CustomRegionFieldsTranslation,
     CustomSellerFields,
@@ -253,31 +255,70 @@ function getDefault(customField: CustomFieldConfig, dbEngine: DataSourceOptions[
     return type === 'datetime' ? formatDefaultDatetime(dbEngine, defaultValue) : defaultValue;
 }
 
+function assertLocaleFieldsNotSpecified(config: VendureConfig, entityName: keyof CustomFields) {
+    const customFields = config.customFields && config.customFields[entityName];
+    if (customFields) {
+        for (const customField of customFields) {
+            if (customField.type === 'localeString' || customField.type === 'localeText') {
+                Logger.error(
+                    `Custom field "${customField.name}" on entity "${entityName}" cannot be of type "localeString" or "localeText". ` +
+                        `This entity does not support localization.`,
+                );
+            }
+        }
+    }
+}
+
 /**
  * Dynamically registers any custom fields with TypeORM. This function should be run at the bootstrap
  * stage of the app lifecycle, before the AppModule is initialized.
  */
 export function registerCustomEntityFields(config: VendureConfig) {
     registerCustomFieldsForEntity(config, 'Address', CustomAddressFields);
+    assertLocaleFieldsNotSpecified(config, 'Address');
+
     registerCustomFieldsForEntity(config, 'Administrator', CustomAdministratorFields);
+    assertLocaleFieldsNotSpecified(config, 'Administrator');
+
     registerCustomFieldsForEntity(config, 'Asset', CustomAssetFields);
+    assertLocaleFieldsNotSpecified(config, 'Asset');
+
     registerCustomFieldsForEntity(config, 'Collection', CustomCollectionFields);
     registerCustomFieldsForEntity(config, 'Collection', CustomCollectionFieldsTranslation, true);
+
     registerCustomFieldsForEntity(config, 'Channel', CustomChannelFields);
+    assertLocaleFieldsNotSpecified(config, 'Channel');
+
     registerCustomFieldsForEntity(config, 'Customer', CustomCustomerFields);
+    assertLocaleFieldsNotSpecified(config, 'Customer');
+
     registerCustomFieldsForEntity(config, 'CustomerGroup', CustomCustomerGroupFields);
+    assertLocaleFieldsNotSpecified(config, 'CustomerGroup');
+
     registerCustomFieldsForEntity(config, 'Facet', CustomFacetFields);
     registerCustomFieldsForEntity(config, 'Facet', CustomFacetFieldsTranslation, true);
+
     registerCustomFieldsForEntity(config, 'FacetValue', CustomFacetValueFields);
     registerCustomFieldsForEntity(config, 'FacetValue', CustomFacetValueFieldsTranslation, true);
+
     registerCustomFieldsForEntity(config, 'Fulfillment', CustomFulfillmentFields);
+    assertLocaleFieldsNotSpecified(config, 'Fulfillment');
+
     registerCustomFieldsForEntity(config, 'Order', CustomOrderFields);
+    assertLocaleFieldsNotSpecified(config, 'Order');
+
     registerCustomFieldsForEntity(config, 'OrderLine', CustomOrderLineFields);
+    assertLocaleFieldsNotSpecified(config, 'OrderLine');
+
     registerCustomFieldsForEntity(config, 'PaymentMethod', CustomPaymentMethodFields);
+    registerCustomFieldsForEntity(config, 'PaymentMethod', CustomPaymentMethodFieldsTranslation, true);
+
     registerCustomFieldsForEntity(config, 'Product', CustomProductFields);
     registerCustomFieldsForEntity(config, 'Product', CustomProductFieldsTranslation, true);
+
     registerCustomFieldsForEntity(config, 'ProductOption', CustomProductOptionFields);
     registerCustomFieldsForEntity(config, 'ProductOption', CustomProductOptionFieldsTranslation, true);
+
     registerCustomFieldsForEntity(config, 'ProductOptionGroup', CustomProductOptionGroupFields);
     registerCustomFieldsForEntity(
         config,
@@ -285,18 +326,36 @@ export function registerCustomEntityFields(config: VendureConfig) {
         CustomProductOptionGroupFieldsTranslation,
         true,
     );
+
     registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFields);
     registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFieldsTranslation, true);
+
     registerCustomFieldsForEntity(config, 'Promotion', CustomPromotionFields);
+    registerCustomFieldsForEntity(config, 'Promotion', CustomPromotionFieldsTranslation, true);
+
     registerCustomFieldsForEntity(config, 'TaxCategory', CustomTaxCategoryFields);
+    assertLocaleFieldsNotSpecified(config, 'TaxCategory');
+
     registerCustomFieldsForEntity(config, 'TaxRate', CustomTaxRateFields);
+    assertLocaleFieldsNotSpecified(config, 'TaxRate');
+
     registerCustomFieldsForEntity(config, 'User', CustomUserFields);
+    assertLocaleFieldsNotSpecified(config, 'User');
     registerCustomFieldsForEntity(config, 'GlobalSettings', CustomGlobalSettingsFields);
+    assertLocaleFieldsNotSpecified(config, 'GlobalSettings');
+
     registerCustomFieldsForEntity(config, 'Region', CustomRegionFields);
     registerCustomFieldsForEntity(config, 'Region', CustomRegionFieldsTranslation, true);
+
     registerCustomFieldsForEntity(config, 'Seller', CustomSellerFields);
+    assertLocaleFieldsNotSpecified(config, 'Seller');
+
     registerCustomFieldsForEntity(config, 'ShippingMethod', CustomShippingMethodFields);
     registerCustomFieldsForEntity(config, 'ShippingMethod', CustomShippingMethodFieldsTranslation, true);
+
     registerCustomFieldsForEntity(config, 'StockLocation', CustomStockLocationFields);
+    assertLocaleFieldsNotSpecified(config, 'StockLocation');
+
     registerCustomFieldsForEntity(config, 'Zone', CustomZoneFields);
+    assertLocaleFieldsNotSpecified(config, 'Zone');
 }

+ 0 - 1
packages/core/src/event-bus/vendure-entity-event.ts

@@ -5,7 +5,6 @@ import { VendureEvent } from './vendure-event';
 /**
  * @description
  * The base class for all entity events used by the EventBus system.
- * * For event type `'updated'` the entity is the one before applying the patch (if not documented otherwise).
  * * For event type `'deleted'` the input will most likely be an `id: ID`
  *
  * @docsCategory events

+ 2 - 1
packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts

@@ -249,8 +249,9 @@ export class MysqlSearchStrategy implements SearchStrategy {
             qb.andWhere('FIND_IN_SET (:collectionSlug, si.collectionSlugs)', { collectionSlug });
         }
 
-        applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode);
         qb.andWhere('si.channelId = :channelId', { channelId: ctx.channelId });
+        applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode);
+
         if (input.groupByProduct === true) {
             qb.groupBy('si.productId');
             qb.addSelect('BIT_OR(si.enabled)', 'productEnabled');

+ 3 - 1
packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts

@@ -247,11 +247,13 @@ export class PostgresSearchStrategy implements SearchStrategy {
             });
         }
 
-        applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode);
         qb.andWhere('si.channelId = :channelId', { channelId: ctx.channelId });
+        applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode);
+
         if (input.groupByProduct === true) {
             qb.groupBy('si.productId');
         }
+
         return qb;
     }
 

+ 0 - 8
packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-common.ts

@@ -22,14 +22,6 @@ export const fieldsToSelect = [
     'productVariantPreviewFocalPoint',
 ];
 
-export const identifierFields = [
-    'channelId',
-    'productVariantId',
-    'productId',
-    'productAssetId',
-    'productVariantAssetId',
-];
-
 export function getFieldsToSelect(includeStockStatus: boolean = false) {
     return includeStockStatus ? [...fieldsToSelect, 'inStock', 'productInStock'] : fieldsToSelect;
 }

+ 15 - 14
packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts

@@ -9,12 +9,10 @@ import {
 } from '@vendure/common/lib/generated-types';
 import { ID } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
-import { QueryBuilder, SelectQueryBuilder } from 'typeorm';
+import { Brackets, QueryBuilder, SelectQueryBuilder } from 'typeorm';
 
 import { SearchIndexItem } from '../entities/search-index-item.entity';
 
-import { identifierFields } from './search-strategy-common';
-
 /**
  * Maps a raw database result to a SearchResult.
  */
@@ -131,31 +129,34 @@ export function applyLanguageConstraints(
     defaultLanguageCode: LanguageCode,
 ) {
     const lcEscaped = qb.escape('languageCode');
+    const ciEscaped = qb.escape('channelId');
+    const pviEscaped = qb.escape('productVariantId');
+
     if (languageCode === defaultLanguageCode) {
-        qb.andWhere(`si.${lcEscaped} = :languageCode`, { languageCode });
+        qb.andWhere(`si.${lcEscaped} = :languageCode`, {
+            languageCode,
+        });
     } else {
         qb.andWhere(`si.${lcEscaped} IN (:...languageCodes)`, {
             languageCodes: [languageCode, defaultLanguageCode],
         });
 
-        const joinFieldConditions = identifierFields
-            .map(field => `si.${qb.escape(field)} = sil.${qb.escape(field)}`)
-            .join(' AND ');
-
         qb.leftJoin(
             SearchIndexItem,
             'sil',
-            `
-            ${joinFieldConditions}
-            AND si.${lcEscaped} != sil.${lcEscaped}
-            AND sil.${lcEscaped} = :languageCode
-        `,
+            `sil.${lcEscaped} = :languageCode AND sil.${ciEscaped} = si.${ciEscaped} AND sil.${pviEscaped} = si.${pviEscaped}`,
             {
                 languageCode,
             },
         );
 
-        qb.andWhere(`sil.${lcEscaped} IS NULL`);
+        qb.andWhere(
+            new Brackets(qb1 => {
+                qb1.where(`si.${lcEscaped} = :languageCode1`, {
+                    languageCode1: languageCode,
+                }).orWhere(`sil.${lcEscaped} IS NULL`);
+            }),
+        );
     }
 
     return qb;

+ 3 - 2
packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts

@@ -99,7 +99,8 @@ export class SqliteSearchStrategy implements SearchStrategy {
         }
         if (sort) {
             if (sort.name) {
-                qb.addOrderBy('si.productName', sort.name);
+                // TODO: v3 - set the collation on the SearchIndexItem entity
+                qb.addOrderBy('si.productName COLLATE NOCASE', sort.name);
             }
             if (sort.price) {
                 qb.addOrderBy('si.price', sort.price);
@@ -230,8 +231,8 @@ export class SqliteSearchStrategy implements SearchStrategy {
             });
         }
 
-        applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode);
         qb.andWhere('si.channelId = :channelId', { channelId: ctx.channelId });
+        applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode);
 
         if (input.groupByProduct === true) {
             qb.groupBy('si.productId');

+ 21 - 0
packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts

@@ -302,6 +302,27 @@ export class EntityHydrator {
         if (!a) {
             return b;
         }
+        if (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.length > 1) {
+            if (a[0].hasOwnProperty('id')) {
+                // If the array contains entities, we can use the id to match them up
+                // so that we ensure that we don't merge properties from different entities
+                // with the same index.
+                const aIds = a.map(e => e.id);
+                const bIds = b.map(e => e.id);
+                if (JSON.stringify(aIds) !== JSON.stringify(bIds)) {
+                    // The entities in the arrays are not in the same order, so we can't
+                    // safely merge them. We need to sort the `b` array so that the entities
+                    // are in the same order as the `a` array.
+                    const idToIndexMap = new Map();
+                    a.forEach((item, index) => {
+                        idToIndexMap.set(item.id, index);
+                    });
+                    b.sort((_a, _b) => {
+                        return idToIndexMap.get(_a.id) - idToIndexMap.get(_b.id);
+                    });
+                }
+            }
+        }
         for (const [key, value] of Object.entries(b)) {
             if (Object.getOwnPropertyDescriptor(b, key)?.writable) {
                 if (Array.isArray(value)) {

+ 3 - 0
packages/core/src/service/helpers/fulfillment-state-machine/fulfillment-state-machine.ts

@@ -63,6 +63,9 @@ export class FulfillmentStateMachine {
             Logger.error(`The fulfillment process has an invalid configuration:`);
             throw new Error(validationResult.error);
         }
+        if (validationResult.valid && validationResult.error) {
+            Logger.warn(`Fulfillment process: ${validationResult.error}`);
+        }
         return {
             transitions: allTransitions,
             onTransitionStart: async (fromState, toState, data) => {

+ 3 - 0
packages/core/src/service/helpers/order-state-machine/order-state-machine.ts

@@ -56,6 +56,9 @@ export class OrderStateMachine {
             Logger.error(`The order process has an invalid configuration:`);
             throw new Error(validationResult.error);
         }
+        if (validationResult.valid && validationResult.error) {
+            Logger.warn(`Order process: ${validationResult.error}`);
+        }
         return {
             transitions: allTransitions,
             onTransitionStart: async (fromState, toState, data) => {

+ 3 - 0
packages/core/src/service/helpers/payment-state-machine/payment-state-machine.ts

@@ -58,6 +58,9 @@ export class PaymentStateMachine {
             Logger.error(`The payment process has an invalid configuration:`);
             throw new Error(validationResult.error);
         }
+        if (validationResult.valid && validationResult.error) {
+            Logger.warn(`Payment process: ${validationResult.error}`);
+        }
         return {
             transitions: allTransitions,
             onTransitionStart: async (fromState, toState, data) => {

+ 1 - 0
packages/core/src/service/services/order.service.ts

@@ -876,6 +876,7 @@ export class OrderService {
             let shippingLine: ShippingLine | undefined = order.shippingLines[i];
             if (shippingLine) {
                 shippingLine.shippingMethod = shippingMethod;
+                shippingLine.shippingMethodId = shippingMethod.id;
             } else {
                 shippingLine = await this.connection.getRepository(ctx, ShippingLine).save(
                     new ShippingLine({

+ 3 - 3
packages/create/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/create",
-    "version": "2.1.3",
+    "version": "2.1.4",
     "license": "MIT",
     "bin": {
         "create": "./index.js"
@@ -28,14 +28,14 @@
         "@types/fs-extra": "^9.0.1",
         "@types/handlebars": "^4.1.0",
         "@types/semver": "^6.2.2",
-        "@vendure/core": "^2.1.3",
+        "@vendure/core": "^2.1.4",
         "rimraf": "^3.0.2",
         "ts-node": "^10.9.1",
         "typescript": "4.9.5"
     },
     "dependencies": {
         "@clack/prompts": "^0.7.0",
-        "@vendure/common": "^2.1.3",
+        "@vendure/common": "^2.1.4",
         "commander": "^11.0.0",
         "cross-spawn": "^7.0.3",
         "detect-port": "^1.5.1",

+ 1 - 1
packages/dev-server/load-testing/graphql/shop/complete-order.graphql

@@ -1,4 +1,4 @@
-mutation SetShippingMethod($id: ID!) {
+mutation SetShippingMethod($id: [ID!]!) {
     setOrderShippingMethod(shippingMethodId: $id) {
         ...on Order {
             code

+ 15 - 1
packages/dev-server/load-testing/init-load-test.ts

@@ -2,7 +2,7 @@
 /// <reference path="../../core/typings.d.ts" />
 import { bootstrap, JobQueueService, Logger } from '@vendure/core';
 import { populate } from '@vendure/core/cli/populate';
-import { clearAllTables, populateCustomers } from '@vendure/testing';
+import { clearAllTables, populateCustomers, SimpleGraphQLClient } from '@vendure/testing';
 import stringify from 'csv-stringify';
 import fs from 'fs';
 import path from 'path';
@@ -17,6 +17,8 @@ import {
     getProductCsvFilePath,
 } from './load-test-config';
 
+import { awaitRunningJobs } from '../../core/e2e/utils/await-running-jobs';
+
 /* eslint-disable no-console */
 
 /**
@@ -49,6 +51,18 @@ if (require.main === module) {
                             csvFile,
                         ),
                     )
+                    .then(async app => {
+                        console.log('synchronize on search index updated...');
+                        const { port, adminApiPath, shopApiPath } = config.apiOptions;
+                        const adminClient = new SimpleGraphQLClient(
+                            config,
+                            `http://localhost:${port}/${adminApiPath!}`,
+                        );
+                        await adminClient.asSuperAdmin();
+                        await new Promise(resolve => setTimeout(resolve, 5000));
+                        await awaitRunningJobs(adminClient, 5000000);
+                        return app;
+                    })
                     .then(async app => {
                         console.log('populating customers...');
                         await populateCustomers(app, 10, message => Logger.error(message));

+ 1 - 1
packages/dev-server/load-testing/load-test-config.ts

@@ -72,7 +72,7 @@ export function getLoadTestConfig(
                 assetUploadDir: path.join(__dirname, 'static/assets'),
                 route: 'assets',
             }),
-            DefaultSearchPlugin,
+            DefaultSearchPlugin.init({ bufferUpdates: false, indexStockStatus: false }),
             DefaultJobQueuePlugin.init({
                 pollInterval: 1000,
             }),

+ 2 - 2
packages/dev-server/load-testing/run-load-test.ts

@@ -26,7 +26,7 @@ if (require.main === module) {
         stdio: 'inherit',
     });
 
-    init.on('exit', code => {
+    init.on('exit', async code => {
         if (code === 0) {
             const databaseName = `vendure-load-testing-${count}`;
             return bootstrap(getLoadTestConfig('cookie', databaseName))
@@ -49,7 +49,7 @@ if (require.main === module) {
     });
 }
 
-function runLoadTestScript(script: string): Promise<LoadTestSummary> {
+async function runLoadTestScript(script: string): Promise<LoadTestSummary> {
     const rawResultsFile = `${script}.${count}.json`;
 
     return new Promise((resolve, reject) => {

+ 6 - 4
packages/dev-server/load-testing/scripts/search-and-checkout.js

@@ -26,10 +26,12 @@ export default function () {
         addToCart(randomItem(product.variants).id);
     }
     setShippingAddressAndCustomer();
-    const data = getShippingMethodsQuery.post().data;
-    const result = completeOrderMutation.post({ id: data.eligibleShippingMethods[0].id }).data;
-    check(result, {
-        'Order completed': r => r.addPaymentToOrder.state === 'PaymentAuthorized',
+    const { data: shippingMethods } = getShippingMethodsQuery.post();
+    const { data: order } = completeOrderMutation.post({
+        id: [shippingMethods.eligibleShippingMethods.at(0).id],
+    });
+    check(order, {
+        'Order completed': o => o.addPaymentToOrder.state === 'PaymentAuthorized',
     });
 }
 

+ 7 - 4
packages/dev-server/load-testing/utils/api-request.js

@@ -17,13 +17,16 @@ export class ApiRequest {
     post(variables = {}, authToken) {
         const res = http.post(
             this.apiUrl,
-            {
+            JSON.stringify({
                 query: this.document,
-                variables: JSON.stringify(variables),
-            },
+                variables,
+            }),
             {
                 timeout: 120 * 1000,
-                headers: { Authorization: authToken ? `Bearer ${authToken}` : undefined },
+                headers: {
+                    Authorization: authToken ? `Bearer ${authToken}` : '',
+                    'Content-Type': 'application/json',
+                },
             },
         );
         check(res, {

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

@@ -1,6 +1,6 @@
 {
     "name": "dev-server",
-    "version": "2.1.3",
+    "version": "2.1.4",
     "main": "index.js",
     "license": "MIT",
     "private": true,
@@ -15,18 +15,18 @@
     },
     "dependencies": {
         "@nestjs/axios": "^3.0.0",
-        "@vendure/admin-ui-plugin": "^2.1.3",
-        "@vendure/asset-server-plugin": "^2.1.3",
-        "@vendure/common": "^2.1.3",
-        "@vendure/core": "^2.1.3",
-        "@vendure/elasticsearch-plugin": "^2.1.3",
-        "@vendure/email-plugin": "^2.1.3",
+        "@vendure/admin-ui-plugin": "^2.1.4",
+        "@vendure/asset-server-plugin": "^2.1.4",
+        "@vendure/common": "^2.1.4",
+        "@vendure/core": "^2.1.4",
+        "@vendure/elasticsearch-plugin": "^2.1.4",
+        "@vendure/email-plugin": "^2.1.4",
         "typescript": "4.9.5"
     },
     "devDependencies": {
         "@types/csv-stringify": "^3.1.0",
-        "@vendure/testing": "^2.1.3",
-        "@vendure/ui-devkit": "^2.1.3",
+        "@vendure/testing": "^2.1.4",
+        "@vendure/ui-devkit": "^2.1.4",
         "commander": "^7.1.0",
         "concurrently": "^8.2.1",
         "csv-stringify": "^5.3.3",

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

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

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

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

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

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/harden-plugin",
-    "version": "2.1.3",
+    "version": "2.1.4",
     "license": "MIT",
     "main": "lib/index.js",
     "types": "lib/index.d.ts",
@@ -21,7 +21,7 @@
         "graphql-query-complexity": "^0.12.0"
     },
     "devDependencies": {
-        "@vendure/common": "^2.1.3",
-        "@vendure/core": "^2.1.3"
+        "@vendure/common": "^2.1.4",
+        "@vendure/core": "^2.1.4"
     }
 }

+ 3 - 3
packages/job-queue-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/job-queue-plugin",
-    "version": "2.1.3",
+    "version": "2.1.4",
     "license": "MIT",
     "main": "package/index.js",
     "types": "package/index.d.ts",
@@ -23,8 +23,8 @@
     },
     "devDependencies": {
         "@google-cloud/pubsub": "^2.8.0",
-        "@vendure/common": "^2.1.3",
-        "@vendure/core": "^2.1.3",
+        "@vendure/common": "^2.1.4",
+        "@vendure/core": "^2.1.4",
         "bullmq": "^3.15.5",
         "ioredis": "^5.3.0",
         "rimraf": "^3.0.2",

+ 4 - 4
packages/payments-plugin/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/payments-plugin",
-    "version": "2.1.3",
+    "version": "2.1.4",
     "license": "MIT",
     "main": "package/index.js",
     "types": "package/index.d.ts",
@@ -46,9 +46,9 @@
         "@mollie/api-client": "^3.7.0",
         "@types/braintree": "^2.22.15",
         "@types/localtunnel": "2.0.1",
-        "@vendure/common": "^2.1.3",
-        "@vendure/core": "^2.1.3",
-        "@vendure/testing": "^2.1.3",
+        "@vendure/common": "^2.1.4",
+        "@vendure/core": "^2.1.4",
+        "@vendure/testing": "^2.1.4",
         "braintree": "^3.16.0",
         "localtunnel": "2.0.2",
         "nock": "^13.1.4",

+ 3 - 3
packages/testing/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@vendure/testing",
-    "version": "2.1.3",
+    "version": "2.1.4",
     "description": "End-to-end testing tools for Vendure projects",
     "keywords": [
         "vendure",
@@ -38,7 +38,7 @@
     "dependencies": {
         "@graphql-typed-document-node/core": "^3.2.0",
         "@types/node-fetch": "^2.6.4",
-        "@vendure/common": "^2.1.3",
+        "@vendure/common": "^2.1.4",
         "faker": "^4.1.0",
         "form-data": "^4.0.0",
         "graphql": "16.8.0",
@@ -49,7 +49,7 @@
     "devDependencies": {
         "@types/mysql": "^2.15.15",
         "@types/pg": "^7.14.5",
-        "@vendure/core": "^2.1.3",
+        "@vendure/core": "^2.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": "2.1.3",
+    "version": "2.1.4",
     "description": "A library for authoring Vendure Admin UI extensions",
     "keywords": [
         "vendure",
@@ -40,8 +40,8 @@
         "@angular/cli": "^16.2.0",
         "@angular/compiler": "^16.2.2",
         "@angular/compiler-cli": "^16.2.2",
-        "@vendure/admin-ui": "^2.1.3",
-        "@vendure/common": "^2.1.3",
+        "@vendure/admin-ui": "^2.1.4",
+        "@vendure/common": "^2.1.4",
         "chalk": "^4.1.0",
         "chokidar": "^3.5.3",
         "fs-extra": "^11.1.1",
@@ -51,7 +51,7 @@
     "devDependencies": {
         "@rollup/plugin-node-resolve": "^15.2.1",
         "@types/fs-extra": "^11.0.1",
-        "@vendure/core": "^2.1.3",
+        "@vendure/core": "^2.1.4",
         "react": "^18.2.0",
         "react-dom": "^18.2.0",
         "rimraf": "^3.0.2",

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio