1
0
Эх сурвалжийг харах

Merge branch 'minor' into major

Michael Bromley 3 жил өмнө
parent
commit
3c406444bf
69 өөрчлөгдсөн 883 нэмэгдсэн , 197 устгасан
  1. 18 0
      docs/content/developer-guide/customizing-the-order-process/index.md
  2. 12 0
      docs/content/developer-guide/payment-integrations/index.md
  3. 35 3
      docs/content/plugins/extending-the-admin-ui/admin-ui-theming-branding/_index.md
  4. 40 0
      docs/content/plugins/plugin-examples/extending-graphql-api.md
  5. 2 2
      packages/admin-ui/README.md
  6. 2 2
      packages/admin-ui/scripts/compile-styles.js
  7. 1 1
      packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.scss
  8. 0 1
      packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.scss
  9. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/data-table/data-table.component.html
  10. 64 46
      packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.html
  11. 5 2
      packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.scss
  12. 52 10
      packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.ts
  13. 15 9
      packages/admin-ui/src/lib/order/src/components/order-custom-fields-card/order-custom-fields-card.component.html
  14. 25 2
      packages/admin-ui/src/lib/order/src/components/order-custom-fields-card/order-custom-fields-card.component.ts
  15. 2 10
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html
  16. 20 17
      packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.ts
  17. 4 2
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  18. 4 2
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  19. 4 2
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  20. 4 2
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  21. 4 2
      packages/admin-ui/src/lib/static/i18n-messages/fr.json
  22. 4 2
      packages/admin-ui/src/lib/static/i18n-messages/it.json
  23. 4 2
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  24. 4 2
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  25. 4 2
      packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json
  26. 4 2
      packages/admin-ui/src/lib/static/i18n-messages/ru.json
  27. 4 2
      packages/admin-ui/src/lib/static/i18n-messages/uk.json
  28. 4 2
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  29. 4 2
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  30. 3 0
      packages/admin-ui/src/lib/static/styles/_variables.scss
  31. 3 0
      packages/admin-ui/src/lib/static/styles/global/_sass-overrides.scss
  32. 1 0
      packages/admin-ui/src/lib/static/styles/styles.scss
  33. 1 0
      packages/admin-ui/src/lib/static/styles/ui-extension-theme.scss
  34. BIN
      packages/core/e2e/fixtures/assets/guitar.jpg
  35. 2 2
      packages/core/e2e/fixtures/e2e-product-import-asset-urls.csv
  36. 2 2
      packages/core/e2e/fixtures/test-payment-methods.ts
  37. 16 1
      packages/core/e2e/fixtures/test-plugins/with-api-extensions.ts
  38. 2 0
      packages/core/e2e/fixtures/test-plugins/with-config.ts
  39. 1 1
      packages/core/e2e/import.e2e-spec.ts
  40. 1 1
      packages/core/e2e/list-query-builder.e2e-spec.ts
  41. 17 0
      packages/core/e2e/order.e2e-spec.ts
  42. 38 0
      packages/core/e2e/plugin.e2e-spec.ts
  43. 152 15
      packages/core/e2e/product.e2e-spec.ts
  44. 15 2
      packages/core/src/api/config/generate-resolvers.ts
  45. 2 0
      packages/core/src/api/schema/admin-api/order.api.graphql
  46. 13 3
      packages/core/src/bootstrap.ts
  47. 3 1
      packages/core/src/config/fulfillment/custom-fulfillment-process.ts
  48. 1 0
      packages/core/src/config/logger/vendure-logger.ts
  49. 7 2
      packages/core/src/config/order/custom-order-process.ts
  50. 3 1
      packages/core/src/config/payment/custom-payment-process.ts
  51. 6 0
      packages/core/src/entity/order-line/order-line.entity.ts
  52. 1 1
      packages/core/src/plugin/plugin-metadata.ts
  53. 11 2
      packages/core/src/plugin/vendure-plugin.ts
  54. 15 1
      packages/core/src/service/helpers/fulfillment-state-machine/fulfillment-state.ts
  55. 11 2
      packages/core/src/service/helpers/list-query-builder/connection-utils.ts
  56. 25 9
      packages/core/src/service/helpers/list-query-builder/list-query-builder.ts
  57. 18 0
      packages/core/src/service/helpers/order-calculator/order-calculator.ts
  58. 10 0
      packages/core/src/service/helpers/order-modifier/order-modifier.ts
  59. 10 1
      packages/core/src/service/helpers/order-state-machine/order-state.ts
  60. 16 1
      packages/core/src/service/helpers/payment-state-machine/payment-state.ts
  61. 26 2
      packages/core/src/service/helpers/product-price-applicator/product-price-applicator.ts
  62. 17 1
      packages/core/src/service/helpers/slug-validator/slug-validator.ts
  63. 28 1
      packages/core/src/service/helpers/translatable-saver/translatable-saver.ts
  64. 2 0
      packages/core/src/service/services/order.service.ts
  65. 11 8
      packages/core/src/service/services/product.service.ts
  66. 1 1
      packages/dev-server/dev-config.ts
  67. 23 3
      packages/ui-devkit/src/compiler/scaffold.ts
  68. 18 1
      packages/ui-devkit/src/compiler/types.ts
  69. 5 0
      packages/ui-devkit/src/compiler/utils.ts

+ 18 - 0
docs/content/developer-guide/customizing-the-order-process/index.md

@@ -49,6 +49,7 @@ export const customerValidationProcess: CustomOrderProcess<'ValidatingCustomer'>
   },
   },
 };
 };
 ```
 ```
+
 This object means:
 This object means:
 
 
 * the `AddingItems` state may _only_ transition to the `ValidatingCustomer` state (`mergeStrategy: 'replace'` tells Vendure to discard any existing transition targets and replace with this one). 
 * the `AddingItems` state may _only_ transition to the `ValidatingCustomer` state (`mergeStrategy: 'replace'` tells Vendure to discard any existing transition targets and replace with this one). 
