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)
 
 

+ 6 - 2
README.md

@@ -72,15 +72,19 @@ Vendure uses [TypeORM](http://typeorm.io), and officially supports **MySQL**, **
 
 ### 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
 ```
-
 If you do not specify the `DB` argument, it will default to "mysql".
 
 ### 6. Launch the admin ui
 
-1. `cd admin-ui`
+1. `cd packages/admin-ui`
 2. `yarn start`
 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
 
+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.
  
 ```TypeScript
@@ -27,40 +31,140 @@ class TopSellersResolver {
 {{< 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.
 {{< /alert >}}
+
 ```TypeScript
 // top-sellers.service.ts
 import { Injectable } from '@nestjs/common';
+import { RequestContext } from '@vendure/core';
 
 @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
 // top-sellers.plugin.ts
 import gql from 'graphql-tag';
-import { VendurePlugin } from '@vendure/core';
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
 import { TopSellersService } from './top-sellers.service'
 import { TopSellersResolver } from './top-sellers.resolver'
 
 @VendurePlugin({
+  imports: [PluginCommonModule],
   providers: [TopSellersService],
   adminApiExtensions: {
     schema: gql`
       extend type Query {
         topSellers(from: DateTime! to: DateTime!): [Product!]!
-      }
-    `,
+    }`,
     resolvers: [TopSellersResolver]
   }
 })
 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/*"
   ],
-  "version": "0.18.0",
+  "version": "0.18.1",
   "npmClient": "yarn",
   "useWorkspaces": true,
   "command": {

+ 1 - 0
package.json

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

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui-plugin",
-  "version": "0.18.0",
+  "version": "0.18.1",
   "main": "lib/index.js",
   "types": "lib/index.d.ts",
   "files": [
@@ -19,8 +19,8 @@
   "devDependencies": {
     "@types/express": "^4.17.8",
     "@types/fs-extra": "^9.0.1",
-    "@vendure/common": "^0.18.0",
-    "@vendure/core": "^0.18.0",
+    "@vendure/common": "^0.18.1",
+    "@vendure/core": "^0.18.1",
     "express": "^4.17.1",
     "rimraf": "^3.0.2",
     "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 path from 'path';
 
-import { DEFAULT_APP_PATH, defaultAvailableLanguages, defaultLanguage, loggerCtx } from './constants';
+import { defaultAvailableLanguages, defaultLanguage, DEFAULT_APP_PATH, loggerCtx } from './constants';
 
 /**
  * @description
@@ -255,6 +255,15 @@ export class AdminUiPlugin implements OnVendureBootstrap, OnVendureClose {
             defaultLanguage: propOrDefault('defaultLanguage', defaultLanguage),
             availableLanguages: propOrDefault('availableLanguages', defaultAvailableLanguages),
             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": {
     "cs": {
-      "tokenCount": 748,
-      "translatedCount": 687,
-      "percentage": 92
+      "tokenCount": 752,
+      "translatedCount": 751,
+      "percentage": 100
     },
     "de": {
-      "tokenCount": 748,
+      "tokenCount": 752,
       "translatedCount": 596,
-      "percentage": 80
+      "percentage": 79
     },
     "en": {
-      "tokenCount": 748,
-      "translatedCount": 747,
+      "tokenCount": 752,
+      "translatedCount": 752,
       "percentage": 100
     },
     "es": {
-      "tokenCount": 748,
-      "translatedCount": 455,
+      "tokenCount": 752,
+      "translatedCount": 458,
       "percentage": 61
     },
     "fr": {
-      "tokenCount": 748,
+      "tokenCount": 752,
       "translatedCount": 692,
-      "percentage": 93
+      "percentage": 92
     },
     "pl": {
-      "tokenCount": 748,
+      "tokenCount": 752,
       "translatedCount": 551,
-      "percentage": 74
+      "percentage": 73
     },
     "pt_BR": {
-      "tokenCount": 748,
+      "tokenCount": 752,
       "translatedCount": 642,
-      "percentage": 86
+      "percentage": 85
     },
     "zh_Hans": {
-      "tokenCount": 748,
+      "tokenCount": 752,
       "translatedCount": 533,
       "percentage": 71
     },
     "zh_Hant": {
-      "tokenCount": 748,
+      "tokenCount": 752,
       "translatedCount": 533,
       "percentage": 71
     }

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

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/admin-ui",
-  "version": "0.18.0",
+  "version": "0.18.1",
   "license": "MIT",
   "scripts": {
     "ng": "ng",
@@ -29,14 +29,14 @@
     "@angular/router": "10.1.4",
     "@apollo/client": "^3.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",
     "@ngx-translate/core": "^13.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",
     "apollo-angular": "^2.0.4",
     "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.
 const outFile = path.join(__dirname, '../package/static/theme.min.css');
 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,
     includePaths: [path.join(__dirname, '../src/lib/static/styles')],
     outputStyle: 'compressed',
@@ -15,7 +15,6 @@ const result = sass.renderSync({
 
 fs.writeFileSync(outFile, result.css, 'utf8');
 
-
 function importer(url, prev, done) {
     let file = url;
     // 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"
             [formControl]="searchTerm"
             [placeholder]="'asset.search-asset-name' | translate"
-            class="clr-input search-input"
+            class="search-input ml3"
         />
     </vdr-ab-left>
     <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 {
     display: flex;
@@ -12,7 +11,7 @@ vdr-asset-gallery {
 
 .paging-controls {
     padding-top: 6px;
-    border-top: 1px solid $color-grey-200;
+    border-top: 1px solid var(--color-component-border-100);
     display: flex;
     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>
             <tr *ngFor="let row of variantsPreview$ | async">
                 <td>{{ row.name }}</td>
-                <td>{{ row.price / 100 | currency: currentChannel?.currencyCode }}</td>
+                <td>{{ row.price | localeCurrency: currentChannel?.currencyCode }}</td>
                 <td>
                     <ng-template [ngIf]="selectedChannel" [ngIfElse]="noChannelSelected">
-                        {{ row.pricePreview / 100 | currency: selectedChannel?.currencyCode }}
+                        {{ row.pricePreview | localeCurrency: selectedChannel?.currencyCode }}
                     </ng-template>
                     <ng-template #noChannelSelected> - </ng-template>
                 </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 {
     min-width: 200px;

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

@@ -7,7 +7,6 @@
     <input
         type="text"
         [placeholder]="'catalog.filter-by-name' | translate"
-        class="clr-input"
         [formControl]="filterTermControl"
     />
 </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 {
-    background-color: $color-grey-100;
+    background-color: var(--color-component-bg-100);
     position: sticky;
     top: 0;
     padding: 6px;
     z-index: 1;
-    border-bottom: 1px solid $color-grey-300;
+    border-bottom: 1px solid var(--color-component-border-200);
 
     .header-title-row {
         display: flex;

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

@@ -21,6 +21,7 @@ import {
     CustomFieldConfig,
     DataService,
     encodeConfigArgValue,
+    findTranslation,
     getConfigArgValue,
     LanguageCode,
     ModalService,
@@ -105,7 +106,7 @@ export class CollectionDetailComponent
             .pipe(take(1))
             .subscribe(([entity, languageCode]) => {
                 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;
                 if (slugControl && slugControl.pristine && currentSlugIsEmpty) {
                     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.
      */
     protected setFormValues(entity: Collection.Fragment, languageCode: LanguageCode) {
-        const currentTranslation = entity.translations.find(t => t.languageCode === languageCode);
+        const currentTranslation = findTranslation(entity, languageCode);
 
         this.detailForm.patchValue({
             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-ab-left>
-        <clr-checkbox-wrapper class="expand-all-toggle">
+        <clr-checkbox-wrapper class="expand-all-toggle ml3">
             <input type="checkbox" clrCheckbox [(ngModel)]="expandAll" />
             <label>{{ 'catalog.expand-all-collections' | translate }}</label>
         </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 {
     height: 100%;
@@ -48,7 +47,7 @@
 
 .paging-controls {
     padding-top: 6px;
-    border-top: 1px solid $color-grey-200;
+    border-top: 1px solid var(--color-component-border-100);
     display: flex;
     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 {
     display: block;
 }
 .collection {
-    background-color: white;
+    background-color: var(--color-component-bg-100);
     font-size: 0.65rem;
-    color: rgba(0, 0, 0, 0.87);
     transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
     margin-bottom: 2px;
     border-left: 2px solid transparent;
     transition: border-left-color 0.2s;
 
     &.private {
-        background-color: $color-grey-200;
+        background-color: var(--color-component-bg-200);
     }
 
     .collection-detail {
@@ -21,10 +19,10 @@
         display: flex;
         align-items: center;
         justify-content: space-between;
-        border-bottom: 1px solid $color-grey-200;
+        border-bottom: 1px solid var(--color-component-border-100);
 
         &.active {
-            background-color: $color-grey-200;
+            background-color: var(--clr-global-selection-color);
         }
 
         &.depth-1 { padding-left: 12px + 24px; }
@@ -41,18 +39,18 @@
 
 .tree-node {
     display: block;
-    background: white;
+    background: var(--color-component-bg-100);
     overflow: hidden;
     &.cdk-drop-list-dragging {
         > .collection {
-            border-left-color: $color-primary-300;
+            border-left-color: var(--color-primary-300);
         }
     }
 }
 
 .drag-placeholder {
     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);
 }
 

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

@@ -1,4 +1,3 @@
-@import "variables";
 
 .visible-toggle {
     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,
     DeletionResult,
     FacetWithValues,
+    findTranslation,
     LanguageCode,
     ModalService,
     NotificationService,
@@ -269,7 +270,7 @@ export class FacetDetailComponent
      * Sets the values of the form on changes to the facet or current language.
      */
     protected setFormValues(facet: FacetWithValues.Fragment, languageCode: LanguageCode) {
-        const currentTranslation = facet.translations.find(t => t.languageCode === languageCode);
+        const currentTranslation = findTranslation(facet, languageCode);
 
         this.detailForm.patchValue({
             facet: {
@@ -299,8 +300,7 @@ export class FacetDetailComponent
         currentValuesFormArray.clear();
         this.values = [...facet.values];
         facet.values.forEach((value, i) => {
-            const valueTranslation =
-                value.translations && value.translations.find(t => t.languageCode === languageCode);
+            const valueTranslation = findTranslation(value, languageCode);
             const group = {
                 id: value.id,
                 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 {
     &.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 {
     display: block;
@@ -21,8 +20,8 @@
 .variants-preview {
     tr.disabled {
         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 {
     background-color: white;
     border-radius: 3px !important;
-    border: 1px solid $color-grey-300 !important;
+    border: 1px solid var(--color-grey-300) !important;
     cursor: text;
 
     &.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 {
@@ -25,7 +24,7 @@
             outline: none;
         }
         &:disabled {
-            background-color: $color-grey-100;
+            background-color: var(--color-component-bg-100);
         }
     }
 }
@@ -39,8 +38,8 @@ vdr-chip {
     }
     &.selected {
         ::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;
         }
     }

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

@@ -1,4 +1,3 @@
-@import "variables";
 
 :host {
     width: 340px;
@@ -10,12 +9,12 @@
 
 .placeholder {
     text-align: center;
-    color: $color-grey-300;
+    color: var(--color-grey-300);
 }
 
 .featured-asset {
     text-align: center;
-    background: $color-grey-200;
+    background: var(--color-component-bg-200);
     padding: 6px;
     cursor: pointer;
 
@@ -42,16 +41,16 @@
     .asset-thumb {
         margin: 3px;
         padding: 0;
-        border: 2px solid $color-grey-200;
+        border: 2px solid var(--color-component-border-100);
         cursor: pointer;
 
         &.featured {
-            border-color: $color-primary-500;
+            border-color: var(--color-primary-500);
         }
     }
 
     .remove-asset {
-        color: $color-warning-500;
+        color: var(--color-warning-500);
     }
 
     &.compact {
@@ -67,7 +66,7 @@
     align-items: center;
     justify-content: center;
     width: 50px;
-    background-color: $color-grey-100;
+    background-color: var(--color-component-bg-100);
     opacity: 0.9;
     box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
     0 8px 10px 1px rgba(0, 0, 0, 0.14),
@@ -79,7 +78,7 @@
     width: 60px;
     height: 50px;
     .asset-thumb {
-        background-color: $color-grey-300;
+        background-color: var(--color-component-bg-300);
         height: 100%;
         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)"
                                 />
                             </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
                                 [label]="'catalog.slug' | translate"
                                 for="slug"
@@ -189,7 +205,6 @@
                         <div class="variant-filter">
                             <input
                                 [formControl]="filterInput"
-                                class="clr-input"
                                 [placeholder]="'catalog.filter-by-name-or-sku' | translate"
                             />
                             <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 {
     ::ng-deep {
@@ -21,7 +20,8 @@ vdr-action-bar clr-toggle-wrapper {
         border-radius: 3px 0 0 3px !important;
     }
     .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-left: none;
     }
@@ -44,3 +44,15 @@ vdr-action-bar clr-toggle-wrapper {
 .channel-assignment {
     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,
     DataService,
     FacetWithValues,
+    findTranslation,
     flattenFacetValues,
     GlobalFlag,
     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 { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { unique } from '@vendure/common/lib/unique';
-import { combineLatest, EMPTY, merge, Observable } from 'rxjs';
+import { combineLatest, EMPTY, merge, Observable, of } from 'rxjs';
 import {
     debounceTime,
     distinctUntilChanged,
@@ -43,7 +44,7 @@ import {
     withLatestFrom,
 } 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 { AssignProductsToChannelDialogComponent } from '../assign-products-to-channel-dialog/assign-products-to-channel-dialog.component';
 import { CreateProductVariantsConfig } from '../generate-product-variants/generate-product-variants.component';
@@ -121,6 +122,7 @@ export class ProductDetailComponent
             product: this.formBuilder.group({
                 enabled: true,
                 name: ['', Validators.required],
+                autoUpdateVariantNames: true,
                 slug: '',
                 description: '',
                 facetValueIds: [[]],
@@ -326,7 +328,7 @@ export class ProductDetailComponent
             .pipe(take(1))
             .subscribe(([entity, languageCode]) => {
                 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;
                 if (slugControl && slugControl.pristine && currentSlugIsEmpty) {
                     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) {
@@ -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(
@@ -515,7 +531,7 @@ export class ProductDetailComponent
      * Sets the values of the form on changes to the product or current language.
      */
     protected setFormValues(product: ProductWithVariants.Fragment, languageCode: LanguageCode) {
-        const currentTranslation = product.translations.find(t => t.languageCode === languageCode);
+        const currentTranslation = findTranslation(product, languageCode);
         this.detailForm.patchValue({
             product: {
                 enabled: product.enabled,
@@ -544,7 +560,7 @@ export class ProductDetailComponent
 
         const variantsFormArray = this.detailForm.get('variants') as FormArray;
         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 group: VariantFormValue = {
                 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)"
                 (facetValueChange)="setFacetValueIds($event)"
             ></vdr-product-search-input>
-            <vdr-dropdown class="search-settings-menu">
+            <vdr-dropdown class="search-settings-menu mr3">
                 <button type="button" class="icon-button" vdrDropdownTrigger>
                     <clr-icon shape="cog"></clr-icon>
                 </button>

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

@@ -3,10 +3,10 @@
 .image-placeholder {
     width: 50px;
     height: 50px;
-    background-color: $color-grey-200;
+    background-color: var(--color-component-bg-200);
     .placeholder {
         text-align: center;
-        color: $color-grey-300;
+        color: var(--color-grey-300);
     }
 }
 .search-form {
@@ -25,5 +25,5 @@
     margin: 0 12px;
 }
 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";
 
 :host {
@@ -19,10 +18,10 @@ ng-select {
 
 .search-header {
     padding: 8px 10px;
-    border-bottom: 1px solid $color-grey-200;
+    border-bottom: 1px solid var(--color-component-border-100);
     cursor: pointer;
 
     &.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])"
                     ></vdr-currency-input>
                 </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>
                 <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 {
     display: flex;
@@ -15,8 +14,8 @@
 .variants-preview {
     tr.disabled {
         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 { 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 {
     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;
     min-height: 52px;
     align-items: center;
-    border: 1px solid $color-grey-200;
+    border: 1px solid var(--color-component-border-100);
     border-radius: 3px;
     padding: 6px 18px;
 
@@ -21,7 +21,7 @@
     transition: background-color 0.2s;
     min-height: 330px;
     &.disabled {
-        background-color: $color-grey-200;
+        background-color: var(--color-component-bg-200);
     }
     .header-row {
         display: flex;
@@ -118,7 +118,7 @@
     }
 
     .option-group-name {
-        color: $color-grey-400;
+        color: var(--color-text-200);
         text-transform: uppercase;
         font-size: 10px;
         margin-right: 3px;
@@ -127,7 +127,7 @@
 
     .options-facets {
         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() selectionChange = new EventEmitter<string[]>();
     @Output() selectFacetValueClick = new EventEmitter<string[]>();
-    @Output() updateProductOption = new EventEmitter<UpdateProductOptionInput>();
+    @Output() updateProductOption = new EventEmitter<UpdateProductOptionInput & { autoUpdate: boolean }>();
     selectedVariantIds: string[] = [];
     pagination: PaginationInstance = {
         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 {
-    color: $color-grey-300;
+    color: var(--color-grey-300);
 }
 
 .stock, .price {
@@ -13,6 +12,6 @@
 td {
     transition: background-color 0.2s;
     &.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">
     <input id="code" type="text" #codeInput="ngModel" required [(ngModel)]="code" pattern="[a-z0-9_-]+" />
 </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">
     <label>{{ 'common.custom-fields' | translate }}</label>
     <ng-container *ngFor="let customField of customFields">
@@ -30,7 +34,11 @@
     <button
         type="submit"
         (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"
     >
         {{ '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'],
     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
     productOption: ProductVariant.Options;
     activeLanguage: LanguageCode;
@@ -29,7 +31,7 @@ export class UpdateProductOptionDialogComponent implements Dialog<UpdateProductO
 
     ngOnInit(): void {
         const currentTranslation = this.productOption.translations.find(
-            (t) => t.languageCode === this.activeLanguage,
+            t => t.languageCode === this.activeLanguage,
         );
         this.name = currentTranslation?.name ?? '';
         this.code = this.productOption.code;
@@ -64,7 +66,7 @@ export class UpdateProductOptionDialogComponent implements Dialog<UpdateProductO
                 name: '',
             },
         });
-        this.resolveWith(result);
+        this.resolveWith({ ...result, autoUpdate: this.updateVariantName });
     }
 
     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">
     {{
         '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>

+ 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(
             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,
     DeletionResult,
     FacetWithValues,
+    findTranslation,
     LanguageCode,
+    ProductWithVariants,
     UpdateProductInput,
     UpdateProductMutation,
     UpdateProductOptionInput,
@@ -17,7 +19,9 @@ import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { forkJoin, Observable, of, throwError } from 'rxjs';
 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
@@ -30,13 +34,13 @@ export class ProductDetailService {
     constructor(private dataService: DataService) {}
 
     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() {
         return this.dataService.settings
             .getTaxCategories()
-            .mapSingle((data) => data.taxCategories)
+            .mapSingle(data => data.taxCategories)
             .pipe(shareReplay(1));
     }
 
@@ -46,14 +50,14 @@ export class ProductDetailService {
         languageCode: LanguageCode,
     ) {
         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);
 
         return forkJoin(createProduct$, createOptionGroups$).pipe(
             mergeMap(([{ createProduct }, optionGroups]) => {
                 const addOptionsToProduct$ = optionGroups.length
                     ? forkJoin(
-                          optionGroups.map((optionGroup) => {
+                          optionGroups.map(optionGroup => {
                               return this.dataService.product.addOptionGroupToProduct({
                                   productId: createProduct.id,
                                   optionGroupId: optionGroup.id,
@@ -68,10 +72,10 @@ export class ProductDetailService {
                 );
             }),
             mergeMap(({ createProduct, optionGroups }) => {
-                const variants = createVariantsConfig.variants.map((v) => {
+                const variants = createVariantsConfig.variants.map(v => {
                     const optionIds = optionGroups.length
                         ? 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) {
                                   throw new Error(
                                       `Could not find a matching ProductOption "${optionName}" when creating variant`,
@@ -85,7 +89,7 @@ export class ProductDetailService {
                         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);
             }),
         );
@@ -94,17 +98,17 @@ export class ProductDetailService {
     createProductOptionGroups(groups: Array<{ name: string; values: string[] }>, languageCode: LanguageCode) {
         return groups.length
             ? forkJoin(
-                  groups.map((c) => {
+                  groups.map(c => {
                       return this.dataService.product
                           .createProductOptionGroups({
                               code: normalizeString(c.name, '-'),
                               translations: [{ languageCode, name: c.name }],
-                              options: c.values.map((v) => ({
+                              options: c.values.map(v => ({
                                   code: normalizeString(v, '-'),
                                   translations: [{ languageCode, name: v }],
                               })),
                           })
-                          .pipe(map((data) => data.createProductOptionGroup));
+                          .pipe(map(data => data.createProductOptionGroup));
                   }),
               )
             : of([]);
@@ -116,12 +120,12 @@ export class ProductDetailService {
         options: Array<{ id: string; name: string }>,
         languageCode: LanguageCode,
     ) {
-        const variants: CreateProductVariantInput[] = variantData.map((v) => {
+        const variants: CreateProductVariantInput[] = variantData.map(v => {
             const name = options.length
                 ? `${product.name} ${v.optionIds
-                      .map((id) => options.find((o) => o.id === id))
+                      .map(id => options.find(o => o.id === id))
                       .filter(notNullOrUndefined)
-                      .map((o) => o.name)
+                      .map(o => o.name)
                       .join(' ')}`
                 : product.name;
             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 updateVariantsInput = variantsInput || [];
         if (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);
     }
 
-    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) {
         return this.dataService.product.deleteProductVariant(id).pipe(
-            switchMap((result) => {
+            switchMap(result => {
                 if (result.deleteProductVariant.result === DeletionResult.DELETED) {
                     return this.dataService.product.getProduct(productId).single$;
                 } 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/update-product-option-dialog/update-product-option-dialog.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/collection-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 {
     position: absolute;
     overflow: hidden;
     height: 4px;
-    background-color: $color-grey-500;
+    background-color: var(--color-grey-500);
     opacity: 0;
     transition: opacity 0.1s;
     &.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 { map } from 'rxjs/operators';
 
@@ -11,12 +12,22 @@ import { DataService } from './data/providers/data.service';
 })
 export class AppComponent implements OnInit {
     loading$: Observable<boolean>;
+    private _document?: Document;
 
-    constructor(private dataService: DataService) {}
+    constructor(private dataService: DataService, @Inject(DOCUMENT) private document?: any) {
+        this._document = document;
+    }
 
     ngOnInit() {
         this.loading$ = this.dataService.client
             .getNetworkStatus()
             .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;
   /** Authenticates the user using a named authentication strategy */
   authenticate: AuthenticationResult;
+  cancelJob: Job;
   cancelOrder: CancelOrderResult;
   /** Create a new Administrator */
   createAdministrator: Administrator;
@@ -421,7 +422,8 @@ export type Mutation = {
   setAsLoggedIn: UserStatus;
   setAsLoggedOut: UserStatus;
   setOrderCustomFields?: Maybe<Order>;
-  setUiLanguage?: Maybe<LanguageCode>;
+  setUiLanguage: LanguageCode;
+  setUiTheme: Scalars['String'];
   settlePayment: SettlePaymentResult;
   settleRefund: SettleRefundResult;
   transitionFulfillmentToState: TransitionFulfillmentToStateResult;
@@ -536,6 +538,11 @@ export type MutationAuthenticateArgs = {
 };
 
 
+export type MutationCancelJobArgs = {
+  jobId: Scalars['ID'];
+};
+
+
 export type MutationCancelOrderArgs = {
   input: CancelOrderInput;
 };
@@ -829,7 +836,12 @@ export type MutationSetOrderCustomFieldsArgs = {
 
 
 export type MutationSetUiLanguageArgs = {
-  languageCode?: Maybe<LanguageCode>;
+  languageCode: LanguageCode;
+};
+
+
+export type MutationSetUiThemeArgs = {
+  theme: Scalars['String'];
 };
 
 
@@ -1392,7 +1404,8 @@ export enum JobState {
   RUNNING = 'RUNNING',
   COMPLETED = 'COMPLETED',
   RETRYING = 'RETRYING',
-  FAILED = 'FAILED'
+  FAILED = 'FAILED',
+  CANCELLED = 'CANCELLED'
 }
 
 export type JobList = PaginatedList & {
@@ -2003,6 +2016,7 @@ export type ProductTranslationInput = {
 
 export type CreateProductInput = {
   featuredAssetId?: Maybe<Scalars['ID']>;
+  enabled?: Maybe<Scalars['Boolean']>;
   assetIds?: Maybe<Array<Scalars['ID']>>;
   facetValueIds?: Maybe<Array<Scalars['ID']>>;
   translations: Array<ProductTranslationInput>;
@@ -4630,6 +4644,7 @@ export type UserStatus = {
 export type UiState = {
   __typename?: 'UiState';
   language: LanguageCode;
+  theme: Scalars['String'];
 };
 
 export type CurrentUserChannelInput = {
@@ -4892,6 +4907,13 @@ export type SetUiLanguageMutationVariables = Exact<{
 
 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; }>;
 
 
@@ -4913,7 +4935,7 @@ export type GetUiStateQueryVariables = Exact<{ [key: string]: never; }>;
 
 export type GetUiStateQuery = { uiState: (
     { __typename?: 'UiState' }
-    & Pick<UiState, 'language'>
+    & Pick<UiState, 'language' | 'theme'>
   ) };
 
 export type GetClientStateQueryVariables = Exact<{ [key: string]: never; }>;
@@ -4927,7 +4949,7 @@ export type GetClientStateQuery = { networkStatus: (
     & UserStatusFragment
   ), uiState: (
     { __typename?: 'UiState' }
-    & Pick<UiState, 'language'>
+    & Pick<UiState, 'language' | 'theme'>
   ) };
 
 export type SetActiveChannelMutationVariables = Exact<{
@@ -5460,6 +5482,12 @@ export type OrderFragment = (
   & { customer?: Maybe<(
     { __typename?: 'Customer' }
     & 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'>
   )> };
 
+export type CancelJobMutationVariables = Exact<{
+  id: Scalars['ID'];
+}>;
+
+
+export type CancelJobMutation = { cancelJob: (
+    { __typename?: 'Job' }
+    & JobInfoFragment
+  ) };
+
 export type ReindexMutationVariables = Exact<{ [key: string]: never; }>;
 
 
@@ -7829,6 +7867,11 @@ export namespace SetUiLanguage {
   export type Mutation = SetUiLanguageMutation;
 }
 
+export namespace SetUiTheme {
+  export type Variables = SetUiThemeMutationVariables;
+  export type Mutation = SetUiThemeMutation;
+}
+
 export namespace GetNetworkStatus {
   export type Variables = GetNetworkStatusQueryVariables;
   export type Query = GetNetworkStatusQuery;
@@ -8136,6 +8179,8 @@ export namespace OrderAddress {
 export namespace Order {
   export type Fragment = OrderFragment;
   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 {
@@ -8928,6 +8973,12 @@ export namespace GetJobQueueList {
   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 type Variables = ReindexMutationVariables;
   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 { findTranslation } from './find-translation';
+
 export interface TranslatableUpdateOptions<T extends { translations: any[] } & MayHaveCustomFields> {
     translatable: T;
     updatedFields: { [key: string]: any };
@@ -25,7 +27,7 @@ export function createUpdatedTranslatable<T extends { translations: any[] } & Ma
 ): T {
     const { translatable, updatedFields, languageCode, customFieldConfig, defaultTranslation } = options;
     const currentTranslation =
-        translatable.translations.find(t => t.languageCode === languageCode) || defaultTranslation;
+        findTranslation(translatable, languageCode) || defaultTranslation || ({} as any);
     const index = translatable.translations.indexOf(currentTranslation);
     const newTranslation = patchObject(currentTranslation, updatedFields);
     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 {
     if (!input || input === '') {
-        return '#fff';
+        return 'var(--color-component-bg-100)';
     }
     const safeColors = [
-        // '#FF4343',
-        // '#9A0089',
-        // '#881798',
         '#10893E',
         '#107C10',
         '#7E735F',
         '#2F5646',
-        // '#D13438',
-        // '#C239B3',
-        // '#B146C2',
         '#498205',
         '#847545',
-        // '#EF6950',
-        // '#BF0077',
         '#744DA9',
         '#018574',
         '#486860',
         '#525E54',
         '#647C64',
         '#567C73',
-        // '#DA3B01',
         '#8764B8',
-        // '#C30052',
         '#515C6B',
         '#4A5459',
         '#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.
-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-header>
         <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 class="header-nav"></div>
         <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 {
     display: block;
@@ -14,7 +13,7 @@
     li:not(:last-child)::after {
         content: '›';
         top: 0;
-        color: #7e7e7e;
+        color: var(--color-grey-400);
         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>
         <button class="btn btn-link active-channel" vdrDropdownTrigger>
             <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>
         </button>
         <vdr-dropdown-menu vdrPosition="bottom-right">
+            <input
+                *ngIf="((channelCount$ | async) || 0) >= displayFilterThreshold"
+                [formControl]="filterControl"
+                type="text"
+                class="ml2 mr2"
+                [placeholder]="'common.filter' | translate"
+            />
             <button
                 *ngFor="let channel of channels$ | async"
                 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 {
     display: flex;
@@ -8,7 +7,7 @@
 }
 
 .active-channel {
-    color: $color-grey-200;
+    color: var(--color-grey-200);
 
     clr-icon {
         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 { FormControl } from '@angular/forms';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 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 { DataService } from '../../data/providers/data.service';
@@ -16,12 +17,26 @@ import { LocalStorageService } from '../../providers/local-storage/local-storage
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class ChannelSwitcherComponent implements OnInit {
+    readonly displayFilterThreshold = 10;
     channels$: Observable<CurrentUserChannel[]>;
+    channelCount$: Observable<number>;
+    filterControl = new FormControl('');
     activeChannelCode$: Observable<string>;
     constructor(private dataService: DataService, private localStorageService: LocalStorageService) {}
 
     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
             .userStatus()
             .mapStream(data => data.userStatus.channels.find(c => c.id === data.userStatus.activeChannelId))
@@ -35,6 +50,7 @@ export class ChannelSwitcherComponent implements OnInit {
             if (activeChannel) {
                 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 {
     // flex: 0 0 auto;
     order: -1;
-    background-color: $color-grey-200;
+    background-color: var(--clr-nav-background-color);
 }
 
 nav.sidenav {
     height: 100%;
     width: 10.8rem;
+    border-right-color: var(--clr-sidenav-border-color);
 }
 
 .nav-list clr-icon {
@@ -25,19 +26,19 @@ nav.sidenav {
     height: 10px;
     position: absolute;
     border-radius: 50%;
-    border: 1px solid $color-grey-200;
+    border: 1px solid var(--color-component-border-100);
 
     &.info {
-        background-color: $color-primary-600;
+        background-color: var(--color-primary-600);
     }
     &.success {
-        background-color: $color-success-500;
+        background-color: var(--color-success-500);
     }
     &.warning {
-        background-color: $color-warning-500;
+        background-color: var(--color-warning-500);
     }
     &.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 {
     0% { opacity: 0; }
@@ -19,22 +18,22 @@
         max-width: 98vw;
         word-wrap: break-word;
         padding: 10px;
-        background-color: $color-grey-500;
+        background-color: var(--color-grey-500);
         color: white;
         transition: opacity 1s, top 0.3s;
         opacity: 0;
 
         &.success {
-            background-color: $color-success-500;
+            background-color: var(--color-success-500);
         }
         &.error {
-            background-color: $color-error-500;
+            background-color: var(--color-error-500);
         }
         &.warning {
-            background-color: $color-warning-500;
+            background-color: var(--color-warning-500);
         }
         &.info {
-            background-color: $color-secondary-500;
+            background-color: var(--color-secondary-500);
         }
 
         &.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 }}
             </button>
-            <div class="dropdown-divider"></div>
         </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()">
             <clr-icon shape="logout"></clr-icon> {{ 'common.log-out' | translate }}
         </button>

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

@@ -8,7 +8,7 @@
 }
 
 .user-name {
-    color: $color-grey-200;
+    color: var(--color-grey-200);
     margin-right: 12px;
     @media screen and (max-width: $breakpoint-small) {
         display: none;
@@ -18,7 +18,3 @@
 .trigger clr-icon {
     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 { HttpClient } from '@angular/common/http';
 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 { TranslateCompiler, TranslateLoader, TranslateModule } from '@ngx-translate/core';
 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 { NotificationComponent } from './components/notification/notification.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 { UserMenuComponent } from './components/user-menu/user-menu.component';
 import { DataModule } from './data/data.module';
@@ -39,7 +40,11 @@ import { SharedModule } from './shared/shared.module';
             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],
     declarations: [
         AppShellComponent,
@@ -50,11 +55,17 @@ import { SharedModule } from './shared/shared.module';
         NotificationComponent,
         UiLanguageSwitcherDialogComponent,
         ChannelSwitcherComponent,
+        ThemeSwitcherComponent,
     ],
 })
 export class CoreModule {
-    constructor(private i18nService: I18nService, private localStorageService: LocalStorageService) {
+    constructor(
+        private i18nService: I18nService,
+        private localStorageService: LocalStorageService,
+        private titleService: Title,
+    ) {
         this.initUiLanguages();
+        this.initUiTitle();
     }
 
     private initUiLanguages() {
@@ -76,6 +87,12 @@ export class CoreModule {
         this.i18nService.setDefaultLanguage(defaultLanguage);
         this.i18nService.setAvailableLanguages(availableLanguages || [defaultLanguage]);
     }
+
+    private initUiTitle() {
+        const title = getAppConfig().brand || 'VendureAdmin';
+
+        this.titleService.setTitle(title);
+    }
 }
 
 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 { getDefaultUiLanguage } from '../../common/utilities/get-default-ui-language';
 import { LocalStorageService } from '../../providers/local-storage/local-storage.service';
 
 export function getClientDefaults(localStorageService: LocalStorageService) {
     const currentLanguage = localStorageService.get('uiLanguageCode') || getDefaultUiLanguage();
+    const activeTheme = localStorageService.get('activeTheme') || 'default';
     return {
         networkStatus: {
             inFlightRequests: 0,
@@ -20,6 +22,7 @@ export function getClientDefaults(localStorageService: LocalStorageService) {
         } as GetUserStatus.UserStatus,
         uiState: {
             language: currentLanguage,
+            theme: activeTheme,
             __typename: '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,
     SetAsLoggedIn,
     SetUiLanguage,
+    SetUiTheme,
     UpdateUserChannels,
     UserStatus,
 } from '../../common/generated-types';
@@ -69,15 +70,31 @@ export const clientResolvers: ResolverDefinition = {
             return data.userStatus;
         },
         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 = {
                 uiState: {
                     __typename: 'UiState',
                     language: args.languageCode,
+                    theme: previous.uiState.theme,
                 },
             };
             cache.writeQuery({ query: GET_UI_STATE, data });
             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 => {
             // tslint:disable-next-line:no-non-null-assertion
             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!
     setAsLoggedIn(input: UserStatusInput!): UserStatus!
     setAsLoggedOut: UserStatus!
-    setUiLanguage(languageCode: LanguageCode): LanguageCode
+    setUiLanguage(languageCode: LanguageCode!): LanguageCode!
+    setUiTheme(theme: String!): String!
     setActiveChannel(channelId: ID!): UserStatus!
     updateUserChannels(channels: [CurrentUserChannelInput!]!): UserStatus!
 }
@@ -29,6 +30,7 @@ type UserStatus {
 
 type UiState {
     language: LanguageCode!
+    theme: String!
 }
 
 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`
     query GetNetworkStatus {
         networkStatus @client {
@@ -73,6 +79,7 @@ export const GET_UI_STATE = gql`
     query GetUiState {
         uiState @client {
             language
+            theme
         }
     }
 `;
@@ -87,6 +94,7 @@ export const GET_CLIENT_STATE = gql`
         }
         uiState @client {
             language
+            theme
         }
     }
     ${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
             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`
     mutation Reindex {
         reindex {

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

@@ -10,6 +10,7 @@ import {
     SetActiveChannel,
     SetAsLoggedIn,
     SetUiLanguage,
+    SetUiTheme,
     UpdateUserChannels,
 } from '../../common/generated-types';
 import {
@@ -22,6 +23,7 @@ import {
     SET_AS_LOGGED_IN,
     SET_AS_LOGGED_OUT,
     SET_UI_LANGUAGE,
+    SET_UI_THEME,
     UPDATE_USER_CHANNELS,
 } 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) {
         return this.baseDataService.mutate<SetActiveChannel.Mutation, SetActiveChannel.Variables>(
             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) {
         const input: CreateProduct.Variables = {
             input: pick(product, [
+                'enabled',
                 'translations',
                 'customFields',
                 '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 {
     AddMembersToZone,
+    CancelJob,
     CreateChannel,
     CreateChannelInput,
     CreateCountry,
@@ -57,6 +58,7 @@ import {
 } from '../../common/generated-types';
 import {
     ADD_MEMBERS_TO_ZONE,
+    CANCEL_JOB,
     CREATE_CHANNEL,
     CREATE_COUNTRY,
     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),
                 new Map<string, JobInfoFragment>(),
             ),
-            map((jobMap) => Array.from(jobMap.values())),
+            map(jobMap => Array.from(jobMap.values())),
             debounceTime(500),
             shareReplay(1),
         );
 
         this.subscription = this.activeJobs$
             .pipe(
-                switchMap((jobs) => {
+                switchMap(jobs => {
                     if (jobs.length) {
                         return interval(2500).pipe(mapTo(jobs));
                     } else {
@@ -38,10 +38,10 @@ export class JobQueueService implements OnDestroy {
                     }
                 }),
             )
-            .subscribe((jobs) => {
+            .subscribe(jobs => {
                 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);
                         });
                     });
@@ -61,8 +61,8 @@ export class JobQueueService implements OnDestroy {
     checkForJobs(delay: number = 1000) {
         timer(delay)
             .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) {
                         return this.dataService.settings.getRunningJobs().single$;
                     } 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) {
@@ -92,6 +92,7 @@ export class JobQueueService implements OnDestroy {
                 break;
             case JobState.COMPLETED:
             case JobState.FAILED:
+            case JobState.CANCELLED:
                 jobMap.delete(job.id);
                 const handler = this.onCompleteHandlers.get(job.id);
                 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;
     orderListLastCustomFilters: any;
     dashboardWidgetLayout: WidgetLayoutDefinition;
+    activeTheme: string;
 };
 
 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/utilities/configurable-operation-utils';
 export * from './common/utilities/create-updated-translatable';
+export * from './common/utilities/find-translation';
 export * from './common/utilities/flatten-facet-values';
 export * from './common/utilities/get-default-ui-language';
 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/notification/notification.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/user-menu/user-menu.component';
 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/pipes/asset-preview.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/duration.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 {
     display: flex;
     justify-content: space-between;
     align-items: flex-start;
-    background-color: $color-grey-100;
+    background-color: var(--color-component-bg-100);
     position: sticky;
     top: -24px;
     z-index: 25;
-    border-bottom: 1px solid $color-grey-300;
+    border-bottom: 1px solid var(--color-component-border-200);
 
     > .grow {
         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 {
     display: inline-flex;
 }
 
 .affix {
+    color: var(--color-grey-800);
     display: flex;
     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;
     padding: 3px;
     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 {
     display: none;
@@ -6,8 +5,8 @@
 
 .drop-zone {
     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;
     visibility: hidden;
     z-index: 1000;
@@ -17,7 +16,7 @@
     justify-content: center;
 
     &.visible {
-        opacity: 1;
+        opacity: 0.3;
         visibility: visible;
         transition: opacity 0.2s, background-color 0.2s, border 0.2s, visibility 0s;
     }
@@ -28,13 +27,14 @@
         padding: 24px;
         font-size: 32px;
         pointer-events: none;
-        opacity: 0.2;
+        opacity: 0.5;
         transition: opacity 0.2s;
     }
 
     &.dragging-over {
         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;
         .drop-label {
             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;
 
     .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 {
     opacity: 0;
     position: absolute;
-    color: $color-success-500;
+    color: var(--color-success-500);
     background-color: white;
     border-radius: 50%;
     top: -12px;
@@ -41,8 +41,8 @@
 }
 
 .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 {
         opacity: 1;
@@ -80,7 +80,7 @@
         &.visible {
             opacity: 1;
             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;
         }
     }
@@ -108,7 +108,7 @@
 
     .placeholder {
         text-align: center;
-        color: $color-grey-300;
+        color: var(--color-grey-300);
     }
 
     .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">
         {{ 'asset.select-assets' | translate }}
         <vdr-asset-file-input
+            class="ml3"
             (selectFiles)="createAssets($event)"
             [uploading]="uploading"
             dropZoneTarget=".modal-content"
@@ -13,7 +14,7 @@
     name="searchTerm"
     [formControl]="searchTerm"
     [placeholder]="'asset.search-asset-name' | translate"
-    class="clr-input search-input"
+    class="search-input"
 />
 <vdr-asset-gallery
     [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 {
     display: flex;
@@ -18,7 +17,7 @@ vdr-asset-gallery {
 
 .paging-controls {
     padding-top: 6px;
-    border-top: 1px solid $color-grey-200;
+    border-top: 1px solid var(--color-component-border-100);
     display: flex;
     justify-content: space-between;
     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 {
     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 {
     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 {
     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 {
     display: inline-block;
@@ -6,38 +6,40 @@
 
 .wrapper {
     display: flex;
-    border: 1px solid $color-grey-400;
+    border: 1px solid var(--color-component-border-300);
     border-radius: 3px;
     margin: 6px;
     &.with-background {
-        color: transparentize($color-grey-100, 0.2);
+        color: var(--color-grey-100);
         border-color: transparent;
+        .chip-label {
+            opacity: 0.9;
+        }
     }
 
     &.warning {
-        border-color: $color-warning-200;
+        border-color: var(--color-chip-warning-border);
         .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 {
-        border-color: $color-success-200;
+        border-color: var(--color-chip-success-border);
         .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 {
-        border-color: $color-error-200;
+        border-color: var(--color-chip-error-border);
         .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 {
@@ -50,7 +52,7 @@
 }
 
 .chip-icon {
-    border-left: 1px solid transparentize($color-grey-100, 0.5);
+    border-left: 1px solid var(--color-component-border-200);
     padding: 0 3px;
     line-height: 1em;
     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
         type="number"
         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 { FormsModule } from '@angular/forms';
 import { By } from '@angular/platform-browser';
 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 { CurrencyInputComponent } from './currency-input.component';
@@ -13,12 +16,13 @@ describe('CurrencyInputComponent', () => {
     beforeEach(async(() => {
         TestBed.configureTestingModule({
             imports: [FormsModule],
+            providers: [{ provide: DataService, useClass: MockDataService }],
             declarations: [
                 TestControlValueAccessorComponent,
                 TestSimpleComponent,
                 CurrencyInputComponent,
                 AffixedInputComponent,
-                CurrencyNamePipe,
+                LocaleCurrencyNamePipe,
             ],
         }).compileComponents();
     }));
@@ -66,11 +70,35 @@ describe('CurrencyInputComponent', () => {
         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>(
         component: Type<T>,
         priceValue = 123,
+        currencyCode: string = '',
     ): ComponentFixture<T> {
         const fixture = TestBed.createComponent(component);
+        if (fixture.componentInstance instanceof TestSimpleComponent && currencyCode) {
+            fixture.componentInstance.currencyCode = currencyCode;
+        }
         fixture.componentInstance.price = priceValue;
         fixture.detectChanges();
         tick();
@@ -96,9 +124,30 @@ class TestControlValueAccessorComponent {
 @Component({
     selector: 'vdr-test-component',
     template: `
-        <vdr-currency-input [value]="price"></vdr-currency-input>
+        <vdr-currency-input [value]="price" [currencyCode]="currencyCode"></vdr-currency-input>
     `,
 })
 class TestSimpleComponent {
+    currencyCode = '';
     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 { 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
@@ -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() readonly = false;
     @Input() value: number;
     @Input() currencyCode = '';
     @Output() valueChange = new EventEmitter();
+    prefix$: Observable<string>;
+    suffix$: Observable<string>;
     onChange: (val: any) => void;
     onTouch: () => void;
     _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) {
         if ('value' in changes) {
             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 {
     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 {
     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">
     <input
         readonly
-        [ngModel]="selected$ | async | date: 'medium'"
+        [ngModel]="selected$ | async | localeDate: 'medium'"
         class="selected-datetime"
         (keydown.enter)="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 {
     display: flex;
@@ -21,9 +20,9 @@ input.selected-datetime {
     margin: 0;
     border-radius: 0;
     border-left: none;
-    border-color: $color-grey-300;
+    border-color: var(--color-component-border-200);
     background-color: white;
-    color: $color-grey-500;
+    color: var(--color-grey-500);
     display: none;
     &.visible {
         display: block;
@@ -42,8 +41,8 @@ input.selected-datetime {
 table.calendar-table {
     padding: 6px;
     &: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 {
         width: 24px;
@@ -52,31 +51,31 @@ table.calendar-table {
         user-select: none;
     }
     .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;
         transition: background-color 0.1s;
         &.current-month {
             background-color: white;
-            color: $color-grey-800;
+            color: var(--color-grey-800);
         }
         &.selected {
-            background-color: $color-primary-500;
+            background-color: var(--color-primary-500);
             color: white;
         }
         &.viewing:not(.selected) {
-            background-color: $color-primary-200;
+            background-color: var(--color-primary-200);
         }
         &.today {
-            border: 1px solid $color-grey-400;
+            border: 1px solid var(--color-component-border-300);
         }
         &:hover:not(.selected):not(.disabled) {
-            background-color: $color-primary-100;
+            background-color: var(--color-primary-100);
         }
         &.disabled {
             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'])
     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({
     selector: 'vdr-dropdown',
@@ -10,6 +10,13 @@ export class DropdownComponent {
     private isOpen = false;
     private onOpenChangeCallbacks: Array<(isOpen: boolean) => void> = [];
     public trigger: ElementRef;
+    @Input() manualToggle = false;
+
+    onClick() {
+        if (!this.manualToggle) {
+            this.toggleOpen();
+        }
+    }
 
     toggleOpen() {
         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 {
     display: flex;
     justify-content: space-between;
     align-items: baseline;
     .public {
-        color: $color-warning-500;
+        color: var(--color-warning-500);
     }
     .private {
-        color: $color-success-500;
+        color: var(--color-success-500);
     }
 }
 textarea.note {

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