Browse Source

Merge branch 'master' into next

Michael Bromley 5 years ago
parent
commit
d1b5648f56
100 changed files with 1034 additions and 292 deletions
  1. 1 0
      .github/FUNDING.yml
  2. 24 0
      CHANGELOG.md
  3. 6 2
      README.md
  4. 102 0
      docs/content/docs/plugins/extending-the-admin-ui/admin-ui-theming-branding/_index.md
  5. 114 10
      docs/content/docs/plugins/plugin-examples/extending-graphql-api.md
  6. 1 1
      lerna.json
  7. 1 0
      package.json
  8. 3 3
      packages/admin-ui-plugin/package.json
  9. 10 1
      packages/admin-ui-plugin/src/plugin.ts
  10. 19 19
      packages/admin-ui/i18n-coverage.json
  11. 6 6
      packages/admin-ui/package.json
  12. 1 2
      packages/admin-ui/scripts/compile-styles.js
  13. 1 1
      packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.html
  14. 1 2
      packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.scss
  15. 2 2
      packages/admin-ui/src/lib/catalog/src/components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component.html
  16. 0 1
      packages/admin-ui/src/lib/catalog/src/components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component.scss
  17. 0 1
      packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.html
  18. 2 3
      packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.scss
  19. 3 2
      packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.ts
  20. 1 1
      packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.html
  21. 1 2
      packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.scss
  22. 7 9
      packages/admin-ui/src/lib/catalog/src/components/collection-tree/collection-tree-node.component.scss
  23. 0 1
      packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.scss
  24. 3 3
      packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.ts
  25. 1 2
      packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.scss
  26. 2 3
      packages/admin-ui/src/lib/catalog/src/components/generate-product-variants/generate-product-variants.component.scss
  27. 6 7
      packages/admin-ui/src/lib/catalog/src/components/option-value-input/option-value-input.component.scss
  28. 7 8
      packages/admin-ui/src/lib/catalog/src/components/product-assets/product-assets.component.scss
  29. 16 1
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html
  30. 14 2
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.scss
  31. 35 19
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts
  32. 1 1
      packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html
  33. 3 3
      packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.scss
  34. 2 3
      packages/admin-ui/src/lib/catalog/src/components/product-search-input/product-search-input.component.scss
  35. 1 1
      packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.html
  36. 2 3
      packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.scss
  37. 1 1
      packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.ts
  38. 4 4
      packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.scss
  39. 1 1
      packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.ts
  40. 2 3
      packages/admin-ui/src/lib/catalog/src/components/product-variants-table/product-variants-table.component.scss
  41. 9 1
      packages/admin-ui/src/lib/catalog/src/components/update-product-option-dialog/update-product-option-dialog.component.html
  42. 6 4
      packages/admin-ui/src/lib/catalog/src/components/update-product-option-dialog/update-product-option-dialog.component.ts
  43. 1 1
      packages/admin-ui/src/lib/catalog/src/components/variant-price-detail/variant-price-detail.component.html
  44. 1 1
      packages/admin-ui/src/lib/catalog/src/components/variant-price-detail/variant-price-detail.component.ts
  45. 100 20
      packages/admin-ui/src/lib/catalog/src/providers/product-detail/product-detail.service.ts
  46. 24 0
      packages/admin-ui/src/lib/catalog/src/providers/product-detail/replace-last.spec.ts
  47. 17 0
      packages/admin-ui/src/lib/catalog/src/providers/product-detail/replace-last.ts
  48. 2 1
      packages/admin-ui/src/lib/catalog/src/public_api.ts
  49. 1 2
      packages/admin-ui/src/lib/core/src/app.component.scss
  50. 13 2
      packages/admin-ui/src/lib/core/src/app.component.ts
  51. 56 5
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  52. 3 1
      packages/admin-ui/src/lib/core/src/common/utilities/create-updated-translatable.ts
  53. 17 0
      packages/admin-ui/src/lib/core/src/common/utilities/find-translation.ts
  54. 1 11
      packages/admin-ui/src/lib/core/src/common/utilities/string-to-color.ts
  55. 1 1
      packages/admin-ui/src/lib/core/src/common/version.ts
  56. 1 1
      packages/admin-ui/src/lib/core/src/components/app-shell/app-shell.component.html
  57. 1 2
      packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.scss
  58. 11 2
      packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.html
  59. 1 2
      packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.scss
  60. 19 3
      packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.ts
  61. 7 6
      packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.scss
  62. 5 6
      packages/admin-ui/src/lib/core/src/components/notification/notification.component.scss
  63. 9 0
      packages/admin-ui/src/lib/core/src/components/theme-switcher/theme-switcher.component.html
  64. 36 0
      packages/admin-ui/src/lib/core/src/components/theme-switcher/theme-switcher.component.scss
  65. 28 0
      packages/admin-ui/src/lib/core/src/components/theme-switcher/theme-switcher.component.ts
  66. 4 1
      packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.html
  67. 1 5
      packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.scss
  68. 20 3
      packages/admin-ui/src/lib/core/src/core.module.ts
  69. 3 0
      packages/admin-ui/src/lib/core/src/data/client-state/client-defaults.ts
  70. 17 0
      packages/admin-ui/src/lib/core/src/data/client-state/client-resolvers.ts
  71. 3 1
      packages/admin-ui/src/lib/core/src/data/client-state/client-types.graphql
  72. 8 0
      packages/admin-ui/src/lib/core/src/data/definitions/client-definitions.ts
  73. 5 0
      packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts
  74. 9 0
      packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts
  75. 8 0
      packages/admin-ui/src/lib/core/src/data/providers/client-data.service.ts
  76. 1 0
      packages/admin-ui/src/lib/core/src/data/providers/product-data.service.ts
  77. 8 0
      packages/admin-ui/src/lib/core/src/data/providers/settings-data.service.ts
  78. 9 8
      packages/admin-ui/src/lib/core/src/providers/job-queue/job-queue.service.ts
  79. 1 0
      packages/admin-ui/src/lib/core/src/providers/local-storage/local-storage.service.ts
  80. 3 1
      packages/admin-ui/src/lib/core/src/public_api.ts
  81. 2 3
      packages/admin-ui/src/lib/core/src/shared/components/action-bar/action-bar.component.scss
  82. 3 3
      packages/admin-ui/src/lib/core/src/shared/components/affixed-input/affixed-input.component.scss
  83. 6 6
      packages/admin-ui/src/lib/core/src/shared/components/asset-file-input/asset-file-input.component.scss
  84. 7 7
      packages/admin-ui/src/lib/core/src/shared/components/asset-gallery/asset-gallery.component.scss
  85. 2 1
      packages/admin-ui/src/lib/core/src/shared/components/asset-picker-dialog/asset-picker-dialog.component.html
  86. 1 2
      packages/admin-ui/src/lib/core/src/shared/components/asset-picker-dialog/asset-picker-dialog.component.scss
  87. 0 1
      packages/admin-ui/src/lib/core/src/shared/components/asset-preview/asset-preview.component.scss
  88. 0 1
      packages/admin-ui/src/lib/core/src/shared/components/channel-assignment-control/channel-assignment-control.component.scss
  89. 0 1
      packages/admin-ui/src/lib/core/src/shared/components/channel-badge/channel-badge.component.scss
  90. 16 14
      packages/admin-ui/src/lib/core/src/shared/components/chip/chip.component.scss
  91. 4 1
      packages/admin-ui/src/lib/core/src/shared/components/currency-input/currency-input.component.html
  92. 53 4
      packages/admin-ui/src/lib/core/src/shared/components/currency-input/currency-input.component.spec.ts
  93. 39 2
      packages/admin-ui/src/lib/core/src/shared/components/currency-input/currency-input.component.ts
  94. 0 1
      packages/admin-ui/src/lib/core/src/shared/components/customer-label/customer-label.component.scss
  95. 0 1
      packages/admin-ui/src/lib/core/src/shared/components/data-table/data-table.component.scss
  96. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.component.html
  97. 12 13
      packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.component.scss
  98. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-item.directive.ts
  99. 8 1
      packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown.component.ts
  100. 2 3
      packages/admin-ui/src/lib/core/src/shared/components/edit-note-dialog/edit-note-dialog.component.scss

+ 1 - 0
.github/FUNDING.yml

@@ -0,0 +1 @@
+github: [michaelbromley]

+ 24 - 0
CHANGELOG.md