@@ -122,6 +123,23 @@ const customerValidationProcess: CustomOrderProcess<'ValidatingCustomer'> = {
 
 
 ```
 ```
 
 
+## TypeScript Typings
+
+To make your custom states compatible with standard services you should declare your new states in the following way:
+
+```TypeScript
+// types.ts
+import { CustomOrderStates } from '@vendure/core';
+
+declare module '@vendure/core' {
+  interface CustomOrderStates {
+    ValidatingCustomer: never;
+  }
+}
+```
+
+This technique uses advanced TypeScript features - [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces) and  [ambient modules](https://www.typescriptlang.org/docs/handbook/modules.html#ambient-modules).
+
 ## Controlling custom states in the Admin UI
 ## Controlling custom states in the Admin UI
 
 
 If you have defined custom order states, the Admin UI will allow you to manually transition an 
 If you have defined custom order states, the Admin UI will allow you to manually transition an 

+ 12 - 0
docs/content/developer-guide/payment-integrations/index.md

@@ -151,6 +151,18 @@ If you need to support an entirely different payment flow than the above, it is
 Here's an example which adds a new "Validating" state to the Payment state machine, and combines it with a [CustomOrderProcess]({{< relref "custom-order-process" >}}), [PaymentMethodHandler]({{< relref "payment-method-handler" >}}) and [OrderPlacedStrategy]({{< relref "order-placed-strategy" >}}).
 Here's an example which adds a new "Validating" state to the Payment state machine, and combines it with a [CustomOrderProcess]({{< relref "custom-order-process" >}}), [PaymentMethodHandler]({{< relref "payment-method-handler" >}}) and [OrderPlacedStrategy]({{< relref "order-placed-strategy" >}}).
 
 
 ```TypeScript
 ```TypeScript
+// types.ts
+import { CustomOrderStates } from '@vendure/core';
+
+/**
+ * Declare your custom state in special interface to make it type-safe
+ */
+declare module '@vendure/core' {
+  interface CustomPaymentStates {
+    Validating: never;
+  }
+}
+
 /**
 /**
  * Define a new "Validating" Payment state, and set up the
  * Define a new "Validating" Payment state, and set up the
  * permitted transitions to/from it.
  * permitted transitions to/from it.

+ 35 - 3
docs/content/plugins/extending-the-admin-ui/admin-ui-theming-branding/_index.md

@@ -65,7 +65,7 @@ You can replace the Vendure logos and favicon with your own brand logo:
 
 
 ## Theming
 ## Theming
 
 
-Much of the visual styling of the Admin UI can be customized by providing your own themes in a Sass stylesheet. The Admin UI uses [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) to control colors and other styles. Here's a simple example which changes the color of links:
+Much of the visual styling of the Admin UI can be customized by providing your own themes in a Sass stylesheet. For the most part, the Admin UI uses [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) to control colors and other styles. Here's a simple example which changes the color of links:
 
 
 1. Install `@vendure/ui-devkit`
 1. Install `@vendure/ui-devkit`
 2. Create a custom stylesheet which overrides one or more of the CSS custom properties used in the Admin UI:
 2. Create a custom stylesheet which overrides one or more of the CSS custom properties used in the Admin UI:
@@ -78,13 +78,13 @@ Much of the visual styling of the Admin UI can be customized by providing your o
       --clr-link-visited-color: hsl(110, 55%, 75%);
       --clr-link-visited-color: hsl(110, 55%, 75%);
     }
     }
     ```
     ```
-   To get an idea of which custom properties are avaiable for theming, take a look at the source of the [Default theme](https://github.com/vendure-ecommerce/vendure/tree/master/packages/admin-ui/src/lib/static/styles/theme/default.scss) and the [Dark theme](https://github.com/vendure-ecommerce/vendure/tree/master/packages/admin-ui/src/lib/static/styles/theme/dark.scss)
+   To get an idea of which custom properties are available for theming, take a look at the source of the [Default theme](https://github.com/vendure-ecommerce/vendure/tree/master/packages/admin-ui/src/lib/static/styles/theme/default.scss) and the [Dark theme](https://github.com/vendure-ecommerce/vendure/tree/master/packages/admin-ui/src/lib/static/styles/theme/dark.scss)
 3. Set this as a globalStyles extension:   
 3. Set this as a globalStyles extension:   
     ```TypeScript
     ```TypeScript
     import path from 'path';
     import path from 'path';
     import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
     import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
     import { VendureConfig } from '@vendure/core';
     import { VendureConfig } from '@vendure/core';
-    import { compileUiExtensions, setBranding } from '@vendure/ui-devkit/compiler';
+    import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
     
     
     export const config: VendureConfig = {
     export const config: VendureConfig = {
       // ...
       // ...
@@ -100,3 +100,35 @@ Much of the visual styling of the Admin UI can be customized by providing your o
       ],
       ],
     }
     }
     ```
     ```
+
+Some customizable styles in [Clarity](https://clarity.design/), Admin UI's Design System framework, are controlled by Sass variables, which can be found on the [project's GitHub page](https://github.com/vmware-clarity/ng-clarity/blob/689a572344149aea90df1676eae04479795754f3/projects/angular/src/utils/_variables.clarity.scss). Similar to above, you can also provide your own values, which will override defaults set by the framework. Here's an example which changes the [height of the main header](https://github.com/vmware-clarity/ng-clarity/blob/689a572344149aea90df1676eae04479795754f3/projects/angular/src/layout/main-container/_variables.header.scss#L10):
+
+1. Install `@vendure/ui-devkit` if not already installed
+2. Create a custom stylesheet which overrides the target variable(s):
+    ```css
+    /* my-variables.scss */
+    $clr-header-height: 4rem;
+    ```
+3. Set this as a sassVariableOverrides extension:
+    ```TypeScript
+    import path from 'path';
+    import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
+    import { VendureConfig } from '@vendure/core';
+    import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
+
+    export const config: VendureConfig = {
+      // ...
+      plugins: [
+        AdminUiPlugin.init({
+          app: compileUiExtensions({
+            outputPath: path.join(__dirname, 'admin-ui'),
+            extensions: [{
+              sassVariableOverrides: path.join(__dirname, 'my-variables.scss')
+            }],
+          }),
+        }),
+      ],
+    }
+    ```
+
+globalStyles and sassVariableOverrides extensions can be used in conjunction or separately.

+ 40 - 0
docs/content/plugins/plugin-examples/extending-graphql-api.md

@@ -224,3 +224,43 @@ This resolver is then passed in to your plugin metadata like any other resolver:
 })
 })
 export class MyPlugin {}
 export class MyPlugin {}
 ```
 ```
+
+## Defining custom scalars
+
+By default, Vendure bundles `DateTime` and a `JSON` custom scalars (from the [graphql-scalars library](https://github.com/Urigo/graphql-scalars)). From v1.7.0, you can also define your own custom scalars for use in your schema extensions:
+
+```TypeScript
+import { GraphQLScalarType} from 'graphql';
+import { GraphQLEmailAddress } from 'graphql-scalars';
+
+// Scalars can be custom-built as like this one,
+// or imported from a pre-made scalar library like
+// the GraphQLEmailAddress example.
+const FooScalar = new GraphQLScalarType({
+  name: 'Foo',
+  description: 'A test scalar',
+  serialize(value) {
+    // ...
+  },
+  parseValue(value) {
+    // ...
+  },
+});
+
+@VendurePlugin({
+  imports: [PluginCommonModule],
+  shopApiExtensions: {
+    schema: gql`
+      scalar Foo
+      scalar EmailAddress
+    `,
+    scalars: { 
+      // The key must match the scalar name
+      // given in the schema  
+      Foo: FooScalar,
+      EmailAddress: GraphQLEmailAddress,
+    },
+  },
+})
+export class CustomScalarsPlugin {}
+```

+ 2 - 2
packages/admin-ui/README.md

@@ -4,13 +4,13 @@ This is the administration interface for Vendure.
 
 
 It is an Angular application built with the Angular CLI.
 It is an Angular application built with the Angular CLI.
 
 
-The UI is powered by the [Clarity Design System](https://vmware.github.io/clarity/).
+The UI is powered by the [Clarity Design System](https://clarity.design).
 
 
 ## Structure
 ## Structure
 
 
 ### Library
 ### Library
 
 
-The Admin UI is structured as an Angular library conforming to the [ng-packagr format](https://github.com/ng-packagr/ng-packagr). This library is what is published to npm as `@vendure/admin-ui`. The libary consists
+The Admin UI is structured as an Angular library conforming to the [ng-packagr format](https://github.com/ng-packagr/ng-packagr). This library is what is published to npm as `@vendure/admin-ui`. The library consists
 of a set of modules which are accessible from consuming applications as sub-packages:
 of a set of modules which are accessible from consuming applications as sub-packages:
 
 
 * `@vendure/admin-ui/core`
 * `@vendure/admin-ui/core`

+ 2 - 2
packages/admin-ui/scripts/compile-styles.js

@@ -15,7 +15,7 @@ const result = sass.renderSync({
 
 
 fs.writeFileSync(outFile, result.css, 'utf8');
 fs.writeFileSync(outFile, result.css, 'utf8');
 
 
-function importer(url, prev, done) {
+function importer(url, prev) {
     let file = url;
     let file = url;
     // Handle the imports prefixed with ~
     // Handle the imports prefixed with ~
     // which are usually resolved by Webpack.
     // which are usually resolved by Webpack.
@@ -29,7 +29,7 @@ function importer(url, prev, done) {
     // library styles which are not needed in external
     // library styles which are not needed in external
     // apps.
     // apps.
     if (/^~@(ng-select|angular)/.test(url)) {
     if (/^~@(ng-select|angular)/.test(url)) {
-        return false;
+        return null;
     }
     }
 
 
     return { file };
     return { file };

+ 1 - 1
packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.scss

@@ -14,7 +14,7 @@
 }
 }
 vdr-breadcrumb {
 vdr-breadcrumb {
     @media screen and (min-width: $breakpoint-small){
     @media screen and (min-width: $breakpoint-small){
-        margin-left: 10.8rem;
+        margin-left: $clr-sidenav-width;
     }
     }
 }
 }
 .header-actions {
 .header-actions {

+ 0 - 1
packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.scss

@@ -8,7 +8,6 @@
 
 
 nav.sidenav {
 nav.sidenav {
     height: 100%;
     height: 100%;
-    width: 10.8rem;
     border-right-color: var(--clr-sidenav-border-color);
     border-right-color: var(--clr-sidenav-border-color);
 }
 }
 
 

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/data-table/data-table.component.html

@@ -49,7 +49,7 @@
             [itemsPerPage]="itemsPerPage"
             [itemsPerPage]="itemsPerPage"
             (itemsPerPageChange)="itemsPerPageChange.emit($event)"
             (itemsPerPageChange)="itemsPerPageChange.emit($event)"
         ></vdr-items-per-page-controls>
         ></vdr-items-per-page-controls>
-        <div>
+        <div *ngIf="totalItems" class="p5">
             {{ 'common.total-items' | translate: { currentStart, currentEnd, totalItems } }}
             {{ 'common.total-items' | translate: { currentStart, currentEnd, totalItems } }}
         </div>
         </div>
 
 

+ 64 - 46
packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.html

@@ -1,5 +1,13 @@
 <vdr-action-bar>
 <vdr-action-bar>
-    <vdr-ab-left> </vdr-ab-left>
+    <vdr-ab-left>
+        <input
+            type="text"
+            name="emailSearchTerm"
+            [formControl]="searchTerm"
+            [placeholder]="'customer.search-by-group-name' | translate"
+            class="search-input ml3"
+        />
+    </vdr-ab-left>
     <vdr-ab-right>
     <vdr-ab-right>
         <vdr-action-bar-items locationId="customer-group-list"></vdr-action-bar-items>
         <vdr-action-bar-items locationId="customer-group-list"></vdr-action-bar-items>
         <button class="btn btn-primary" *vdrIfPermissions="'CreateCustomerGroup'" (click)="create()">
         <button class="btn btn-primary" *vdrIfPermissions="'CreateCustomerGroup'" (click)="create()">
@@ -10,56 +18,66 @@
 </vdr-action-bar>
 </vdr-action-bar>
 <div class="group-wrapper">
 <div class="group-wrapper">
     <div class="group-list">
     <div class="group-list">
-        <table
-            class="table mt0"
+        <vdr-data-table
             [class.expanded]="activeGroup$ | async"
             [class.expanded]="activeGroup$ | async"
-            *ngIf="!(listIsEmpty$ | async); else emptyPlaceholder"
+            [items]="items$ | async"
+            [itemsPerPage]="itemsPerPage$ | async"
+            [totalItems]="totalItems$ | async"
+            [currentPage]="currentPage$ | async"
+            (pageChange)="setPageNumber($event)"
+            (itemsPerPageChange)="setItemsPerPage($event)"
         >
         >
+            <ng-template let-group="item">
+                <td class="left align-middle" [class.active]="group.id === activeGroupId">
+                    <vdr-entity-info [entity]="group"></vdr-entity-info>
+                </td>
+                <td class="left align-middle" [class.active]="group.id === activeGroupId">
+                    <vdr-chip [colorFrom]="group.id">{{ group.name }}</vdr-chip>
+                </td>
+                <td class="left align-middle" [class.active]="group.id === activeGroupId">
+                    <a
+                        class="btn btn-link btn-sm"
+                        [routerLink]="['./', { contents: group.id }]"
+                        queryParamsHandling="preserve"
+                    >
+                        <clr-icon shape="view-list"></clr-icon>
+                        {{ 'customer.view-group-members' | translate }}
+                    </a>
+                </td>
+                <td class="right align-middle" [class.active]="group.id === activeGroupId">
+                    <button class="btn btn-link btn-sm" (click)="update(group)">
+                        <clr-icon shape="edit"></clr-icon>
+                        {{ 'common.edit' | translate }}
+                    </button>
+                </td>
+                <td [class.active]="group.id === activeGroupId">
+                    <vdr-dropdown>
+                        <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
+                            {{ 'common.actions' | translate }}
+                            <clr-icon shape="caret down"></clr-icon>
+                        </button>
+                        <vdr-dropdown-menu vdrPosition="bottom-right">
+                            <button
+                                class="button"
+                                vdrDropdownItem
+                                (click)="delete(group.id)"
+                                [disabled]="!('DeleteCustomerGroup' | hasPermission)"
+                            >
+                                <clr-icon shape="trash" class="is-danger"></clr-icon>
+                                {{ 'common.delete' | translate }}
+                            </button>
+                        </vdr-dropdown-menu>
+                    </vdr-dropdown>
+                </td>
+            </ng-template>
+        </vdr-data-table>
+
+        <table class="table mt0" *ngIf="!(listIsEmpty$ | async); else emptyPlaceholder">
             <tbody>
             <tbody>
                 <tr
                 <tr
-                    *ngFor="let group of groups$ | async"
+                    *ngFor="let group of items$ | async"
                     [class.active]="group.id === (activeGroup$ | async)?.id"
                     [class.active]="group.id === (activeGroup$ | async)?.id"
-                >
-                    <td class="left align-middle"><vdr-entity-info [entity]="group"></vdr-entity-info></td>
-                    <td class="left align-middle">
-                        <vdr-chip [colorFrom]="group.id">{{ group.name }}</vdr-chip>
-                    </td>
-                    <td class="text-right align-middle">
-                        <a
-                            class="btn btn-link btn-sm"
-                            [routerLink]="['./', { contents: group.id }]"
-                            queryParamsHandling="preserve"
-                        >
-                            <clr-icon shape="view-list"></clr-icon>
-                            {{ 'customer.view-group-members' | translate }}
-                        </a>
-                    </td>
-                    <td class="align-middle">
-                        <button class="btn btn-link btn-sm" (click)="update(group)">
-                            <clr-icon shape="edit"></clr-icon>
-                            {{ 'common.edit' | translate }}
-                        </button>
-                    </td>
-                    <td class="align-middle">
-                        <vdr-dropdown>
-                            <button type="button" class="btn btn-link btn-sm" vdrDropdownTrigger>
-                                {{ 'common.actions' | translate }}
-                                <clr-icon shape="caret down"></clr-icon>
-                            </button>
-                            <vdr-dropdown-menu vdrPosition="bottom-right">
-                                <button
-                                    class="button"
-                                    vdrDropdownItem
-                                    (click)="delete(group.id)"
-                                    [disabled]="!('DeleteCustomerGroup' | hasPermission)"
-                                >
-                                    <clr-icon shape="trash" class="is-danger"></clr-icon>
-                                    {{ 'common.delete' | translate }}
-                                </button>
-                            </vdr-dropdown-menu>
-                        </vdr-dropdown>
-                    </td>
-                </tr>
+                ></tr>
             </tbody>
             </tbody>
         </table>
         </table>
     </div>
     </div>

+ 5 - 2
packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.scss

@@ -9,8 +9,8 @@
         overflow: auto;
         overflow: auto;
         margin-top: 0;
         margin-top: 0;
 
 
-        tr.active {
-            background-color: var(--color-component-bg-200);
+        .active {
+            background-color: var(--clr-global-selection-color);
         }
         }
         &.expanded {
         &.expanded {
             // Fix for Firefox layout https://github.com/vendure-ecommerce/vendure/issues/531
             // Fix for Firefox layout https://github.com/vendure-ecommerce/vendure/issues/531
@@ -18,6 +18,9 @@
         }
         }
     }
     }
 }
 }
+vdr-data-table ::ng-deep table {
+    margin-top: 0;
+}
 .group-members {
 .group-members {
     height: 100%;
     height: 100%;
     width: 0;
     width: 0;

+ 52 - 10
packages/admin-ui/src/lib/customer/src/components/customer-group-list/customer-group-list.component.ts

@@ -1,17 +1,30 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
 import {
+    BaseListComponent,
     DataService,
     DataService,
     DeletionResult,
     DeletionResult,
     GetCustomerGroupsQuery,
     GetCustomerGroupsQuery,
     GetCustomerGroupWithCustomersQuery,
     GetCustomerGroupWithCustomersQuery,
     ItemOf,
     ItemOf,
+    LogicalOperator,
     ModalService,
     ModalService,
     NotificationService,
     NotificationService,
+    SortOrder,
 } from '@vendure/admin-ui/core';
 } from '@vendure/admin-ui/core';
 import { BehaviorSubject, combineLatest, EMPTY, Observable, of } from 'rxjs';
 import { BehaviorSubject, combineLatest, EMPTY, Observable, of } from 'rxjs';
-import { distinctUntilChanged, map, mapTo, switchMap, tap } from 'rxjs/operators';
+import {
+    debounceTime,
+    distinctUntilChanged,
+    filter,
+    map,
+    mapTo,
+    switchMap,
+    takeUntil,
+    tap,
+} from 'rxjs/operators';
 
 
 import { AddCustomerToGroupDialogComponent } from '../add-customer-to-group-dialog/add-customer-to-group-dialog.component';
 import { AddCustomerToGroupDialogComponent } from '../add-customer-to-group-dialog/add-customer-to-group-dialog.component';
 import { CustomerGroupDetailDialogComponent } from '../customer-group-detail-dialog/customer-group-detail-dialog.component';
 import { CustomerGroupDetailDialogComponent } from '../customer-group-detail-dialog/customer-group-detail-dialog.component';
@@ -23,9 +36,16 @@ import { CustomerGroupMemberFetchParams } from '../customer-group-member-list/cu
     styleUrls: ['./customer-group-list.component.scss'],
     styleUrls: ['./customer-group-list.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 })
-export class CustomerGroupListComponent implements OnInit {
+export class CustomerGroupListComponent
+    extends BaseListComponent<
+        GetCustomerGroupsQuery,
+        GetCustomerGroupsQuery['customerGroups']['items'][number]
+    >
+    implements OnInit
+{
+    searchTerm = new FormControl('');
     activeGroup$: Observable<ItemOf<GetCustomerGroupsQuery, 'customerGroups'> | undefined>;
     activeGroup$: Observable<ItemOf<GetCustomerGroupsQuery, 'customerGroups'> | undefined>;
-    groups$: Observable<Array<ItemOf<GetCustomerGroupsQuery, 'customerGroups'>>>;
+    activeGroupId: string | undefined;
     listIsEmpty$: Observable<boolean>;
     listIsEmpty$: Observable<boolean>;
     members$: Observable<
     members$: Observable<
         NonNullable<GetCustomerGroupWithCustomersQuery['customerGroup']>['customers']['items']
         NonNullable<GetCustomerGroupWithCustomersQuery['customerGroup']>['customers']['items']
@@ -44,25 +64,47 @@ export class CustomerGroupListComponent implements OnInit {
         private notificationService: NotificationService,
         private notificationService: NotificationService,
         private modalService: ModalService,
         private modalService: ModalService,
         public route: ActivatedRoute,
         public route: ActivatedRoute,
-        private router: Router,
-    ) {}
+        protected router: Router,
+    ) {
+        super(router, route);
+        super.setQueryFn(
+            (...args: any[]) =>
+                this.dataService.customer.getCustomerGroupList(...args).refetchOnChannelChange(),
+            data => data.customerGroups,
+            (skip, take) => ({
+                options: {
+                    skip,
+                    take,
+                    filter: {
+                        name: { contains: this.searchTerm.value },
+                    },
+                },
+            }),
+        );
+    }
 
 
     ngOnInit(): void {
     ngOnInit(): void {
-        this.groups$ = this.dataService.customer
-            .getCustomerGroupList()
-            .mapStream(data => data.customerGroups.items);
+        super.ngOnInit();
+        this.searchTerm.valueChanges
+            .pipe(
+                filter(value => 2 < value.length || value.length === 0),
+                debounceTime(250),
+                takeUntil(this.destroy$),
+            )
+            .subscribe(() => this.refresh());
         const activeGroupId$ = this.route.paramMap.pipe(
         const activeGroupId$ = this.route.paramMap.pipe(
             map(pm => pm.get('contents')),
             map(pm => pm.get('contents')),
             distinctUntilChanged(),
             distinctUntilChanged(),
             tap(() => (this.selectedCustomerIds = [])),
             tap(() => (this.selectedCustomerIds = [])),
         );
         );
-        this.listIsEmpty$ = this.groups$.pipe(map(groups => groups.length === 0));
-        this.activeGroup$ = combineLatest(this.groups$, activeGroupId$).pipe(
+        this.listIsEmpty$ = this.items$.pipe(map(groups => groups.length === 0));
+        this.activeGroup$ = combineLatest(this.items$, activeGroupId$).pipe(
             map(([groups, activeGroupId]) => {
             map(([groups, activeGroupId]) => {
                 if (activeGroupId) {
                 if (activeGroupId) {
                     return groups.find(g => g.id === activeGroupId);
                     return groups.find(g => g.id === activeGroupId);
                 }
                 }
             }),
             }),
+            tap(val => (this.activeGroupId = val?.id)),
         );
         );
         const membersResult$ = combineLatest(
         const membersResult$ = combineLatest(
             this.activeGroup$,
             this.activeGroup$,

+ 15 - 9
packages/admin-ui/src/lib/order/src/components/order-custom-fields-card/order-custom-fields-card.component.html

@@ -4,15 +4,13 @@
     </div>
     </div>
     <div class="card-block">
     <div class="card-block">
         <div class="card-text custom-field-form" [class.editable]="editable">
         <div class="card-text custom-field-form" [class.editable]="editable">
-            <ng-container *ngFor="let customField of customFieldsConfig">
-                <vdr-custom-field-control
-                    entityName="Order"
-                    [customFieldsFormGroup]="customFieldForm"
-                    [compact]="true"
-                    [readonly]="customField.readonly || !editable"
-                    [customField]="customField"
-                ></vdr-custom-field-control>
-            </ng-container>
+            <vdr-tabbed-custom-fields
+                entityName="Order"
+                [customFields]="customFieldsConfig"
+                [customFieldsFormGroup]="customFieldForm"
+                [readonly]="!editable"
+                [compact]="true"
+            ></vdr-tabbed-custom-fields>
         </div>
         </div>
     </div>
     </div>
     <div class="card-footer">
     <div class="card-footer">
@@ -29,5 +27,13 @@
             <clr-icon shape="check"></clr-icon>
             <clr-icon shape="check"></clr-icon>
             {{ 'common.update' | translate }}
             {{ 'common.update' | translate }}
         </button>
         </button>
+        <button
+            class="btn btn-sm btn-secondary"
+            (click)="onCancelClick()"
+            *ngIf="editable"
+        >
+            <clr-icon shape="times"></clr-icon>
+            {{ 'common.cancel' | translate }}
+        </button>
     </div>
     </div>
 </div>
 </div>

+ 25 - 2
packages/admin-ui/src/lib/order/src/components/order-custom-fields-card/order-custom-fields-card.component.ts

@@ -1,6 +1,7 @@
 import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
 import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
 import { FormBuilder, FormGroup } from '@angular/forms';
 import { FormBuilder, FormGroup } from '@angular/forms';
-import { CustomFieldConfig } from '@vendure/admin-ui/core';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { CustomFieldConfig, ModalService } from '@vendure/admin-ui/core';
 
 
 @Component({
 @Component({
     selector: 'vdr-order-custom-fields-card',
     selector: 'vdr-order-custom-fields-card',
@@ -14,7 +15,7 @@ export class OrderCustomFieldsCardComponent implements OnInit {
     @Output() updateClick = new EventEmitter<any>();
     @Output() updateClick = new EventEmitter<any>();
     customFieldForm: FormGroup;
     customFieldForm: FormGroup;
     editable = false;
     editable = false;
-    constructor(private formBuilder: FormBuilder) {}
+    constructor(private formBuilder: FormBuilder, private modalService: ModalService) {}
 
 
     ngOnInit() {
     ngOnInit() {
         this.customFieldForm = this.formBuilder.group({});
         this.customFieldForm = this.formBuilder.group({});
@@ -31,4 +32,26 @@ export class OrderCustomFieldsCardComponent implements OnInit {
         this.customFieldForm.markAsPristine();
         this.customFieldForm.markAsPristine();
         this.editable = false;
         this.editable = false;
     }
     }
+
+    onCancelClick() {
+        if (this.customFieldForm.dirty) {
+            this.modalService
+                .dialog({
+                    title: _('catalog.confirm-cancel'),
+                    buttons: [
+                        { type: 'secondary', label: _('common.keep-editing') },
+                        { type: 'danger', label: _('common.discard-changes'), returnValue: true },
+                    ],
+                })
+                .subscribe(result => {
+                    if (result) {
+                        this.customFieldForm.reset();
+                        this.customFieldForm.markAsPristine();
+                        this.editable = false;
+                    }
+                });
+        } else {
+            this.editable = false;
+        }
+    }
 }
 }

+ 2 - 10
packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.html

@@ -19,19 +19,11 @@
                     <clr-icon shape="angle down"></clr-icon>
                     <clr-icon shape="angle down"></clr-icon>
                 </button>
                 </button>
             </div>
             </div>
-
-            <input
-                type="text"
-                name="searchTerm"
-                [formControl]="searchOrderCodeControl"
-                [placeholder]="'order.search-by-order-code' | translate"
-                class="search-input"
-            />
             <input
             <input
                 type="text"
                 type="text"
                 name="searchTerm"
                 name="searchTerm"
-                [formControl]="searchLastNameControl"
-                [placeholder]="'order.search-by-customer-last-name' | translate"
+                [formControl]="searchControl"
+                [placeholder]="'order.search-by-order-filters' | translate"
                 class="search-input"
                 class="search-input"
             />
             />
         </div>
         </div>

+ 20 - 17
packages/admin-ui/src/lib/order/src/components/order-list/order-list.component.ts

@@ -8,6 +8,7 @@ import {
     GetOrderListQuery,
     GetOrderListQuery,
     ItemOf,
     ItemOf,
     LocalStorageService,
     LocalStorageService,
+    LogicalOperator,
     OrderListOptions,
     OrderListOptions,
     ServerConfigService,
     ServerConfigService,
     SortOrder,
     SortOrder,
@@ -37,6 +38,7 @@ export class OrderListComponent
     extends BaseListComponent<GetOrderListQuery, ItemOf<GetOrderListQuery, 'orders'>>
     extends BaseListComponent<GetOrderListQuery, ItemOf<GetOrderListQuery, 'orders'>>
     implements OnInit
     implements OnInit
 {
 {
+    searchControl = new FormControl('');
     searchOrderCodeControl = new FormControl('');
     searchOrderCodeControl = new FormControl('');
     searchLastNameControl = new FormControl('');
     searchLastNameControl = new FormControl('');
     customFilterForm: FormGroup;
     customFilterForm: FormGroup;
@@ -95,8 +97,7 @@ export class OrderListComponent
                 this.createQueryOptions(
                 this.createQueryOptions(
                     skip,
                     skip,
                     take,
                     take,
-                    this.searchOrderCodeControl.value,
-                    this.searchLastNameControl.value,
+                    this.searchControl.value,
                     this.route.snapshot.queryParamMap.get('filter') || 'open',
                     this.route.snapshot.queryParamMap.get('filter') || 'open',
                 ),
                 ),
         );
         );
@@ -112,10 +113,7 @@ export class OrderListComponent
             map(qpm => qpm.get('filter') || 'open'),
             map(qpm => qpm.get('filter') || 'open'),
             distinctUntilChanged(),
             distinctUntilChanged(),
         );
         );
-        const searchTerms$ = merge(
-            this.searchOrderCodeControl.valueChanges,
-            this.searchLastNameControl.valueChanges,
-        ).pipe(
+        const searchTerms$ = merge(this.searchControl.valueChanges).pipe(
             filter(value => 2 < value.length || value.length === 0),
             filter(value => 2 < value.length || value.length === 0),
             debounceTime(250),
             debounceTime(250),
         );
         );
@@ -166,13 +164,13 @@ export class OrderListComponent
         // tslint:disable-next-line:no-shadowed-variable
         // tslint:disable-next-line:no-shadowed-variable
         skip: number,
         skip: number,
         take: number,
         take: number,
-        orderCodeSearchTerm: string,
-        customerNameSearchTerm: string,
+        searchTerm: string,
         activeFilterPreset?: string,
         activeFilterPreset?: string,
     ): { options: OrderListOptions } {
     ): { options: OrderListOptions } {
         const filterConfig = this.filterPresets.find(p => p.name === activeFilterPreset);
         const filterConfig = this.filterPresets.find(p => p.name === activeFilterPreset);
         // tslint:disable-next-line:no-shadowed-variable
         // tslint:disable-next-line:no-shadowed-variable
-        const filter: any = {};
+        let filter: any = {};
+        let filterOperator: LogicalOperator = LogicalOperator.AND;
         if (filterConfig) {
         if (filterConfig) {
             if (filterConfig.config.active != null) {
             if (filterConfig.config.active != null) {
                 filter.active = {
                 filter.active = {
@@ -211,15 +209,19 @@ export class OrderListComponent
                 };
                 };
             }
             }
         }
         }
-        if (customerNameSearchTerm) {
-            filter.customerLastName = {
-                contains: customerNameSearchTerm,
-            };
-        }
-        if (orderCodeSearchTerm) {
-            filter.code = {
-                contains: orderCodeSearchTerm,
+        if (searchTerm) {
+            filter = {
+                customerLastName: {
+                    contains: searchTerm,
+                },
+                transactionId: {
+                    contains: searchTerm,
+                },
+                code: {
+                    contains: searchTerm,
+                },
             };
             };
+            filterOperator = LogicalOperator.OR;
         }
         }
         return {
         return {
             options: {
             options: {
@@ -231,6 +233,7 @@ export class OrderListComponent
                 sort: {
                 sort: {
                     updatedAt: SortOrder.DESC,
                     updatedAt: SortOrder.DESC,
                 },
                 },
+                filterOperator,
             },
             },
         };
         };
     }
     }

+ 4 - 2
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -66,6 +66,7 @@
     "auto-update-product-variant-name": "Automaticky aktualizovat jména variant",
     "auto-update-product-variant-name": "Automaticky aktualizovat jména variant",
     "channel-price-preview": "Náhled ceny v kanálu",
     "channel-price-preview": "Náhled ceny v kanálu",
     "collection-contents": "Obsah kolekce",
     "collection-contents": "Obsah kolekce",
+    "confirm-cancel": "",
     "confirm-delete-administrator": "Smazat administrátora?",
     "confirm-delete-administrator": "Smazat administrátora?",
     "confirm-delete-assets": "Smazat {count} {count, plural, one {médium} few {média} other {médií}}?",
     "confirm-delete-assets": "Smazat {count} {count, plural, one {médium} few {média} other {médií}}?",
     "confirm-delete-channel": "Smazat kanál?",
     "confirm-delete-channel": "Smazat kanál?",
@@ -212,6 +213,7 @@
     "guest": "Host",
     "guest": "Host",
     "items-per-page-option": "{ count } na stránku",
     "items-per-page-option": "{ count } na stránku",
     "items-selected-count": "",
     "items-selected-count": "",
+    "keep-editing": "",
     "language": "Jazyk",
     "language": "Jazyk",
     "launch-extension": "Spustit rozšíření",
     "launch-extension": "Spustit rozšíření",
     "live-update": "Živé aktualizace",
     "live-update": "Živé aktualizace",
@@ -324,6 +326,7 @@
     "registered": "Registrován",
     "registered": "Registrován",
     "remove-customers-from-group-success": "Odebrán: {customerCount, plural, one {1 zákazník} other {{customerCount} zákazníci/zákazníků}} z \"{ groupName }\"",
     "remove-customers-from-group-success": "Odebrán: {customerCount, plural, one {1 zákazník} other {{customerCount} zákazníci/zákazníků}} z \"{ groupName }\"",
     "remove-from-group": "Odebrat ze skupiny",
     "remove-from-group": "Odebrat ze skupiny",
+    "search-by-group-name": "Hledat výraz",
     "search-customers-by-email": "Hledat podle e-mailové adresy",
     "search-customers-by-email": "Hledat podle e-mailové adresy",
     "search-customers-by-email-last-name-postal-code": "",
     "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "",
     "select-customer": "",
@@ -553,8 +556,7 @@
     "refund-with-amount": "Refundovat {amount}",
     "refund-with-amount": "Refundovat {amount}",
     "refunded-count": "{count} {count, plural, one {položka} other {položky}} refundovány",
     "refunded-count": "{count} {count, plural, one {položka} other {položky}} refundovány",
     "removed-items": "Odebrané položky",
     "removed-items": "Odebrané položky",
-    "search-by-customer-last-name": "",
-    "search-by-order-code": "Hledat na základě kódu objednávky",
+    "search-by-order-filters": "Hledat na základě kódu objednávky / Příjmení / ID transakce",
     "select-state": "Vyberte stav",
     "select-state": "Vyberte stav",
     "set-coupon-codes": "",
     "set-coupon-codes": "",
     "set-fulfillment-state": "Označit jako {state}",
     "set-fulfillment-state": "Označit jako {state}",

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

@@ -66,6 +66,7 @@
     "auto-update-product-variant-name": "Automatisch Namen der Produktvariante aktualisieren",
     "auto-update-product-variant-name": "Automatisch Namen der Produktvariante aktualisieren",
     "channel-price-preview": "Kanal-Preisvorschau",
     "channel-price-preview": "Kanal-Preisvorschau",
     "collection-contents": "Inhalt der Sammlung",
     "collection-contents": "Inhalt der Sammlung",
+    "confirm-cancel": "",
     "confirm-delete-administrator": "Administrator löschen?",
     "confirm-delete-administrator": "Administrator löschen?",
     "confirm-delete-assets": "{count} {count, plural, one {Asset} other {Assets}} löschen?",
     "confirm-delete-assets": "{count} {count, plural, one {Asset} other {Assets}} löschen?",
     "confirm-delete-channel": "Kanal löschen?",
     "confirm-delete-channel": "Kanal löschen?",
@@ -212,6 +213,7 @@
     "guest": "Gast",
     "guest": "Gast",
     "items-per-page-option": "{ count } pro Seite",
     "items-per-page-option": "{ count } pro Seite",
     "items-selected-count": "",
     "items-selected-count": "",
+    "keep-editing": "",
     "language": "Sprache",
     "language": "Sprache",
     "launch-extension": "Erweiterung starten",
     "launch-extension": "Erweiterung starten",
     "live-update": "Live-Aktualisierung",
     "live-update": "Live-Aktualisierung",
@@ -324,6 +326,7 @@
     "registered": "Registriert",
     "registered": "Registriert",
     "remove-customers-from-group-success": "{customerCount, plural, one {Kunde} other {{customerCount} Kunden}} aus \"{ groupName }\" entfernt",
     "remove-customers-from-group-success": "{customerCount, plural, one {Kunde} other {{customerCount} Kunden}} aus \"{ groupName }\" entfernt",
     "remove-from-group": "Aus dieser Gruppe entfernen",
     "remove-from-group": "Aus dieser Gruppe entfernen",
+    "search-by-group-name": "Suche nach Begriff",
     "search-customers-by-email": "Suche nach E-Mail-Adresse",
     "search-customers-by-email": "Suche nach E-Mail-Adresse",
     "search-customers-by-email-last-name-postal-code": "",
     "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "Kunde auswählen",
     "select-customer": "Kunde auswählen",
@@ -553,8 +556,7 @@
     "refund-with-amount": "Rückzahlung {amount}",
     "refund-with-amount": "Rückzahlung {amount}",
     "refunded-count": "{count} {count, plural, one {Artikel} other {Artikel}} erstattet",
     "refunded-count": "{count} {count, plural, one {Artikel} other {Artikel}} erstattet",
     "removed-items": "Entfernte Artikel",
     "removed-items": "Entfernte Artikel",
-    "search-by-customer-last-name": "",
-    "search-by-order-code": "Suche nach Bestellnummer",
+    "search-by-order-filters": "Suche nach Name / Bestellnummer / Transaktions-ID",
     "select-state": "Status auswählen",
     "select-state": "Status auswählen",
     "set-coupon-codes": "",
     "set-coupon-codes": "",
     "set-fulfillment-state": "Abwicklungsstatus wählen",
     "set-fulfillment-state": "Abwicklungsstatus wählen",

+ 4 - 2
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -66,6 +66,7 @@
     "auto-update-product-variant-name": "Automatically update the names of ProductVariants",
     "auto-update-product-variant-name": "Automatically update the names of ProductVariants",
     "channel-price-preview": "Channel price preview",
     "channel-price-preview": "Channel price preview",
     "collection-contents": "Collection contents",
     "collection-contents": "Collection contents",
+    "confirm-cancel": "Cancel?",
     "confirm-delete-administrator": "Delete administrator?",
     "confirm-delete-administrator": "Delete administrator?",
     "confirm-delete-assets": "Delete {count} {count, plural, one {asset} other {assets}}?",
     "confirm-delete-assets": "Delete {count} {count, plural, one {asset} other {assets}}?",
     "confirm-delete-channel": "Delete channel?",
     "confirm-delete-channel": "Delete channel?",
@@ -212,6 +213,7 @@
     "guest": "Guest",
     "guest": "Guest",
     "items-per-page-option": "{ count } per page",
     "items-per-page-option": "{ count } per page",
     "items-selected-count": "{ count } {count, plural, one {item} other {items}} selected",
     "items-selected-count": "{ count } {count, plural, one {item} other {items}} selected",
+    "keep-editing": "Keep editing",
     "language": "Language",
     "language": "Language",
     "launch-extension": "Launch extension",
     "launch-extension": "Launch extension",
     "live-update": "Live update",
     "live-update": "Live update",
@@ -324,6 +326,7 @@
     "registered": "Registered",
     "registered": "Registered",
     "remove-customers-from-group-success": "Removed {customerCount, plural, one {1 customer} other {{customerCount} customers}} from \"{ groupName }\"",
     "remove-customers-from-group-success": "Removed {customerCount, plural, one {1 customer} other {{customerCount} customers}} from \"{ groupName }\"",
     "remove-from-group": "Remove from this group",
     "remove-from-group": "Remove from this group",
+    "search-by-group-name": "Search by group name",
     "search-customers-by-email": "Search by email address",
     "search-customers-by-email": "Search by email address",
     "search-customers-by-email-last-name-postal-code": "Search by email / last name / postal code",
     "search-customers-by-email-last-name-postal-code": "Search by email / last name / postal code",
     "select-customer": "Select customer",
     "select-customer": "Select customer",
@@ -553,8 +556,7 @@
     "refund-with-amount": "Refund {amount}",
     "refund-with-amount": "Refund {amount}",
     "refunded-count": "{count} {count, plural, one {item} other {items}} refunded",
     "refunded-count": "{count} {count, plural, one {item} other {items}} refunded",
     "removed-items": "Removed items",
     "removed-items": "Removed items",
-    "search-by-customer-last-name": "Search by customer last name",
-    "search-by-order-code": "Search by order code",
+    "search-by-order-filters": "Search by name / code / transaction ID",
     "select-state": "Select state",
     "select-state": "Select state",
     "set-coupon-codes": "Set coupon codes",
     "set-coupon-codes": "Set coupon codes",
     "set-fulfillment-state": "Mark as {state}",
     "set-fulfillment-state": "Mark as {state}",

+ 4 - 2
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -66,6 +66,7 @@
     "auto-update-product-variant-name": "Actualiza los nombres de las variantes de producto automáticamente",
     "auto-update-product-variant-name": "Actualiza los nombres de las variantes de producto automáticamente",
     "channel-price-preview": "Vista previa de precio para el canal de ventas",
     "channel-price-preview": "Vista previa de precio para el canal de ventas",
     "collection-contents": "Contenidos de la colección",
     "collection-contents": "Contenidos de la colección",
+    "confirm-cancel": "",
     "confirm-delete-administrator": "¿Eliminar administrador?",
     "confirm-delete-administrator": "¿Eliminar administrador?",
     "confirm-delete-assets": "¿Eliminar recurso?",
     "confirm-delete-assets": "¿Eliminar recurso?",
     "confirm-delete-channel": "¿Eliminar canal de ventas?",
     "confirm-delete-channel": "¿Eliminar canal de ventas?",
@@ -212,6 +213,7 @@
     "guest": "Invitado",
     "guest": "Invitado",
     "items-per-page-option": "{ count } por página",
     "items-per-page-option": "{ count } por página",
     "items-selected-count": "",
     "items-selected-count": "",
+    "keep-editing": "",
     "language": "Idioma",
     "language": "Idioma",
     "launch-extension": "Ejecutar extensión",
     "launch-extension": "Ejecutar extensión",
     "live-update": "Actualización en vivo",
     "live-update": "Actualización en vivo",
@@ -324,6 +326,7 @@
     "registered": "Registrado",
     "registered": "Registrado",
     "remove-customers-from-group-success": "{customerCount, plural, one {1 cliente} other {{customerCount} clientes}} {customerCount, plural, one {eliminado} other {eliminados}} de \"{ groupName }\"",
     "remove-customers-from-group-success": "{customerCount, plural, one {1 cliente} other {{customerCount} clientes}} {customerCount, plural, one {eliminado} other {eliminados}} de \"{ groupName }\"",
     "remove-from-group": "Eliminar del grupo",
     "remove-from-group": "Eliminar del grupo",
+    "search-by-group-name": "Buscar por término",
     "search-customers-by-email": "Buscar por correo electrónico",
     "search-customers-by-email": "Buscar por correo electrónico",
     "search-customers-by-email-last-name-postal-code": "Buscar por apellido / correo electrónico / Código postal",
     "search-customers-by-email-last-name-postal-code": "Buscar por apellido / correo electrónico / Código postal",
     "select-customer": "Seleccionar cliente",
     "select-customer": "Seleccionar cliente",
@@ -553,8 +556,7 @@
     "refund-with-amount": "Reembolso {amount}",
     "refund-with-amount": "Reembolso {amount}",
     "refunded-count": "{count} {count, plural, one {artículo} other {artículos}} {count, plural, one {reembolsado} other {reembolsados}}",
     "refunded-count": "{count} {count, plural, one {artículo} other {artículos}} {count, plural, one {reembolsado} other {reembolsados}}",
     "removed-items": "Artículos eliminados",
     "removed-items": "Artículos eliminados",
-    "search-by-customer-last-name": "Buscar por el apellido del cliente",
-    "search-by-order-code": "Buscar por el código de pedido",
+    "search-by-order-filters": "Buscar por el código de pedido / apellido del cliente / ID de transacción",
     "select-state": "Seleccionar estado",
     "select-state": "Seleccionar estado",
     "set-coupon-codes": "",
     "set-coupon-codes": "",
     "set-fulfillment-state": "Fijar como {state}",
     "set-fulfillment-state": "Fijar como {state}",

+ 4 - 2
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -66,6 +66,7 @@
     "auto-update-product-variant-name": "Mettre à jour automatiquement les noms de variations du produit ",
     "auto-update-product-variant-name": "Mettre à jour automatiquement les noms de variations du produit ",
     "channel-price-preview": "Prévisualisation du prix du canal",
     "channel-price-preview": "Prévisualisation du prix du canal",
     "collection-contents": "Contenu de la Collection",
     "collection-contents": "Contenu de la Collection",
+    "confirm-cancel": "",
     "confirm-delete-administrator": "Supprimer administrateur?",
     "confirm-delete-administrator": "Supprimer administrateur?",
     "confirm-delete-assets": "Supprimer {count} {count, plural, one {fichier} other {fichiers}} ?",
     "confirm-delete-assets": "Supprimer {count} {count, plural, one {fichier} other {fichiers}} ?",
     "confirm-delete-channel": "Supprimer canal ?",
     "confirm-delete-channel": "Supprimer canal ?",
@@ -212,6 +213,7 @@
     "guest": "Invité",
     "guest": "Invité",
     "items-per-page-option": "{ count } par page",
     "items-per-page-option": "{ count } par page",
     "items-selected-count": "",
     "items-selected-count": "",
+    "keep-editing": "",
     "language": "Langue",
     "language": "Langue",
     "launch-extension": "Lancer extension",
     "launch-extension": "Lancer extension",
     "live-update": "Mise à jour automatique",
     "live-update": "Mise à jour automatique",
@@ -324,6 +326,7 @@
     "registered": "Inscrit",
     "registered": "Inscrit",
     "remove-customers-from-group-success": "Retrait {customerCount, plural, one {d'un client} other {de {customerCount} clients}} de \"{ groupName }\"",
     "remove-customers-from-group-success": "Retrait {customerCount, plural, one {d'un client} other {de {customerCount} clients}} de \"{ groupName }\"",
     "remove-from-group": "Retirer de ce groupe",
     "remove-from-group": "Retirer de ce groupe",
+    "search-by-group-name": "Chercher le terme",
     "search-customers-by-email": "Chercher par adresse email",
     "search-customers-by-email": "Chercher par adresse email",
     "search-customers-by-email-last-name-postal-code": "Rechercher par nom / adresse email / code postal",
     "search-customers-by-email-last-name-postal-code": "Rechercher par nom / adresse email / code postal",
     "select-customer": "Sélectionner client",
     "select-customer": "Sélectionner client",
@@ -553,8 +556,7 @@
     "refund-with-amount": "Rembourser {amount}",
     "refund-with-amount": "Rembourser {amount}",
     "refunded-count": "{count} {count, plural, one {article remboursé} other {articles remboursés}}",
     "refunded-count": "{count} {count, plural, one {article remboursé} other {articles remboursés}}",
     "removed-items": "Articles supprimés",
     "removed-items": "Articles supprimés",
-    "search-by-customer-last-name": "Rechercher par nom du client",
-    "search-by-order-code": "Rehercher par numéro de commande",
+    "search-by-order-filters": "Rehercher par numéro de commande / nom du client / Numéro de transaction",
     "select-state": "Sélectionner un état",
     "select-state": "Sélectionner un état",
     "set-coupon-codes": "",
     "set-coupon-codes": "",
     "set-fulfillment-state": "Marquer {state}",
     "set-fulfillment-state": "Marquer {state}",

+ 4 - 2
packages/admin-ui/src/lib/static/i18n-messages/it.json

@@ -66,6 +66,7 @@
     "auto-update-product-variant-name": "Aggiorna automaticamente i nomi delle Varianti",
     "auto-update-product-variant-name": "Aggiorna automaticamente i nomi delle Varianti",
     "channel-price-preview": "Anteprima prezzo canale",
     "channel-price-preview": "Anteprima prezzo canale",
     "collection-contents": "Contenuti della Collezione",
     "collection-contents": "Contenuti della Collezione",
+    "confirm-cancel": "",
     "confirm-delete-administrator": "Eliminare l'amministratore?",
     "confirm-delete-administrator": "Eliminare l'amministratore?",
     "confirm-delete-assets": "Eliminare {count} {count, plural, one {media} other {media}}?",
     "confirm-delete-assets": "Eliminare {count} {count, plural, one {media} other {media}}?",
     "confirm-delete-channel": "Eliminare il canale?",
     "confirm-delete-channel": "Eliminare il canale?",
@@ -212,6 +213,7 @@
     "guest": "Ospite",
     "guest": "Ospite",
     "items-per-page-option": "{ count } per pagina",
     "items-per-page-option": "{ count } per pagina",
     "items-selected-count": "",
     "items-selected-count": "",
+    "keep-editing": "",
     "language": "Lingua",
     "language": "Lingua",
     "launch-extension": "Lancia estensione",
     "launch-extension": "Lancia estensione",
     "live-update": "Aggiornamenti live",
     "live-update": "Aggiornamenti live",
@@ -324,6 +326,7 @@
     "registered": "Registrato",
     "registered": "Registrato",
     "remove-customers-from-group-success": "Ho rimosso {customerCount, plural, one {1 cuclientestomer} other {{customerCount} clienti}} from \"{ groupName }\"",
     "remove-customers-from-group-success": "Ho rimosso {customerCount, plural, one {1 cuclientestomer} other {{customerCount} clienti}} from \"{ groupName }\"",
     "remove-from-group": "Rimuovi da questo gruppo",
     "remove-from-group": "Rimuovi da questo gruppo",
+    "search-by-group-name": "Ricerca termine",
     "search-customers-by-email": "Cerca per indirizzo email",
     "search-customers-by-email": "Cerca per indirizzo email",
     "search-customers-by-email-last-name-postal-code": "Cerca per cognome / indirizzo email / CAP",
     "search-customers-by-email-last-name-postal-code": "Cerca per cognome / indirizzo email / CAP",
     "select-customer": "Seleziona cliente",
     "select-customer": "Seleziona cliente",
@@ -553,8 +556,7 @@
     "refund-with-amount": "Rimborsa {amount}",
     "refund-with-amount": "Rimborsa {amount}",
     "refunded-count": "Hai rimborsato {count} {count, plural, one {prodotto} other {prodotti}}",
     "refunded-count": "Hai rimborsato {count} {count, plural, one {prodotto} other {prodotti}}",
     "removed-items": "Prodotti rimossi",
     "removed-items": "Prodotti rimossi",
-    "search-by-customer-last-name": "Cerca per cognome cliente",
-    "search-by-order-code": "Cerca per codice ordine",
+    "search-by-order-filters": "Cerca per codice ordine / cognome cliente / ID transazione",
     "select-state": "Seleziona stato",
     "select-state": "Seleziona stato",
     "set-coupon-codes": "",
     "set-coupon-codes": "",
     "set-fulfillment-state": "Segna come {state}",
     "set-fulfillment-state": "Segna come {state}",

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

@@ -66,6 +66,7 @@
     "auto-update-product-variant-name": "",
     "auto-update-product-variant-name": "",
     "channel-price-preview": "Podgląd cen kanału",
     "channel-price-preview": "Podgląd cen kanału",
     "collection-contents": "Zawartość kolekcji",
     "collection-contents": "Zawartość kolekcji",
+    "confirm-cancel": "",
     "confirm-delete-administrator": "",
     "confirm-delete-administrator": "",
     "confirm-delete-assets": "",
     "confirm-delete-assets": "",
     "confirm-delete-channel": "Usunąć kanał?",
     "confirm-delete-channel": "Usunąć kanał?",
@@ -212,6 +213,7 @@
     "guest": "Gość",
     "guest": "Gość",
     "items-per-page-option": "{ count } na stronę",
     "items-per-page-option": "{ count } na stronę",
     "items-selected-count": "",
     "items-selected-count": "",
+    "keep-editing": "",
     "language": "Język",
     "language": "Język",
     "launch-extension": "Uruchom rozszerzenie",
     "launch-extension": "Uruchom rozszerzenie",
     "live-update": "Aktualizacja live",
     "live-update": "Aktualizacja live",
@@ -324,6 +326,7 @@
     "registered": "Zarejestrowany",
     "registered": "Zarejestrowany",
     "remove-customers-from-group-success": "",
     "remove-customers-from-group-success": "",
     "remove-from-group": "",
     "remove-from-group": "",
+    "search-by-group-name": "Szukaj frazy",
     "search-customers-by-email": "Szukaj przez email",
     "search-customers-by-email": "Szukaj przez email",
     "search-customers-by-email-last-name-postal-code": "",
     "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "",
     "select-customer": "",
@@ -553,8 +556,7 @@
     "refund-with-amount": "Zwróć {amount}",
     "refund-with-amount": "Zwróć {amount}",
     "refunded-count": "{count} {count, plural, one {zamówienie} other {zamówień}} zwrócono",
     "refunded-count": "{count} {count, plural, one {zamówienie} other {zamówień}} zwrócono",
     "removed-items": "",
     "removed-items": "",
-    "search-by-customer-last-name": "",
-    "search-by-order-code": "Szukaj po numerze zamówienia",
+    "search-by-order-filters": "Szukaj po numerze zamówienia / Nazwisko / Numer transakcji",
     "select-state": "",
     "select-state": "",
     "set-coupon-codes": "",
     "set-coupon-codes": "",
     "set-fulfillment-state": "",
     "set-fulfillment-state": "",

+ 4 - 2
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -66,6 +66,7 @@
     "auto-update-product-variant-name": "Atualizar automaticamente os nomes das variações do produto",
     "auto-update-product-variant-name": "Atualizar automaticamente os nomes das variações do produto",
     "channel-price-preview": "Visualizar preço do canal",
     "channel-price-preview": "Visualizar preço do canal",
     "collection-contents": "Conteúdo da categoria",
     "collection-contents": "Conteúdo da categoria",
+    "confirm-cancel": "",
     "confirm-delete-administrator": "Excluir administrador?",
     "confirm-delete-administrator": "Excluir administrador?",
     "confirm-delete-assets": "Excluir {count} {count, plural, one {asset} other {assets}}?",
     "confirm-delete-assets": "Excluir {count} {count, plural, one {asset} other {assets}}?",
     "confirm-delete-channel": "Exluir canal?",
     "confirm-delete-channel": "Exluir canal?",
@@ -212,6 +213,7 @@
     "guest": "Convidado",
     "guest": "Convidado",
     "items-per-page-option": "{ count } por página",
     "items-per-page-option": "{ count } por página",
     "items-selected-count": "",
     "items-selected-count": "",
+    "keep-editing": "",
     "language": "Idioma",
     "language": "Idioma",
     "launch-extension": "Iniciar extensão",
     "launch-extension": "Iniciar extensão",
     "live-update": "Atualização ao vivo",
     "live-update": "Atualização ao vivo",
@@ -324,6 +326,7 @@
     "registered": "Registrado",
     "registered": "Registrado",
     "remove-customers-from-group-success": "Excluído {customerCount, plural, one {1 customer} other {{customerCount} customers}} from \"{ groupName }\"",
     "remove-customers-from-group-success": "Excluído {customerCount, plural, one {1 customer} other {{customerCount} customers}} from \"{ groupName }\"",
     "remove-from-group": "Excluir deste grupo",
     "remove-from-group": "Excluir deste grupo",
+    "search-by-group-name": "Pesquisar termo",
     "search-customers-by-email": "Busca por email",
     "search-customers-by-email": "Busca por email",
     "search-customers-by-email-last-name-postal-code": "Busca por email / sobrenome / cep",
     "search-customers-by-email-last-name-postal-code": "Busca por email / sobrenome / cep",
     "select-customer": "",
     "select-customer": "",
@@ -553,8 +556,7 @@
     "refund-with-amount": "Reembolso {amount}",
     "refund-with-amount": "Reembolso {amount}",
     "refunded-count": "{count} {count, plural, one {item} other {items}} reembolsado",
     "refunded-count": "{count} {count, plural, one {item} other {items}} reembolsado",
     "removed-items": "Itens removidos",
     "removed-items": "Itens removidos",
-    "search-by-customer-last-name": "",
-    "search-by-order-code": "Buscar por código do pedido",
+    "search-by-order-filters": "Buscar por código do pedido / Sobrenome / Código ID da transação",
     "select-state": "Selecionar estado",
     "select-state": "Selecionar estado",
     "set-coupon-codes": "",
     "set-coupon-codes": "",
     "set-fulfillment-state": "Marcar como {state}",
     "set-fulfillment-state": "Marcar como {state}",

+ 4 - 2
packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json

@@ -66,6 +66,7 @@
     "auto-update-product-variant-name": "Actualizar automaticamente os nomes das variantes do produto",
     "auto-update-product-variant-name": "Actualizar automaticamente os nomes das variantes do produto",
     "channel-price-preview": "Visualizar preço do canal",
     "channel-price-preview": "Visualizar preço do canal",
     "collection-contents": "Conteúdo da categoria",
     "collection-contents": "Conteúdo da categoria",
+    "confirm-cancel": "",
     "confirm-delete-administrator": "Eliminar administrador?",
     "confirm-delete-administrator": "Eliminar administrador?",
     "confirm-delete-assets": "Eliminar {count} {count, plural, one {imagem} other {imagens}}?",
     "confirm-delete-assets": "Eliminar {count} {count, plural, one {imagem} other {imagens}}?",
     "confirm-delete-channel": "Exluir canal?",
     "confirm-delete-channel": "Exluir canal?",
@@ -212,6 +213,7 @@
     "guest": "Convidado",
     "guest": "Convidado",
     "items-per-page-option": "{ count } por página",
     "items-per-page-option": "{ count } por página",
     "items-selected-count": "",
     "items-selected-count": "",
+    "keep-editing": "",
     "language": "Idioma",
     "language": "Idioma",
     "launch-extension": "Iniciar extensão",
     "launch-extension": "Iniciar extensão",
     "live-update": "Actualização em tempo real",
     "live-update": "Actualização em tempo real",
@@ -324,6 +326,7 @@
     "registered": "Registado",
     "registered": "Registado",
     "remove-customers-from-group-success": "{customerCount, plural, one {1 cliente eliminado} other {{customerCount} clientes eliminados}} do grupo \"{ groupName }\"",
     "remove-customers-from-group-success": "{customerCount, plural, one {1 cliente eliminado} other {{customerCount} clientes eliminados}} do grupo \"{ groupName }\"",
     "remove-from-group": "Eliminar deste grupo",
     "remove-from-group": "Eliminar deste grupo",
+    "search-by-group-name": "Pesquisar termo",
     "search-customers-by-email": "Pesquisar por email",
     "search-customers-by-email": "Pesquisar por email",
     "search-customers-by-email-last-name-postal-code": "Pesquisar pelo apelido / email / código postal",
     "search-customers-by-email-last-name-postal-code": "Pesquisar pelo apelido / email / código postal",
     "select-customer": "Seleccione o cliente",
     "select-customer": "Seleccione o cliente",
@@ -553,8 +556,7 @@
     "refund-with-amount": "Reembolso {amount}",
     "refund-with-amount": "Reembolso {amount}",
     "refunded-count": "{count} {count, plural, one {item reembolsado} other {itens reembolsados}}",
     "refunded-count": "{count} {count, plural, one {item reembolsado} other {itens reembolsados}}",
     "removed-items": "Itens removidos",
     "removed-items": "Itens removidos",
-    "search-by-customer-last-name": "Pesquisar pelo apelido do cliente",
-    "search-by-order-code": "Pesqusiar pelo código da encomenda",
+    "search-by-order-filters": "Pesqusiar pelo código da encomenda / apelido do cliente / ID da transação",
     "select-state": "Seleccionar estado",
     "select-state": "Seleccionar estado",
     "set-coupon-codes": "",
     "set-coupon-codes": "",
     "set-fulfillment-state": "Marcar como {state}",
     "set-fulfillment-state": "Marcar como {state}",

+ 4 - 2
packages/admin-ui/src/lib/static/i18n-messages/ru.json

@@ -66,6 +66,7 @@
     "auto-update-product-variant-name": "Автоматически обновлять названия вариантов товара",
     "auto-update-product-variant-name": "Автоматически обновлять названия вариантов товара",
     "channel-price-preview": "Предварительный просмотр цен канала",
     "channel-price-preview": "Предварительный просмотр цен канала",
     "collection-contents": "Содержание коллекции",
     "collection-contents": "Содержание коллекции",
+    "confirm-cancel": "",
     "confirm-delete-administrator": "Удалить администратора?",
     "confirm-delete-administrator": "Удалить администратора?",
     "confirm-delete-assets": "Удалить {count} {count, plural, one {медиа-объект} other {медиа-объектов}}?",
     "confirm-delete-assets": "Удалить {count} {count, plural, one {медиа-объект} other {медиа-объектов}}?",
     "confirm-delete-channel": "Удалить канал?",
     "confirm-delete-channel": "Удалить канал?",
@@ -212,6 +213,7 @@
     "guest": "Гость",
     "guest": "Гость",
     "items-per-page-option": "{ count } на странице",
     "items-per-page-option": "{ count } на странице",
     "items-selected-count": "",
     "items-selected-count": "",
+    "keep-editing": "",
     "language": "Язык",
     "language": "Язык",
     "launch-extension": "Запуск расширения",
     "launch-extension": "Запуск расширения",
     "live-update": "Обновление в режиме реального времени",
     "live-update": "Обновление в режиме реального времени",
@@ -324,6 +326,7 @@
     "registered": "Зарегистрировано",
     "registered": "Зарегистрировано",
     "remove-customers-from-group-success": "Удален {customerCount, plural, one {1 клиент} other {{customerCount} клиентов}} из \"{ groupName }\"",
     "remove-customers-from-group-success": "Удален {customerCount, plural, one {1 клиент} other {{customerCount} клиентов}} из \"{ groupName }\"",
     "remove-from-group": "Удалить из этой группы",
     "remove-from-group": "Удалить из этой группы",
+    "search-by-group-name": "Искать по фразе",
     "search-customers-by-email": "Поиск по адресу электронной почты",
     "search-customers-by-email": "Поиск по адресу электронной почты",
     "search-customers-by-email-last-name-postal-code": "",
     "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "Выберите клиента",
     "select-customer": "Выберите клиента",
@@ -553,8 +556,7 @@
     "refund-with-amount": "Возврат {amount}",
     "refund-with-amount": "Возврат {amount}",
     "refunded-count": "{count} {count, plural, one {позиция} other {позиций}} возвращен",
     "refunded-count": "{count} {count, plural, one {позиция} other {позиций}} возвращен",
     "removed-items": "Удаленные позиции",
     "removed-items": "Удаленные позиции",
-    "search-by-customer-last-name": "Поиск по фамилии клиента",
-    "search-by-order-code": "Поиск по коду заказа",
+    "search-by-order-filters": "Поиск по фамилии клиента / коду заказа / ID транзакции",
     "select-state": "Выберите состояние",
     "select-state": "Выберите состояние",
     "set-coupon-codes": "",
     "set-coupon-codes": "",
     "set-fulfillment-state": "Отметить как {state}",
     "set-fulfillment-state": "Отметить как {state}",

+ 4 - 2
packages/admin-ui/src/lib/static/i18n-messages/uk.json

@@ -66,6 +66,7 @@
     "auto-update-product-variant-name": "Автоматично оновлювати назви варіантів товару",
     "auto-update-product-variant-name": "Автоматично оновлювати назви варіантів товару",
     "channel-price-preview": "Попередній перегляд цін каналу",
     "channel-price-preview": "Попередній перегляд цін каналу",
     "collection-contents": "Зміст колекції",
     "collection-contents": "Зміст колекції",
+    "confirm-cancel": "",
     "confirm-delete-administrator": "Видалити адміністратора?",
     "confirm-delete-administrator": "Видалити адміністратора?",
     "confirm-delete-assets": "Видалити {count} {count, plural, one {медіа-об'єкт} other {медіа-об'єктів}}?",
     "confirm-delete-assets": "Видалити {count} {count, plural, one {медіа-об'єкт} other {медіа-об'єктів}}?",
     "confirm-delete-channel": "Видалити канал?",
     "confirm-delete-channel": "Видалити канал?",
@@ -212,6 +213,7 @@
     "guest": "Гість",
     "guest": "Гість",
     "items-per-page-option": "{ count } на сторінці",
     "items-per-page-option": "{ count } на сторінці",
     "items-selected-count": "",
     "items-selected-count": "",
+    "keep-editing": "",
     "language": "Мова",
     "language": "Мова",
     "launch-extension": "Запуск розширення",
     "launch-extension": "Запуск розширення",
     "live-update": "Оновлення в режимі реального часу",
     "live-update": "Оновлення в режимі реального часу",
@@ -324,6 +326,7 @@
     "registered": "Зареєстровано",
     "registered": "Зареєстровано",
     "remove-customers-from-group-success": "Видалено {customerCount, plural, one {1 клієнт} other {{customerCount} клієнтів}} из \"{ groupName }\"",
     "remove-customers-from-group-success": "Видалено {customerCount, plural, one {1 клієнт} other {{customerCount} клієнтів}} из \"{ groupName }\"",
     "remove-from-group": "Вилучити з цієї групи",
     "remove-from-group": "Вилучити з цієї групи",
+    "search-by-group-name": "Шукати по фразі",
     "search-customers-by-email": "Пошук за адресою електронної пошти",
     "search-customers-by-email": "Пошук за адресою електронної пошти",
     "search-customers-by-email-last-name-postal-code": "",
     "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "Виберіть клієнта",
     "select-customer": "Виберіть клієнта",
@@ -553,8 +556,7 @@
     "refund-with-amount": "Повернення {amount}",
     "refund-with-amount": "Повернення {amount}",
     "refunded-count": "{count} {count, plural, one {позиція} other {позицій}} повернено",
     "refunded-count": "{count} {count, plural, one {позиція} other {позицій}} повернено",
     "removed-items": "Видалені позиції",
     "removed-items": "Видалені позиції",
-    "search-by-customer-last-name": "Пошук за прізвищем клієнта",
-    "search-by-order-code": "Пошук по коду замовлення",
+    "search-by-order-filters": "Пошук за коду замовлення / прізвищем клієнта / ID транзакції",
     "select-state": "Виберіть стан",
     "select-state": "Виберіть стан",
     "set-coupon-codes": "",
     "set-coupon-codes": "",
     "set-fulfillment-state": "Помітити як {state}",
     "set-fulfillment-state": "Помітити як {state}",

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

@@ -66,6 +66,7 @@
     "auto-update-product-variant-name": "自动更新不同商品变体名称",
     "auto-update-product-variant-name": "自动更新不同商品变体名称",
     "channel-price-preview": "渠道价格预览",
     "channel-price-preview": "渠道价格预览",
     "collection-contents": "系列产品",
     "collection-contents": "系列产品",
+    "confirm-cancel": "",
     "confirm-delete-administrator": "确认删除管理员吗?",
     "confirm-delete-administrator": "确认删除管理员吗?",
     "confirm-delete-assets": "确认删除{count}个资源吗?",
     "confirm-delete-assets": "确认删除{count}个资源吗?",
     "confirm-delete-channel": "确认删除销售渠道?",
     "confirm-delete-channel": "确认删除销售渠道?",
@@ -212,6 +213,7 @@
     "guest": "游客",
     "guest": "游客",
     "items-per-page-option": "每页显示 { count } 条",
     "items-per-page-option": "每页显示 { count } 条",
     "items-selected-count": "",
     "items-selected-count": "",
+    "keep-editing": "",
     "language": "语言",
     "language": "语言",
     "launch-extension": "启动扩展插件",
     "launch-extension": "启动扩展插件",
     "live-update": "在线更新",
     "live-update": "在线更新",
@@ -324,6 +326,7 @@
     "registered": "已注册",
     "registered": "已注册",
     "remove-customers-from-group-success": "成功从分组\"{ groupName }\"中移除{customerCount}个客户",
     "remove-customers-from-group-success": "成功从分组\"{ groupName }\"中移除{customerCount}个客户",
     "remove-from-group": "从分组中移除",
     "remove-from-group": "从分组中移除",
+    "search-by-group-name": "输入搜索条目",
     "search-customers-by-email": "输入要搜索的客户邮件地址",
     "search-customers-by-email": "输入要搜索的客户邮件地址",
     "search-customers-by-email-last-name-postal-code": "",
     "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "选择客户",
     "select-customer": "选择客户",
@@ -553,8 +556,7 @@
     "refund-with-amount": "退款金额{amount}",
     "refund-with-amount": "退款金额{amount}",
     "refunded-count": "{count}个商品已退款",
     "refunded-count": "{count}个商品已退款",
     "removed-items": "",
     "removed-items": "",
-    "search-by-customer-last-name": "",
-    "search-by-order-code": "输入要搜索的订单编号",
+    "search-by-order-filters": "输入要搜索的订单编号 / 姓 / 交易ID ",
     "select-state": "",
     "select-state": "",
     "set-coupon-codes": "",
     "set-coupon-codes": "",
     "set-fulfillment-state": "",
     "set-fulfillment-state": "",

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

@@ -66,6 +66,7 @@
     "auto-update-product-variant-name": "",
     "auto-update-product-variant-name": "",
     "channel-price-preview": "渠道價格覽",
     "channel-price-preview": "渠道價格覽",
     "collection-contents": "系列產品",
     "collection-contents": "系列產品",
+    "confirm-cancel": "",
     "confirm-delete-administrator": "",
     "confirm-delete-administrator": "",
     "confirm-delete-assets": "",
     "confirm-delete-assets": "",
     "confirm-delete-channel": "確認移除渠道?",
     "confirm-delete-channel": "確認移除渠道?",
@@ -212,6 +213,7 @@
     "guest": "游客",
     "guest": "游客",
     "items-per-page-option": "每页顯示 { count } 條",
     "items-per-page-option": "每页顯示 { count } 條",
     "items-selected-count": "",
     "items-selected-count": "",
+    "keep-editing": "",
     "language": "語言",
     "language": "語言",
     "launch-extension": "启動扩展插件",
     "launch-extension": "启動扩展插件",
     "live-update": "",
     "live-update": "",
@@ -324,6 +326,7 @@
     "registered": "已注册",
     "registered": "已注册",
     "remove-customers-from-group-success": "",
     "remove-customers-from-group-success": "",
     "remove-from-group": "",
     "remove-from-group": "",
+    "search-by-group-name": "輸入搜索條目",
     "search-customers-by-email": "輸入要搜索的客户電郵地址",
     "search-customers-by-email": "輸入要搜索的客户電郵地址",
     "search-customers-by-email-last-name-postal-code": "",
     "search-customers-by-email-last-name-postal-code": "",
     "select-customer": "",
     "select-customer": "",
@@ -553,8 +556,7 @@
     "refund-with-amount": "退款金額{amount}",
     "refund-with-amount": "退款金額{amount}",
     "refunded-count": "{count}個商品已退款",
     "refunded-count": "{count}個商品已退款",
     "removed-items": "",
     "removed-items": "",
-    "search-by-customer-last-name": "",
-    "search-by-order-code": "輸入要搜索的訂單編號",
+    "search-by-order-filters": "輸入要搜索的訂單編號 / 姓 / 交易編號 ",
     "select-state": "",
     "select-state": "",
     "set-coupon-codes": "",
     "set-coupon-codes": "",
     "set-fulfillment-state": "",
     "set-fulfillment-state": "",

+ 3 - 0
packages/admin-ui/src/lib/static/styles/_variables.scss

@@ -1,2 +1,5 @@
+@import "~@clr/ui/src/utils/_mixins";
+@import "~@clr/ui/src/utils/_variables.clarity.scss";
+
 // breakpoints
 // breakpoints
 $breakpoint-small: 768px;
 $breakpoint-small: 768px;

+ 3 - 0
packages/admin-ui/src/lib/static/styles/global/_sass-overrides.scss

@@ -0,0 +1,3 @@
+// Note: variables defined in this file should use the default! flag so they can be
+// overridden by custom Admin UI applications
+$clr-sidenav-width: 10.8rem !default;

+ 1 - 0
packages/admin-ui/src/lib/static/styles/styles.scss

@@ -1,3 +1,4 @@
+@import "global/sass-overrides";
 @import "~@clr/icons/clr-icons.min.css";
 @import "~@clr/icons/clr-icons.min.css";
 @import "~@clr/ui/src/utils/components.clarity";
 @import "~@clr/ui/src/utils/components.clarity";
 @import "~@ng-select/ng-select/themes/default.theme.css";
 @import "~@ng-select/ng-select/themes/default.theme.css";

+ 1 - 0
packages/admin-ui/src/lib/static/styles/ui-extension-theme.scss

@@ -1,6 +1,7 @@
 // This file is used to compile the theme.min.css file which can be used
 // This file is used to compile the theme.min.css file which can be used
 // by ui extensions to get the same styles as the main app.
 // by ui extensions to get the same styles as the main app.
 
 
+@import "global/sass-overrides";
 // Clarity Component SCSS
 // Clarity Component SCSS
 @import "~@clr/ui/src/main";
 @import "~@clr/ui/src/main";
 @import "global/overrides";
 @import "global/overrides";

BIN
packages/core/e2e/fixtures/assets/guitar.jpg


+ 2 - 2
packages/core/e2e/fixtures/e2e-product-import-asset-urls.csv

@@ -1,2 +1,2 @@
-name  ,slug  ,description,assets                          ,facets,optionGroups,optionValues,sku    ,price ,taxCategory,stockOnHand,trackInventory,variantAssets,variantFacets,product:pageType,variant:weight,product:owner,product:keywords,product:localName
-Guitar,guitar,A guitar.  ,http://localhost:3456/guitar.png,      ,            ,            ,GUIT001,133.99,standard   ,0          ,false         ,             ,             ,default         ,100           ,             ,music|instrument,
+name  ,slug  ,description  ,assets                          ,facets,optionGroups,optionValues,sku     ,price,taxCategory,stockOnHand,trackInventory,variantAssets,variantFacets
+Guitar,guitar,A nice guitar,http://localhost:3456/guitar.jpg,      ,            ,            ,GUIT0001,8999 ,standard   ,25         ,true          ,             ,

+ 2 - 2
packages/core/e2e/fixtures/test-payment-methods.ts

@@ -31,7 +31,7 @@ export const twoStagePaymentMethod = new PaymentMethodHandler({
         return {
         return {
             amount,
             amount,
             state: 'Authorized',
             state: 'Authorized',
-            transactionId: '12345',
+            transactionId: '12345-' + order.code,
             metadata: { public: metadata },
             metadata: { public: metadata },
         };
         };
     },
     },
@@ -143,7 +143,7 @@ export const failsToSettlePaymentMethod = new PaymentMethodHandler({
         return {
         return {
             amount,
             amount,
             state: 'Authorized',
             state: 'Authorized',
-            transactionId: '12345',
+            transactionId: '12345-' + order.code,
             metadata: {
             metadata: {
                 privateCreatePaymentData: 'secret',
                 privateCreatePaymentData: 'secret',
                 public: {
                 public: {

+ 16 - 1
packages/core/e2e/fixtures/test-plugins/with-api-extensions.ts

@@ -1,5 +1,6 @@
 import { Query, Resolver } from '@nestjs/graphql';
 import { Query, Resolver } from '@nestjs/graphql';
 import { VendurePlugin } from '@vendure/core';
 import { VendurePlugin } from '@vendure/core';
+import { GraphQLScalarType } from 'graphql';
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
 
 
 @Resolver()
 @Resolver()
@@ -12,7 +13,7 @@ export class TestAdminPluginResolver {
     @Query()
     @Query()
     barList() {
     barList() {
         return {
         return {
-            items: [{ id: 1, name: 'Test' }],
+            items: [{ id: 1, name: 'Test', pizzaType: 'Cheese' }],
             totalItems: 1,
             totalItems: 1,
         };
         };
     }
     }
@@ -26,6 +27,17 @@ export class TestShopPluginResolver {
     }
     }
 }
 }
 
 
+const PizzaScalar = new GraphQLScalarType({
+    name: 'Pizza',
+    description: 'Everything is pizza',
+    serialize(value) {
+        return value.toString() + ' pizza!';
+    },
+    parseValue(value) {
+        return value;
+    },
+});
+
 @VendurePlugin({
 @VendurePlugin({
     shopApiExtensions: {
     shopApiExtensions: {
         resolvers: [TestShopPluginResolver],
         resolvers: [TestShopPluginResolver],
@@ -38,6 +50,7 @@ export class TestShopPluginResolver {
     adminApiExtensions: {
     adminApiExtensions: {
         resolvers: [TestAdminPluginResolver],
         resolvers: [TestAdminPluginResolver],
         schema: gql`
         schema: gql`
+            scalar Pizza
             extend type Query {
             extend type Query {
                 foo: [String]!
                 foo: [String]!
                 barList(options: BarListOptions): BarList!
                 barList(options: BarListOptions): BarList!
@@ -47,12 +60,14 @@ export class TestShopPluginResolver {
             type Bar implements Node {
             type Bar implements Node {
                 id: ID!
                 id: ID!
                 name: String!
                 name: String!
+                pizzaType: Pizza!
             }
             }
             type BarList implements PaginatedList {
             type BarList implements PaginatedList {
                 items: [Bar!]!
                 items: [Bar!]!
                 totalItems: Int!
                 totalItems: Int!
             }
             }
         `,
         `,
+        scalars: { Pizza: PizzaScalar },
     },
     },
 })
 })
 export class TestAPIExtensionPlugin {}
 export class TestAPIExtensionPlugin {}

+ 2 - 0
packages/core/e2e/fixtures/test-plugins/with-config.ts

@@ -6,10 +6,12 @@ import { ConfigModule, VendurePlugin } from '@vendure/core';
     configuration: config => {
     configuration: config => {
         // tslint:disable-next-line:no-non-null-assertion
         // tslint:disable-next-line:no-non-null-assertion
         config.defaultLanguageCode = LanguageCode.zh;
         config.defaultLanguageCode = LanguageCode.zh;
+        TestPluginWithConfig.configSpy();
         return config;
         return config;
     },
     },
 })
 })
 export class TestPluginWithConfig {
 export class TestPluginWithConfig {
+    static configSpy = jest.fn();
     static setup() {
     static setup() {
         return TestPluginWithConfig;
         return TestPluginWithConfig;
     }
     }

+ 1 - 1
packages/core/e2e/import.e2e-spec.ts

@@ -498,7 +498,7 @@ describe('Import resolver', () => {
 
 
             expect(productResult.products.items.length).toBe(1);
             expect(productResult.products.items.length).toBe(1);
             expect(productResult.products.items[0].featuredAsset.preview).toBe(
             expect(productResult.products.items[0].featuredAsset.preview).toBe(
-                'test-url/test-assets/guitar__preview.png',
+                'test-url/test-assets/guitar__preview.jpg',
             );
             );
         });
         });
     });
     });

+ 1 - 1
packages/core/e2e/list-query-builder.e2e-spec.ts

@@ -964,7 +964,7 @@ describe('ListQueryBuilder', () => {
     });
     });
 
 
     // https://github.com/vendure-ecommerce/vendure/issues/1611
     // https://github.com/vendure-ecommerce/vendure/issues/1611
-    xdescribe('translations handling', () => {
+    describe('translations handling', () => {
         const allTranslations = [
         const allTranslations = [
             [
             [
                 { languageCode: LanguageCode.en, name: 'apple' },
                 { languageCode: LanguageCode.en, name: 'apple' },

+ 17 - 0
packages/core/e2e/order.e2e-spec.ts

@@ -560,6 +560,23 @@ describe('Orders resolver', () => {
                 },
                 },
             ]);
             ]);
         });
         });
+
+        it('filter by transactionId', async () => {
+            const result = await adminClient.query<GetOrderList.Query, GetOrderList.Variables>(
+                GET_ORDERS_LIST,
+                {
+                    options: {
+                        filter: {
+                            transactionId: {
+                                eq: '12345-' + firstOrderCode,
+                            },
+                        },
+                    },
+                },
+            );
+            expect(result.orders.totalItems).toEqual(1);
+            expect(result.orders.items[0].code).toBe(firstOrderCode);
+        });
     });
     });
 
 
     describe('fulfillment', () => {
     describe('fulfillment', () => {

+ 38 - 0
packages/core/e2e/plugin.e2e-spec.ts

@@ -48,6 +48,7 @@ describe('Plugins', () => {
         const configService = server.app.get(ConfigService);
         const configService = server.app.get(ConfigService);
         expect(configService instanceof ConfigService).toBe(true);
         expect(configService instanceof ConfigService).toBe(true);
         expect(configService.defaultLanguageCode).toBe(LanguageCode.zh);
         expect(configService.defaultLanguageCode).toBe(LanguageCode.zh);
+        expect(TestPluginWithConfig.configSpy).toHaveBeenCalledTimes(1);
     });
     });
 
 
     it('extends the admin API', async () => {
     it('extends the admin API', async () => {
@@ -68,6 +69,22 @@ describe('Plugins', () => {
         expect(result.baz).toEqual(['quux']);
         expect(result.baz).toEqual(['quux']);
     });
     });
 
 
+    it('custom scalar', async () => {
+        const result = await adminClient.query(gql`
+            query {
+                barList(options: { skip: 0, take: 1 }) {
+                    items {
+                        id
+                        pizzaType
+                    }
+                }
+            }
+        `);
+        expect(result.barList).toEqual({
+            items: [{ id: 'T_1', pizzaType: 'Cheese pizza!' }],
+        });
+    });
+
     it('allows lazy evaluation of API extension', async () => {
     it('allows lazy evaluation of API extension', async () => {
         const result = await shopClient.query(gql`
         const result = await shopClient.query(gql`
             query {
             query {
@@ -137,3 +154,24 @@ describe('Plugins', () => {
         });
         });
     });
     });
 });
 });
+
+describe('Multiple bootstraps in same process', () => {
+    const activeConfig = testConfig();
+    const { server, adminClient, shopClient } = createTestEnvironment({
+        ...activeConfig,
+        plugins: [TestPluginWithConfig.setup()],
+    });
+
+    beforeAll(async () => {
+        await server.init({
+            initialData,
+            productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
+            customerCount: 1,
+        });
+        await adminClient.asSuperAdmin();
+    }, TEST_SETUP_TIMEOUT_MS);
+
+    it('plugin `configure` function called only once', async () => {
+        expect(TestPluginWithConfig.configSpy).toHaveBeenCalledTimes(1);
+    });
+});

+ 152 - 15
packages/core/e2e/product.e2e-spec.ts

@@ -21,6 +21,8 @@ import {
     GET_PRODUCT_LIST,
     GET_PRODUCT_LIST,
     GET_PRODUCT_SIMPLE,
     GET_PRODUCT_SIMPLE,
     GET_PRODUCT_WITH_VARIANTS,
     GET_PRODUCT_WITH_VARIANTS,
+    UPDATE_CHANNEL,
+    UPDATE_GLOBAL_SETTINGS,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_VARIANTS,
     UPDATE_PRODUCT_VARIANTS,
 } from './graphql/shared-definitions';
 } from './graphql/shared-definitions';
@@ -29,12 +31,18 @@ import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 // tslint:disable:no-non-null-assertion
 // tslint:disable:no-non-null-assertion
 
 
 describe('Product resolver', () => {
 describe('Product resolver', () => {
-    const { server, adminClient, shopClient } = createTestEnvironment(testConfig());
+    const { server, adminClient, shopClient } = createTestEnvironment({
+        ...testConfig(),
+    });
 
 
     const removeOptionGuard: ErrorResultGuard<Codegen.ProductWithOptionsFragment> = createErrorResultGuard(
     const removeOptionGuard: ErrorResultGuard<Codegen.ProductWithOptionsFragment> = createErrorResultGuard(
         input => !!input.optionGroups,
         input => !!input.optionGroups,
     );
     );
 
 
+    const updateChannelGuard: ErrorResultGuard<Codegen.ChannelFragment> = createErrorResultGuard(
+        input => !!input.id,
+    );
+
     beforeAll(async () => {
     beforeAll(async () => {
         await server.init({
         await server.init({
             initialData,
             initialData,
@@ -1705,6 +1713,135 @@ describe('Product resolver', () => {
 
 
                 expect(product?.variants.length).toBe(1);
                 expect(product?.variants.length).toBe(1);
             });
             });
+
+            // https://github.com/vendure-ecommerce/vendure/issues/1631
+            describe('changing the Channel default language', () => {
+                let productId: string;
+
+                function getProductWithVariantsInLanguage(
+                    id: string,
+                    languageCode: LanguageCode,
+                    variantListOptions?: Codegen.ProductVariantListOptions,
+                ) {
+                    return adminClient.query<
+                        Codegen.GetProductWithVariantListQuery,
+                        Codegen.GetProductWithVariantListQueryVariables
+                    >(GET_PRODUCT_WITH_VARIANT_LIST, { id, variantListOptions }, { languageCode });
+                }
+
+                beforeAll(async () => {
+                    await adminClient.query<
+                        Codegen.UpdateGlobalSettingsMutation,
+                        Codegen.UpdateGlobalSettingsMutationVariables
+                    >(UPDATE_GLOBAL_SETTINGS, {
+                        input: {
+                            availableLanguages: [LanguageCode.en, LanguageCode.de],
+                        },
+                    });
+                    const { createProduct } = await adminClient.query<
+                        Codegen.CreateProductMutation,
+                        Codegen.CreateProductMutationVariables
+                    >(CREATE_PRODUCT, {
+                        input: {
+                            translations: [
+                                {
+                                    languageCode: LanguageCode.en,
+                                    name: 'Bottle',
+                                    slug: 'bottle',
+                                    description: 'A container for liquids',
+                                },
+                            ],
+                        },
+                    });
+
+                    productId = createProduct.id;
+                    await adminClient.query<
+                        Codegen.CreateProductVariantsMutation,
+                        Codegen.CreateProductVariantsMutationVariables
+                    >(CREATE_PRODUCT_VARIANTS, {
+                        input: [
+                            {
+                                productId,
+                                sku: 'BOTTLE111',
+                                optionIds: [],
+                                translations: [{ languageCode: LanguageCode.en, name: 'Bottle' }],
+                            },
+                        ],
+                    });
+                });
+
+                afterAll(async () => {
+                    // Restore the default language to English for the subsequent tests
+                    await adminClient.query<
+                        Codegen.UpdateChannelMutation,
+                        Codegen.UpdateChannelMutationVariables
+                    >(UPDATE_CHANNEL, {
+                        input: {
+                            id: 'T_1',
+                            defaultLanguageCode: LanguageCode.en,
+                        },
+                    });
+                });
+
+                it('returns all variants', async () => {
+                    const { product: product1 } = await adminClient.query<
+                        Codegen.GetProductWithVariantsQuery,
+                        Codegen.GetProductWithVariantsQueryVariables
+                    >(
+                        GET_PRODUCT_WITH_VARIANTS,
+                        {
+                            id: productId,
+                        },
+                        { languageCode: LanguageCode.en },
+                    );
+                    expect(product1?.variants.length).toBe(1);
+
+                    // Change the default language of the channel to "de"
+                    const { updateChannel } = await adminClient.query<
+                        Codegen.UpdateChannelMutation,
+                        Codegen.UpdateChannelMutationVariables
+                    >(UPDATE_CHANNEL, {
+                        input: {
+                            id: 'T_1',
+                            defaultLanguageCode: LanguageCode.de,
+                        },
+                    });
+                    updateChannelGuard.assertSuccess(updateChannel);
+                    expect(updateChannel.defaultLanguageCode).toBe(LanguageCode.de);
+
+                    // Fetch the product in en, it should still return 1 variant
+                    const { product: product2 } = await getProductWithVariantsInLanguage(
+                        productId,
+                        LanguageCode.en,
+                    );
+                    expect(product2?.variantList.items.length).toBe(1);
+
+                    // Fetch the product in de, it should still return 1 variant
+                    const { product: product3 } = await getProductWithVariantsInLanguage(
+                        productId,
+                        LanguageCode.de,
+                    );
+                    expect(product3?.variantList.items.length).toBe(1);
+                });
+
+                it('returns all variants when sorting on variant name', async () => {
+                    // Fetch the product in en, it should still return 1 variant
+                    const { product: product1 } = await getProductWithVariantsInLanguage(
+                        productId,
+                        LanguageCode.en,
+                        { sort: { name: SortOrder.ASC } },
+                    );
+                    expect(product1?.variantList.items.length).toBe(1);
+
+                    // Fetch the product in de, it should still return 1 variant
+                    const { product: product2 } = await getProductWithVariantsInLanguage(
+                        productId,
+                        LanguageCode.de,
+                        { sort: { name: SortOrder.ASC } },
+                    );
+                    expect(product2?.variantList.items.length).toBe(1);
+                });
+            });
         });
         });
     });
     });
 
 
@@ -1844,21 +1981,21 @@ describe('Product resolver', () => {
 
 
         // https://github.com/vendure-ecommerce/vendure/issues/1505
         // https://github.com/vendure-ecommerce/vendure/issues/1505
         it('attempting to re-use deleted slug twice is not allowed', async () => {
         it('attempting to re-use deleted slug twice is not allowed', async () => {
-            const result = await adminClient.query<CreateProduct.Mutation, CreateProduct.Variables>(
-                CREATE_PRODUCT,
-                {
-                    input: {
-                        translations: [
-                            {
-                                languageCode: LanguageCode.en,
-                                name: 'Product reusing deleted slug',
-                                slug: productToDelete.slug,
-                                description: 'stuff',
-                            },
-                        ],
-                    },
+            const result = await adminClient.query<
+                Codegen.CreateProductMutation,
+                Codegen.CreateProductMutationVariables
+            >(CREATE_PRODUCT, {
+                input: {
+                    translations: [
+                        {
+                            languageCode: LanguageCode.en,
+                            name: 'Product reusing deleted slug',
+                            slug: productToDelete.slug,
+                            description: 'stuff',
+                        },
+                    ],
                 },
                 },
-            );
+            });
 
 
             expect(result.createProduct.slug).not.toBe(productToDelete.slug);
             expect(result.createProduct.slug).not.toBe(productToDelete.slug);
             expect(result.createProduct.slug).toBe('laptop-2');
             expect(result.createProduct.slug).toBe('laptop-2');

+ 15 - 2
packages/core/src/api/config/generate-resolvers.ts

@@ -13,6 +13,7 @@ import { shopErrorOperationTypeResolvers } from '../../common/error/generated-gr
 import { Translatable } from '../../common/types/locale-types';
 import { Translatable } from '../../common/types/locale-types';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
 import { CustomFieldConfig, RelationCustomFieldConfig } from '../../config/custom-field/custom-field-types';
 import { CustomFieldConfig, RelationCustomFieldConfig } from '../../config/custom-field/custom-field-types';
+import { getPluginAPIExtensions } from '../../plugin/plugin-metadata';
 import { CustomFieldRelationResolverService } from '../common/custom-field-relation-resolver.service';
 import { CustomFieldRelationResolverService } from '../common/custom-field-relation-resolver.service';
 import { ApiType } from '../common/get-api-type';
 import { ApiType } from '../common/get-api-type';
 import { RequestContext } from '../common/request-context';
 import { RequestContext } from '../common/request-context';
@@ -118,8 +119,8 @@ export function generateResolvers(
 
 
     const resolvers =
     const resolvers =
         apiType === 'admin'
         apiType === 'admin'
-            ? { ...commonResolvers, ...adminResolvers }
-            : { ...commonResolvers, ...shopResolvers };
+            ? { ...commonResolvers, ...adminResolvers, ...getCustomScalars(configService, 'admin') }
+            : { ...commonResolvers, ...shopResolvers, ...getCustomScalars(configService, 'shop') };
     return resolvers;
     return resolvers;
 }
 }
 
 
@@ -209,6 +210,18 @@ function generateCustomFieldRelationResolvers(
     return { adminResolvers, shopResolvers };
     return { adminResolvers, shopResolvers };
 }
 }
 
 
+function getCustomScalars(configService: ConfigService, apiType: 'admin' | 'shop') {
+    return getPluginAPIExtensions(configService.plugins, apiType)
+        .map(e => (typeof e.scalars === 'function' ? e.scalars() : e.scalars ?? {}))
+        .reduce(
+            (all, scalarMap) => ({
+                ...all,
+                ...scalarMap,
+            }),
+            {},
+        );
+}
+
 function isRelationalType(input: CustomFieldConfig): input is RelationCustomFieldConfig {
 function isRelationalType(input: CustomFieldConfig): input is RelationCustomFieldConfig {
     return input.type === 'relation';
     return input.type === 'relation';
 }
 }

+ 2 - 0
packages/core/src/api/schema/admin-api/order.api.graphql

@@ -39,10 +39,12 @@ type Order {
 
 
 input OrderFilterParameter {
 input OrderFilterParameter {
     customerLastName: StringOperators
     customerLastName: StringOperators
+    transactionId: StringOperators
 }
 }
 
 
 input OrderSortParameter {
 input OrderSortParameter {
     customerLastName: SortOrder
     customerLastName: SortOrder
+    transactionId: SortOrder
 }
 }
 
 
 # generated by generateListOptions function
 # generated by generateListOptions function

+ 13 - 3
packages/core/src/bootstrap.ts

@@ -98,9 +98,7 @@ export async function bootstrap(userConfig: Partial<VendureConfig>): Promise<INe
 export async function bootstrapWorker(userConfig: Partial<VendureConfig>): Promise<VendureWorker> {
 export async function bootstrapWorker(userConfig: Partial<VendureConfig>): Promise<VendureWorker> {
     const vendureConfig = await preBootstrapConfig(userConfig);
     const vendureConfig = await preBootstrapConfig(userConfig);
     const config = disableSynchronize(vendureConfig);
     const config = disableSynchronize(vendureConfig);
-    if (config.logger instanceof DefaultLogger) {
-        config.logger.setDefaultContext('Vendure Worker');
-    }
+    config.logger.setDefaultContext?.('Vendure Worker');
     Logger.useLogger(config.logger);
     Logger.useLogger(config.logger);
     Logger.info(`Bootstrapping Vendure Worker (pid: ${process.pid})...`);
     Logger.info(`Bootstrapping Vendure Worker (pid: ${process.pid})...`);
 
 
@@ -156,6 +154,13 @@ export async function preBootstrapConfig(
     return config;
     return config;
 }
 }
 
 
+// This is here to prevent a plugin's `configure` function from executing more than once in the
+// same process. Running more than once can occur e.g. if a script runs the `runMigrations()` function
+// followed by the `bootstrap()` function, and will lead to very hard-to-debug errors caused by
+// mutating the config object twice, e.g. plugins pushing custom fields again resulting in duplicate
+// custom field definitions.
+const pluginConfigDidRun = new WeakSet<RuntimeVendureConfig['plugins'][number]>();
+
 /**
 /**
  * Initialize any configured plugins.
  * Initialize any configured plugins.
  */
  */
@@ -163,7 +168,12 @@ async function runPluginConfigurations(config: RuntimeVendureConfig): Promise<Ru
     for (const plugin of config.plugins) {
     for (const plugin of config.plugins) {
         const configFn = getConfigurationFunction(plugin);
         const configFn = getConfigurationFunction(plugin);
         if (typeof configFn === 'function') {
         if (typeof configFn === 'function') {
+            const configAlreadyRan = pluginConfigDidRun.has(plugin);
+            if (configAlreadyRan) {
+                continue;
+            }
             config = await configFn(config);
             config = await configFn(config);
+            pluginConfigDidRun.add(plugin);
         }
         }
     }
     }
     return config;
     return config;

+ 3 - 1
packages/core/src/config/fulfillment/custom-fulfillment-process.ts

@@ -6,6 +6,7 @@ import {
 } from '../../common/finite-state-machine/types';
 } from '../../common/finite-state-machine/types';
 import { InjectableStrategy } from '../../common/types/injectable-strategy';
 import { InjectableStrategy } from '../../common/types/injectable-strategy';
 import {
 import {
+    CustomFulfillmentStates,
     FulfillmentState,
     FulfillmentState,
     FulfillmentTransitionData,
     FulfillmentTransitionData,
 } from '../../service/helpers/fulfillment-state-machine/fulfillment-state';
 } from '../../service/helpers/fulfillment-state-machine/fulfillment-state';
@@ -18,7 +19,8 @@ import {
  *
  *
  * @docsCategory fulfillment
  * @docsCategory fulfillment
  */
  */
-export interface CustomFulfillmentProcess<State extends string> extends InjectableStrategy {
+export interface CustomFulfillmentProcess<State extends keyof CustomFulfillmentStates | string>
+    extends InjectableStrategy {
     transitions?: Transitions<State, State | FulfillmentState> &
     transitions?: Transitions<State, State | FulfillmentState> &
         Partial<Transitions<FulfillmentState | State>>;
         Partial<Transitions<FulfillmentState | State>>;
     onTransitionStart?: OnTransitionStartFn<State | FulfillmentState, FulfillmentTransitionData>;
     onTransitionStart?: OnTransitionStartFn<State | FulfillmentState, FulfillmentTransitionData>;

+ 1 - 0
packages/core/src/config/logger/vendure-logger.ts

@@ -50,6 +50,7 @@ export interface VendureLogger {
     info(message: string, context?: string): void;
     info(message: string, context?: string): void;
     verbose(message: string, context?: string): void;
     verbose(message: string, context?: string): void;
     debug(message: string, context?: string): void;
     debug(message: string, context?: string): void;
+    setDefaultContext?(defaultContext: string): void;
 }
 }
 
 
 const noopLogger: VendureLogger = {
 const noopLogger: VendureLogger = {

+ 7 - 2
packages/core/src/config/order/custom-order-process.ts

@@ -6,7 +6,11 @@ import {
     Transitions,
     Transitions,
 } from '../../common/finite-state-machine/types';
 } from '../../common/finite-state-machine/types';
 import { InjectableStrategy } from '../../common/types/injectable-strategy';
 import { InjectableStrategy } from '../../common/types/injectable-strategy';
-import { OrderState, OrderTransitionData } from '../../service/helpers/order-state-machine/order-state';
+import {
+    CustomOrderStates,
+    OrderState,
+    OrderTransitionData,
+} from '../../service/helpers/order-state-machine/order-state';
 
 
 /**
 /**
  * @description
  * @description
@@ -16,7 +20,8 @@ import { OrderState, OrderTransitionData } from '../../service/helpers/order-sta
  *
  *
  * @docsCategory orders
  * @docsCategory orders
  */
  */
-export interface CustomOrderProcess<State extends string> extends InjectableStrategy {
+export interface CustomOrderProcess<State extends keyof CustomOrderStates | string>
+    extends InjectableStrategy {
     transitions?: Transitions<State, State | OrderState> & Partial<Transitions<OrderState | State>>;
     transitions?: Transitions<State, State | OrderState> & Partial<Transitions<OrderState | State>>;
     onTransitionStart?: OnTransitionStartFn<State | OrderState, OrderTransitionData>;
     onTransitionStart?: OnTransitionStartFn<State | OrderState, OrderTransitionData>;
     onTransitionEnd?: OnTransitionEndFn<State | OrderState, OrderTransitionData>;
     onTransitionEnd?: OnTransitionEndFn<State | OrderState, OrderTransitionData>;

+ 3 - 1
packages/core/src/config/payment/custom-payment-process.ts

@@ -6,6 +6,7 @@ import {
 } from '../../common/finite-state-machine/types';
 } from '../../common/finite-state-machine/types';
 import { InjectableStrategy } from '../../common/types/injectable-strategy';
 import { InjectableStrategy } from '../../common/types/injectable-strategy';
 import {
 import {
+    CustomPaymentStates,
     PaymentState,
     PaymentState,
     PaymentTransitionData,
     PaymentTransitionData,
 } from '../../service/helpers/payment-state-machine/payment-state';
 } from '../../service/helpers/payment-state-machine/payment-state';
@@ -18,7 +19,8 @@ import {
  *
  *
  * @docsCategory fulfillment
  * @docsCategory fulfillment
  */
  */
-export interface CustomPaymentProcess<State extends string> extends InjectableStrategy {
+export interface CustomPaymentProcess<State extends keyof CustomPaymentStates | string>
+    extends InjectableStrategy {
     transitions?: Transitions<State, State | PaymentState> & Partial<Transitions<PaymentState | State>>;
     transitions?: Transitions<State, State | PaymentState> & Partial<Transitions<PaymentState | State>>;
     onTransitionStart?: OnTransitionStartFn<State | PaymentState, PaymentTransitionData>;
     onTransitionStart?: OnTransitionStartFn<State | PaymentState, PaymentTransitionData>;
     onTransitionEnd?: OnTransitionEndFn<State | PaymentState, PaymentTransitionData>;
     onTransitionEnd?: OnTransitionEndFn<State | PaymentState, PaymentTransitionData>;

+ 6 - 0
packages/core/src/entity/order-line/order-line.entity.ts

@@ -6,6 +6,7 @@ import { Column, Entity, Index, ManyToOne, OneToMany } from 'typeorm';
 import { Calculated } from '../../common/calculated-decorator';
 import { Calculated } from '../../common/calculated-decorator';
 import { grossPriceOf, netPriceOf } from '../../common/tax-utils';
 import { grossPriceOf, netPriceOf } from '../../common/tax-utils';
 import { HasCustomFields } from '../../config/custom-field/custom-field-types';
 import { HasCustomFields } from '../../config/custom-field/custom-field-types';
+import { Logger } from '../../config/index';
 import { Asset } from '../asset/asset.entity';
 import { Asset } from '../asset/asset.entity';
 import { VendureEntity } from '../base/base.entity';
 import { VendureEntity } from '../base/base.entity';
 import { CustomOrderLineFields } from '../custom-entity-fields';
 import { CustomOrderLineFields } from '../custom-entity-fields';
@@ -266,6 +267,11 @@ export class OrderLine extends VendureEntity implements HasCustomFields {
      * Returns all non-cancelled OrderItems on this line.
      * Returns all non-cancelled OrderItems on this line.
      */
      */
     get activeItems(): OrderItem[] {
     get activeItems(): OrderItem[] {
+        if (this.items == null) {
+            Logger.warn(
+                `Attempted to access OrderLine.items without first joining the relation: { relations: ['items'] }`,
+            );
+        }
         return (this.items || []).filter(i => !i.cancelled);
         return (this.items || []).filter(i => !i.cancelled);
     }
     }
 
 

+ 1 - 1
packages/core/src/plugin/plugin-metadata.ts

@@ -67,7 +67,7 @@ export function graphQLResolversFor(
     return apiExtensions
     return apiExtensions
         ? typeof apiExtensions.resolvers === 'function'
         ? typeof apiExtensions.resolvers === 'function'
             ? apiExtensions.resolvers()
             ? apiExtensions.resolvers()
-            : apiExtensions.resolvers
+            : apiExtensions.resolvers ?? []
         : [];
         : [];
 }
 }
 
 

+ 11 - 2
packages/core/src/plugin/vendure-plugin.ts

@@ -4,7 +4,7 @@ import { ModuleMetadata } from '@nestjs/common/interfaces';
 import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
 import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
 import { pick } from '@vendure/common/lib/pick';
 import { pick } from '@vendure/common/lib/pick';
 import { Type } from '@vendure/common/lib/shared-types';
 import { Type } from '@vendure/common/lib/shared-types';
-import { DocumentNode } from 'graphql';
+import { DocumentNode, GraphQLScalarType } from 'graphql';
 
 
 import { RuntimeVendureConfig } from '../config/vendure-config';
 import { RuntimeVendureConfig } from '../config/vendure-config';
 
 
@@ -71,7 +71,16 @@ export interface APIExtensionDefinition {
      * An array of resolvers for the schema extensions. Should be defined as [Nestjs GraphQL resolver](https://docs.nestjs.com/graphql/resolvers-map)
      * An array of resolvers for the schema extensions. Should be defined as [Nestjs GraphQL resolver](https://docs.nestjs.com/graphql/resolvers-map)
      * classes, i.e. using the Nest `\@Resolver()` decorator etc.
      * classes, i.e. using the Nest `\@Resolver()` decorator etc.
      */
      */
-    resolvers: Array<Type<any>> | (() => Array<Type<any>>);
+    resolvers?: Array<Type<any>> | (() => Array<Type<any>>);
+    /**
+     * @description
+     * A map of GraphQL scalar types which should correspond to any custom scalars defined in your schema.
+     * Read more about defining custom scalars in the
+     * [Apollo Server Custom Scalars docs](https://www.apollographql.com/docs/apollo-server/schema/custom-scalars)
+     *
+     * @since 1.7.0
+     */
+    scalars?: Record<string, GraphQLScalarType> | (() => Record<string, GraphQLScalarType>);
 }
 }
 
 
 /**
 /**

+ 15 - 1
packages/core/src/service/helpers/fulfillment-state-machine/fulfillment-state.ts

@@ -3,13 +3,27 @@ import { Transitions } from '../../../common/finite-state-machine/types';
 import { Fulfillment } from '../../../entity/fulfillment/fulfillment.entity';
 import { Fulfillment } from '../../../entity/fulfillment/fulfillment.entity';
 import { Order } from '../../../entity/order/order.entity';
 import { Order } from '../../../entity/order/order.entity';
 
 
+/**
+ * @description
+ * An interface to extend standard {@link FulfillmentState}.
+ *
+ * @docsCategory fulfillment
+ */
+export interface CustomFulfillmentStates {}
+
 /**
 /**
  * @description
  * @description
  * These are the default states of the fulfillment process.
  * These are the default states of the fulfillment process.
  *
  *
  * @docsCategory fulfillment
  * @docsCategory fulfillment
  */
  */
-export type FulfillmentState = 'Created' | 'Pending' | 'Shipped' | 'Delivered' | 'Cancelled';
+export type FulfillmentState =
+    | 'Created'
+    | 'Pending'
+    | 'Shipped'
+    | 'Delivered'
+    | 'Cancelled'
+    | keyof CustomFulfillmentStates;
 
 
 export const fulfillmentStateTransitions: Transitions<FulfillmentState> = {
 export const fulfillmentStateTransitions: Transitions<FulfillmentState> = {
     Created: {
     Created: {

+ 11 - 2
packages/core/src/service/helpers/list-query-builder/connection-utils.ts

@@ -2,7 +2,8 @@ import { Type } from '@vendure/common/lib/shared-types';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
 import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
 
 
-import { CalculatedColumnDefinition, CALCULATED_PROPERTIES } from '../../../common/calculated-decorator';
+import { Translation } from '../../../common/index';
+import { VendureEntity } from '../../../entity/index';
 
 
 /**
 /**
  * @description
  * @description
@@ -16,9 +17,17 @@ export function getColumnMetadata<T>(connection: Connection, entity: Type<T>) {
 
 
     const translationRelation = relations.find(r => r.propertyName === 'translations');
     const translationRelation = relations.find(r => r.propertyName === 'translations');
     if (translationRelation) {
     if (translationRelation) {
+        const commonFields: Array<keyof (Translation<T> & VendureEntity)> = [
+            'id',
+            'createdAt',
+            'updatedAt',
+            'languageCode',
+        ];
         const translationMetadata = connection.getMetadata(translationRelation.type);
         const translationMetadata = connection.getMetadata(translationRelation.type);
         translationColumns = translationColumns.concat(
         translationColumns = translationColumns.concat(
-            translationMetadata.columns.filter(c => !c.relationMetadata),
+            translationMetadata.columns.filter(
+                c => !c.relationMetadata && !commonFields.includes(c.propertyName as any),
+            ),
         );
         );
     }
     }
     const alias = metadata.name.toLowerCase();
     const alias = metadata.name.toLowerCase();

+ 25 - 9
packages/core/src/service/helpers/list-query-builder/list-query-builder.ts

@@ -17,7 +17,7 @@ import { FindOptionsUtils } from 'typeorm/find-options/FindOptionsUtils';
 import { ApiType } from '../../../api/common/get-api-type';
 import { ApiType } from '../../../api/common/get-api-type';
 import { RequestContext } from '../../../api/common/request-context';
 import { RequestContext } from '../../../api/common/request-context';
 import { UserInputError } from '../../../common/error/errors';
 import { UserInputError } from '../../../common/error/errors';
-import { FilterParameter, ListQueryOptions, SortParameter } from '../../../common/types/common-types';
+import { ListQueryOptions, NullOptionals, SortParameter } from '../../../common/types/common-types';
 import { ConfigService } from '../../../config/config.service';
 import { ConfigService } from '../../../config/config.service';
 import { CustomFields } from '../../../config/index';
 import { CustomFields } from '../../../config/index';
 import { Logger } from '../../../config/logger/vendure-logger';
 import { Logger } from '../../../config/logger/vendure-logger';
@@ -212,8 +212,6 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         // tslint:disable-next-line:no-non-null-assertion
         // tslint:disable-next-line:no-non-null-assertion
         FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
         FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
 
 
-        this.applyTranslationConditions(qb, entity, extendedOptions.ctx, extendedOptions.entityAlias);
-
         // join the tables required by calculated columns
         // join the tables required by calculated columns
         this.joinCalculatedColumnRelations(qb, entity, options);
         this.joinCalculatedColumnRelations(qb, entity, options);
 
 
@@ -222,10 +220,18 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
             this.normalizeCustomPropertyMap(customPropertyMap, options, qb);
             this.normalizeCustomPropertyMap(customPropertyMap, options, qb);
         }
         }
         const customFieldsForType = this.configService.customFields[entity.name as keyof CustomFields];
         const customFieldsForType = this.configService.customFields[entity.name as keyof CustomFields];
+        const sortParams = Object.assign({}, options.sort, extendedOptions.orderBy);
+        this.applyTranslationConditions(
+            qb,
+            entity,
+            sortParams,
+            extendedOptions.ctx,
+            extendedOptions.entityAlias,
+        );
         const sort = parseSortParams(
         const sort = parseSortParams(
             rawConnection,
             rawConnection,
             entity,
             entity,
-            Object.assign({}, options.sort, extendedOptions.orderBy),
+            sortParams,
             customPropertyMap,
             customPropertyMap,
             entityAlias,
             entityAlias,
             customFieldsForType,
             customFieldsForType,
@@ -513,13 +519,15 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
     }
     }
 
 
     /**
     /**
-     * If this entity is Translatable, then we need to apply appropriate WHERE clauses to limit
-     * the joined translation relations. This method applies a simple "WHERE" on the languageCode
-     * in the case of the default language, otherwise we use a more complex.
+     * @description
+     * If this entity is Translatable, and we are sorting on one of the translatable fields,
+     * then we need to apply appropriate WHERE clauses to limit
+     * the joined translation relations.
      */
      */
     private applyTranslationConditions<T extends VendureEntity>(
     private applyTranslationConditions<T extends VendureEntity>(
         qb: SelectQueryBuilder<any>,
         qb: SelectQueryBuilder<any>,
         entity: Type<T>,
         entity: Type<T>,
+        sortParams: NullOptionals<SortParameter<T>> & FindOneOptions<T>['order'],
         ctx?: RequestContext,
         ctx?: RequestContext,
         entityAlias?: string,
         entityAlias?: string,
     ) {
     ) {
@@ -532,7 +540,15 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         } = getColumnMetadata(this.connection.rawConnection, entity);
         } = getColumnMetadata(this.connection.rawConnection, entity);
         const alias = entityAlias ?? defaultAlias;
         const alias = entityAlias ?? defaultAlias;
 
 
-        if (translationColumns.length) {
+        const sortKeys = Object.keys(sortParams);
+        let sortingOnTranslatableKey = false;
+        for (const translationColumn of translationColumns) {
+            if (sortKeys.includes(translationColumn.propertyName)) {
+                sortingOnTranslatableKey = true;
+            }
+        }
+
+        if (translationColumns.length && sortingOnTranslatableKey) {
             const translationsAlias = qb.connection.namingStrategy.eagerJoinRelationAlias(
             const translationsAlias = qb.connection.namingStrategy.eagerJoinRelationAlias(
                 alias,
                 alias,
                 'translations',
                 'translations',
@@ -584,7 +600,7 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
                     }
                     }
                     qb.setParameters({
                     qb.setParameters({
                         nonDefaultLanguageCode: languageCode,
                         nonDefaultLanguageCode: languageCode,
-                        defaultLanguageCode: this.configService.defaultLanguageCode,
+                        defaultLanguageCode,
                     });
                     });
                 }),
                 }),
             );
             );

+ 18 - 0
packages/core/src/service/helpers/order-calculator/order-calculator.ts

@@ -20,6 +20,17 @@ import { ShippingCalculator } from '../shipping-calculator/shipping-calculator';
 
 
 import { prorate } from './prorate';
 import { prorate } from './prorate';
 
 
+/**
+ * @description
+ * This helper is used when making changes to an Order, to apply all applicable price adjustments to that Order,
+ * including:
+ *
+ * - Promotions
+ * - Taxes
+ * - Shipping
+ *
+ * @docsCategory service-helpers
+ */
 @Injectable()
 @Injectable()
 export class OrderCalculator {
 export class OrderCalculator {
     constructor(
     constructor(
@@ -32,6 +43,7 @@ export class OrderCalculator {
     ) {}
     ) {}
 
 
     /**
     /**
+     * @description
      * Applies taxes and promotions to an Order. Mutates the order object.
      * Applies taxes and promotions to an Order. Mutates the order object.
      * Returns an array of any OrderItems which had new adjustments
      * Returns an array of any OrderItems which had new adjustments
      * applied, either tax or promotions.
      * applied, either tax or promotions.
@@ -95,6 +107,7 @@ export class OrderCalculator {
     }
     }
 
 
     /**
     /**
+     * @description
      * Applies the correct TaxRate to each OrderItem in the order.
      * Applies the correct TaxRate to each OrderItem in the order.
      */
      */
     private async applyTaxes(ctx: RequestContext, order: Order, activeZone: Zone) {
     private async applyTaxes(ctx: RequestContext, order: Order, activeZone: Zone) {
@@ -106,6 +119,7 @@ export class OrderCalculator {
     }
     }
 
 
     /**
     /**
+     * @description
      * Applies the correct TaxRate to an OrderLine
      * Applies the correct TaxRate to an OrderLine
      */
      */
     private async applyTaxesToOrderLine(
     private async applyTaxesToOrderLine(
@@ -129,6 +143,7 @@ export class OrderCalculator {
     }
     }
 
 
     /**
     /**
+     * @description
      * Returns a memoized function for performing an efficient
      * Returns a memoized function for performing an efficient
      * lookup of the correct TaxRate for a given TaxCategory.
      * lookup of the correct TaxRate for a given TaxCategory.
      */
      */
@@ -150,6 +165,7 @@ export class OrderCalculator {
     }
     }
 
 
     /**
     /**
+     * @description
      * Applies any eligible promotions to each OrderItem in the order. Returns an array of
      * Applies any eligible promotions to each OrderItem in the order. Returns an array of
      * any OrderItems which had their Adjustments modified.
      * any OrderItems which had their Adjustments modified.
      */
      */
@@ -168,6 +184,7 @@ export class OrderCalculator {
     }
     }
 
 
     /**
     /**
+     * @description
      * Applies promotions to OrderItems. This is a quite complex function, due to the inherent complexity
      * Applies promotions to OrderItems. This is a quite complex function, due to the inherent complexity
      * of applying the promotions, and also due to added complexity in the name of performance
      * of applying the promotions, and also due to added complexity in the name of performance
      * optimization. Therefore it is heavily annotated so that the purpose of each step is clear.
      * optimization. Therefore it is heavily annotated so that the purpose of each step is clear.
@@ -253,6 +270,7 @@ export class OrderCalculator {
     }
     }
 
 
     /**
     /**
+     * @description
      * An OrderLine may have promotion adjustments from Promotions which are no longer applicable.
      * An OrderLine may have promotion adjustments from Promotions which are no longer applicable.
      * For example, a coupon code might have caused a discount to be applied, and now that code has
      * For example, a coupon code might have caused a discount to be applied, and now that code has
      * been removed from the order. The adjustment will still be there on each OrderItem it was applied
      * been removed from the order. The adjustment will still be there on each OrderItem it was applied

+ 10 - 0
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -61,6 +61,8 @@ import { translateDeep } from '../utils/translate-entity';
  * So this helper was mainly extracted to isolate the huge `modifyOrder` method since the
  * So this helper was mainly extracted to isolate the huge `modifyOrder` method since the
  * OrderService was just growing too large. Future refactoring could improve the organization
  * OrderService was just growing too large. Future refactoring could improve the organization
  * of these Order-related methods into a more clearly-delineated set of classes.
  * of these Order-related methods into a more clearly-delineated set of classes.
+ *
+ * @docsCategory service-helpers
  */
  */
 @Injectable()
 @Injectable()
 export class OrderModifier {
 export class OrderModifier {
@@ -80,6 +82,7 @@ export class OrderModifier {
     ) {}
     ) {}
 
 
     /**
     /**
+     * @description
      * Ensure that the ProductVariant has sufficient saleable stock to add the given
      * Ensure that the ProductVariant has sufficient saleable stock to add the given
      * quantity to an Order.
      * quantity to an Order.
      */
      */
@@ -97,6 +100,11 @@ export class OrderModifier {
         return correctedQuantity;
         return correctedQuantity;
     }
     }
 
 
+    /**
+     * @description
+     * Given a ProductVariant ID and optional custom fields, this method will return an existing OrderLine that
+     * matches, or `undefined` if no match is found.
+     */
     async getExistingOrderLine(
     async getExistingOrderLine(
         ctx: RequestContext,
         ctx: RequestContext,
         order: Order,
         order: Order,
@@ -114,6 +122,7 @@ export class OrderModifier {
     }
     }
 
 
     /**
     /**
+     * @description
      * Returns the OrderLine to which a new OrderItem belongs, creating a new OrderLine
      * Returns the OrderLine to which a new OrderItem belongs, creating a new OrderLine
      * if no existing line is found.
      * if no existing line is found.
      */
      */
@@ -162,6 +171,7 @@ export class OrderModifier {
     }
     }
 
 
     /**
     /**
+     * @description
      * Updates the quantity of an OrderLine, taking into account the available saleable stock level.
      * Updates the quantity of an OrderLine, taking into account the available saleable stock level.
      * Returns the actual quantity that the OrderLine was updated to (which may be less than the
      * Returns the actual quantity that the OrderLine was updated to (which may be less than the
      * `quantity` argument if insufficient stock was available.
      * `quantity` argument if insufficient stock was available.

+ 10 - 1
packages/core/src/service/helpers/order-state-machine/order-state.ts

@@ -2,6 +2,14 @@ import { RequestContext } from '../../../api/common/request-context';
 import { Transitions } from '../../../common/finite-state-machine/types';
 import { Transitions } from '../../../common/finite-state-machine/types';
 import { Order } from '../../../entity/order/order.entity';
 import { Order } from '../../../entity/order/order.entity';
 
 
+/**
+ * @description
+ * An interface to extend standard {@link OrderState}.
+ *
+ * @docsCategory orders
+ */
+export interface CustomOrderStates {}
+
 /**
 /**
  * @description
  * @description
  * These are the default states of the Order process. They can be augmented and
  * These are the default states of the Order process. They can be augmented and
@@ -21,7 +29,8 @@ export type OrderState =
     | 'Delivered'
     | 'Delivered'
     | 'Modifying'
     | 'Modifying'
     | 'ArrangingAdditionalPayment'
     | 'ArrangingAdditionalPayment'
-    | 'Cancelled';
+    | 'Cancelled'
+    | keyof CustomOrderStates;
 
 
 export const orderStateTransitions: Transitions<OrderState> = {
 export const orderStateTransitions: Transitions<OrderState> = {
     Created: {
     Created: {

+ 16 - 1
packages/core/src/service/helpers/payment-state-machine/payment-state.ts

@@ -3,13 +3,28 @@ import { Transitions } from '../../../common/finite-state-machine/types';
 import { Order } from '../../../entity/order/order.entity';
 import { Order } from '../../../entity/order/order.entity';
 import { Payment } from '../../../entity/payment/payment.entity';
 import { Payment } from '../../../entity/payment/payment.entity';
 
 
+/**
+ * @description
+ * An interface to extend standard {@link PaymentState}.
+ *
+ * @docsCategory payment
+ */
+export interface CustomPaymentStates {}
+
 /**
 /**
  * @description
  * @description
  * These are the default states of the payment process.
  * These are the default states of the payment process.
  *
  *
  * @docsCategory payment
  * @docsCategory payment
  */
  */
-export type PaymentState = 'Created' | 'Authorized' | 'Settled' | 'Declined' | 'Error' | 'Cancelled';
+export type PaymentState =
+    | 'Created'
+    | 'Authorized'
+    | 'Settled'
+    | 'Declined'
+    | 'Error'
+    | 'Cancelled'
+    | keyof CustomPaymentStates;
 
 
 export const paymentStateTransitions: Transitions<PaymentState> = {
 export const paymentStateTransitions: Transitions<PaymentState> = {
     Created: {
     Created: {

+ 26 - 2
packages/core/src/service/helpers/product-price-applicator/product-price-applicator.ts

@@ -13,7 +13,30 @@ import { ZoneService } from '../../services/zone.service';
 /**
 /**
  * @description
  * @description
  * This helper is used to apply the correct price to a ProductVariant based on the current context
  * This helper is used to apply the correct price to a ProductVariant based on the current context
- * including active Channel, any current Order, etc.
+ * including active Channel, any current Order, etc. If you use the {@link TransactionalConnection} to
+ * directly query ProductVariants, you will find that the `price` and `priceWithTax` properties will
+ * always be `0` until you use the `applyChannelPriceAndTax()` method:
+ *
+ * @example
+ * ```TypeScript
+ * export class MyCustomService {
+ *   constructor(private connection: TransactionalConnection,
+ *               private productPriceApplicator: ProductPriceApplicator) {}
+ *
+ *   getVariant(ctx: RequestContext, id: ID) {
+ *     const productVariant = await this.connection
+ *       .getRepository(ctx, ProductVariant)
+ *       .findOne(id, { relations: ['taxCategory'] });
+ *
+ *     await this.productPriceApplicator
+ *       .applyChannelPriceAndTax(productVariant, ctx);
+ *
+ *     return productVariant;
+ *   }
+ * }
+ * ```
+ *
+ * @docsCategory service-helpers
  */
  */
 @Injectable()
 @Injectable()
 export class ProductPriceApplicator {
 export class ProductPriceApplicator {
@@ -26,7 +49,8 @@ export class ProductPriceApplicator {
 
 
     /**
     /**
      * @description
      * @description
-     * Populates the `price` field with the price for the specified channel.
+     * Populates the `price` field with the price for the specified channel. Make sure that
+     * the ProductVariant being passed in has its `taxCategory` relation joined.
      */
      */
     async applyChannelPriceAndTax(
     async applyChannelPriceAndTax(
         variant: ProductVariant,
         variant: ProductVariant,

+ 17 - 1
packages/core/src/service/helpers/slug-validator/slug-validator.ts

@@ -8,6 +8,10 @@ import { TransactionalConnection } from '../../../connection/transactional-conne
 import { VendureEntity } from '../../../entity/base/base.entity';
 import { VendureEntity } from '../../../entity/base/base.entity';
 import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
 import { ProductOptionGroup } from '../../../entity/product-option-group/product-option-group.entity';
 
 
+/**
+ * @docsCategory service-helpers
+ * @docsPage SlugValidator
+ */
 export type InputWithSlug = {
 export type InputWithSlug = {
     id?: ID | null;
     id?: ID | null;
     translations?: Array<{
     translations?: Array<{
@@ -17,6 +21,10 @@ export type InputWithSlug = {
     }> | null;
     }> | null;
 };
 };
 
 
+/**
+ * @docsCategory service-helpers
+ * @docsPage SlugValidator
+ */
 export type TranslationEntity = VendureEntity & {
 export type TranslationEntity = VendureEntity & {
     id: ID;
     id: ID;
     languageCode: LanguageCode;
     languageCode: LanguageCode;
@@ -24,6 +32,14 @@ export type TranslationEntity = VendureEntity & {
     base: any;
     base: any;
 };
 };
 
 
+/**
+ * @description
+ * Used to validate slugs to ensure they are URL-safe and unique. Designed to be used with translatable
+ * entities such as {@link Product} and {@link Collection}.
+ *
+ * @docsCategory service-helpers
+ * @docsWeight 0
+ */
 @Injectable()
 @Injectable()
 export class SlugValidator {
 export class SlugValidator {
     constructor(private connection: TransactionalConnection) {}
     constructor(private connection: TransactionalConnection) {}
@@ -53,7 +69,7 @@ export class SlugValidator {
                             .where(`translation.slug = :slug`, { slug: t.slug })
                             .where(`translation.slug = :slug`, { slug: t.slug })
                             .andWhere(`translation.languageCode = :languageCode`, {
                             .andWhere(`translation.languageCode = :languageCode`, {
                                 languageCode: t.languageCode,
                                 languageCode: t.languageCode,
-                            })
+                            });
                         if (input.id) {
                         if (input.id) {
                             qb.andWhere(`translation.base != :id`, { id: input.id });
                             qb.andWhere(`translation.base != :id`, { id: input.id });
                         }
                         }

+ 28 - 1
packages/core/src/service/helpers/translatable-saver/translatable-saver.ts

@@ -25,13 +25,39 @@ export interface UpdateTranslatableOptions<T extends Translatable> extends Creat
 }
 }
 
 
 /**
 /**
- * A helper which contains methods for creating and updating entities which implement the Translatable interface.
+ * @description
+ * A helper which contains methods for creating and updating entities which implement the {@link Translatable} interface.
+ *
+ * @example
+ * ```TypeScript
+ * export class MyService {
+ *   constructor(private translatableSaver: TranslatableSaver) {}
+ *
+ *   async create(ctx: RequestContext, input: CreateFacetInput): Promise<Translated<Facet>> {
+ *     const facet = await this.translatableSaver.create({
+ *       ctx,
+ *       input,
+ *       entityType: Facet,
+ *       translationType: FacetTranslation,
+ *       beforeSave: async f => {
+ *           f.code = await this.ensureUniqueCode(ctx, f.code);
+ *       },
+ *     });
+ *     return facet;
+ *   }
+ *
+ *   // ...
+ * }
+ * ```
+ *
+ * @docsCategory service-helpers
  */
  */
 @Injectable()
 @Injectable()
 export class TranslatableSaver {
 export class TranslatableSaver {
     constructor(private connection: TransactionalConnection) {}
     constructor(private connection: TransactionalConnection) {}
 
 
     /**
     /**
+     * @description
      * Create a translatable entity, including creating any translation entities according
      * Create a translatable entity, including creating any translation entities according
      * to the `translations` array.
      * to the `translations` array.
      */
      */
@@ -59,6 +85,7 @@ export class TranslatableSaver {
     }
     }
 
 
     /**
     /**
+     * @description
      * Update a translatable entity. Performs a diff of the `translations` array in order to
      * Update a translatable entity. Performs a diff of the `translations` array in order to
      * perform the correct operation on the translations.
      * perform the correct operation on the translations.
      */
      */

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

@@ -193,10 +193,12 @@ export class OrderService {
                     'lines.items',
                     'lines.items',
                     'channels',
                     'channels',
                     'shippingLines',
                     'shippingLines',
+                    'payments',
                 ],
                 ],
                 channelId: ctx.channelId,
                 channelId: ctx.channelId,
                 customPropertyMap: {
                 customPropertyMap: {
                     customerLastName: 'customer.lastName',
                     customerLastName: 'customer.lastName',
+                    transactionId: 'payment.transactionId',
                 },
                 },
             })
             })
             .getManyAndCount()
             .getManyAndCount()

+ 11 - 8
packages/core/src/service/services/product.service.ts

@@ -172,28 +172,31 @@ export class ProductService {
     ): Promise<Translated<Product> | undefined> {
     ): Promise<Translated<Product> | undefined> {
         const qb = this.connection.getRepository(ctx, Product).createQueryBuilder('product');
         const qb = this.connection.getRepository(ctx, Product).createQueryBuilder('product');
         FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, {
         FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, {
-            relations: (relations && false) || this.relations,
+            relations: relations ? [...new Set(this.relations.concat(relations))] : this.relations,
         });
         });
         // tslint:disable-next-line:no-non-null-assertion
         // tslint:disable-next-line:no-non-null-assertion
         FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
         FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
         const translationQb = this.connection
         const translationQb = this.connection
             .getRepository(ctx, ProductTranslation)
             .getRepository(ctx, ProductTranslation)
-            .createQueryBuilder('product_translation')
-            .select('product_translation.baseId')
-            .andWhere('product_translation.slug = :slug', { slug });
+            .createQueryBuilder('_product_translation')
+            .select('_product_translation.baseId')
+            .andWhere('_product_translation.slug = :slug', { slug });
 
 
-        return qb
-            .leftJoin('product.channels', 'channel')
+        const translationsAlias = relations?.includes('translations' as any)
+            ? 'product__translations'
+            : 'product_translations';
+        qb.leftJoin('product.channels', 'channel')
             .andWhere('product.id IN (' + translationQb.getQuery() + ')')
             .andWhere('product.id IN (' + translationQb.getQuery() + ')')
             .setParameters(translationQb.getParameters())
             .setParameters(translationQb.getParameters())
             .andWhere('product.deletedAt IS NULL')
             .andWhere('product.deletedAt IS NULL')
             .andWhere('channel.id = :channelId', { channelId: ctx.channelId })
             .andWhere('channel.id = :channelId', { channelId: ctx.channelId })
             .addSelect(
             .addSelect(
                 // tslint:disable-next-line:max-line-length
                 // tslint:disable-next-line:max-line-length
-                `CASE product_translations.languageCode WHEN '${ctx.languageCode}' THEN 2 WHEN '${ctx.channel.defaultLanguageCode}' THEN 1 ELSE 0 END`,
+                `CASE ${translationsAlias}.languageCode WHEN '${ctx.languageCode}' THEN 2 WHEN '${ctx.channel.defaultLanguageCode}' THEN 1 ELSE 0 END`,
                 'sort_order',
                 'sort_order',
             )
             )
-            .orderBy('sort_order', 'DESC')
+            .orderBy('sort_order', 'DESC');
+        return qb
             .getOne()
             .getOne()
             .then(product =>
             .then(product =>
                 product
                 product

+ 1 - 1
packages/dev-server/dev-config.ts

@@ -67,7 +67,7 @@ export const devConfig: VendureConfig = {
         }),
         }),
         DefaultSearchPlugin.init({ bufferUpdates: true, indexStockStatus: false }),
         DefaultSearchPlugin.init({ bufferUpdates: true, indexStockStatus: false }),
         BullMQJobQueuePlugin.init({}),
         BullMQJobQueuePlugin.init({}),
-        // DefaultJobQueuePlugin.init(),
+        // DefaultJobQueuePlugin.init({}),
         // JobQueueTestPlugin.init({ queueCount: 10 }),
         // JobQueueTestPlugin.init({ queueCount: 10 }),
         // ElasticsearchPlugin.init({
         // ElasticsearchPlugin.init({
         //     host: 'http://localhost',
         //     host: 'http://localhost',

+ 23 - 3
packages/ui-devkit/src/compiler/scaffold.ts

@@ -17,6 +17,7 @@ import {
     AdminUiExtensionSharedModule,
     AdminUiExtensionSharedModule,
     Extension,
     Extension,
     GlobalStylesExtension,
     GlobalStylesExtension,
+    SassVariableOverridesExtension,
     StaticAssetExtension,
     StaticAssetExtension,
 } from './types';
 } from './types';
 import {
 import {
@@ -24,6 +25,7 @@ import {
     copyUiDevkit,
     copyUiDevkit,
     isAdminUiExtension,
     isAdminUiExtension,
     isGlobalStylesExtension,
     isGlobalStylesExtension,
+    isSassVariableOverridesExtension,
     isStaticAssetExtension,
     isStaticAssetExtension,
     isTranslationExtension,
     isTranslationExtension,
     logger,
     logger,
@@ -43,7 +45,8 @@ export async function setupScaffold(outputPath: string, extensions: Extension[])
     await copyStaticAssets(outputPath, staticAssetExtensions);
     await copyStaticAssets(outputPath, staticAssetExtensions);
 
 
     const globalStyleExtensions = extensions.filter(isGlobalStylesExtension);
     const globalStyleExtensions = extensions.filter(isGlobalStylesExtension);
-    await addGlobalStyles(outputPath, globalStyleExtensions);
+    const sassVariableOverridesExtension = extensions.find(isSassVariableOverridesExtension);
+    await addGlobalStyles(outputPath, globalStyleExtensions, sassVariableOverridesExtension);
 
 
     const allTranslationFiles = getAllTranslationFiles(extensions.filter(isTranslationExtension));
     const allTranslationFiles = getAllTranslationFiles(extensions.filter(isTranslationExtension));
     await mergeExtensionTranslations(outputPath, allTranslationFiles);
     await mergeExtensionTranslations(outputPath, allTranslationFiles);
@@ -93,12 +96,17 @@ async function copyStaticAssets(outputPath: string, extensions: Array<Partial<St
     }
     }
 }
 }
 
 
-async function addGlobalStyles(outputPath: string, extensions: GlobalStylesExtension[]) {
+async function addGlobalStyles(
+    outputPath: string,
+    globalStylesExtensions: GlobalStylesExtension[],
+    sassVariableOverridesExtension?: SassVariableOverridesExtension,
+) {
     const globalStylesDir = path.join(outputPath, 'src', GLOBAL_STYLES_OUTPUT_DIR);
     const globalStylesDir = path.join(outputPath, 'src', GLOBAL_STYLES_OUTPUT_DIR);
     await fs.remove(globalStylesDir);
     await fs.remove(globalStylesDir);
     await fs.ensureDir(globalStylesDir);
     await fs.ensureDir(globalStylesDir);
+
     const imports: string[] = [];
     const imports: string[] = [];
-    for (const extension of extensions) {
+    for (const extension of globalStylesExtensions) {
         const styleFiles = Array.isArray(extension.globalStyles)
         const styleFiles = Array.isArray(extension.globalStyles)
             ? extension.globalStyles
             ? extension.globalStyles
             : [extension.globalStyles];
             : [extension.globalStyles];
@@ -107,7 +115,19 @@ async function addGlobalStyles(outputPath: string, extensions: GlobalStylesExten
             imports.push(path.basename(styleFile, path.extname(styleFile)));
             imports.push(path.basename(styleFile, path.extname(styleFile)));
         }
         }
     }
     }
+
+    let overridesImport = '';
+    if (sassVariableOverridesExtension) {
+        const overridesFile = sassVariableOverridesExtension.sassVariableOverrides;
+        await copyGlobalStyleFile(outputPath, overridesFile);
+        overridesImport = `@import "./${GLOBAL_STYLES_OUTPUT_DIR}/${path.basename(
+            overridesFile,
+            path.extname(overridesFile),
+        )}";\n`;
+    }
+
     const globalStylesSource =
     const globalStylesSource =
