Browse Source

Merge branch 'master' into minor

Michael Bromley 4 years ago
parent
commit
bacaf89e5d

+ 19 - 0
docs/content/developer-guide/customizing-models.md

@@ -81,6 +81,25 @@ The built-in form inputs are listed in the [DefaultFormConfigHash docs]({{< relr
 
 If you want to use a completely custom form input component which is not provided by the Admin UI, you'll need to create a plugin which [extends the Admin UI]({{< relref "extending-the-admin-ui" >}}) with [custom form inputs]({{< relref "custom-form-inputs" >}}). 
 
+## Tabbed custom fields
+
+With a large, complex project, it's common for lots of custom fields to be required. This can get visually noisy in the UI, so Vendure supports tabbed custom fields. Just specify the tab name in the `ui` object, and those fields with the same tab name will be grouped in the UI! The tab name can also be a translation token if you need to support multiple languages.
+
+```TypeScript
+const config = {
+  // ...
+  customFields: {
+    Product: [
+      { name: 'additionalInfo', type: 'text', ui: { component: 'rich-text-form-input' } },
+      { name: 'specs', type: 'text', ui: { component: 'json-editor-form-input' } },
+      { name: 'width', type: 'int', ui: { tab: 'Shipping' } },
+      { name: 'height', type: 'int', ui: { tab: 'Shipping' } },
+      { name: 'depth', type: 'int', ui: { tab: 'Shipping' } },
+      { name: 'weight', type: 'int', ui: { tab: 'Shipping' } },
+    ],
+  },
+}
+```
 
 ## Configurable Order Products
 

+ 18 - 2
docs/content/developer-guide/multi-tenant/index.md

@@ -27,7 +27,7 @@ First we will create a new Role, and grant all permissions on the `ace-parts` Ch
 
 Next we create a new Administrator, and assign the Role that was just created.
 
-{{< figure src="create-admin.png" title="Creating a Channel-specific Role" >}}
+{{< figure src="create-admin.png" title="Creating a Channel-specific Administrator" >}}
 
 Repeat the steps of creating a Role and Administrator for the `best-choice` Channel.
 
@@ -62,8 +62,24 @@ For example, switching to the `ace-parts` Channel, and then creating a new Produ
 
 ## The Storefront
 
-Your storefront applications will need to specify which channel they are interested in. This is done by adding a query parameter or header to each API requests, with the key being `vendure-token` and the value being the target Channel's `token` property.
+Your storefront applications will need to specify which channel they are interested in. This is done by adding a **query parameter** or **header** to each API requests, with the key being `vendure-token` and the value being the target Channel's `token` property.
 
 ```text
 https://my-vendure-server.com/shop-api?vendure-token=best-choice
 ```
+
+## Determining the Active Channel
+
+When developing plugins and writing custom server code in general, you'll often want to know the Channel that the current request is using. This can be determined from the [RequestContext.channel]({{< relref "/docs/typescript-api/request/request-context" >}}#channel) property.
+
+```TypeScript
+createPayment: async (ctx, order, amount, args) => {
+  if (ctx.channel.code === 'ace-parts') {
+    // Use the Ace Parts account to process
+    // the payment  
+  } else {
+    // Use the Best Choice account to process
+    // the payment  
+  }
+}
+```

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

@@ -8,7 +8,7 @@ showtoc: true
 Vendure can support many kinds of payment workflows, such as authorizing and capturing payment in a single step upon checkout or authorizing on checkout and then capturing on fulfillment.
 
 {{< alert "primary" >}}
-For a complete working example of a real payment integration, see the [real-world-vendure Braintree plugin](https://github.com/vendure-ecommerce/real-world-vendure/tree/master/src/plugins/braintree)
+For complete working examples of real payment integrations, see the [payments-plugins](https://github.com/vendure-ecommerce/vendure/tree/master/packages/payments-plugin/src)
 {{< /alert >}}
 
 ## Authorization & Settlement

+ 1 - 1
docs/content/plugins/extending-the-admin-ui/_index.md

@@ -19,7 +19,7 @@ A UI extension is an [Angular module](https://angular.io/guide/ngmodules) which
 
 ## Use Your Favourite Framework
 
-The Vendure Admin UI is build with Angular, and writing UI extensions in Angular is seamless and powerful. But if you are not familiar with Angular, that's no problem! You can write UI extensions using **React**, **Vue**, or **any other** web technology of choice!
+The Vendure Admin UI is built with Angular, and writing UI extensions in Angular is seamless and powerful. But if you are not familiar with Angular, that's no problem! You can write UI extensions using **React**, **Vue**, or **any other** web technology of choice!
 
 * [UI extensions in Angular]({{< relref "using-angular" >}})
 * [UI extensions in other frameworks]({{< relref "using-other-frameworks" >}})

+ 0 - 1
docs/content/plugins/extending-the-admin-ui/using-other-frameworks/_index.md

@@ -89,7 +89,6 @@ import path from 'path';
 import { VendureConfig } from '@vendure/core';
 import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
 import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
-import { reactUiExtension } from './ui-extension/ui-extension';
 
 export const config: VendureConfig = {
   // ...

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

@@ -15,7 +15,7 @@
                         ></vdr-status-badge>
                     </ng-container>
                     <input [id]="section.id" type="checkbox" [checked]="section.collapsedByDefault" />
-                    <label [for]="section.id">{{ section.label | translate }}</label>
+                    <label class="nav-group-header" [for]="section.id">{{ section.label | translate }}</label>
                     <ul class="nav-list">
                         <ng-container *ngFor="let item of section.items">
                             <li *ngIf="shouldDisplayLink(item)">

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

@@ -12,7 +12,22 @@ nav.sidenav {
     border-right-color: var(--clr-sidenav-border-color);
 }
 
+.sidenav .nav-group {
+    .nav-list {
+        margin: 0;
+    }
+    .nav-group-header {
+        margin: 0;
+    }
+    .nav-link {
+        display: inline-flex;
+        line-height: 1rem;
+        padding-right: 0.6rem;
+    }
+}
+
 .nav-list clr-icon {
+    flex-shrink: 0;
     margin-right: 12px;
 }
 

+ 1 - 1
packages/core/src/api/schema/admin-api/collection.api.graphql

@@ -1,6 +1,6 @@
 type Query {
     collections(options: CollectionListOptions): CollectionList!
-    "Get a Collection either by id or slug. If neither id nor slug is speicified, an error will result."
+    "Get a Collection either by id or slug. If neither id nor slug is specified, an error will result."
     collection(id: ID, slug: String): Collection
     collectionFilters: [ConfigurableOperationDefinition!]!
 }

+ 2 - 2
packages/core/src/api/schema/shop-api/shop.api.graphql

@@ -5,7 +5,7 @@ type Query {
     activeCustomer: Customer
     """
     The active Order. Will be `null` until an Order is created via `addItemToOrder`. Once an Order reaches the
-    state of `PaymentApproved` or `PaymentSettled`, then that Order is no longer considered "active" and this
+    state of `PaymentAuthorized` or `PaymentSettled`, then that Order is no longer considered "active" and this
     query will once again return `null`.
     """
     activeOrder: Order
@@ -13,7 +13,7 @@ type Query {
     availableCountries: [Country!]!
     "A list of Collections available to the shop"
     collections(options: CollectionListOptions): CollectionList!
-    "Returns a Collection either by its id or slug. If neither 'id' nor 'slug' is speicified, an error will result."
+    "Returns a Collection either by its id or slug. If neither 'id' nor 'slug' is specified, an error will result."
     collection(id: ID, slug: String): Collection
     "Returns a list of eligible shipping methods based on the current active Order"
     eligibleShippingMethods: [ShippingMethodQuote!]!

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

@@ -86,13 +86,13 @@ export type ExtendedListQueryOptions<T extends VendureEntity> = {
  * }
  *
  * # Generated at run-time by Vendure
- * input ProductListOptions
+ * input BlogPostListOptions
  *
  * extend type Query {
  *    blogPosts(options: BlogPostListOptions): BlogPostList!
  * }
  * ```
- * When Vendure bootstraps, it will find the `ProductListOptions` input and, because it is used in a query
+ * When Vendure bootstraps, it will find the `BlogPostListOptions` input and, because it is used in a query
  * returning a `PaginatedList` type, it knows that it should dynamically generate this input. This means
  * all primitive field of the `BlogPost` type (namely, "published", "title" and "body") will have `filter` and
  * `sort` inputs created for them, as well a `skip` and `take` fields for pagination.

+ 24 - 12
packages/email-plugin/src/default-email-handlers.ts

@@ -3,9 +3,12 @@ import {
     AccountRegistrationEvent,
     EntityHydrator,
     IdentifierChangeRequestEvent,
+    Injector,
     NativeAuthenticationMethod,
+    Order,
     OrderStateTransitionEvent,
     PasswordResetEvent,
+    RequestContext,
     ShippingLine,
 } from '@vendure/core';
 
@@ -24,18 +27,8 @@ export const orderConfirmationHandler = new EmailEventListener('order-confirmati
         event =>
             event.toState === 'PaymentSettled' && event.fromState !== 'Modifying' && !!event.order.customer,
     )
-    .loadData(async context => {
-        const shippingLines: ShippingLine[] = [];
-        const entityHydrator = context.injector.get(EntityHydrator);
-
-        for (const line of context.event.order.shippingLines || []) {
-            await entityHydrator.hydrate(context.event.ctx, line, {
-                relations: ['shippingMethod'],
-            });
-            if (line.shippingMethod) {
-                shippingLines.push(line);
-            }
-        }
+    .loadData(async ({ event, injector }) => {
+        const shippingLines = await hydrateShippingLines(event.ctx, event.order, injector);
         return { shippingLines };
     })
     .setRecipient(event => event.order.customer!.emailAddress)
@@ -87,3 +80,22 @@ export const defaultEmailHandlers: Array<EmailEventHandler<any, any>> = [
     passwordResetHandler,
     emailAddressChangeHandler,
 ];
+
+export async function hydrateShippingLines(
+    ctx: RequestContext,
+    order: Order,
+    injector: Injector,
+): Promise<ShippingLine[]> {
+    const shippingLines: ShippingLine[] = [];
+    const entityHydrator = injector.get(EntityHydrator);
+
+    for (const line of order.shippingLines || []) {
+        await entityHydrator.hydrate(ctx, line, {
+            relations: ['shippingMethod'],
+        });
+        if (line.shippingMethod) {
+            shippingLines.push(line);
+        }
+    }
+    return shippingLines;
+}