@@ -1,3 +1,27 @@
+## <small>0.18.1 (2021-01-08)</small>
+
+
+#### Fixes
+
+* **admin-ui** Refresh ShippingMethodList on channel change ([6811ca8](https://github.com/vendure-ecommerce/vendure/commit/6811ca8)), closes [#595](https://github.com/vendure-ecommerce/vendure/issues/595)
+* **admin-ui** Shipping method validators fix ([bbdd5be](https://github.com/vendure-ecommerce/vendure/commit/bbdd5be))
+* **admin-ui** Translate to Spanish all languages available ([b56e45d](https://github.com/vendure-ecommerce/vendure/commit/b56e45d))
+* **core** Always include customFields on OrderAddress type ([c5e3c6d](https://github.com/vendure-ecommerce/vendure/commit/c5e3c6d)), closes [#616](https://github.com/vendure-ecommerce/vendure/issues/616)
+* **core** Fix error when creating Product in sub-channel ([96c5103](https://github.com/vendure-ecommerce/vendure/commit/96c5103)), closes [#556](https://github.com/vendure-ecommerce/vendure/issues/556) [#613](https://github.com/vendure-ecommerce/vendure/issues/613)
+
+#### Features
+
+* **admin-ui** Add dark mode theme & switcher component ([76f80f6](https://github.com/vendure-ecommerce/vendure/commit/76f80f6)), closes [#391](https://github.com/vendure-ecommerce/vendure/issues/391)
+* **admin-ui** Add default branding values to vendure-ui-config ([50aeb2b](https://github.com/vendure-ecommerce/vendure/commit/50aeb2b)), closes [#391](https://github.com/vendure-ecommerce/vendure/issues/391)
+* **admin-ui** Add support for job cancellation ([c6004c1](https://github.com/vendure-ecommerce/vendure/commit/c6004c1)), closes [#614](https://github.com/vendure-ecommerce/vendure/issues/614)
+* **admin-ui** Allow "enabled" state to be set when creating products ([3e006ce](https://github.com/vendure-ecommerce/vendure/commit/3e006ce)), closes [#608](https://github.com/vendure-ecommerce/vendure/issues/608)
+* **admin-ui** Enable theming by use of css custom properties ([68107d2](https://github.com/vendure-ecommerce/vendure/commit/68107d2)), closes [#391](https://github.com/vendure-ecommerce/vendure/issues/391)
+* **core** Add `cancelJob` mutation ([2d099cf](https://github.com/vendure-ecommerce/vendure/commit/2d099cf)), closes [#614](https://github.com/vendure-ecommerce/vendure/issues/614)
+* **core** Allow "enabled" state to be set when creating products ([02eb9f7](https://github.com/vendure-ecommerce/vendure/commit/02eb9f7)), closes [#608](https://github.com/vendure-ecommerce/vendure/issues/608)
+* **ui-devkit** Allow custom global styles to be specified ([2081a15](https://github.com/vendure-ecommerce/vendure/commit/2081a15)), closes [#391](https://github.com/vendure-ecommerce/vendure/issues/391)
+* **ui-devkit** Allow extensions consisting of only static assets ([5ea3422](https://github.com/vendure-ecommerce/vendure/commit/5ea3422)), closes [#391](https://github.com/vendure-ecommerce/vendure/issues/391) [#309](https://github.com/vendure-ecommerce/vendure/issues/309)
+* **ui-devkit** Export helper function to set brand images ([6cde0d8](https://github.com/vendure-ecommerce/vendure/commit/6cde0d8)), closes [#391](https://github.com/vendure-ecommerce/vendure/issues/391)
+
 ## 0.18.0 (2020-12-31)
 ## 0.18.0 (2020-12-31)
 
 
 
 

+ 6 - 2
README.md

@@ -72,15 +72,19 @@ Vendure uses [TypeORM](http://typeorm.io), and officially supports **MySQL**, **
 
 
 ### 5. Run the dev server
 ### 5. Run the dev server
 
 
+```
+cd packages/dev-server
+DB=<mysql|postgres|sqlite> yarn start
+```
+Or if you are in the root package 
 ```
 ```
 DB=<mysql|postgres|sqlite> yarn dev-server:start
 DB=<mysql|postgres|sqlite> yarn dev-server:start
 ```
 ```
-
 If you do not specify the `DB` argument, it will default to "mysql".
 If you do not specify the `DB` argument, it will default to "mysql".
 
 
 ### 6. Launch the admin ui
 ### 6. Launch the admin ui
 
 
-1. `cd admin-ui`
+1. `cd packages/admin-ui`
 2. `yarn start`
 2. `yarn start`
 3. Go to http://localhost:4200 and log in with "superadmin", "superadmin"
 3. Go to http://localhost:4200 and log in with "superadmin", "superadmin"
 
 

+ 102 - 0
docs/content/docs/plugins/extending-the-admin-ui/admin-ui-theming-branding/_index.md

@@ -0,0 +1,102 @@
+---
+title: 'Admin UI Theming & Branding'
+---
+
+# Admin UI Theming & Branding
+
+The Vendure Admin UI can be themed to your company's style and branding.
+    
+## AdminUiPlugin branding settings
+
+The `AdminUiPlugin` allows you to specify your "brand" name, and allows you to control whether to display the Vendure name and version in the UI. Specifying a brand name will also set it as the title of the Admin UI in the browser.
+
+```TypeScript
+// vendure-config.ts
+import path from 'path';
+import { VendureConfig } from '@vendure/core';
+import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
+
+export const config: VendureConfig = {
+  // ...
+  plugins: [
+    AdminUiPlugin.init({
+      adminUiConfig:{
+        brand: 'My Store',
+        hideVendureBranding: false,
+        hideVersion: false,
+      }
+    }),
+  ],
+};
+```
+
+## Specifying custom logos
+
+You can replace the Vendure logos and favicon with your own brand logo:
+
+1. Install `@vendure/ui-devkit`
+2. Configure the AdminUiPlugin to compile a custom build featuring your logos:
+    ```TypeScript
+    import path from 'path';
+    import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
+    import { VendureConfig } from '@vendure/core';
+    import { compileUiExtensions, setBranding } from '@vendure/ui-devkit/compiler';
+    
+    export const config: VendureConfig = {
+      // ...
+      plugins: [
+        AdminUiPlugin.init({
+          app: compileUiExtensions({
+            outputPath: path.join(__dirname, '__admin-ui'),
+            extensions: [
+              setBranding({
+                // The small logo appears in the top left of the screen  
+                smallLogoPath: path.join(__dirname, 'images/my-logo-sm.png'),
+                // The large logo is used on the login page  
+                largeLogoPath: path.join(__dirname, 'images/my-logo-lg.png'),
+                faviconPath: path.join(__dirname, 'images/my-favicon.ico'),
+              }),
+            ],
+          }),
+        }),
+      ],
+    }
+    ```
+
+## 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:
+
+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:
+    ```css
+    /* my-theme.scss */
+    :root {
+      --clr-link-active-color: hsl(110, 65%, 57%);
+      --clr-link-color: hsl(110, 65%, 57%);
+      --clr-link-hover-color: hsl(110, 65%, 57%);
+      --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)
+3. Set this as a globalStyles extension:   
+    ```TypeScript
+    import path from 'path';
+    import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
+    import { VendureConfig } from '@vendure/core';
+    import { compileUiExtensions, setBranding } from '@vendure/ui-devkit/compiler';
+    
+    export const config: VendureConfig = {
+      // ...
+      plugins: [
+        AdminUiPlugin.init({
+          app: compileUiExtensions({
+            outputPath: path.join(__dirname, '__admin-ui'),
+            extensions: [{
+              globalStyles: path.join(__dirname, 'my-theme.scss')
+            }],
+          }),
+        }),
+      ],
+    }
+    ```

+ 114 - 10
docs/content/docs/plugins/plugin-examples/extending-graphql-api.md

@@ -5,6 +5,10 @@ showtoc: true
 
 
 # Extending the GraphQL API
 # Extending the GraphQL API
 
 
+There are a number of ways the GraphQL APIs can be modified by a plugin.
+
+## Adding a new Query or Mutation
+
 This example adds a new query to the GraphQL Admin API. It also demonstrates how [Nest's dependency injection](https://docs.nestjs.com/providers) can be used to encapsulate and inject services within the plugin module.
 This example adds a new query to the GraphQL Admin API. It also demonstrates how [Nest's dependency injection](https://docs.nestjs.com/providers) can be used to encapsulate and inject services within the plugin module.
  
  
 ```TypeScript
 ```TypeScript
@@ -27,40 +31,140 @@ class TopSellersResolver {
 {{< alert "primary" >}}
 {{< alert "primary" >}}
   **Note:** The `@Ctx` decorator gives you access to [the `RequestContext`]({{< relref "request-context" >}}), which is an object containing useful information about the current request - active user, current channel etc.
   **Note:** The `@Ctx` decorator gives you access to [the `RequestContext`]({{< relref "request-context" >}}), which is an object containing useful information about the current request - active user, current channel etc.
 {{< /alert >}}
 {{< /alert >}}
+
 ```TypeScript
 ```TypeScript
 // top-sellers.service.ts
 // top-sellers.service.ts
 import { Injectable } from '@nestjs/common';
 import { Injectable } from '@nestjs/common';
+import { RequestContext } from '@vendure/core';
 
 
 @Injectable()
 @Injectable()
-class TopSellersService { 
-  getTopSellers() { /* ... */ }
+class TopSellersService {
+    getTopSellers(ctx: RequestContext, from: Date, to: Date) { 
+        /* ... */
+    }
 }
 }
 ```
 ```
 
 
+The GraphQL schema is extended with the `topSellers` query (the query name should match the name of the corresponding resolver method):
+
 ```TypeScript
 ```TypeScript
 // top-sellers.plugin.ts
 // top-sellers.plugin.ts
 import gql from 'graphql-tag';
 import gql from 'graphql-tag';
-import { VendurePlugin } from '@vendure/core';
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
 import { TopSellersService } from './top-sellers.service'
 import { TopSellersService } from './top-sellers.service'
 import { TopSellersResolver } from './top-sellers.resolver'
 import { TopSellersResolver } from './top-sellers.resolver'
 
 
 @VendurePlugin({
 @VendurePlugin({
+  imports: [PluginCommonModule],
   providers: [TopSellersService],
   providers: [TopSellersService],
   adminApiExtensions: {
   adminApiExtensions: {
     schema: gql`
     schema: gql`
       extend type Query {
       extend type Query {
         topSellers(from: DateTime! to: DateTime!): [Product!]!
         topSellers(from: DateTime! to: DateTime!): [Product!]!
-      }
-    `,
+    }`,
     resolvers: [TopSellersResolver]
     resolvers: [TopSellersResolver]
   }
   }
 })
 })
 export class TopSellersPlugin {}
 export class TopSellersPlugin {}
 ```
 ```
 
 
-Using the `gql` tag, it is possible to:
+New mutations are defined in the same way, except that the `@Mutation()` decorator is used in the resolver, and the schema Mutation type is extended:
+
+```GraphQL
+extend type Mutation { 
+    myCustomProductMutation(id: ID!): Product!
+}
+```
+
+## Add fields to existing types
+
+Let's say you want to add a new field, "availability" to the ProductVariant type, to allow the storefront to display some indication of whether a variant is available to purchase. First you define a resolver function:
+
+```TypeScript
+import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
+import { Ctx, RequestContext, ProductVariant } from '@vendure/core';
+
+@Resolver('ProductVariant')
+export class ProductVariantEntityResolver {
+  
+  @ResolveField()
+  availability(@Ctx() ctx: RequestContext, @Parent() variant: ProductVariant) {
+    return this.getAvailbilityForVariant(ctx, variant.id);
+  }
+  
+  private getAvailbilityForVariant(ctx: RequestContext, id: ID): string {
+    // implementation omitted, but calculates the
+    // available salable stock and returns a string
+    // such as "in stock", "2 remaining" or "out of stock"
+  }
+}
+```
+
+Then in the plugin metadata, we extend the ProductVariant type and pass the resolver:
+
+```TypeScript
+import gql from 'graphql-tag';
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { ProductVariantEntityResolver } from './product-variant-entity.resolver'
+
+@VendurePlugin({
+  imports: [PluginCommonModule],
+  shopApiExtensions: {
+    schema: gql`
+      extend type ProductVariant {
+        availability: String!
+      }`,
+    resolvers: [ProductVariantEntityResolver]
+  }
+})
+export class AvailabilityPlugin {}
+```
+
+## Override built-in resolvers
+
+It is also possible to override an existing built-in resolver function with one of your own. To do so, you need to define a resolver with the same name as the query or mutation you wish to override. When that query or mutation is then executed, your code, rather than the default Vendure resolver, will handle it.
+
+```TypeScript
+import { Args, Query, Mutation, Resolver } from '@nestjs/graphql';
+import { Ctx, RequestContext } from '@vendure/core'
+
+@Resolver()
+class OverrideExampleResolver {
+
+  @Query()
+  products(@Ctx() ctx: RequestContext, @Args() args: any) {
+    // when the `products` query is executed, this resolver function will
+    // now handle it.
+  }
+  
+  @Transaction()
+  @Mutation()
+  addItemToOrder(@Ctx() ctx: RequestContext, @Args() args: any) {
+    // when the `addItemToOrder` mutation is executed, this resolver function will
+    // now handle it.
+  }
+
+}
+```
+
+The same can be done for resolving fields:
+
+```TypeScript
+import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
+import { Ctx, RequestContext, Product } from '@vendure/core';
+
+@Resolver('Product')
+export class FieldOverrideExampleResolver {
+  
+  @ResolveField()
+  description(@Ctx() ctx: RequestContext, @Parent() variant: Product) {
+    return this.wrapInFormatting(ctx, variant.id);
+  }
+  
+  private wrapInFormatting(ctx: RequestContext, id: ID): string {
+    // implementation omitted, but wraps the description
+    // text in some special formatting required by the storefront
+  }
+}
+```
 
 
-* add new queries `extend type Query { ... }`
-* add new mutations (`extend type Mutation { ... }`)
-* define brand new types `type MyNewType { ... }`
-* add new fields to built-in types (`extend type Product { newField: String }`)

+ 1 - 1
lerna.json

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

+ 1 - 0
package.json

@@ -7,6 +7,7 @@
   },
   },
   "scripts": {
   "scripts": {
     "watch": "lerna run watch --parallel",
     "watch": "lerna run watch --parallel",
+    "watch:core-common": "lerna run --scope @vendure/common --scope @vendure/core watch --parallel",
     "lint": "yarn tslint --fix",
     "lint": "yarn tslint --fix",
     "format": "prettier --write --html-whitespace-sensitivity ignore",
     "format": "prettier --write --html-whitespace-sensitivity ignore",
     "bootstrap": "lerna bootstrap",
     "bootstrap": "lerna bootstrap",

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

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

+ 10 - 1
packages/admin-ui-plugin/src/plugin.ts

@@ -21,7 +21,7 @@ import fs from 'fs-extra';
 import { Server } from 'http';
 import { Server } from 'http';
 import path from 'path';
 import path from 'path';
 
 
-import { DEFAULT_APP_PATH, defaultAvailableLanguages, defaultLanguage, loggerCtx } from './constants';
+import { defaultAvailableLanguages, defaultLanguage, DEFAULT_APP_PATH, loggerCtx } from './constants';
 
 
 /**
 /**
  * @description
  * @description
@@ -255,6 +255,15 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
             defaultLanguage: propOrDefault('defaultLanguage', defaultLanguage),
             defaultLanguage: propOrDefault('defaultLanguage', defaultLanguage),
             availableLanguages: propOrDefault('availableLanguages', defaultAvailableLanguages),
             availableLanguages: propOrDefault('availableLanguages', defaultAvailableLanguages),
             loginUrl: AdminUiPlugin.options.adminUiConfig?.loginUrl,
             loginUrl: AdminUiPlugin.options.adminUiConfig?.loginUrl,
+            brand: AdminUiPlugin.options.adminUiConfig?.brand,
+            hideVendureBranding: propOrDefault(
+                'hideVendureBranding',
+                AdminUiPlugin.options.adminUiConfig?.hideVendureBranding || false,
+            ),
+            hideVersion: propOrDefault(
+                'hideVersion',
+                AdminUiPlugin.options.adminUiConfig?.hideVersion || false,
+            ),
         };
         };
     }
     }
 
 

+ 19 - 19
packages/admin-ui/i18n-coverage.json

@@ -1,49 +1,49 @@
 {
 {
-  "generatedOn": "2020-12-23T10:30:40.834Z",
-  "lastCommit": "52ecb3373341617646affa8b2060589cd4e4c36f",
+  "generatedOn": "2021-01-12T10:57:57.374Z",
+  "lastCommit": "b2b37a8e8ec51354855e37ed9ed6f6be03518c15",
   "translationStatus": {
   "translationStatus": {
     "cs": {
     "cs": {
-      "tokenCount": 748,
-      "translatedCount": 687,
-      "percentage": 92
+      "tokenCount": 752,
+      "translatedCount": 751,
+      "percentage": 100
     },
     },
     "de": {
     "de": {
-      "tokenCount": 748,
+      "tokenCount": 752,
       "translatedCount": 596,
       "translatedCount": 596,
-      "percentage": 80
+      "percentage": 79
     },
     },
     "en": {
     "en": {
-      "tokenCount": 748,
-      "translatedCount": 747,
+      "tokenCount": 752,
+      "translatedCount": 752,
       "percentage": 100
       "percentage": 100
     },
     },
     "es": {
     "es": {
-      "tokenCount": 748,
-      "translatedCount": 455,
+      "tokenCount": 752,
+      "translatedCount": 458,
       "percentage": 61
       "percentage": 61
     },
     },
     "fr": {
     "fr": {
-      "tokenCount": 748,
+      "tokenCount": 752,
       "translatedCount": 692,
       "translatedCount": 692,
-      "percentage": 93
+      "percentage": 92
     },
     },
     "pl": {
     "pl": {
-      "tokenCount": 748,
+      "tokenCount": 752,
       "translatedCount": 551,
       "translatedCount": 551,
-      "percentage": 74
+      "percentage": 73
     },
     },
     "pt_BR": {
     "pt_BR": {
-      "tokenCount": 748,
+      "tokenCount": 752,
       "translatedCount": 642,
       "translatedCount": 642,
-      "percentage": 86
+      "percentage": 85
     },
     },
     "zh_Hans": {
     "zh_Hans": {
-      "tokenCount": 748,
+      "tokenCount": 752,
       "translatedCount": 533,
       "translatedCount": 533,
       "percentage": 71
       "percentage": 71
     },
     },
     "zh_Hant": {
     "zh_Hant": {
-      "tokenCount": 748,
+      "tokenCount": 752,
       "translatedCount": 533,
       "translatedCount": 533,
       "percentage": 71
       "percentage": 71
     }
     }

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

@@ -1,6 +1,6 @@
 {
 {
   "name": "@vendure/admin-ui",
   "name": "@vendure/admin-ui",
-  "version": "0.18.0",
+  "version": "0.18.1",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "ng": "ng",
     "ng": "ng",
@@ -29,14 +29,14 @@
     "@angular/router": "10.1.4",
     "@angular/router": "10.1.4",
     "@apollo/client": "^3.0.0",
     "@apollo/client": "^3.0.0",
     "@biesbjerg/ngx-translate-extract-marker": "^1.0.0",
     "@biesbjerg/ngx-translate-extract-marker": "^1.0.0",
-    "@clr/angular": "^4.0.3",
-    "@clr/core": "^4.0.3",
-    "@clr/icons": "^4.0.3",
-    "@clr/ui": "^4.0.3",
+    "@clr/angular": "^4.0.8",
+    "@clr/core": "^4.0.8",
+    "@clr/icons": "^4.0.8",
+    "@clr/ui": "^4.0.8",
     "@ng-select/ng-select": "^5.0.3",
     "@ng-select/ng-select": "^5.0.3",
     "@ngx-translate/core": "^13.0.0",
     "@ngx-translate/core": "^13.0.0",
     "@ngx-translate/http-loader": "^6.0.0",
     "@ngx-translate/http-loader": "^6.0.0",
-    "@vendure/common": "^0.18.0",
+    "@vendure/common": "^0.18.1",
     "@webcomponents/custom-elements": "^1.2.4",
     "@webcomponents/custom-elements": "^1.2.4",
     "apollo-angular": "^2.0.4",
     "apollo-angular": "^2.0.4",
     "apollo-upload-client": "^12.1.0",
     "apollo-upload-client": "^12.1.0",

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

@@ -6,7 +6,7 @@ const sass = require('sass');
 // non-Angular ui extensions.
 // non-Angular ui extensions.
 const outFile = path.join(__dirname, '../package/static/theme.min.css');
 const outFile = path.join(__dirname, '../package/static/theme.min.css');
 const result = sass.renderSync({
 const result = sass.renderSync({
-    file: path.join(__dirname, '../src/lib/static/styles/theme.scss'),
+    file: path.join(__dirname, '../src/lib/static/styles/ui-extension-theme.scss'),
     importer,
     importer,
     includePaths: [path.join(__dirname, '../src/lib/static/styles')],
     includePaths: [path.join(__dirname, '../src/lib/static/styles')],
     outputStyle: 'compressed',
     outputStyle: 'compressed',
@@ -15,7 +15,6 @@ 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, done) {
     let file = url;
     let file = url;
     // Handle the imports prefixed with ~
     // Handle the imports prefixed with ~

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.html

@@ -5,7 +5,7 @@
             name="searchTerm"
             name="searchTerm"
             [formControl]="searchTerm"
             [formControl]="searchTerm"
             [placeholder]="'asset.search-asset-name' | translate"
             [placeholder]="'asset.search-asset-name' | translate"
-            class="clr-input search-input"
+            class="search-input ml3"
         />
         />
     </vdr-ab-left>
     </vdr-ab-left>
     <vdr-ab-right>
     <vdr-ab-right>

+ 1 - 2
packages/admin-ui/src/lib/catalog/src/components/asset-list/asset-list.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     display: flex;
     display: flex;
@@ -12,7 +11,7 @@ vdr-asset-gallery {
 
 
 .paging-controls {
 .paging-controls {
     padding-top: 6px;
     padding-top: 6px;
-    border-top: 1px solid $color-grey-200;
+    border-top: 1px solid var(--color-component-border-100);
     display: flex;
     display: flex;
     justify-content: space-between;
     justify-content: space-between;
 }
 }

+ 2 - 2
packages/admin-ui/src/lib/catalog/src/components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component.html

@@ -48,10 +48,10 @@
         <tbody>
         <tbody>
             <tr *ngFor="let row of variantsPreview$ | async">
             <tr *ngFor="let row of variantsPreview$ | async">
                 <td>{{ row.name }}</td>
                 <td>{{ row.name }}</td>
-                <td>{{ row.price / 100 | currency: currentChannel?.currencyCode }}</td>
+                <td>{{ row.price | localeCurrency: currentChannel?.currencyCode }}</td>
                 <td>
                 <td>
                     <ng-template [ngIf]="selectedChannel" [ngIfElse]="noChannelSelected">
                     <ng-template [ngIf]="selectedChannel" [ngIfElse]="noChannelSelected">
-                        {{ row.pricePreview / 100 | currency: selectedChannel?.currencyCode }}
+                        {{ row.pricePreview | localeCurrency: selectedChannel?.currencyCode }}
                     </ng-template>
                     </ng-template>
                     <ng-template #noChannelSelected> - </ng-template>
                     <ng-template #noChannelSelected> - </ng-template>
                 </td>
                 </td>

+ 0 - 1
packages/admin-ui/src/lib/catalog/src/components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 vdr-channel-assignment-control {
 vdr-channel-assignment-control {
     min-width: 200px;
     min-width: 200px;

+ 0 - 1
packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.html

@@ -7,7 +7,6 @@
     <input
     <input
         type="text"
         type="text"
         [placeholder]="'catalog.filter-by-name' | translate"
         [placeholder]="'catalog.filter-by-name' | translate"
-        class="clr-input"
         [formControl]="filterTermControl"
         [formControl]="filterTermControl"
     />
     />
 </div>
 </div>

+ 2 - 3
packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.scss

@@ -1,12 +1,11 @@
-@import "variables";
 
 
 .contents-header {
 .contents-header {
-    background-color: $color-grey-100;
+    background-color: var(--color-component-bg-100);
     position: sticky;
     position: sticky;
     top: 0;
     top: 0;
     padding: 6px;
     padding: 6px;
     z-index: 1;
     z-index: 1;
-    border-bottom: 1px solid $color-grey-300;
+    border-bottom: 1px solid var(--color-component-border-200);
 
 
     .header-title-row {
     .header-title-row {
         display: flex;
         display: flex;

+ 3 - 2
packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.ts

@@ -21,6 +21,7 @@ import {
     CustomFieldConfig,
     CustomFieldConfig,
     DataService,
     DataService,
     encodeConfigArgValue,
     encodeConfigArgValue,
+    findTranslation,
     getConfigArgValue,
     getConfigArgValue,
     LanguageCode,
     LanguageCode,
     ModalService,
     ModalService,
@@ -105,7 +106,7 @@ export class CollectionDetailComponent
             .pipe(take(1))
             .pipe(take(1))
             .subscribe(([entity, languageCode]) => {
             .subscribe(([entity, languageCode]) => {
                 const slugControl = this.detailForm.get(['slug']);
                 const slugControl = this.detailForm.get(['slug']);
-                const currentTranslation = entity.translations.find(t => t.languageCode === languageCode);
+                const currentTranslation = findTranslation(entity, languageCode);
                 const currentSlugIsEmpty = !currentTranslation || !currentTranslation.slug;
                 const currentSlugIsEmpty = !currentTranslation || !currentTranslation.slug;
                 if (slugControl && slugControl.pristine && currentSlugIsEmpty) {
                 if (slugControl && slugControl.pristine && currentSlugIsEmpty) {
                     slugControl.setValue(normalizeString(`${nameValue}`, '-'));
                     slugControl.setValue(normalizeString(`${nameValue}`, '-'));
@@ -223,7 +224,7 @@ export class CollectionDetailComponent
      * Sets the values of the form on changes to the category or current language.
      * Sets the values of the form on changes to the category or current language.
      */
      */
     protected setFormValues(entity: Collection.Fragment, languageCode: LanguageCode) {
     protected setFormValues(entity: Collection.Fragment, languageCode: LanguageCode) {
-        const currentTranslation = entity.translations.find(t => t.languageCode === languageCode);
+        const currentTranslation = findTranslation(entity, languageCode);
 
 
         this.detailForm.patchValue({
         this.detailForm.patchValue({
             name: currentTranslation ? currentTranslation.name : '',
             name: currentTranslation ? currentTranslation.name : '',

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.html

@@ -1,6 +1,6 @@
 <vdr-action-bar>
 <vdr-action-bar>
     <vdr-ab-left>
     <vdr-ab-left>
-        <clr-checkbox-wrapper class="expand-all-toggle">
+        <clr-checkbox-wrapper class="expand-all-toggle ml3">
             <input type="checkbox" clrCheckbox [(ngModel)]="expandAll" />
             <input type="checkbox" clrCheckbox [(ngModel)]="expandAll" />
             <label>{{ 'catalog.expand-all-collections' | translate }}</label>
             <label>{{ 'catalog.expand-all-collections' | translate }}</label>
         </clr-checkbox-wrapper>
         </clr-checkbox-wrapper>

+ 1 - 2
packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     height: 100%;
     height: 100%;
@@ -48,7 +47,7 @@
 
 
 .paging-controls {
 .paging-controls {
     padding-top: 6px;
     padding-top: 6px;
-    border-top: 1px solid $color-grey-200;
+    border-top: 1px solid var(--color-component-border-100);
     display: flex;
     display: flex;
     justify-content: space-between;
     justify-content: space-between;
 }
 }

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

@@ -1,19 +1,17 @@
-@import "variables";
 
 
 :host {
 :host {
     display: block;
     display: block;
 }
 }
 .collection {
 .collection {
-    background-color: white;
+    background-color: var(--color-component-bg-100);
     font-size: 0.65rem;
     font-size: 0.65rem;
-    color: rgba(0, 0, 0, 0.87);
     transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
     transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
     margin-bottom: 2px;
     margin-bottom: 2px;
     border-left: 2px solid transparent;
     border-left: 2px solid transparent;
     transition: border-left-color 0.2s;
     transition: border-left-color 0.2s;
 
 
     &.private {
     &.private {
-        background-color: $color-grey-200;
+        background-color: var(--color-component-bg-200);
     }
     }
 
 
     .collection-detail {
     .collection-detail {
@@ -21,10 +19,10 @@
         display: flex;
         display: flex;
         align-items: center;
         align-items: center;
         justify-content: space-between;
         justify-content: space-between;
-        border-bottom: 1px solid $color-grey-200;
+        border-bottom: 1px solid var(--color-component-border-100);
 
 
         &.active {
         &.active {
-            background-color: $color-grey-200;
+            background-color: var(--clr-global-selection-color);
         }
         }
 
 
         &.depth-1 { padding-left: 12px + 24px; }
         &.depth-1 { padding-left: 12px + 24px; }
@@ -41,18 +39,18 @@
 
 
 .tree-node {
 .tree-node {
     display: block;
     display: block;
-    background: white;
+    background: var(--color-component-bg-100);
     overflow: hidden;
     overflow: hidden;
     &.cdk-drop-list-dragging {
     &.cdk-drop-list-dragging {
         > .collection {
         > .collection {
-            border-left-color: $color-primary-300;
+            border-left-color: var(--color-primary-300);
         }
         }
     }
     }
 }
 }
 
 
 .drag-placeholder {
 .drag-placeholder {
     min-height: 120px;
     min-height: 120px;
-    background-color: $color-grey-300;
+    background-color: var(--color-component-bg-300);
     transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
     transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
 }
 }
 
 

+ 0 - 1
packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 .visible-toggle {
 .visible-toggle {
     margin-top: -3px !important;
     margin-top: -3px !important;

+ 3 - 3
packages/admin-ui/src/lib/catalog/src/components/facet-detail/facet-detail.component.ts

@@ -11,6 +11,7 @@ import {
     DataService,
     DataService,
     DeletionResult,
     DeletionResult,
     FacetWithValues,
     FacetWithValues,
+    findTranslation,
     LanguageCode,
     LanguageCode,
     ModalService,
     ModalService,
     NotificationService,
     NotificationService,
@@ -269,7 +270,7 @@ export class FacetDetailComponent
      * Sets the values of the form on changes to the facet or current language.
      * Sets the values of the form on changes to the facet or current language.
      */
      */
     protected setFormValues(facet: FacetWithValues.Fragment, languageCode: LanguageCode) {
     protected setFormValues(facet: FacetWithValues.Fragment, languageCode: LanguageCode) {
-        const currentTranslation = facet.translations.find(t => t.languageCode === languageCode);
+        const currentTranslation = findTranslation(facet, languageCode);
 
 
         this.detailForm.patchValue({
         this.detailForm.patchValue({
             facet: {
             facet: {
@@ -299,8 +300,7 @@ export class FacetDetailComponent
         currentValuesFormArray.clear();
         currentValuesFormArray.clear();
         this.values = [...facet.values];
         this.values = [...facet.values];
         facet.values.forEach((value, i) => {
         facet.values.forEach((value, i) => {
-            const valueTranslation =
-                value.translations && value.translations.find(t => t.languageCode === languageCode);
+            const valueTranslation = findTranslation(value, languageCode);
             const group = {
             const group = {
                 id: value.id,
                 id: value.id,
                 code: value.code,
                 code: value.code,

+ 1 - 2
packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list.component.scss

@@ -1,7 +1,6 @@
-@import "variables";
 
 
 td {
 td {
     &.private {
     &.private {
-        background-color: $color-grey-200;
+        background-color: var(--color-component-bg-200);
     }
     }
 }
 }

+ 2 - 3
packages/admin-ui/src/lib/catalog/src/components/generate-product-variants/generate-product-variants.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     display: block;
     display: block;
@@ -21,8 +20,8 @@
 .variants-preview {
 .variants-preview {
     tr.disabled {
     tr.disabled {
         td {
         td {
-            background-color: $color-grey-100;
-            color: $color-grey-400;
+            background-color: var(--color-component-bg-100);
+            color: var(--color-grey-400);
         }
         }
     }
     }
 }
 }

+ 6 - 7
packages/admin-ui/src/lib/catalog/src/components/option-value-input/option-value-input.component.scss

@@ -1,14 +1,13 @@
-@import "variables";
 
 
 .input-wrapper {
 .input-wrapper {
     background-color: white;
     background-color: white;
     border-radius: 3px !important;
     border-radius: 3px !important;
-    border: 1px solid $color-grey-300 !important;
+    border: 1px solid var(--color-grey-300) !important;
     cursor: text;
     cursor: text;
 
 
     &.focus {
     &.focus {
-        border-color: $color-primary-500 !important;
-        box-shadow: 0 0 1px 1px $color-primary-100;
+        border-color: var(--color-primary-500) !important;
+        box-shadow: 0 0 1px 1px var(--color-primary-100);
     }
     }
 
 
     .chips {
     .chips {
@@ -25,7 +24,7 @@
             outline: none;
             outline: none;
         }
         }
         &:disabled {
         &:disabled {
-            background-color: $color-grey-100;
+            background-color: var(--color-component-bg-100);
         }
         }
     }
     }
 }
 }
@@ -39,8 +38,8 @@ vdr-chip {
     }
     }
     &.selected {
     &.selected {
         ::ng-deep .wrapper {
         ::ng-deep .wrapper {
-            border-color: $color-warning-500 !important;
-            box-shadow: 0 0 1px 1px $color-warning-400;
+            border-color: var(--color-warning-500) !important;
+            box-shadow: 0 0 1px 1px var(--color-warning-400);
             opacity: 0.6;
             opacity: 0.6;
         }
         }
     }
     }

+ 7 - 8
packages/admin-ui/src/lib/catalog/src/components/product-assets/product-assets.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     width: 340px;
     width: 340px;
@@ -10,12 +9,12 @@
 
 
 .placeholder {
 .placeholder {
     text-align: center;
     text-align: center;
-    color: $color-grey-300;
+    color: var(--color-grey-300);
 }
 }
 
 
 .featured-asset {
 .featured-asset {
     text-align: center;
     text-align: center;
-    background: $color-grey-200;
+    background: var(--color-component-bg-200);
     padding: 6px;
     padding: 6px;
     cursor: pointer;
     cursor: pointer;
 
 
@@ -42,16 +41,16 @@
     .asset-thumb {
     .asset-thumb {
         margin: 3px;
         margin: 3px;
         padding: 0;
         padding: 0;
-        border: 2px solid $color-grey-200;
+        border: 2px solid var(--color-component-border-100);
         cursor: pointer;
         cursor: pointer;
 
 
         &.featured {
         &.featured {
-            border-color: $color-primary-500;
+            border-color: var(--color-primary-500);
         }
         }
     }
     }
 
 
     .remove-asset {
     .remove-asset {
-        color: $color-warning-500;
+        color: var(--color-warning-500);
     }
     }
 
 
     &.compact {
     &.compact {
@@ -67,7 +66,7 @@
     align-items: center;
     align-items: center;
     justify-content: center;
     justify-content: center;
     width: 50px;
     width: 50px;
-    background-color: $color-grey-100;
+    background-color: var(--color-component-bg-100);
     opacity: 0.9;
     opacity: 0.9;
     box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
     box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
     0 8px 10px 1px rgba(0, 0, 0, 0.14),
     0 8px 10px 1px rgba(0, 0, 0, 0.14),
@@ -79,7 +78,7 @@
     width: 60px;
     width: 60px;
     height: 50px;
     height: 50px;
     .asset-thumb {
     .asset-thumb {
-        background-color: $color-grey-300;
+        background-color: var(--color-component-bg-300);
         height: 100%;
         height: 100%;
         width: 54px;
         width: 54px;
     }
     }

+ 16 - 1
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html

@@ -94,6 +94,22 @@
                                     (input)="updateSlug($event.target.value)"
                                     (input)="updateSlug($event.target.value)"
                                 />
                                 />
                             </vdr-form-field>
                             </vdr-form-field>
+                            <div
+                                class="auto-rename-wrapper"
+                                [class.visible]="(isNew$ | async) === false && detailForm.get(['product', 'name'])?.dirty"
+                            >
+                                <clr-checkbox-wrapper>
+                                    <input
+                                        clrCheckbox
+                                        type="checkbox"
+                                        id="auto-update"
+                                        formControlName="autoUpdateVariantNames"
+                                    />
+                                    <label>{{
+                                        'catalog.auto-update-product-variant-name' | translate
+                                    }}</label>
+                                </clr-checkbox-wrapper>
+                            </div>
                             <vdr-form-field
                             <vdr-form-field
                                 [label]="'catalog.slug' | translate"
                                 [label]="'catalog.slug' | translate"
                                 for="slug"
                                 for="slug"
@@ -189,7 +205,6 @@
                         <div class="variant-filter">
                         <div class="variant-filter">
                             <input
                             <input
                                 [formControl]="filterInput"
                                 [formControl]="filterInput"
-                                class="clr-input"
                                 [placeholder]="'catalog.filter-by-name-or-sku' | translate"
                                 [placeholder]="'catalog.filter-by-name-or-sku' | translate"
                             />
                             />
                             <button class="icon-button" (click)="filterInput.setValue('')">
                             <button class="icon-button" (click)="filterInput.setValue('')">

+ 14 - 2
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     ::ng-deep {
     ::ng-deep {
@@ -21,7 +20,8 @@ vdr-action-bar clr-toggle-wrapper {
         border-radius: 3px 0 0 3px !important;
         border-radius: 3px 0 0 3px !important;
     }
     }
     .icon-button {
     .icon-button {
-        border: 1px solid $color-grey-300;
+        border: 1px solid var(--color-component-border-300);
+        background-color: var(--color-component-bg-100);
         border-radius: 0 3px 3px 0;
         border-radius: 0 3px 3px 0;
         border-left: none;
         border-left: none;
     }
     }
@@ -44,3 +44,15 @@ vdr-action-bar clr-toggle-wrapper {
 .channel-assignment {
 .channel-assignment {
     flex-wrap: wrap;
     flex-wrap: wrap;
 }
 }
+
+.auto-rename-wrapper {
+    overflow: hidden;
+    max-height: 0;
+    padding-left: 9.5rem;
+    margin-bottom: 0;
+    transition: max-height 0.2s, margin-bottom 0.2s;
+    &.visible {
+        max-height: 24px;
+        margin-bottom: 12px;
+    }
+}

+ 35 - 19
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts

@@ -10,6 +10,7 @@ import {
     CustomFieldConfig,
     CustomFieldConfig,
     DataService,
     DataService,
     FacetWithValues,
     FacetWithValues,
+    findTranslation,
     flattenFacetValues,
     flattenFacetValues,
     GlobalFlag,
     GlobalFlag,
     IGNORE_CAN_DEACTIVATE_GUARD,
     IGNORE_CAN_DEACTIVATE_GUARD,
@@ -29,7 +30,7 @@ import { normalizeString } from '@vendure/common/lib/normalize-string';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { unique } from '@vendure/common/lib/unique';
 import { unique } from '@vendure/common/lib/unique';
-import { combineLatest, EMPTY, merge, Observable } from 'rxjs';
+import { combineLatest, EMPTY, merge, Observable, of } from 'rxjs';
 import {
 import {
     debounceTime,
     debounceTime,
     distinctUntilChanged,
     distinctUntilChanged,
@@ -43,7 +44,7 @@ import {
     withLatestFrom,
     withLatestFrom,
 } from 'rxjs/operators';
 } from 'rxjs/operators';
 
 
-import { ProductDetailService } from '../../providers/product-detail.service';
+import { ProductDetailService } from '../../providers/product-detail/product-detail.service';
 import { ApplyFacetDialogComponent } from '../apply-facet-dialog/apply-facet-dialog.component';
 import { ApplyFacetDialogComponent } from '../apply-facet-dialog/apply-facet-dialog.component';
 import { AssignProductsToChannelDialogComponent } from '../assign-products-to-channel-dialog/assign-products-to-channel-dialog.component';
 import { AssignProductsToChannelDialogComponent } from '../assign-products-to-channel-dialog/assign-products-to-channel-dialog.component';
 import { CreateProductVariantsConfig } from '../generate-product-variants/generate-product-variants.component';
 import { CreateProductVariantsConfig } from '../generate-product-variants/generate-product-variants.component';
@@ -121,6 +122,7 @@ export class ProductDetailComponent
             product: this.formBuilder.group({
             product: this.formBuilder.group({
                 enabled: true,
                 enabled: true,
                 name: ['', Validators.required],
                 name: ['', Validators.required],
+                autoUpdateVariantNames: true,
                 slug: '',
                 slug: '',
                 description: '',
                 description: '',
                 facetValueIds: [[]],
                 facetValueIds: [[]],
@@ -326,7 +328,7 @@ export class ProductDetailComponent
             .pipe(take(1))
             .pipe(take(1))
             .subscribe(([entity, languageCode]) => {
             .subscribe(([entity, languageCode]) => {
                 const slugControl = this.detailForm.get(['product', 'slug']);
                 const slugControl = this.detailForm.get(['product', 'slug']);
-                const currentTranslation = entity.translations.find(t => t.languageCode === languageCode);
+                const currentTranslation = findTranslation(entity, languageCode);
                 const currentSlugIsEmpty = !currentTranslation || !currentTranslation.slug;
                 const currentSlugIsEmpty = !currentTranslation || !currentTranslation.slug;
                 if (slugControl && slugControl.pristine && currentSlugIsEmpty) {
                 if (slugControl && slugControl.pristine && currentSlugIsEmpty) {
                     slugControl.setValue(normalizeString(`${nameValue}`, '-'));
                     slugControl.setValue(normalizeString(`${nameValue}`, '-'));
@@ -347,19 +349,26 @@ export class ProductDetailComponent
         });
         });
     }
     }
 
 
-    updateProductOption(input: UpdateProductOptionInput) {
-        this.productDetailService.updateProductOption(input).subscribe(
-            () => {
-                this.notificationService.success(_('common.notify-update-success'), {
-                    entity: 'ProductOption',
-                });
-            },
-            err => {
-                this.notificationService.error(_('common.notify-update-error'), {
-                    entity: 'ProductOption',
-                });
-            },
-        );
+    updateProductOption(input: UpdateProductOptionInput & { autoUpdate: boolean }) {
+        combineLatest(this.product$, this.languageCode$)
+            .pipe(
+                take(1),
+                mergeMap(([product, languageCode]) =>
+                    this.productDetailService.updateProductOption(input, product, languageCode),
+                ),
+            )
+            .subscribe(
+                () => {
+                    this.notificationService.success(_('common.notify-update-success'), {
+                        entity: 'ProductOption',
+                    });
+                },
+                err => {
+                    this.notificationService.error(_('common.notify-update-error'), {
+                        entity: 'ProductOption',
+                    });
+                },
+            );
     }
     }
 
 
     removeProductFacetValue(facetValueId: string) {
     removeProductFacetValue(facetValueId: string) {
@@ -485,7 +494,14 @@ export class ProductDetailComponent
                         );
                         );
                     }
                     }
 
 
-                    return this.productDetailService.updateProduct(productInput, variantsInput);
+                    return this.productDetailService.updateProduct({
+                        product,
+                        languageCode,
+                        autoUpdate:
+                            this.detailForm.get(['product', 'autoUpdateVariantNames'])?.value ?? false,
+                        productInput,
+                        variantsInput,
+                    });
                 }),
                 }),
             )
             )
             .subscribe(
             .subscribe(
@@ -515,7 +531,7 @@ export class ProductDetailComponent
      * Sets the values of the form on changes to the product or current language.
      * Sets the values of the form on changes to the product or current language.
      */
      */
     protected setFormValues(product: ProductWithVariants.Fragment, languageCode: LanguageCode) {
     protected setFormValues(product: ProductWithVariants.Fragment, languageCode: LanguageCode) {
-        const currentTranslation = product.translations.find(t => t.languageCode === languageCode);
+        const currentTranslation = findTranslation(product, languageCode);
         this.detailForm.patchValue({
         this.detailForm.patchValue({
             product: {
             product: {
                 enabled: product.enabled,
                 enabled: product.enabled,
@@ -544,7 +560,7 @@ export class ProductDetailComponent
 
 
         const variantsFormArray = this.detailForm.get('variants') as FormArray;
         const variantsFormArray = this.detailForm.get('variants') as FormArray;
         product.variants.forEach((variant, i) => {
         product.variants.forEach((variant, i) => {
-            const variantTranslation = variant.translations.find(t => t.languageCode === languageCode);
+            const variantTranslation = findTranslation(variant, languageCode);
             const facetValueIds = variant.facetValues.map(fv => fv.id);
             const facetValueIds = variant.facetValues.map(fv => fv.id);
             const group: VariantFormValue = {
             const group: VariantFormValue = {
                 id: variant.id,
                 id: variant.id,

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.html

@@ -7,7 +7,7 @@
                 (searchTermChange)="setSearchTerm($event)"
                 (searchTermChange)="setSearchTerm($event)"
                 (facetValueChange)="setFacetValueIds($event)"
                 (facetValueChange)="setFacetValueIds($event)"
             ></vdr-product-search-input>
             ></vdr-product-search-input>
-            <vdr-dropdown class="search-settings-menu">
+            <vdr-dropdown class="search-settings-menu mr3">
                 <button type="button" class="icon-button" vdrDropdownTrigger>
                 <button type="button" class="icon-button" vdrDropdownTrigger>
                     <clr-icon shape="cog"></clr-icon>
                     <clr-icon shape="cog"></clr-icon>
                 </button>
                 </button>

+ 3 - 3
packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.scss

@@ -3,10 +3,10 @@
 .image-placeholder {
 .image-placeholder {
     width: 50px;
     width: 50px;
     height: 50px;
     height: 50px;
-    background-color: $color-grey-200;
+    background-color: var(--color-component-bg-200);
     .placeholder {
     .placeholder {
         text-align: center;
         text-align: center;
-        color: $color-grey-300;
+        color: var(--color-grey-300);
     }
     }
 }
 }
 .search-form {
 .search-form {
@@ -25,5 +25,5 @@
     margin: 0 12px;
     margin: 0 12px;
 }
 }
 td.disabled {
 td.disabled {
-    background-color: $color-grey-200;
+    background-color: var(--color-component-bg-200);
 }
 }

+ 2 - 3
packages/admin-ui/src/lib/catalog/src/components/product-search-input/product-search-input.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 @import "mixins";
 @import "mixins";
 
 
 :host {
 :host {
@@ -19,10 +18,10 @@ ng-select {
 
 
 .search-header {
 .search-header {
     padding: 8px 10px;
     padding: 8px 10px;
-    border-bottom: 1px solid $color-grey-200;
+    border-bottom: 1px solid var(--color-component-border-100);
     cursor: pointer;
     cursor: pointer;
 
 
     &.selected, &:hover {
     &.selected, &:hover {
-        background-color: $color-grey-200;
+        background-color: var(--color-component-bg-200);
     }
     }
 }
 }

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.html

@@ -89,7 +89,7 @@
                         (ngModelChange)="onFormChanged(variantFormValues[variant.id])"
                         (ngModelChange)="onFormChanged(variantFormValues[variant.id])"
                     ></vdr-currency-input>
                     ></vdr-currency-input>
                 </clr-input-container>
                 </clr-input-container>
-                <span *ngIf="variantFormValues[variant.id].existing">{{ variantFormValues[variant.id].price / 100 | currency: currencyCode }}</span>
+                <span *ngIf="variantFormValues[variant.id].existing">{{ variantFormValues[variant.id].price | localeCurrency: currencyCode }}</span>
             </td>
             </td>
             <td>
             <td>
                 <clr-input-container *ngIf="!variantFormValues[variant.id].existing">
                 <clr-input-container *ngIf="!variantFormValues[variant.id].existing">

+ 2 - 3
packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 .option-groups {
 .option-groups {
     display: flex;
     display: flex;
@@ -15,8 +14,8 @@
 .variants-preview {
 .variants-preview {
     tr.disabled {
     tr.disabled {
         td {
         td {
-            background-color: $color-grey-100;
-            color: $color-grey-400;
+            background-color: var(--color-component-bg-100);
+            color: var(--color-grey-400);
         }
         }
     }
     }
 }
 }

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.ts

@@ -19,7 +19,7 @@ import { generateAllCombinations, notNullOrUndefined } from '@vendure/common/lib
 import { EMPTY, forkJoin, Observable, of } from 'rxjs';
 import { EMPTY, forkJoin, Observable, of } from 'rxjs';
 import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators';
 import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators';
 
 
-import { ProductDetailService } from '../../providers/product-detail.service';
+import { ProductDetailService } from '../../providers/product-detail/product-detail.service';
 
 
 export interface VariantInfo {
 export interface VariantInfo {
     productVariantId?: string;
     productVariantId?: string;

+ 4 - 4
packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.scss

@@ -4,7 +4,7 @@
     display: flex;
     display: flex;
     min-height: 52px;
     min-height: 52px;
     align-items: center;
     align-items: center;
-    border: 1px solid $color-grey-200;
+    border: 1px solid var(--color-component-border-100);
     border-radius: 3px;
     border-radius: 3px;
     padding: 6px 18px;
     padding: 6px 18px;
 
 
@@ -21,7 +21,7 @@
     transition: background-color 0.2s;
     transition: background-color 0.2s;
     min-height: 330px;
     min-height: 330px;
     &.disabled {
     &.disabled {
-        background-color: $color-grey-200;
+        background-color: var(--color-component-bg-200);
     }
     }
     .header-row {
     .header-row {
         display: flex;
         display: flex;
@@ -118,7 +118,7 @@
     }
     }
 
 
     .option-group-name {
     .option-group-name {
-        color: $color-grey-400;
+        color: var(--color-text-200);
         text-transform: uppercase;
         text-transform: uppercase;
         font-size: 10px;
         font-size: 10px;
         margin-right: 3px;
         margin-right: 3px;
@@ -127,7 +127,7 @@
 
 
     .options-facets {
     .options-facets {
         display: flex;
         display: flex;
-        color: $color-grey-400;
+        color: var(--color-grey-400);
     }
     }
 }
 }
 
 

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.ts

@@ -64,7 +64,7 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
     @Output() assetChange = new EventEmitter<VariantAssetChange>();
     @Output() assetChange = new EventEmitter<VariantAssetChange>();
     @Output() selectionChange = new EventEmitter<string[]>();
     @Output() selectionChange = new EventEmitter<string[]>();
     @Output() selectFacetValueClick = new EventEmitter<string[]>();
     @Output() selectFacetValueClick = new EventEmitter<string[]>();
-    @Output() updateProductOption = new EventEmitter<UpdateProductOptionInput>();
+    @Output() updateProductOption = new EventEmitter<UpdateProductOptionInput & { autoUpdate: boolean }>();
     selectedVariantIds: string[] = [];
     selectedVariantIds: string[] = [];
     pagination: PaginationInstance = {
     pagination: PaginationInstance = {
         currentPage: 1,
         currentPage: 1,

+ 2 - 3
packages/admin-ui/src/lib/catalog/src/components/product-variants-table/product-variants-table.component.scss

@@ -1,7 +1,6 @@
-@import "variables";
 
 
 .placeholder {
 .placeholder {
-    color: $color-grey-300;
+    color: var(--color-grey-300);
 }
 }
 
 
 .stock, .price {
 .stock, .price {
@@ -13,6 +12,6 @@
 td {
 td {
     transition: background-color 0.2s;
     transition: background-color 0.2s;
     &.disabled {
     &.disabled {
-        background-color: $color-grey-200;
+        background-color: var(--color-component-bg-200);
     }
     }
 }
 }

+ 9 - 1
packages/admin-ui/src/lib/catalog/src/components/update-product-option-dialog/update-product-option-dialog.component.html

@@ -12,6 +12,10 @@
 <vdr-form-field [label]="'common.code' | translate" for="code">
 <vdr-form-field [label]="'common.code' | translate" for="code">
     <input id="code" type="text" #codeInput="ngModel" required [(ngModel)]="code" pattern="[a-z0-9_-]+" />
     <input id="code" type="text" #codeInput="ngModel" required [(ngModel)]="code" pattern="[a-z0-9_-]+" />
 </vdr-form-field>
 </vdr-form-field>
+<clr-checkbox-wrapper>
+    <input type="checkbox" clrCheckbox [(ngModel)]="updateVariantName" />
+    <label>{{ 'catalog.auto-update-option-variant-name' | translate }}</label>
+</clr-checkbox-wrapper>
 <section *ngIf="customFields.length">
 <section *ngIf="customFields.length">
     <label>{{ 'common.custom-fields' | translate }}</label>
     <label>{{ 'common.custom-fields' | translate }}</label>
     <ng-container *ngFor="let customField of customFields">
     <ng-container *ngFor="let customField of customFields">
@@ -30,7 +34,11 @@
     <button
     <button
         type="submit"
         type="submit"
         (click)="update()"
         (click)="update()"
-        [disabled]="nameInput.invalid || codeInput.invalid || (nameInput.pristine && codeInput.pristine && customFieldsForm.pristine)"
+        [disabled]="
+            nameInput.invalid ||
+            codeInput.invalid ||
+            (nameInput.pristine && codeInput.pristine && customFieldsForm.pristine)
+        "
         class="btn btn-primary"
         class="btn btn-primary"
     >
     >
         {{ 'catalog.update-product-option' | translate }}
         {{ 'catalog.update-product-option' | translate }}

+ 6 - 4
packages/admin-ui/src/lib/catalog/src/components/update-product-option-dialog/update-product-option-dialog.component.ts

@@ -16,8 +16,10 @@ import { normalizeString } from '@vendure/common/lib/normalize-string';
     styleUrls: ['./update-product-option-dialog.component.scss'],
     styleUrls: ['./update-product-option-dialog.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 })
-export class UpdateProductOptionDialogComponent implements Dialog<UpdateProductOptionInput>, OnInit {
-    resolveWith: (result?: UpdateProductOptionInput) => void;
+export class UpdateProductOptionDialogComponent
+    implements Dialog<UpdateProductOptionInput & { autoUpdate: boolean }>, OnInit {
+    resolveWith: (result?: UpdateProductOptionInput & { autoUpdate: boolean }) => void;
+    updateVariantName = true;
     // Provided by caller
     // Provided by caller
     productOption: ProductVariant.Options;
     productOption: ProductVariant.Options;
     activeLanguage: LanguageCode;
     activeLanguage: LanguageCode;
@@ -29,7 +31,7 @@ export class UpdateProductOptionDialogComponent implements Dialog<UpdateProductO
 
 
     ngOnInit(): void {
     ngOnInit(): void {
         const currentTranslation = this.productOption.translations.find(
         const currentTranslation = this.productOption.translations.find(
-            (t) => t.languageCode === this.activeLanguage,
+            t => t.languageCode === this.activeLanguage,
         );
         );
         this.name = currentTranslation?.name ?? '';
         this.name = currentTranslation?.name ?? '';
         this.code = this.productOption.code;
         this.code = this.productOption.code;
@@ -64,7 +66,7 @@ export class UpdateProductOptionDialogComponent implements Dialog<UpdateProductO
                 name: '',
                 name: '',
             },
             },
         });
         });
-        this.resolveWith(result);
+        this.resolveWith({ ...result, autoUpdate: this.updateVariantName });
     }
     }
 
 
     cancel() {
     cancel() {

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/variant-price-detail/variant-price-detail.component.html

@@ -5,6 +5,6 @@
 <div *ngIf="!priceIncludesTax" class="value">
 <div *ngIf="!priceIncludesTax" class="value">
     {{
     {{
         'catalog.price-with-tax-in-default-zone'
         'catalog.price-with-tax-in-default-zone'
-            | translate: { price: grossPrice$ | async | currency: currencyCode, rate: taxRate$ | async }
+            | translate: { price: grossPrice$ | async | localeCurrency: currencyCode, rate: taxRate$ | async }
     }}
     }}
 </div>
 </div>

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/variant-price-detail/variant-price-detail.component.ts

@@ -51,7 +51,7 @@ export class VariantPriceDetailComponent implements OnInit, OnChanges {
 
 
         this.grossPrice$ = combineLatest(this.taxRate$, this.priceChange$).pipe(
         this.grossPrice$ = combineLatest(this.taxRate$, this.priceChange$).pipe(
             map(([taxRate, price]) => {
             map(([taxRate, price]) => {
-                return Math.round(price * ((100 + taxRate) / 100)) / 100;
+                return Math.round(price * ((100 + taxRate) / 100));
             }),
             }),
         );
         );
     }
     }

+ 100 - 20
packages/admin-ui/src/lib/catalog/src/providers/product-detail.service.ts → packages/admin-ui/src/lib/catalog/src/providers/product-detail/product-detail.service.ts

@@ -5,7 +5,9 @@ import {
     DataService,
     DataService,
     DeletionResult,
     DeletionResult,
     FacetWithValues,
     FacetWithValues,
+    findTranslation,
     LanguageCode,
     LanguageCode,
+    ProductWithVariants,
     UpdateProductInput,
     UpdateProductInput,
     UpdateProductMutation,
     UpdateProductMutation,
     UpdateProductOptionInput,
     UpdateProductOptionInput,
@@ -17,7 +19,9 @@ import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { forkJoin, Observable, of, throwError } from 'rxjs';
 import { forkJoin, Observable, of, throwError } from 'rxjs';
 import { map, mergeMap, shareReplay, switchMap } from 'rxjs/operators';
 import { map, mergeMap, shareReplay, switchMap } from 'rxjs/operators';
 
 
-import { CreateProductVariantsConfig } from '../components/generate-product-variants/generate-product-variants.component';
+import { CreateProductVariantsConfig } from '../../components/generate-product-variants/generate-product-variants.component';
+
+import { replaceLast } from './replace-last';
 
 
 /**
 /**
  * Handles the logic for making the API calls to perform CRUD operations on a Product and its related
  * Handles the logic for making the API calls to perform CRUD operations on a Product and its related
@@ -30,13 +34,13 @@ export class ProductDetailService {
     constructor(private dataService: DataService) {}
     constructor(private dataService: DataService) {}
 
 
     getFacets(): Observable<FacetWithValues.Fragment[]> {
     getFacets(): Observable<FacetWithValues.Fragment[]> {
-        return this.dataService.facet.getAllFacets().mapSingle((data) => data.facets.items);
+        return this.dataService.facet.getAllFacets().mapSingle(data => data.facets.items);
     }
     }
 
 
     getTaxCategories() {
     getTaxCategories() {
         return this.dataService.settings
         return this.dataService.settings
             .getTaxCategories()
             .getTaxCategories()
-            .mapSingle((data) => data.taxCategories)
+            .mapSingle(data => data.taxCategories)
             .pipe(shareReplay(1));
             .pipe(shareReplay(1));
     }
     }
 
 
@@ -46,14 +50,14 @@ export class ProductDetailService {
         languageCode: LanguageCode,
         languageCode: LanguageCode,
     ) {
     ) {
         const createProduct$ = this.dataService.product.createProduct(input);
         const createProduct$ = this.dataService.product.createProduct(input);
-        const nonEmptyOptionGroups = createVariantsConfig.groups.filter((g) => 0 < g.values.length);
+        const nonEmptyOptionGroups = createVariantsConfig.groups.filter(g => 0 < g.values.length);
         const createOptionGroups$ = this.createProductOptionGroups(nonEmptyOptionGroups, languageCode);
         const createOptionGroups$ = this.createProductOptionGroups(nonEmptyOptionGroups, languageCode);
 
 
         return forkJoin(createProduct$, createOptionGroups$).pipe(
         return forkJoin(createProduct$, createOptionGroups$).pipe(
             mergeMap(([{ createProduct }, optionGroups]) => {
             mergeMap(([{ createProduct }, optionGroups]) => {
                 const addOptionsToProduct$ = optionGroups.length
                 const addOptionsToProduct$ = optionGroups.length
                     ? forkJoin(
                     ? forkJoin(
-                          optionGroups.map((optionGroup) => {
+                          optionGroups.map(optionGroup => {
                               return this.dataService.product.addOptionGroupToProduct({
                               return this.dataService.product.addOptionGroupToProduct({
                                   productId: createProduct.id,
                                   productId: createProduct.id,
                                   optionGroupId: optionGroup.id,
                                   optionGroupId: optionGroup.id,
@@ -68,10 +72,10 @@ export class ProductDetailService {
                 );
                 );
             }),
             }),
             mergeMap(({ createProduct, optionGroups }) => {
             mergeMap(({ createProduct, optionGroups }) => {
-                const variants = createVariantsConfig.variants.map((v) => {
+                const variants = createVariantsConfig.variants.map(v => {
                     const optionIds = optionGroups.length
                     const optionIds = optionGroups.length
                         ? v.optionValues.map((optionName, index) => {
                         ? v.optionValues.map((optionName, index) => {
-                              const option = optionGroups[index].options.find((o) => o.name === optionName);
+                              const option = optionGroups[index].options.find(o => o.name === optionName);
                               if (!option) {
                               if (!option) {
                                   throw new Error(
                                   throw new Error(
                                       `Could not find a matching ProductOption "${optionName}" when creating variant`,
                                       `Could not find a matching ProductOption "${optionName}" when creating variant`,
@@ -85,7 +89,7 @@ export class ProductDetailService {
                         optionIds,
                         optionIds,
                     };
                     };
                 });
                 });
-                const options = optionGroups.map((og) => og.options).reduce((flat, o) => [...flat, ...o], []);
+                const options = optionGroups.map(og => og.options).reduce((flat, o) => [...flat, ...o], []);
                 return this.createProductVariants(createProduct, variants, options, languageCode);
                 return this.createProductVariants(createProduct, variants, options, languageCode);
             }),
             }),
         );
         );
@@ -94,17 +98,17 @@ export class ProductDetailService {
     createProductOptionGroups(groups: Array<{ name: string; values: string[] }>, languageCode: LanguageCode) {
     createProductOptionGroups(groups: Array<{ name: string; values: string[] }>, languageCode: LanguageCode) {
         return groups.length
         return groups.length
             ? forkJoin(
             ? forkJoin(
-                  groups.map((c) => {
+                  groups.map(c => {
                       return this.dataService.product
                       return this.dataService.product
                           .createProductOptionGroups({
                           .createProductOptionGroups({
                               code: normalizeString(c.name, '-'),
                               code: normalizeString(c.name, '-'),
                               translations: [{ languageCode, name: c.name }],
                               translations: [{ languageCode, name: c.name }],
-                              options: c.values.map((v) => ({
+                              options: c.values.map(v => ({
                                   code: normalizeString(v, '-'),
                                   code: normalizeString(v, '-'),
                                   translations: [{ languageCode, name: v }],
                                   translations: [{ languageCode, name: v }],
                               })),
                               })),
                           })
                           })
-                          .pipe(map((data) => data.createProductOptionGroup));
+                          .pipe(map(data => data.createProductOptionGroup));
                   }),
                   }),
               )
               )
             : of([]);
             : of([]);
@@ -116,12 +120,12 @@ export class ProductDetailService {
         options: Array<{ id: string; name: string }>,
         options: Array<{ id: string; name: string }>,
         languageCode: LanguageCode,
         languageCode: LanguageCode,
     ) {
     ) {
-        const variants: CreateProductVariantInput[] = variantData.map((v) => {
+        const variants: CreateProductVariantInput[] = variantData.map(v => {
             const name = options.length
             const name = options.length
                 ? `${product.name} ${v.optionIds
                 ? `${product.name} ${v.optionIds
-                      .map((id) => options.find((o) => o.id === id))
+                      .map(id => options.find(o => o.id === id))
                       .filter(notNullOrUndefined)
                       .filter(notNullOrUndefined)
-                      .map((o) => o.name)
+                      .map(o => o.name)
                       .join(' ')}`
                       .join(' ')}`
                 : product.name;
                 : product.name;
             return {
             return {
@@ -146,24 +150,100 @@ export class ProductDetailService {
         );
         );
     }
     }
 
 
-    updateProduct(productInput?: UpdateProductInput, variantInput?: UpdateProductVariantInput[]) {
+    updateProduct(updateOptions: {
+        product: ProductWithVariants.Fragment;
+        languageCode: LanguageCode;
+        autoUpdate: boolean;
+        productInput?: UpdateProductInput;
+        variantsInput?: UpdateProductVariantInput[];
+    }) {
+        const { product, languageCode, autoUpdate, productInput, variantsInput } = updateOptions;
         const updateOperations: Array<Observable<UpdateProductMutation | UpdateProductVariantsMutation>> = [];
         const updateOperations: Array<Observable<UpdateProductMutation | UpdateProductVariantsMutation>> = [];
+        const updateVariantsInput = variantsInput || [];
         if (productInput) {
         if (productInput) {
             updateOperations.push(this.dataService.product.updateProduct(productInput));
             updateOperations.push(this.dataService.product.updateProduct(productInput));
+
+            const productOldName = findTranslation(product, languageCode)?.name;
+            const productNewName = findTranslation(productInput, languageCode)?.name;
+            if (productOldName && productNewName && autoUpdate) {
+                for (const variant of product.variants) {
+                    const currentVariantName = findTranslation(variant, languageCode)?.name || '';
+                    let variantInput: UpdateProductVariantInput;
+                    const existingVariantInput = updateVariantsInput.find(i => i.id === variant.id);
+                    if (existingVariantInput) {
+                        variantInput = existingVariantInput;
+                    } else {
+                        variantInput = {
+                            id: variant.id,
+                            translations: [{ languageCode, name: currentVariantName }],
+                        };
+                        updateVariantsInput.push(variantInput);
+                    }
+                    const variantTranslation = findTranslation(variantInput, languageCode);
+                    if (variantTranslation) {
+                        variantTranslation.name = replaceLast(
+                            variantTranslation.name,
+                            productOldName,
+                            productNewName,
+                        );
+                    }
+                }
+            }
         }
         }
-        if (variantInput) {
-            updateOperations.push(this.dataService.product.updateProductVariants(variantInput));
+        if (updateVariantsInput.length) {
+            updateOperations.push(this.dataService.product.updateProductVariants(updateVariantsInput));
         }
         }
         return forkJoin(updateOperations);
         return forkJoin(updateOperations);
     }
     }
 
 
-    updateProductOption(input: UpdateProductOptionInput) {
-        return this.dataService.product.updateProductOption(input);
+    updateProductOption(
+        input: UpdateProductOptionInput & { autoUpdate: boolean },
+        product: ProductWithVariants.Fragment,
+        languageCode: LanguageCode,
+    ) {
+        let updateProductVariantNames$: Observable<any> = of([]);
+        if (input.autoUpdate) {
+            // Update any ProductVariants' names which include the option name
+            let oldOptionName: string | undefined;
+            const newOptionName = findTranslation(input, languageCode)?.name;
+            if (!newOptionName) {
+                updateProductVariantNames$ = of([]);
+            }
+            const variantsToUpdate: UpdateProductVariantInput[] = [];
+            for (const variant of product.variants) {
+                if (variant.options.map(o => o.id).includes(input.id)) {
+                    if (!oldOptionName) {
+                        oldOptionName = findTranslation(
+                            variant.options.find(o => o.id === input.id),
+                            languageCode,
+                        )?.name;
+                    }
+                    const variantName = findTranslation(variant, languageCode)?.name || '';
+                    if (oldOptionName && newOptionName && variantName.includes(oldOptionName)) {
+                        variantsToUpdate.push({
+                            id: variant.id,
+                            translations: [
+                                {
+                                    languageCode,
+                                    name: replaceLast(variantName, oldOptionName, newOptionName),
+                                },
+                            ],
+                        });
+                    }
+                }
+            }
+            if (variantsToUpdate.length) {
+                updateProductVariantNames$ = this.dataService.product.updateProductVariants(variantsToUpdate);
+            }
+        }
+        return this.dataService.product
+            .updateProductOption(input)
+            .pipe(mergeMap(() => updateProductVariantNames$));
     }
     }
 
 
     deleteProductVariant(id: string, productId: string) {
     deleteProductVariant(id: string, productId: string) {
         return this.dataService.product.deleteProductVariant(id).pipe(
         return this.dataService.product.deleteProductVariant(id).pipe(
-            switchMap((result) => {
+            switchMap(result => {
                 if (result.deleteProductVariant.result === DeletionResult.DELETED) {
                 if (result.deleteProductVariant.result === DeletionResult.DELETED) {
                     return this.dataService.product.getProduct(productId).single$;
                     return this.dataService.product.getProduct(productId).single$;
                 } else {
                 } else {

+ 24 - 0
packages/admin-ui/src/lib/catalog/src/providers/product-detail/replace-last.spec.ts

@@ -0,0 +1,24 @@
+import { replaceLast } from './replace-last';
+
+describe('replaceLast()', () => {
+    it('leaves non-matching strings intact', () => {
+        expect(replaceLast('foo bar baz', 'find', 'replace')).toBe('foo bar baz');
+    });
+    it('term is at start of target', () => {
+        expect(replaceLast('find bar baz', 'find', 'replace')).toBe('replace bar baz');
+    });
+    it('term is at end of target', () => {
+        expect(replaceLast('foo bar find', 'find', 'replace')).toBe('foo bar replace');
+    });
+    it('replaces last of 2 occurrences', () => {
+        expect(replaceLast('foo find bar find', 'find', 'replace')).toBe('foo find bar replace');
+    });
+    it('replaces last of 2 consecutive occurrences', () => {
+        expect(replaceLast('foo find find bar', 'find', 'replace')).toBe('foo find replace bar');
+    });
+    it('replaces last of 3 occurrences', () => {
+        expect(replaceLast('find foo find bar find baz', 'find', 'replace')).toBe(
+            'find foo find bar replace baz',
+        );
+    });
+});

+ 17 - 0
packages/admin-ui/src/lib/catalog/src/providers/product-detail/replace-last.ts

@@ -0,0 +1,17 @@
+/**
+ * @description
+ * Like String.prototype.replace(), but replaces the last instance
+ * rather than the first.
+ */
+export function replaceLast(target: string | undefined | null, search: string, replace: string): string {
+    if (!target) {
+        return '';
+    }
+    const lastIndex = target.lastIndexOf(search);
+    if (lastIndex === -1) {
+        return target;
+    }
+    const head = target.substr(0, lastIndex);
+    const tail = target.substr(lastIndex).replace(search, replace);
+    return head + tail;
+}

+ 2 - 1
packages/admin-ui/src/lib/catalog/src/public_api.ts

@@ -25,7 +25,8 @@ export * from './components/product-variants-list/product-variants-list.componen
 export * from './components/product-variants-table/product-variants-table.component';
 export * from './components/product-variants-table/product-variants-table.component';
 export * from './components/update-product-option-dialog/update-product-option-dialog.component';
 export * from './components/update-product-option-dialog/update-product-option-dialog.component';
 export * from './components/variant-price-detail/variant-price-detail.component';
 export * from './components/variant-price-detail/variant-price-detail.component';
-export * from './providers/product-detail.service';
+export * from './providers/product-detail/product-detail.service';
+export * from './providers/product-detail/replace-last';
 export * from './providers/routing/asset-resolver';
 export * from './providers/routing/asset-resolver';
 export * from './providers/routing/collection-resolver';
 export * from './providers/routing/collection-resolver';
 export * from './providers/routing/facet-resolver';
 export * from './providers/routing/facet-resolver';

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

@@ -1,9 +1,8 @@
-@import "variables";
 .progress {
 .progress {
     position: absolute;
     position: absolute;
     overflow: hidden;
     overflow: hidden;
     height: 4px;
     height: 4px;
-    background-color: $color-grey-500;
+    background-color: var(--color-grey-500);
     opacity: 0;
     opacity: 0;
     transition: opacity 0.1s;
     transition: opacity 0.1s;
     &.visible {
     &.visible {

+ 13 - 2
packages/admin-ui/src/lib/core/src/app.component.ts

@@ -1,4 +1,5 @@
-import { Component, OnInit } from '@angular/core';
+import { DOCUMENT } from '@angular/common';
+import { Component, HostBinding, Inject, OnInit } from '@angular/core';
 import { Observable } from 'rxjs';
 import { Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
 import { map } from 'rxjs/operators';
 
 
@@ -11,12 +12,22 @@ import { DataService } from './data/providers/data.service';
 })
 })
 export class AppComponent implements OnInit {
 export class AppComponent implements OnInit {
     loading$: Observable<boolean>;
     loading$: Observable<boolean>;
+    private _document?: Document;
 
 
-    constructor(private dataService: DataService) {}
+    constructor(private dataService: DataService, @Inject(DOCUMENT) private document?: any) {
+        this._document = document;
+    }
 
 
     ngOnInit() {
     ngOnInit() {
         this.loading$ = this.dataService.client
         this.loading$ = this.dataService.client
             .getNetworkStatus()
             .getNetworkStatus()
             .stream$.pipe(map(data => 0 < data.networkStatus.inFlightRequests));
             .stream$.pipe(map(data => 0 < data.networkStatus.inFlightRequests));
+
+        this.dataService.client
+            .uiState()
+            .mapStream(data => data.uiState.theme)
+            .subscribe(theme => {
+                this._document?.body.setAttribute('data-theme', theme);
+            });
     }
     }
 }
 }

+ 56 - 5
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -311,6 +311,7 @@ export type Mutation = {
   assignRoleToAdministrator: Administrator;
   assignRoleToAdministrator: Administrator;
   /** Authenticates the user using a named authentication strategy */
   /** Authenticates the user using a named authentication strategy */
   authenticate: AuthenticationResult;
   authenticate: AuthenticationResult;
+  cancelJob: Job;
   cancelOrder: CancelOrderResult;
   cancelOrder: CancelOrderResult;
   /** Create a new Administrator */
   /** Create a new Administrator */
   createAdministrator: Administrator;
   createAdministrator: Administrator;
@@ -421,7 +422,8 @@ export type Mutation = {
   setAsLoggedIn: UserStatus;
   setAsLoggedIn: UserStatus;
   setAsLoggedOut: UserStatus;
   setAsLoggedOut: UserStatus;
   setOrderCustomFields?: Maybe<Order>;
   setOrderCustomFields?: Maybe<Order>;
-  setUiLanguage?: Maybe<LanguageCode>;
+  setUiLanguage: LanguageCode;
+  setUiTheme: Scalars['String'];
   settlePayment: SettlePaymentResult;
   settlePayment: SettlePaymentResult;
   settleRefund: SettleRefundResult;
   settleRefund: SettleRefundResult;
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
@@ -536,6 +538,11 @@ export type MutationAuthenticateArgs = {
 };
 };
 
 
 
 
+export type MutationCancelJobArgs = {
+  jobId: Scalars['ID'];
+};
+
+
 export type MutationCancelOrderArgs = {
 export type MutationCancelOrderArgs = {
   input: CancelOrderInput;
   input: CancelOrderInput;
 };
 };
@@ -829,7 +836,12 @@ export type MutationSetOrderCustomFieldsArgs = {
 
 
 
 
 export type MutationSetUiLanguageArgs = {
 export type MutationSetUiLanguageArgs = {
-  languageCode?: Maybe<LanguageCode>;
+  languageCode: LanguageCode;
+};
+
+
+export type MutationSetUiThemeArgs = {
+  theme: Scalars['String'];
 };
 };
 
 
 
 
@@ -1392,7 +1404,8 @@ export enum JobState {
   RUNNING = 'RUNNING',
   RUNNING = 'RUNNING',
   COMPLETED = 'COMPLETED',
   COMPLETED = 'COMPLETED',
   RETRYING = 'RETRYING',
   RETRYING = 'RETRYING',
-  FAILED = 'FAILED'
+  FAILED = 'FAILED',
+  CANCELLED = 'CANCELLED'
 }
 }
 
 
 export type JobList = PaginatedList & {
 export type JobList = PaginatedList & {
@@ -2003,6 +2016,7 @@ export type ProductTranslationInput = {
 
 
 export type CreateProductInput = {
 export type CreateProductInput = {
   featuredAssetId?: Maybe<Scalars['ID']>;
   featuredAssetId?: Maybe<Scalars['ID']>;
+  enabled?: Maybe<Scalars['Boolean']>;
   assetIds?: Maybe<Array<Scalars['ID']>>;
   assetIds?: Maybe<Array<Scalars['ID']>>;
   facetValueIds?: Maybe<Array<Scalars['ID']>>;
   facetValueIds?: Maybe<Array<Scalars['ID']>>;
   translations: Array<ProductTranslationInput>;
   translations: Array<ProductTranslationInput>;
@@ -4630,6 +4644,7 @@ export type UserStatus = {
 export type UiState = {
 export type UiState = {
   __typename?: 'UiState';
   __typename?: 'UiState';
   language: LanguageCode;
   language: LanguageCode;
+  theme: Scalars['String'];
 };
 };
 
 
 export type CurrentUserChannelInput = {
 export type CurrentUserChannelInput = {
@@ -4892,6 +4907,13 @@ export type SetUiLanguageMutationVariables = Exact<{
 
 
 export type SetUiLanguageMutation = Pick<Mutation, 'setUiLanguage'>;
 export type SetUiLanguageMutation = Pick<Mutation, 'setUiLanguage'>;
 
 
+export type SetUiThemeMutationVariables = Exact<{
+  theme: Scalars['String'];
+}>;
+
+
+export type SetUiThemeMutation = Pick<Mutation, 'setUiTheme'>;
+
 export type GetNetworkStatusQueryVariables = Exact<{ [key: string]: never; }>;
 export type GetNetworkStatusQueryVariables = Exact<{ [key: string]: never; }>;
 
 
 
 
@@ -4913,7 +4935,7 @@ export type GetUiStateQueryVariables = Exact<{ [key: string]: never; }>;
 
 
 export type GetUiStateQuery = { uiState: (
 export type GetUiStateQuery = { uiState: (
     { __typename?: 'UiState' }
     { __typename?: 'UiState' }
-    & Pick<UiState, 'language'>
+    & Pick<UiState, 'language' | 'theme'>
   ) };
   ) };
 
 
 export type GetClientStateQueryVariables = Exact<{ [key: string]: never; }>;
 export type GetClientStateQueryVariables = Exact<{ [key: string]: never; }>;
@@ -4927,7 +4949,7 @@ export type GetClientStateQuery = { networkStatus: (
     & UserStatusFragment
     & UserStatusFragment
   ), uiState: (
   ), uiState: (
     { __typename?: 'UiState' }
     { __typename?: 'UiState' }
-    & Pick<UiState, 'language'>
+    & Pick<UiState, 'language' | 'theme'>
   ) };
   ) };
 
 
 export type SetActiveChannelMutationVariables = Exact<{
 export type SetActiveChannelMutationVariables = Exact<{
@@ -5460,6 +5482,12 @@ export type OrderFragment = (
   & { customer?: Maybe<(
   & { customer?: Maybe<(
     { __typename?: 'Customer' }
     { __typename?: 'Customer' }
     & Pick<Customer, 'id' | 'firstName' | 'lastName'>
     & Pick<Customer, 'id' | 'firstName' | 'lastName'>
+  )>, shippingLines: Array<(
+    { __typename?: 'ShippingLine' }
+    & { shippingMethod: (
+      { __typename?: 'ShippingMethod' }
+      & Pick<ShippingMethod, 'name'>
+    ) }
   )> }
   )> }
 );
 );
 
 
@@ -7381,6 +7409,16 @@ export type GetJobQueueListQuery = { jobQueues: Array<(
     & Pick<JobQueue, 'name' | 'running'>
     & Pick<JobQueue, 'name' | 'running'>
   )> };
   )> };
 
 
+export type CancelJobMutationVariables = Exact<{
+  id: Scalars['ID'];
+}>;
+
+
+export type CancelJobMutation = { cancelJob: (
+    { __typename?: 'Job' }
+    & JobInfoFragment
+  ) };
+
 export type ReindexMutationVariables = Exact<{ [key: string]: never; }>;
 export type ReindexMutationVariables = Exact<{ [key: string]: never; }>;
 
 
 
 
@@ -7829,6 +7867,11 @@ export namespace SetUiLanguage {
   export type Mutation = SetUiLanguageMutation;
   export type Mutation = SetUiLanguageMutation;
 }
 }
 
 
+export namespace SetUiTheme {
+  export type Variables = SetUiThemeMutationVariables;
+  export type Mutation = SetUiThemeMutation;
+}
+
 export namespace GetNetworkStatus {
 export namespace GetNetworkStatus {
   export type Variables = GetNetworkStatusQueryVariables;
   export type Variables = GetNetworkStatusQueryVariables;
   export type Query = GetNetworkStatusQuery;
   export type Query = GetNetworkStatusQuery;
@@ -8136,6 +8179,8 @@ export namespace OrderAddress {
 export namespace Order {
 export namespace Order {
   export type Fragment = OrderFragment;
   export type Fragment = OrderFragment;
   export type Customer = (NonNullable<OrderFragment['customer']>);
   export type Customer = (NonNullable<OrderFragment['customer']>);
+  export type ShippingLines = NonNullable<(NonNullable<OrderFragment['shippingLines']>)[number]>;
+  export type ShippingMethod = (NonNullable<NonNullable<(NonNullable<OrderFragment['shippingLines']>)[number]>['shippingMethod']>);
 }
 }
 
 
 export namespace Fulfillment {
 export namespace Fulfillment {
@@ -8928,6 +8973,12 @@ export namespace GetJobQueueList {
   export type JobQueues = NonNullable<(NonNullable<GetJobQueueListQuery['jobQueues']>)[number]>;
   export type JobQueues = NonNullable<(NonNullable<GetJobQueueListQuery['jobQueues']>)[number]>;
 }
 }
 
 
+export namespace CancelJob {
+  export type Variables = CancelJobMutationVariables;
+  export type Mutation = CancelJobMutation;
+  export type CancelJob = (NonNullable<CancelJobMutation['cancelJob']>);
+}
+
 export namespace Reindex {
 export namespace Reindex {
   export type Variables = ReindexMutationVariables;
   export type Variables = ReindexMutationVariables;
   export type Mutation = ReindexMutation;
   export type Mutation = ReindexMutation;

+ 3 - 1
packages/admin-ui/src/lib/core/src/common/utilities/create-updated-translatable.ts

@@ -3,6 +3,8 @@ import { assertNever } from '@vendure/common/lib/shared-utils';
 
 
 import { CustomFieldConfig, LanguageCode } from '../generated-types';
 import { CustomFieldConfig, LanguageCode } from '../generated-types';
 
 
+import { findTranslation } from './find-translation';
+
 export interface TranslatableUpdateOptions<T extends { translations: any[] } & MayHaveCustomFields> {
 export interface TranslatableUpdateOptions<T extends { translations: any[] } & MayHaveCustomFields> {
     translatable: T;
     translatable: T;
     updatedFields: { [key: string]: any };
     updatedFields: { [key: string]: any };
@@ -25,7 +27,7 @@ export function createUpdatedTranslatable<T extends { translations: any[] } & Ma
 ): T {
 ): T {
     const { translatable, updatedFields, languageCode, customFieldConfig, defaultTranslation } = options;
     const { translatable, updatedFields, languageCode, customFieldConfig, defaultTranslation } = options;
     const currentTranslation =
     const currentTranslation =
-        translatable.translations.find(t => t.languageCode === languageCode) || defaultTranslation;
+        findTranslation(translatable, languageCode) || defaultTranslation || ({} as any);
     const index = translatable.translations.indexOf(currentTranslation);
     const index = translatable.translations.indexOf(currentTranslation);
     const newTranslation = patchObject(currentTranslation, updatedFields);
     const newTranslation = patchObject(currentTranslation, updatedFields);
     const newCustomFields: CustomFieldsObject = {};
     const newCustomFields: CustomFieldsObject = {};

+ 17 - 0
packages/admin-ui/src/lib/core/src/common/utilities/find-translation.ts

@@ -0,0 +1,17 @@
+import { LanguageCode } from '../generated-types';
+
+export type Translation<T> = T & { languageCode: LanguageCode };
+export type PossiblyTranslatable<T> = { translations?: Array<Translation<T>> | null };
+export type TranslationOf<E> = E extends PossiblyTranslatable<infer U> ? U : undefined;
+
+/**
+ * @description
+ * Given a translatable entity, returns the translation in the specified LanguageCode if
+ * one exists.
+ */
+export function findTranslation<E extends PossiblyTranslatable<any>>(
+    entity: E | undefined,
+    languageCode: LanguageCode,
+): TranslationOf<E> | undefined {
+    return (entity?.translations || []).find(t => t.languageCode === languageCode);
+}

+ 1 - 11
packages/admin-ui/src/lib/core/src/common/utilities/string-to-color.ts

@@ -3,32 +3,22 @@
  */
  */
 export function stringToColor(input: string): string {
 export function stringToColor(input: string): string {
     if (!input || input === '') {
     if (!input || input === '') {
-        return '#fff';
+        return 'var(--color-component-bg-100)';
     }
     }
     const safeColors = [
     const safeColors = [
-        // '#FF4343',
-        // '#9A0089',
-        // '#881798',
         '#10893E',
         '#10893E',
         '#107C10',
         '#107C10',
         '#7E735F',
         '#7E735F',
         '#2F5646',
         '#2F5646',
-        // '#D13438',
-        // '#C239B3',
-        // '#B146C2',
         '#498205',
         '#498205',
         '#847545',
         '#847545',
-        // '#EF6950',
-        // '#BF0077',
         '#744DA9',
         '#744DA9',
         '#018574',
         '#018574',
         '#486860',
         '#486860',
         '#525E54',
         '#525E54',
         '#647C64',
         '#647C64',
         '#567C73',
         '#567C73',
-        // '#DA3B01',
         '#8764B8',
         '#8764B8',
-        // '#C30052',
         '#515C6B',
         '#515C6B',
         '#4A5459',
         '#4A5459',
         '#69797E',
         '#69797E',

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

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

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

@@ -1,7 +1,7 @@
 <clr-main-container>
 <clr-main-container>
     <clr-header>
     <clr-header>
         <div class="branding">
         <div class="branding">
-            <a [routerLink]="['/']"><img src="assets/cube-logo-75px.png" class="logo" /></a>
+            <a [routerLink]="['/']"><img src="assets/logo-75px.png" class="logo" /></a>
         </div>
         </div>
         <div class="header-nav"></div>
         <div class="header-nav"></div>
         <div class="header-actions">
         <div class="header-actions">

+ 1 - 2
packages/admin-ui/src/lib/core/src/components/breadcrumb/breadcrumb.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     display: block;
     display: block;
@@ -14,7 +13,7 @@
     li:not(:last-child)::after {
     li:not(:last-child)::after {
         content: '›';
         content: '›';
         top: 0;
         top: 0;
-        color: #7e7e7e;
+        color: var(--color-grey-400);
         margin-left: 10px;
         margin-left: 10px;
     }
     }
 }
 }

+ 11 - 2
packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.html

@@ -1,11 +1,20 @@
-<ng-container >
+<ng-container>
     <vdr-dropdown>
     <vdr-dropdown>
         <button class="btn btn-link active-channel" vdrDropdownTrigger>
         <button class="btn btn-link active-channel" vdrDropdownTrigger>
             <vdr-channel-badge [channelCode]="activeChannelCode$ | async"></vdr-channel-badge>
             <vdr-channel-badge [channelCode]="activeChannelCode$ | async"></vdr-channel-badge>
-            <span class="active-channel">{{ activeChannelCode$ | async | channelCodeToLabel | translate }}</span>
+            <span class="active-channel">{{
+                activeChannelCode$ | async | channelCodeToLabel | translate
+            }}</span>
             <span class="trigger"><clr-icon shape="caret down"></clr-icon></span>
             <span class="trigger"><clr-icon shape="caret down"></clr-icon></span>
         </button>
         </button>
         <vdr-dropdown-menu vdrPosition="bottom-right">
         <vdr-dropdown-menu vdrPosition="bottom-right">
+            <input
+                *ngIf="((channelCount$ | async) || 0) >= displayFilterThreshold"
+                [formControl]="filterControl"
+                type="text"
+                class="ml2 mr2"
+                [placeholder]="'common.filter' | translate"
+            />
             <button
             <button
                 *ngFor="let channel of channels$ | async"
                 *ngFor="let channel of channels$ | async"
                 type="button"
                 type="button"

+ 1 - 2
packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     display: flex;
     display: flex;
@@ -8,7 +7,7 @@
 }
 }
 
 
 .active-channel {
 .active-channel {
-    color: $color-grey-200;
+    color: var(--color-grey-200);
 
 
     clr-icon {
     clr-icon {
         color: white;
         color: white;

+ 19 - 3
packages/admin-ui/src/lib/core/src/components/channel-switcher/channel-switcher.component.ts

@@ -1,9 +1,10 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
-import { Observable } from 'rxjs';
-import { filter, map } from 'rxjs/operators';
+import { combineLatest, Observable } from 'rxjs';
+import { filter, map, startWith } from 'rxjs/operators';
 
 
 import { CurrentUserChannel } from '../../common/generated-types';
 import { CurrentUserChannel } from '../../common/generated-types';
 import { DataService } from '../../data/providers/data.service';
 import { DataService } from '../../data/providers/data.service';
@@ -16,12 +17,26 @@ import { LocalStorageService } from '../../providers/local-storage/local-storage
     changeDetection: ChangeDetectionStrategy.OnPush,
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 })
 export class ChannelSwitcherComponent implements OnInit {
 export class ChannelSwitcherComponent implements OnInit {
+    readonly displayFilterThreshold = 10;
     channels$: Observable<CurrentUserChannel[]>;
     channels$: Observable<CurrentUserChannel[]>;
+    channelCount$: Observable<number>;
+    filterControl = new FormControl('');
     activeChannelCode$: Observable<string>;
     activeChannelCode$: Observable<string>;
     constructor(private dataService: DataService, private localStorageService: LocalStorageService) {}
     constructor(private dataService: DataService, private localStorageService: LocalStorageService) {}
 
 
     ngOnInit() {
     ngOnInit() {
-        this.channels$ = this.dataService.client.userStatus().mapStream(data => data.userStatus.channels);
+        const channels$ = this.dataService.client.userStatus().mapStream(data => data.userStatus.channels);
+        const filterTerm$ = this.filterControl.valueChanges.pipe<string>(startWith(''));
+        this.channels$ = combineLatest(channels$, filterTerm$).pipe(
+            map(([channels, filterTerm]) => {
+                return filterTerm
+                    ? channels.filter(c =>
+                          c.code.toLocaleLowerCase().includes(filterTerm.toLocaleLowerCase()),
+                      )
+                    : channels;
+            }),
+        );
+        this.channelCount$ = channels$.pipe(map(channels => channels.length));
         const activeChannel$ = this.dataService.client
         const activeChannel$ = this.dataService.client
             .userStatus()
             .userStatus()
             .mapStream(data => data.userStatus.channels.find(c => c.id === data.userStatus.activeChannelId))
             .mapStream(data => data.userStatus.channels.find(c => c.id === data.userStatus.activeChannelId))
@@ -35,6 +50,7 @@ export class ChannelSwitcherComponent implements OnInit {
             if (activeChannel) {
             if (activeChannel) {
                 this.localStorageService.set('activeChannelToken', activeChannel.token);
                 this.localStorageService.set('activeChannelToken', activeChannel.token);
             }
             }
+            this.filterControl.patchValue('');
         });
         });
     }
     }
 }
 }

+ 7 - 6
packages/admin-ui/src/lib/core/src/components/main-nav/main-nav.component.scss

@@ -3,12 +3,13 @@
 :host {
 :host {
     // flex: 0 0 auto;
     // flex: 0 0 auto;
     order: -1;
     order: -1;
-    background-color: $color-grey-200;
+    background-color: var(--clr-nav-background-color);
 }
 }
 
 
 nav.sidenav {
 nav.sidenav {
     height: 100%;
     height: 100%;
     width: 10.8rem;
     width: 10.8rem;
+    border-right-color: var(--clr-sidenav-border-color);
 }
 }
 
 
 .nav-list clr-icon {
 .nav-list clr-icon {
@@ -25,19 +26,19 @@ nav.sidenav {
     height: 10px;
     height: 10px;
     position: absolute;
     position: absolute;
     border-radius: 50%;
     border-radius: 50%;
-    border: 1px solid $color-grey-200;
+    border: 1px solid var(--color-component-border-100);
 
 
     &.info {
     &.info {
-        background-color: $color-primary-600;
+        background-color: var(--color-primary-600);
     }
     }
     &.success {
     &.success {
-        background-color: $color-success-500;
+        background-color: var(--color-success-500);
     }
     }
     &.warning {
     &.warning {
-        background-color: $color-warning-500;
+        background-color: var(--color-warning-500);
     }
     }
     &.error {
     &.error {
-        background-color: $color-error-400;
+        background-color: var(--color-error-400);
     }
     }
 }
 }
 
 

+ 5 - 6
packages/admin-ui/src/lib/core/src/components/notification/notification.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 @keyframes fadeIn {
 @keyframes fadeIn {
     0% { opacity: 0; }
     0% { opacity: 0; }
@@ -19,22 +18,22 @@
         max-width: 98vw;
         max-width: 98vw;
         word-wrap: break-word;
         word-wrap: break-word;
         padding: 10px;
         padding: 10px;
-        background-color: $color-grey-500;
+        background-color: var(--color-grey-500);
         color: white;
         color: white;
         transition: opacity 1s, top 0.3s;
         transition: opacity 1s, top 0.3s;
         opacity: 0;
         opacity: 0;
 
 
         &.success {
         &.success {
-            background-color: $color-success-500;
+            background-color: var(--color-success-500);
         }
         }
         &.error {
         &.error {
-            background-color: $color-error-500;
+            background-color: var(--color-error-500);
         }
         }
         &.warning {
         &.warning {
-            background-color: $color-warning-500;
+            background-color: var(--color-warning-500);
         }
         }
         &.info {
         &.info {
-            background-color: $color-secondary-500;
+            background-color: var(--color-secondary-500);
         }
         }
 
 
         &.visible {
         &.visible {

+ 9 - 0
packages/admin-ui/src/lib/core/src/components/theme-switcher/theme-switcher.component.html

@@ -0,0 +1,9 @@
+<button *ngIf="activeTheme$ | async as activeTheme" class="theme-toggle" (click)="toggleTheme(activeTheme)">
+    <span>{{ 'common.theme' | translate }}</span>
+    <div class="theme-icon default" [class.active]="activeTheme === 'default'">
+        <clr-icon shape="sun" class="is-solid"></clr-icon>
+    </div>
+    <div class="theme-icon dark" [class.active]="activeTheme === 'dark'">
+        <clr-icon shape="moon" class="is-solid"></clr-icon>
+    </div>
+</button>

+ 36 - 0
packages/admin-ui/src/lib/core/src/components/theme-switcher/theme-switcher.component.scss

@@ -0,0 +1,36 @@
+:host {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+}
+
+button.theme-toggle {
+    position: relative;
+    padding-left: 20px;
+    border: none;
+    background: transparent;
+    color: var(--clr-dropdown-item-color);
+    cursor: pointer;
+}
+
+.theme-icon {
+    position: absolute;
+    top: 0px;
+    left: 6px;
+    z-index: 0;
+    opacity: 0.2;
+    color: var(--color-text-300);
+    transition: opacity 0.3s, left 0.3s;
+
+    &.active {
+        z-index: 1;
+        left: 0px;
+        opacity: 1;
+    }
+    &.default.active {
+        color: #d6ae3f;
+    }
+    &.dark.active {
+        color: #ffdf3a;
+    }
+}

+ 28 - 0
packages/admin-ui/src/lib/core/src/components/theme-switcher/theme-switcher.component.ts

@@ -0,0 +1,28 @@
+import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
+import { Observable } from 'rxjs';
+
+import { DataService } from '../../data/providers/data.service';
+import { LocalStorageService } from '../../providers/local-storage/local-storage.service';
+
+@Component({
+    selector: 'vdr-theme-switcher',
+    templateUrl: './theme-switcher.component.html',
+    styleUrls: ['./theme-switcher.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ThemeSwitcherComponent implements OnInit {
+    activeTheme$: Observable<string>;
+
+    constructor(private dataService: DataService, private localStorageService: LocalStorageService) {}
+
+    ngOnInit() {
+        this.activeTheme$ = this.dataService.client.uiState().mapStream(data => data.uiState.theme);
+    }
+
+    toggleTheme(current: string) {
+        const newTheme = current === 'default' ? 'dark' : 'default';
+        this.dataService.client.setUiTheme(newTheme).subscribe(() => {
+            this.localStorageService.set('activeTheme', newTheme);
+        });
+    }
+}

+ 4 - 1
packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.html

@@ -17,8 +17,11 @@
             >
             >
                 <clr-icon shape="language"></clr-icon> {{ 'lang.' + uiLanguage | translate }}
                 <clr-icon shape="language"></clr-icon> {{ 'lang.' + uiLanguage | translate }}
             </button>
             </button>
-            <div class="dropdown-divider"></div>
         </ng-container>
         </ng-container>
+        <div class="dropdown-item">
+            <vdr-theme-switcher></vdr-theme-switcher>
+        </div>
+        <div class="dropdown-divider"></div>
         <button type="button" vdrDropdownItem (click)="logOut.emit()">
         <button type="button" vdrDropdownItem (click)="logOut.emit()">
             <clr-icon shape="logout"></clr-icon> {{ 'common.log-out' | translate }}
             <clr-icon shape="logout"></clr-icon> {{ 'common.log-out' | translate }}
         </button>
         </button>

+ 1 - 5
packages/admin-ui/src/lib/core/src/components/user-menu/user-menu.component.scss

@@ -8,7 +8,7 @@
 }
 }
 
 
 .user-name {
 .user-name {
-    color: $color-grey-200;
+    color: var(--color-grey-200);
     margin-right: 12px;
     margin-right: 12px;
     @media screen and (max-width: $breakpoint-small) {
     @media screen and (max-width: $breakpoint-small) {
         display: none;
         display: none;
@@ -18,7 +18,3 @@
 .trigger clr-icon {
 .trigger clr-icon {
     color: white;
     color: white;
 }
 }
-.btn.btn-outline:hover {
-    background-color: #e1f1f62e;
-}
-

+ 20 - 3
packages/admin-ui/src/lib/core/src/core.module.ts

@@ -1,7 +1,7 @@
 import { PlatformLocation } from '@angular/common';
 import { PlatformLocation } from '@angular/common';
 import { HttpClient } from '@angular/common/http';
 import { HttpClient } from '@angular/common/http';
 import { NgModule } from '@angular/core';
 import { NgModule } from '@angular/core';
-import { BrowserModule } from '@angular/platform-browser';
+import { BrowserModule, Title } from '@angular/platform-browser';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { TranslateCompiler, TranslateLoader, TranslateModule } from '@ngx-translate/core';
 import { TranslateCompiler, TranslateLoader, TranslateModule } from '@ngx-translate/core';
 import { MessageFormatConfig, MESSAGE_FORMAT_CONFIG } from 'ngx-translate-messageformat-compiler';
 import { MessageFormatConfig, MESSAGE_FORMAT_CONFIG } from 'ngx-translate-messageformat-compiler';
@@ -14,6 +14,7 @@ import { ChannelSwitcherComponent } from './components/channel-switcher/channel-
 import { MainNavComponent } from './components/main-nav/main-nav.component';
 import { MainNavComponent } from './components/main-nav/main-nav.component';
 import { NotificationComponent } from './components/notification/notification.component';
 import { NotificationComponent } from './components/notification/notification.component';
 import { OverlayHostComponent } from './components/overlay-host/overlay-host.component';
 import { OverlayHostComponent } from './components/overlay-host/overlay-host.component';
+import { ThemeSwitcherComponent } from './components/theme-switcher/theme-switcher.component';
 import { UiLanguageSwitcherDialogComponent } from './components/ui-language-switcher-dialog/ui-language-switcher-dialog.component';
 import { UiLanguageSwitcherDialogComponent } from './components/ui-language-switcher-dialog/ui-language-switcher-dialog.component';
 import { UserMenuComponent } from './components/user-menu/user-menu.component';
 import { UserMenuComponent } from './components/user-menu/user-menu.component';
 import { DataModule } from './data/data.module';
 import { DataModule } from './data/data.module';
@@ -39,7 +40,11 @@ import { SharedModule } from './shared/shared.module';
             compiler: { provide: TranslateCompiler, useClass: InjectableTranslateMessageFormatCompiler },
             compiler: { provide: TranslateCompiler, useClass: InjectableTranslateMessageFormatCompiler },
         }),
         }),
     ],
     ],
-    providers: [{ provide: MESSAGE_FORMAT_CONFIG, useFactory: getLocales }, registerDefaultFormInputs()],
+    providers: [
+        { provide: MESSAGE_FORMAT_CONFIG, useFactory: getLocales },
+        registerDefaultFormInputs(),
+        Title,
+    ],
     exports: [SharedModule, OverlayHostComponent],
     exports: [SharedModule, OverlayHostComponent],
     declarations: [
     declarations: [
         AppShellComponent,
         AppShellComponent,
@@ -50,11 +55,17 @@ import { SharedModule } from './shared/shared.module';
         NotificationComponent,
         NotificationComponent,
         UiLanguageSwitcherDialogComponent,
         UiLanguageSwitcherDialogComponent,
         ChannelSwitcherComponent,
         ChannelSwitcherComponent,
+        ThemeSwitcherComponent,
     ],
     ],
 })
 })
 export class CoreModule {
 export class CoreModule {
-    constructor(private i18nService: I18nService, private localStorageService: LocalStorageService) {
+    constructor(
+        private i18nService: I18nService,
+        private localStorageService: LocalStorageService,
+        private titleService: Title,
+    ) {
         this.initUiLanguages();
         this.initUiLanguages();
+        this.initUiTitle();
     }
     }
 
 
     private initUiLanguages() {
     private initUiLanguages() {
@@ -76,6 +87,12 @@ export class CoreModule {
         this.i18nService.setDefaultLanguage(defaultLanguage);
         this.i18nService.setDefaultLanguage(defaultLanguage);
         this.i18nService.setAvailableLanguages(availableLanguages || [defaultLanguage]);
         this.i18nService.setAvailableLanguages(availableLanguages || [defaultLanguage]);
     }
     }
+
+    private initUiTitle() {
+        const title = getAppConfig().brand || 'VendureAdmin';
+
+        this.titleService.setTitle(title);
+    }
 }
 }
 
 
 export function HttpLoaderFactory(http: HttpClient, location: PlatformLocation) {
 export function HttpLoaderFactory(http: HttpClient, location: PlatformLocation) {

+ 3 - 0
packages/admin-ui/src/lib/core/src/data/client-state/client-defaults.ts

@@ -1,9 +1,11 @@
+import { getAppConfig } from '../../app.config';
 import { GetNetworkStatus, GetUiState, GetUserStatus } from '../../common/generated-types';
 import { GetNetworkStatus, GetUiState, GetUserStatus } from '../../common/generated-types';
 import { getDefaultUiLanguage } from '../../common/utilities/get-default-ui-language';
 import { getDefaultUiLanguage } from '../../common/utilities/get-default-ui-language';
 import { LocalStorageService } from '../../providers/local-storage/local-storage.service';
 import { LocalStorageService } from '../../providers/local-storage/local-storage.service';
 
 
 export function getClientDefaults(localStorageService: LocalStorageService) {
 export function getClientDefaults(localStorageService: LocalStorageService) {
     const currentLanguage = localStorageService.get('uiLanguageCode') || getDefaultUiLanguage();
     const currentLanguage = localStorageService.get('uiLanguageCode') || getDefaultUiLanguage();
+    const activeTheme = localStorageService.get('activeTheme') || 'default';
     return {
     return {
         networkStatus: {
         networkStatus: {
             inFlightRequests: 0,
             inFlightRequests: 0,
@@ -20,6 +22,7 @@ export function getClientDefaults(localStorageService: LocalStorageService) {
         } as GetUserStatus.UserStatus,
         } as GetUserStatus.UserStatus,
         uiState: {
         uiState: {
             language: currentLanguage,
             language: currentLanguage,
+            theme: activeTheme,
             __typename: 'UiState',
             __typename: 'UiState',
         } as GetUiState.UiState,
         } as GetUiState.UiState,
     };
     };

+ 17 - 0
packages/admin-ui/src/lib/core/src/data/client-state/client-resolvers.ts

@@ -8,6 +8,7 @@ import {
     SetActiveChannel,
     SetActiveChannel,
     SetAsLoggedIn,
     SetAsLoggedIn,
     SetUiLanguage,
     SetUiLanguage,
+    SetUiTheme,
     UpdateUserChannels,
     UpdateUserChannels,
     UserStatus,
     UserStatus,
 } from '../../common/generated-types';
 } from '../../common/generated-types';
@@ -69,15 +70,31 @@ export const clientResolvers: ResolverDefinition = {
             return data.userStatus;
             return data.userStatus;
         },
         },
         setUiLanguage: (_, args: SetUiLanguage.Variables, { cache }): LanguageCode => {
         setUiLanguage: (_, args: SetUiLanguage.Variables, { cache }): LanguageCode => {
+            // tslint:disable-next-line:no-non-null-assertion
+            const previous = cache.readQuery<GetUiState.Query>({ query: GET_UI_STATE })!;
             const data: GetUiState.Query = {
             const data: GetUiState.Query = {
                 uiState: {
                 uiState: {
                     __typename: 'UiState',
                     __typename: 'UiState',
                     language: args.languageCode,
                     language: args.languageCode,
+                    theme: previous.uiState.theme,
                 },
                 },
             };
             };
             cache.writeQuery({ query: GET_UI_STATE, data });
             cache.writeQuery({ query: GET_UI_STATE, data });
             return args.languageCode;
             return args.languageCode;
         },
         },
+        setUiTheme: (_, args: SetUiTheme.Variables, { cache }): string => {
+            // tslint:disable-next-line:no-non-null-assertion
+            const previous = cache.readQuery<GetUiState.Query>({ query: GET_UI_STATE })!;
+            const data: GetUiState.Query = {
+                uiState: {
+                    __typename: 'UiState',
+                    language: previous.uiState.language,
+                    theme: args.theme,
+                },
+            };
+            cache.writeQuery({ query: GET_UI_STATE, data });
+            return args.theme;
+        },
         setActiveChannel: (_, args: SetActiveChannel.Variables, { cache }): UserStatus => {
         setActiveChannel: (_, args: SetActiveChannel.Variables, { cache }): UserStatus => {
             // tslint:disable-next-line:no-non-null-assertion
             // tslint:disable-next-line:no-non-null-assertion
             const previous = cache.readQuery<GetUserStatus.Query>({ query: GET_USER_STATUS })!;
             const previous = cache.readQuery<GetUserStatus.Query>({ query: GET_USER_STATUS })!;

+ 3 - 1
packages/admin-ui/src/lib/core/src/data/client-state/client-types.graphql

@@ -9,7 +9,8 @@ type Mutation {
     requestCompleted: Int!
     requestCompleted: Int!
     setAsLoggedIn(input: UserStatusInput!): UserStatus!
     setAsLoggedIn(input: UserStatusInput!): UserStatus!
     setAsLoggedOut: UserStatus!
     setAsLoggedOut: UserStatus!
-    setUiLanguage(languageCode: LanguageCode): LanguageCode
+    setUiLanguage(languageCode: LanguageCode!): LanguageCode!
+    setUiTheme(theme: String!): String!
     setActiveChannel(channelId: ID!): UserStatus!
     setActiveChannel(channelId: ID!): UserStatus!
     updateUserChannels(channels: [CurrentUserChannelInput!]!): UserStatus!
     updateUserChannels(channels: [CurrentUserChannelInput!]!): UserStatus!
 }
 }
@@ -29,6 +30,7 @@ type UserStatus {
 
 
 type UiState {
 type UiState {
     language: LanguageCode!
     language: LanguageCode!
+    theme: String!
 }
 }
 
 
 input CurrentUserChannelInput {
 input CurrentUserChannelInput {

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

@@ -52,6 +52,12 @@ export const SET_UI_LANGUAGE = gql`
     }
     }
 `;
 `;
 
 
+export const SET_UI_THEME = gql`
+    mutation SetUiTheme($theme: String!) {
+        setUiTheme(theme: $theme) @client
+    }
+`;
+
 export const GET_NEWTORK_STATUS = gql`
 export const GET_NEWTORK_STATUS = gql`
     query GetNetworkStatus {
     query GetNetworkStatus {
         networkStatus @client {
         networkStatus @client {
@@ -73,6 +79,7 @@ export const GET_UI_STATE = gql`
     query GetUiState {
     query GetUiState {
         uiState @client {
         uiState @client {
             language
             language
+            theme
         }
         }
     }
     }
 `;
 `;
@@ -87,6 +94,7 @@ export const GET_CLIENT_STATE = gql`
         }
         }
         uiState @client {
         uiState @client {
             language
             language
+            theme
         }
         }
     }
     }
     ${USER_STATUS_FRAGMENT}
     ${USER_STATUS_FRAGMENT}

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

@@ -54,6 +54,11 @@ export const ORDER_FRAGMENT = gql`
             firstName
             firstName
             lastName
             lastName
         }
         }
+        shippingLines {
+            shippingMethod {
+                name
+            }
+        }
     }
     }
 `;
 `;
 
 

+ 9 - 0
packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts

@@ -681,6 +681,15 @@ export const GET_JOB_QUEUE_LIST = gql`
     }
     }
 `;
 `;
 
 
+export const CANCEL_JOB = gql`
+    mutation CancelJob($id: ID!) {
+        cancelJob(jobId: $id) {
+            ...JobInfo
+        }
+    }
+    ${JOB_INFO_FRAGMENT}
+`;
+
 export const REINDEX = gql`
 export const REINDEX = gql`
     mutation Reindex {
     mutation Reindex {
         reindex {
         reindex {

+ 8 - 0
packages/admin-ui/src/lib/core/src/data/providers/client-data.service.ts

@@ -10,6 +10,7 @@ import {
     SetActiveChannel,
     SetActiveChannel,
     SetAsLoggedIn,
     SetAsLoggedIn,
     SetUiLanguage,
     SetUiLanguage,
+    SetUiTheme,
     UpdateUserChannels,
     UpdateUserChannels,
 } from '../../common/generated-types';
 } from '../../common/generated-types';
 import {
 import {
@@ -22,6 +23,7 @@ import {
     SET_AS_LOGGED_IN,
     SET_AS_LOGGED_IN,
     SET_AS_LOGGED_OUT,
     SET_AS_LOGGED_OUT,
     SET_UI_LANGUAGE,
     SET_UI_LANGUAGE,
+    SET_UI_THEME,
     UPDATE_USER_CHANNELS,
     UPDATE_USER_CHANNELS,
 } from '../definitions/client-definitions';
 } from '../definitions/client-definitions';
 
 
@@ -78,6 +80,12 @@ export class ClientDataService {
         });
         });
     }
     }
 
 
+    setUiTheme(theme: string) {
+        return this.baseDataService.mutate<SetUiTheme.Mutation, SetUiTheme.Variables>(SET_UI_THEME, {
+            theme,
+        });
+    }
+
     setActiveChannel(channelId: string) {
     setActiveChannel(channelId: string) {
         return this.baseDataService.mutate<SetActiveChannel.Mutation, SetActiveChannel.Variables>(
         return this.baseDataService.mutate<SetActiveChannel.Mutation, SetActiveChannel.Variables>(
             SET_ACTIVE_CHANNEL,
             SET_ACTIVE_CHANNEL,

+ 1 - 0
packages/admin-ui/src/lib/core/src/data/providers/product-data.service.ts

@@ -150,6 +150,7 @@ export class ProductDataService {
     createProduct(product: CreateProductInput) {
     createProduct(product: CreateProductInput) {
         const input: CreateProduct.Variables = {
         const input: CreateProduct.Variables = {
             input: pick(product, [
             input: pick(product, [
+                'enabled',
                 'translations',
                 'translations',
                 'customFields',
                 'customFields',
                 'assetIds',
                 'assetIds',

+ 8 - 0
packages/admin-ui/src/lib/core/src/data/providers/settings-data.service.ts

@@ -3,6 +3,7 @@ import { pick } from '@vendure/common/lib/pick';
 
 
 import {
 import {
     AddMembersToZone,
     AddMembersToZone,
+    CancelJob,
     CreateChannel,
     CreateChannel,
     CreateChannelInput,
     CreateChannelInput,
     CreateCountry,
     CreateCountry,
@@ -57,6 +58,7 @@ import {
 } from '../../common/generated-types';
 } from '../../common/generated-types';
 import {
 import {
     ADD_MEMBERS_TO_ZONE,
     ADD_MEMBERS_TO_ZONE,
+    CANCEL_JOB,
     CREATE_CHANNEL,
     CREATE_CHANNEL,
     CREATE_COUNTRY,
     CREATE_COUNTRY,
     CREATE_TAX_CATEGORY,
     CREATE_TAX_CATEGORY,
@@ -372,4 +374,10 @@ export class SettingsDataService {
             },
             },
         });
         });
     }
     }
+
+    cancelJob(id: string) {
+        return this.baseDataService.mutate<CancelJob.Mutation, CancelJob.Variables>(CANCEL_JOB, {
+            id,
+        });
+    }
 }
 }

+ 9 - 8
packages/admin-ui/src/lib/core/src/providers/job-queue/job-queue.service.ts

@@ -23,14 +23,14 @@ export class JobQueueService implements OnDestroy {
                 (jobMap, job) => this.handleJob(jobMap, job),
                 (jobMap, job) => this.handleJob(jobMap, job),
                 new Map<string, JobInfoFragment>(),
                 new Map<string, JobInfoFragment>(),
             ),
             ),
-            map((jobMap) => Array.from(jobMap.values())),
+            map(jobMap => Array.from(jobMap.values())),
             debounceTime(500),
             debounceTime(500),
             shareReplay(1),
             shareReplay(1),
         );
         );
 
 
         this.subscription = this.activeJobs$
         this.subscription = this.activeJobs$
             .pipe(
             .pipe(
-                switchMap((jobs) => {
+                switchMap(jobs => {
                     if (jobs.length) {
                     if (jobs.length) {
                         return interval(2500).pipe(mapTo(jobs));
                         return interval(2500).pipe(mapTo(jobs));
                     } else {
                     } else {
@@ -38,10 +38,10 @@ export class JobQueueService implements OnDestroy {
                     }
                     }
                 }),
                 }),
             )
             )
-            .subscribe((jobs) => {
+            .subscribe(jobs => {
                 if (jobs.length) {
                 if (jobs.length) {
-                    this.dataService.settings.pollJobs(jobs.map((j) => j.id)).single$.subscribe((data) => {
-                        data.jobsById.forEach((job) => {
+                    this.dataService.settings.pollJobs(jobs.map(j => j.id)).single$.subscribe(data => {
+                        data.jobsById.forEach(job => {
                             this.updateJob$.next(job);
                             this.updateJob$.next(job);
                         });
                         });
                     });
                     });
@@ -61,8 +61,8 @@ export class JobQueueService implements OnDestroy {
     checkForJobs(delay: number = 1000) {
     checkForJobs(delay: number = 1000) {
         timer(delay)
         timer(delay)
             .pipe(
             .pipe(
-                switchMap(() => this.dataService.client.userStatus().mapSingle((data) => data.userStatus)),
-                switchMap((userStatus) => {
+                switchMap(() => this.dataService.client.userStatus().mapSingle(data => data.userStatus)),
+                switchMap(userStatus => {
                     if (userStatus.permissions.includes(Permission.ReadSettings) && userStatus.isLoggedIn) {
                     if (userStatus.permissions.includes(Permission.ReadSettings) && userStatus.isLoggedIn) {
                         return this.dataService.settings.getRunningJobs().single$;
                         return this.dataService.settings.getRunningJobs().single$;
                     } else {
                     } else {
@@ -70,7 +70,7 @@ export class JobQueueService implements OnDestroy {
                     }
                     }
                 }),
                 }),
             )
             )
-            .subscribe((data) => data.jobs.items.forEach((job) => this.updateJob$.next(job)));
+            .subscribe(data => data.jobs.items.forEach(job => this.updateJob$.next(job)));
     }
     }
 
 
     addJob(jobId: string, onComplete?: (job: JobInfoFragment) => void) {
     addJob(jobId: string, onComplete?: (job: JobInfoFragment) => void) {
@@ -92,6 +92,7 @@ export class JobQueueService implements OnDestroy {
                 break;
                 break;
             case JobState.COMPLETED:
             case JobState.COMPLETED:
             case JobState.FAILED:
             case JobState.FAILED:
+            case JobState.CANCELLED:
                 jobMap.delete(job.id);
                 jobMap.delete(job.id);
                 const handler = this.onCompleteHandlers.get(job.id);
                 const handler = this.onCompleteHandlers.get(job.id);
                 if (handler) {
                 if (handler) {

+ 1 - 0
packages/admin-ui/src/lib/core/src/providers/local-storage/local-storage.service.ts

@@ -10,6 +10,7 @@ export type LocalStorageTypeMap = {
     uiLanguageCode: LanguageCode;
     uiLanguageCode: LanguageCode;
     orderListLastCustomFilters: any;
     orderListLastCustomFilters: any;
     dashboardWidgetLayout: WidgetLayoutDefinition;
     dashboardWidgetLayout: WidgetLayoutDefinition;
+    activeTheme: string;
 };
 };
 
 
 export type LocalStorageLocationBasedTypeMap = {
 export type LocalStorageLocationBasedTypeMap = {

+ 3 - 1
packages/admin-ui/src/lib/core/src/public_api.ts

@@ -14,6 +14,7 @@ export * from './common/introspection-result';
 export * from './common/language-translation-strings';
 export * from './common/language-translation-strings';
 export * from './common/utilities/configurable-operation-utils';
 export * from './common/utilities/configurable-operation-utils';
 export * from './common/utilities/create-updated-translatable';
 export * from './common/utilities/create-updated-translatable';
+export * from './common/utilities/find-translation';
 export * from './common/utilities/flatten-facet-values';
 export * from './common/utilities/flatten-facet-values';
 export * from './common/utilities/get-default-ui-language';
 export * from './common/utilities/get-default-ui-language';
 export * from './common/utilities/interpolate-description';
 export * from './common/utilities/interpolate-description';
@@ -25,6 +26,7 @@ export * from './components/channel-switcher/channel-switcher.component';
 export * from './components/main-nav/main-nav.component';
 export * from './components/main-nav/main-nav.component';
 export * from './components/notification/notification.component';
 export * from './components/notification/notification.component';
 export * from './components/overlay-host/overlay-host.component';
 export * from './components/overlay-host/overlay-host.component';
+export * from './components/theme-switcher/theme-switcher.component';
 export * from './components/ui-language-switcher-dialog/ui-language-switcher-dialog.component';
 export * from './components/ui-language-switcher-dialog/ui-language-switcher-dialog.component';
 export * from './components/user-menu/user-menu.component';
 export * from './components/user-menu/user-menu.component';
 export * from './core.module';
 export * from './core.module';
@@ -174,7 +176,7 @@ export * from './shared/dynamic-form-inputs/select-form-input/select-form-input.
 export * from './shared/dynamic-form-inputs/text-form-input/text-form-input.component';
 export * from './shared/dynamic-form-inputs/text-form-input/text-form-input.component';
 export * from './shared/pipes/asset-preview.pipe';
 export * from './shared/pipes/asset-preview.pipe';
 export * from './shared/pipes/channel-label.pipe';
 export * from './shared/pipes/channel-label.pipe';
-export * from './shared/pipes/currency-name.pipe';
+export * from './shared/pipes/locale-currency-name.pipe';
 export * from './shared/pipes/custom-field-label.pipe';
 export * from './shared/pipes/custom-field-label.pipe';
 export * from './shared/pipes/duration.pipe';
 export * from './shared/pipes/duration.pipe';
 export * from './shared/pipes/file-size.pipe';
 export * from './shared/pipes/file-size.pipe';

+ 2 - 3
packages/admin-ui/src/lib/core/src/shared/components/action-bar/action-bar.component.scss

@@ -1,14 +1,13 @@
-@import "variables";
 
 
 :host {
 :host {
     display: flex;
     display: flex;
     justify-content: space-between;
     justify-content: space-between;
     align-items: flex-start;
     align-items: flex-start;
-    background-color: $color-grey-100;
+    background-color: var(--color-component-bg-100);
     position: sticky;
     position: sticky;
     top: -24px;
     top: -24px;
     z-index: 25;
     z-index: 25;
-    border-bottom: 1px solid $color-grey-300;
+    border-bottom: 1px solid var(--color-component-border-200);
 
 
     > .grow {
     > .grow {
         flex: 1;
         flex: 1;

+ 3 - 3
packages/admin-ui/src/lib/core/src/shared/components/affixed-input/affixed-input.component.scss

@@ -1,14 +1,14 @@
-@import "variables";
 
 
 :host {
 :host {
     display: inline-flex;
     display: inline-flex;
 }
 }
 
 
 .affix {
 .affix {
+    color: var(--color-grey-800);
     display: flex;
     display: flex;
     align-items: center;
     align-items: center;
-    background-color: $color-grey-200;
-    border: 1px solid $color-grey-300;
+    background-color: var(--color-grey-200);
+    border: 1px solid var(--color-component-border-300);
     top: 1px;
     top: 1px;
     padding: 3px;
     padding: 3px;
     line-height: .58333rem;
     line-height: .58333rem;

+ 6 - 6
packages/admin-ui/src/lib/core/src/shared/components/asset-file-input/asset-file-input.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 .file-input {
 .file-input {
     display: none;
     display: none;
@@ -6,8 +5,8 @@
 
 
 .drop-zone {
 .drop-zone {
     position: fixed;
     position: fixed;
-    background-color: transparentize($color-primary-500, 0.7);
-    border: 3px dashed $color-grey-400;
+    background-color: var(--color-primary-500);
+    border: 3px dashed var(--color-component-border-300);
     opacity: 0;
     opacity: 0;
     visibility: hidden;
     visibility: hidden;
     z-index: 1000;
     z-index: 1000;
@@ -17,7 +16,7 @@
     justify-content: center;
     justify-content: center;
 
 
     &.visible {
     &.visible {
-        opacity: 1;
+        opacity: 0.3;
         visibility: visible;
         visibility: visible;
         transition: opacity 0.2s, background-color 0.2s, border 0.2s, visibility 0s;
         transition: opacity 0.2s, background-color 0.2s, border 0.2s, visibility 0s;
     }
     }
@@ -28,13 +27,14 @@
         padding: 24px;
         padding: 24px;
         font-size: 32px;
         font-size: 32px;
         pointer-events: none;
         pointer-events: none;
-        opacity: 0.2;
+        opacity: 0.5;
         transition: opacity 0.2s;
         transition: opacity 0.2s;
     }
     }
 
 
     &.dragging-over {
     &.dragging-over {
         border-color: white;
         border-color: white;
-        background-color: transparentize($color-primary-500, 0.3);
+        background-color: var(--color-primary-500);
+        opacity: 0.7;
         transition: background-color 0.2s, border 0.2s;
         transition: background-color 0.2s, border 0.2s;
         .drop-label {
         .drop-label {
             opacity: 1;
             opacity: 1;

+ 7 - 7
packages/admin-ui/src/lib/core/src/shared/components/asset-gallery/asset-gallery.component.scss

@@ -18,8 +18,8 @@
     padding-bottom: 12px;
     padding-bottom: 12px;
 
 
     .card:hover {
     .card:hover {
-        box-shadow: 0 0.125rem 0 0 $color-primary-500;
-        border: 1px solid $color-primary-500;
+        box-shadow: 0 0.125rem 0 0 var(--color-primary-500);
+        border: 1px solid var(--color-primary-500);
     }
     }
 }
 }
 
 
@@ -31,7 +31,7 @@
 .selected-checkbox {
 .selected-checkbox {
     opacity: 0;
     opacity: 0;
     position: absolute;
     position: absolute;
-    color: $color-success-500;
+    color: var(--color-success-500);
     background-color: white;
     background-color: white;
     border-radius: 50%;
     border-radius: 50%;
     top: -12px;
     top: -12px;
@@ -41,8 +41,8 @@
 }
 }
 
 
 .card.selected {
 .card.selected {
-    box-shadow: 0 0.125rem 0 0 $color-primary-500;
-    border: 1px solid $color-primary-500;
+    box-shadow: 0 0.125rem 0 0 var(--color-primary-500);
+    border: 1px solid var(--color-primary-500);
 
 
     .selected-checkbox {
     .selected-checkbox {
         opacity: 1;
         opacity: 1;
@@ -80,7 +80,7 @@
         &.visible {
         &.visible {
             opacity: 1;
             opacity: 1;
             transform: perspective(500px) translateZ(-44px) translateY(0px);
             transform: perspective(500px) translateZ(-44px) translateY(0px);
-            background-color: $color-grey-100;
+            background-color: var(--color-component-bg-100);
             transition: transform 0.3s, color 0.3s;
             transition: transform 0.3s, color 0.3s;
         }
         }
     }
     }
@@ -108,7 +108,7 @@
 
 
     .placeholder {
     .placeholder {
         text-align: center;
         text-align: center;
-        color: $color-grey-300;
+        color: var(--color-grey-300);
     }
     }
 
 
     .preview {
     .preview {

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

@@ -2,6 +2,7 @@
     <div class="title-row">
     <div class="title-row">
         {{ 'asset.select-assets' | translate }}
         {{ 'asset.select-assets' | translate }}
         <vdr-asset-file-input
         <vdr-asset-file-input
+            class="ml3"
             (selectFiles)="createAssets($event)"
             (selectFiles)="createAssets($event)"
             [uploading]="uploading"
             [uploading]="uploading"
             dropZoneTarget=".modal-content"
             dropZoneTarget=".modal-content"
@@ -13,7 +14,7 @@
     name="searchTerm"
     name="searchTerm"
     [formControl]="searchTerm"
     [formControl]="searchTerm"
     [placeholder]="'asset.search-asset-name' | translate"
     [placeholder]="'asset.search-asset-name' | translate"
-    class="clr-input search-input"
+    class="search-input"
 />
 />
 <vdr-asset-gallery
 <vdr-asset-gallery
     [assets]="(assets$ | async)! | paginate: paginationConfig"
     [assets]="(assets$ | async)! | paginate: paginationConfig"

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

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     display: flex;
     display: flex;
@@ -18,7 +17,7 @@ vdr-asset-gallery {
 
 
 .paging-controls {
 .paging-controls {
     padding-top: 6px;
     padding-top: 6px;
-    border-top: 1px solid $color-grey-200;
+    border-top: 1px solid var(--color-component-border-100);
     display: flex;
     display: flex;
     justify-content: space-between;
     justify-content: space-between;
     flex-shrink: 0;
     flex-shrink: 0;

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

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     display: flex;
     display: flex;

+ 0 - 1
packages/admin-ui/src/lib/core/src/shared/components/channel-assignment-control/channel-assignment-control.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     min-width: 200px;
     min-width: 200px;

+ 0 - 1
packages/admin-ui/src/lib/core/src/shared/components/channel-badge/channel-badge.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     display: inline-block;
     display: inline-block;

+ 16 - 14
packages/admin-ui/src/lib/core/src/shared/components/chip/chip.component.scss

@@ -1,4 +1,4 @@
-@import "variables";
+@import 'variables';
 
 
 :host {
 :host {
     display: inline-block;
     display: inline-block;
@@ -6,38 +6,40 @@
 
 
 .wrapper {
 .wrapper {
     display: flex;
     display: flex;
-    border: 1px solid $color-grey-400;
+    border: 1px solid var(--color-component-border-300);
     border-radius: 3px;
     border-radius: 3px;
     margin: 6px;
     margin: 6px;
     &.with-background {
     &.with-background {
-        color: transparentize($color-grey-100, 0.2);
+        color: var(--color-grey-100);
         border-color: transparent;
         border-color: transparent;
+        .chip-label {
+            opacity: 0.9;
+        }
     }
     }
 
 
     &.warning {
     &.warning {
-        border-color: $color-warning-200;
+        border-color: var(--color-chip-warning-border);
         .chip-label {
         .chip-label {
-            color: $color-warning-600;
-            background-color: $color-warning-100;
+            color: var(--color-chip-warning-text);
+            background-color: var(--color-chip-warning-bg);
         }
         }
     }
     }
 
 
     &.success {
     &.success {
-        border-color: $color-success-200;
+        border-color: var(--color-chip-success-border);
         .chip-label {
         .chip-label {
-            color: $color-success-600;
-            background-color: $color-success-100;
+            color: var(--color-chip-success-text);
+            background-color: var(--color-chip-success-bg);
         }
         }
     }
     }
 
 
     &.error {
     &.error {
-        border-color: $color-error-200;
+        border-color: var(--color-chip-error-border);
         .chip-label {
         .chip-label {
-            color: $color-error-600;
-            background-color: $color-error-100;
+            color: var(--color-chip-error-text);
+            background-color: var(--color-chip-error-bg);
         }
         }
     }
     }
-
 }
 }
 
 
 .chip-label {
 .chip-label {
@@ -50,7 +52,7 @@
 }
 }
 
 
 .chip-icon {
 .chip-icon {
-    border-left: 1px solid transparentize($color-grey-100, 0.5);
+    border-left: 1px solid var(--color-component-border-200);
     padding: 0 3px;
     padding: 0 3px;
     line-height: 1em;
     line-height: 1em;
     display: flex;
     display: flex;

+ 4 - 1
packages/admin-ui/src/lib/core/src/shared/components/currency-input/currency-input.component.html

@@ -1,4 +1,7 @@
-<vdr-affixed-input [prefix]="currencyCode | currencyName: 'symbol'">
+<vdr-affixed-input
+    [prefix]="prefix$ | async | localeCurrencyName: 'symbol'"
+    [suffix]="suffix$ | async | localeCurrencyName: 'symbol'"
+>
     <input
     <input
         type="number"
         type="number"
         step="0.01"
         step="0.01"

+ 53 - 4
packages/admin-ui/src/lib/core/src/shared/components/currency-input/currency-input.component.spec.ts

@@ -1,10 +1,13 @@
-import { Component } from '@angular/core';
+import { Component, Injectable } from '@angular/core';
 import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
 import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
 import { FormsModule } from '@angular/forms';
 import { FormsModule } from '@angular/forms';
 import { By } from '@angular/platform-browser';
 import { By } from '@angular/platform-browser';
 import { Type } from '@vendure/common/lib/shared-types';
 import { Type } from '@vendure/common/lib/shared-types';
+import { of } from 'rxjs';
 
 
-import { CurrencyNamePipe } from '../../pipes/currency-name.pipe';
+import { LanguageCode } from '../../../common/generated-types';
+import { DataService } from '../../../data/providers/data.service';
+import { LocaleCurrencyNamePipe } from '../../pipes/locale-currency-name.pipe';
 import { AffixedInputComponent } from '../affixed-input/affixed-input.component';
 import { AffixedInputComponent } from '../affixed-input/affixed-input.component';
 
 
 import { CurrencyInputComponent } from './currency-input.component';
 import { CurrencyInputComponent } from './currency-input.component';
@@ -13,12 +16,13 @@ describe('CurrencyInputComponent', () => {
     beforeEach(async(() => {
     beforeEach(async(() => {
         TestBed.configureTestingModule({
         TestBed.configureTestingModule({
             imports: [FormsModule],
             imports: [FormsModule],
+            providers: [{ provide: DataService, useClass: MockDataService }],
             declarations: [
             declarations: [
                 TestControlValueAccessorComponent,
                 TestControlValueAccessorComponent,
                 TestSimpleComponent,
                 TestSimpleComponent,
                 CurrencyInputComponent,
                 CurrencyInputComponent,
                 AffixedInputComponent,
                 AffixedInputComponent,
-                CurrencyNamePipe,
+                LocaleCurrencyNamePipe,
             ],
             ],
         }).compileComponents();
         }).compileComponents();
     }));
     }));
@@ -66,11 +70,35 @@ describe('CurrencyInputComponent', () => {
         expect(fixture.componentInstance.price).toBe(157);
         expect(fixture.componentInstance.price).toBe(157);
     }));
     }));
 
 
+    describe('currencyCode display', () => {
+        it('displays currency code in correct position (prefix)', fakeAsync(() => {
+            MockDataService.language = LanguageCode.en;
+            const fixture = createAndRunChangeDetection(TestSimpleComponent, 4299, 'GBP');
+            const prefix = fixture.debugElement.query(By.css('.prefix'));
+            const suffix = fixture.debugElement.query(By.css('.suffix'));
+            expect(prefix.nativeElement.innerHTML).toBe('£');
+            expect(suffix).toBeNull();
+        }));
+
+        it('displays currency code in correct position (suffix)', fakeAsync(() => {
+            MockDataService.language = LanguageCode.fr;
+            const fixture = createAndRunChangeDetection(TestSimpleComponent, 4299, 'GBP');
+            const prefix = fixture.debugElement.query(By.css('.prefix'));
+            const suffix = fixture.debugElement.query(By.css('.suffix'));
+            expect(prefix).toBeNull();
+            expect(suffix.nativeElement.innerHTML).toBe('£GB');
+        }));
+    });
+
     function createAndRunChangeDetection<T extends TestControlValueAccessorComponent | TestSimpleComponent>(
     function createAndRunChangeDetection<T extends TestControlValueAccessorComponent | TestSimpleComponent>(
         component: Type<T>,
         component: Type<T>,
         priceValue = 123,
         priceValue = 123,
+        currencyCode: string = '',
     ): ComponentFixture<T> {
     ): ComponentFixture<T> {
         const fixture = TestBed.createComponent(component);
         const fixture = TestBed.createComponent(component);
+        if (fixture.componentInstance instanceof TestSimpleComponent && currencyCode) {
+            fixture.componentInstance.currencyCode = currencyCode;
+        }
         fixture.componentInstance.price = priceValue;
         fixture.componentInstance.price = priceValue;
         fixture.detectChanges();
         fixture.detectChanges();
         tick();
         tick();
@@ -96,9 +124,30 @@ class TestControlValueAccessorComponent {
 @Component({
 @Component({
     selector: 'vdr-test-component',
     selector: 'vdr-test-component',
     template: `
     template: `
-        <vdr-currency-input [value]="price"></vdr-currency-input>
+        <vdr-currency-input [value]="price" [currencyCode]="currencyCode"></vdr-currency-input>
     `,
     `,
 })
 })
 class TestSimpleComponent {
 class TestSimpleComponent {
+    currencyCode = '';
     price = 123;
     price = 123;
 }
 }
+
+@Injectable()
+class MockDataService {
+    static language: LanguageCode = LanguageCode.en;
+    client = {
+        uiState() {
+            return {
+                mapStream(mapFn: any) {
+                    return of(
+                        mapFn({
+                            uiState: {
+                                language: MockDataService.language,
+                            },
+                        }),
+                    );
+                },
+            };
+        },
+    };
+}

+ 39 - 2
packages/admin-ui/src/lib/core/src/shared/components/currency-input/currency-input.component.ts

@@ -1,5 +1,18 @@
-import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
+import {
+    ChangeDetectorRef,
+    Component,
+    EventEmitter,
+    Input,
+    OnChanges,
+    OnInit,
+    Output,
+    SimpleChanges,
+} from '@angular/core';
 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import { DataService } from '../../../data/providers/data.service';
 
 
 /**
 /**
  * A form input control which displays currency in decimal format, whilst working
  * A form input control which displays currency in decimal format, whilst working
@@ -17,16 +30,40 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
         },
         },
     ],
     ],
 })
 })
-export class CurrencyInputComponent implements ControlValueAccessor, OnChanges {
+export class CurrencyInputComponent implements ControlValueAccessor, OnInit, OnChanges {
     @Input() disabled = false;
     @Input() disabled = false;
     @Input() readonly = false;
     @Input() readonly = false;
     @Input() value: number;
     @Input() value: number;
     @Input() currencyCode = '';
     @Input() currencyCode = '';
     @Output() valueChange = new EventEmitter();
     @Output() valueChange = new EventEmitter();
+    prefix$: Observable<string>;
+    suffix$: Observable<string>;
     onChange: (val: any) => void;
     onChange: (val: any) => void;
     onTouch: () => void;
     onTouch: () => void;
     _decimalValue: string;
     _decimalValue: string;
 
 
+    constructor(private dataService: DataService, private changeDetectorRef: ChangeDetectorRef) {}
+
+    ngOnInit() {
+        const languageCode$ = this.dataService.client.uiState().mapStream(data => data.uiState.language);
+        const shouldPrefix$ = languageCode$.pipe(
+            map(languageCode => {
+                if (!this.currencyCode) {
+                    return '';
+                }
+                const locale = languageCode.replace(/_/g, '-');
+                const localised = new Intl.NumberFormat(locale, {
+                    style: 'currency',
+                    currency: this.currencyCode,
+                    currencyDisplay: 'symbol',
+                }).format(undefined as any);
+                return localised.indexOf('NaN') > 0;
+            }),
+        );
+        this.prefix$ = shouldPrefix$.pipe(map(shouldPrefix => (shouldPrefix ? this.currencyCode : '')));
+        this.suffix$ = shouldPrefix$.pipe(map(shouldPrefix => (shouldPrefix ? '' : this.currencyCode)));
+    }
+
     ngOnChanges(changes: SimpleChanges) {
     ngOnChanges(changes: SimpleChanges) {
         if ('value' in changes) {
         if ('value' in changes) {
             this.writeValue(changes['value'].currentValue);
             this.writeValue(changes['value'].currentValue);

+ 0 - 1
packages/admin-ui/src/lib/core/src/shared/components/customer-label/customer-label.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     display: flex;
     display: flex;

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

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     display: block;
     display: block;

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.component.html

@@ -1,7 +1,7 @@
 <div class="input-wrapper">
 <div class="input-wrapper">
     <input
     <input
         readonly
         readonly
-        [ngModel]="selected$ | async | date: 'medium'"
+        [ngModel]="selected$ | async | localeDate: 'medium'"
         class="selected-datetime"
         class="selected-datetime"
         (keydown.enter)="dropdownComponent.toggleOpen()"
         (keydown.enter)="dropdownComponent.toggleOpen()"
         (keydown.space)="dropdownComponent.toggleOpen()"
         (keydown.space)="dropdownComponent.toggleOpen()"

+ 12 - 13
packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.component.scss

@@ -1,4 +1,3 @@
-@import "variables";
 
 
 :host {
 :host {
     display: flex;
     display: flex;
@@ -21,9 +20,9 @@ input.selected-datetime {
     margin: 0;
     margin: 0;
     border-radius: 0;
     border-radius: 0;
     border-left: none;
     border-left: none;
-    border-color: $color-grey-300;
+    border-color: var(--color-component-border-200);
     background-color: white;
     background-color: white;
-    color: $color-grey-500;
+    color: var(--color-grey-500);
     display: none;
     display: none;
     &.visible {
     &.visible {
         display: block;
         display: block;
@@ -42,8 +41,8 @@ input.selected-datetime {
 table.calendar-table {
 table.calendar-table {
     padding: 6px;
     padding: 6px;
     &:focus {
     &:focus {
-        outline: 1px solid $color-primary-500;
-        box-shadow: 0 0 1px 2px $color-primary-100;
+        outline: 1px solid var(--color-primary-500);
+        box-shadow: 0 0 1px 2px var(--color-primary-100);
     }
     }
     td {
     td {
         width: 24px;
         width: 24px;
@@ -52,31 +51,31 @@ table.calendar-table {
         user-select: none;
         user-select: none;
     }
     }
     .day-cell {
     .day-cell {
-        background-color: $color-grey-200;
-        color: $color-grey-500;
+        background-color: var(--color-component-bg-200);
+        color: var(--color-grey-500);
 
 
         cursor: pointer;
         cursor: pointer;
         transition: background-color 0.1s;
         transition: background-color 0.1s;
         &.current-month {
         &.current-month {
             background-color: white;
             background-color: white;
-            color: $color-grey-800;
+            color: var(--color-grey-800);
         }
         }
         &.selected {
         &.selected {
-            background-color: $color-primary-500;
+            background-color: var(--color-primary-500);
             color: white;
             color: white;
         }
         }
         &.viewing:not(.selected) {
         &.viewing:not(.selected) {
-            background-color: $color-primary-200;
+            background-color: var(--color-primary-200);
         }
         }
         &.today {
         &.today {
-            border: 1px solid $color-grey-400;
+            border: 1px solid var(--color-component-border-300);
         }
         }
         &:hover:not(.selected):not(.disabled) {
         &:hover:not(.selected):not(.disabled) {
-            background-color: $color-primary-100;
+            background-color: var(--color-primary-100);
         }
         }
         &.disabled {
         &.disabled {
             cursor: default;
             cursor: default;
-            color: $color-grey-300;
+            color: var(--color-grey-300);
         }
         }
     }
     }
 }
 }

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-item.directive.ts

@@ -12,6 +12,6 @@ export class DropdownItemDirective {
 
 
     @HostListener('click', ['$event'])
     @HostListener('click', ['$event'])
     onDropdownItemClick(event: any): void {
     onDropdownItemClick(event: any): void {
-        this.dropdown.toggleOpen();
+        this.dropdown.onClick();
     }
     }
 }
 }

+ 8 - 1
packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown.component.ts

@@ -1,4 +1,4 @@
-import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core';
+import { ChangeDetectionStrategy, Component, ElementRef, Input } from '@angular/core';
 
 
 @Component({
 @Component({
     selector: 'vdr-dropdown',
     selector: 'vdr-dropdown',
@@ -10,6 +10,13 @@ export class DropdownComponent {
     private isOpen = false;
     private isOpen = false;
     private onOpenChangeCallbacks: Array<(isOpen: boolean) => void> = [];
     private onOpenChangeCallbacks: Array<(isOpen: boolean) => void> = [];
     public trigger: ElementRef;
     public trigger: ElementRef;
+    @Input() manualToggle = false;
+
+    onClick() {
+        if (!this.manualToggle) {
+            this.toggleOpen();
+        }
+    }
 
 
     toggleOpen() {
     toggleOpen() {
         this.isOpen = !this.isOpen;
         this.isOpen = !this.isOpen;

+ 2 - 3
packages/admin-ui/src/lib/core/src/shared/components/edit-note-dialog/edit-note-dialog.component.scss

@@ -1,14 +1,13 @@
-@import "variables";
 
 
 .visibility-select {
 .visibility-select {
     display: flex;
     display: flex;
     justify-content: space-between;
     justify-content: space-between;
     align-items: baseline;
     align-items: baseline;
     .public {
     .public {
-        color: $color-warning-500;
+        color: var(--color-warning-500);
     }
     }
     .private {
     .private {
-        color: $color-success-500;
+        color: var(--color-success-500);
     }
     }
 }
 }
 textarea.note {
 textarea.note {

Some files were not shown because too many files changed in this diff