+        overridesImport +
         `@import "./styles/styles";\n` +
         `@import "./styles/styles";\n` +
         imports.map(file => `@import "./${GLOBAL_STYLES_OUTPUT_DIR}/${file}";`).join('\n');
         imports.map(file => `@import "./${GLOBAL_STYLES_OUTPUT_DIR}/${file}";`).join('\n');
 
 

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

@@ -4,7 +4,8 @@ export type Extension =
     | AdminUiExtension
     | AdminUiExtension
     | TranslationExtension
     | TranslationExtension
     | StaticAssetExtension
     | StaticAssetExtension
-    | GlobalStylesExtension;
+    | GlobalStylesExtension
+    | SassVariableOverridesExtension;
 
 
 /**
 /**
  * @description
  * @description
@@ -65,6 +66,22 @@ export interface GlobalStylesExtension {
     globalStyles: string[] | string;
     globalStyles: string[] | string;
 }
 }
 
 
+/**
+ * @description
+ * Defines an extension which allows overriding Clarity Design System's Sass variables used in styles on the Admin UI.
+ *
+ * @docsCategory UiDevkit
+ * @docsPage AdminUiExtension
+ */
+export interface SassVariableOverridesExtension {
+    /**
+     * @description
+     * Specifies a path to a Sass style file containing variable declarations, which will take precedence over
+     * default values defined in Clarity.
+     */
+    sassVariableOverrides: string;
+}
+
 /**
 /**
  * @description
  * @description
  * Defines extensions to the Admin UI application by specifying additional
  * Defines extensions to the Admin UI application by specifying additional

+ 5 - 0
packages/ui-devkit/src/compiler/utils.ts

@@ -10,6 +10,7 @@ import {
     AdminUiExtension,
     AdminUiExtension,
     Extension,
     Extension,
     GlobalStylesExtension,
     GlobalStylesExtension,
+    SassVariableOverridesExtension,
     StaticAssetDefinition,
     StaticAssetDefinition,
     StaticAssetExtension,
     StaticAssetExtension,
     TranslationExtension,
     TranslationExtension,
@@ -106,3 +107,7 @@ export function isStaticAssetExtension(input: Extension): input is StaticAssetEx
 export function isGlobalStylesExtension(input: Extension): input is GlobalStylesExtension {
 export function isGlobalStylesExtension(input: Extension): input is GlobalStylesExtension {
     return input.hasOwnProperty('globalStyles');
     return input.hasOwnProperty('globalStyles');
 }
 }
+
+export function isSassVariableOverridesExtension(input: Extension): input is SassVariableOverridesExtension {
+    return input.hasOwnProperty('sassVariableOverrides');
+}