Kaynağa Gözat

Merge branch 'master' into minor

Michael Bromley 2 yıl önce
ebeveyn
işleme
2aa5d1d343

+ 0 - 1
docs/docs/guides/core-concepts/payment/index.md

@@ -36,7 +36,6 @@ import {
     SettlePaymentResult,
     SettlePaymentErrorResult
 } from '@vendure/core';
-import { CancelPaymentErrorResult } from '@vendure/core/src/index';
 import { sdk } from 'payment-provider-sdk';
 
 /**

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

@@ -22,7 +22,10 @@ now you need a detail view which can be used to view and edit individual reviews
 
 The detail component itself is an Angular component which extends the [BaseDetailComponent](/reference/admin-ui-api/list-detail-views/base-detail-component/) or [TypedBaseDetailComponent](/reference/admin-ui-api/list-detail-views/typed-base-detail-component) class.
 
+This example assumes you have set up your project to use code generation as described in the [GraphQL code generation guide](/guides/how-to/codegen/#codegen-for-admin-ui-extensions).
+
 ```ts title="src/plugins/reviews/ui/components/review-detail/review-detail.component.ts"
+import { ResultOf } from '@graphql-typed-document-node/core';
 import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core';
 import { FormBuilder } from '@angular/forms';
 import { TypedBaseDetailComponent, LanguageCode, SharedModule } from '@vendure/admin-ui/core';
@@ -30,20 +33,42 @@ import { TypedBaseDetailComponent, LanguageCode, SharedModule } from '@vendure/a
 // This is the TypedDocumentNode & type generated by GraphQL Code Generator
 import { graphql } from '../../gql';
 
-export const getReviewDetailDocument = graphql`
+export const reviewDetailFragment = graphql(`
+  fragment ReviewDetail on ProductReview {
+    id
+    createdAt
+    updatedAt
+    title
+    rating
+    text
+    authorName
+    productId
+  }
+`);
+
+export const getReviewDetailDocument = graphql(`
   query GetReviewDetail($id: ID!) {
     review(id: $id) {
-      id
-      createdAt
-      updatedAt
-      title
-      rating
-      text
-      authorName
-      productId
+      ...ReviewDetail
+    }
+  }
+`);
+
+export const createReviewDocument = graphql(`
+  mutation CreateReview($input: CreateProductReviewInput!) {
+    createProductReview(input: $input) {
+      ...ReviewDetail
+    }
+  }
+`);
+
+export const updateReviewDocument = graphql(`
+  mutation UpdateReview($input: UpdateProductReviewInput!) {
+    updateProductReview(input: $input) {
+      ...ReviewDetail
     }
   }
-`;
+`);
 
 @Component({
     selector: 'review-detail',
@@ -60,7 +85,7 @@ export class ReviewDetailComponent extends TypedBaseDetailComponent<typeof getRe
         authorName: [''],
     });
 
-    constructor(private formBuilder: FormBuilder) {
+    constructor(private formBuilder: FormBuilder, private notificationService: NotificationService) {
         super();
     }
 
@@ -73,14 +98,34 @@ export class ReviewDetailComponent extends TypedBaseDetailComponent<typeof getRe
     }
 
     create() {
-        // Logic to save a Review
+        const { title, rating, authorName } = this.detailForm.value;
+        if (!title || rating == null || !authorName) {
+            return;
+        }
+        this.dataService
+            .mutate(createReviewDocument, {
+                input: { title, rating, authorName },
+            })
+            .subscribe(({ createProductReview }) => {
+                if (createProductReview.id) {
+                    this.notificationService.success('Review created');
+                    this.router.navigate(['extensions', 'reviews', createProductReview.id]);
+                }
+            });
     }
 
     update() {
-        // Logic to update a Review
+        const { title, rating, authorName } = this.detailForm.value;
+        this.dataService
+            .mutate(updateOrganizationDocument, {
+                input: { id: this.id, title, rating, authorName },
+            })
+            .subscribe(() => {
+                this.notificationService.success('Review updated');
+            });
     }
 
-    protected setFormValues(entity: NonNullable<(typeof getReviewDetailDocument)['review']>, languageCode: LanguageCode): void {
+    protected setFormValues(entity: NonNullable<ResultOf<typeof getReviewDetailDocument>['review']>, languageCode: LanguageCode): void {
         this.detailForm.patchValue({
             title: entity.name,
             rating: entity.rating,

+ 3 - 3
docs/docs/guides/extending-the-admin-ui/creating-list-views/index.md

@@ -46,7 +46,7 @@ See the [Paginated Lists guide](/guides/how-to/paginated-list/) for details on h
 
 The list component itself is an Angular component which extends the [BaseListComponent](/reference/admin-ui-api/list-detail-views/base-list-component/) or [TypedBaseListComponent](/reference/admin-ui-api/list-detail-views/typed-base-list-component) class.
 
-This example assumes you have set up your project to use [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) with the [TypedDocumentNode plugin](https://the-guild.dev/graphql/codegen/plugins/typescript/typed-document-node).
+This example assumes you have set up your project to use code generation as described in the [GraphQL code generation guide](/guides/how-to/codegen/#codegen-for-admin-ui-extensions).
 
 ```ts title="src/plugins/reviews/ui/components/review-list/review-list.component.ts"
 import { ChangeDetectionStrategy, Component } from '@angular/core';
@@ -54,7 +54,7 @@ import { TypedBaseListComponent, SharedModule } from '@vendure/admin-ui/core';
 // This is the TypedDocumentNode generated by GraphQL Code Generator
 import { graphql } from '../../gql';
 
-const getReviewListDocument = graphql`
+const getReviewListDocument = graphql(`
   query GetReviewList($options: ReviewListOptions) {
     reviews(options: $options) {
       items {
@@ -70,7 +70,7 @@ const getReviewListDocument = graphql`
       totalItems
     }
   }
-`;
+`);
 
 @Component({
     selector: 'review-list',

+ 264 - 0
docs/docs/guides/how-to/codegen/index.md

@@ -0,0 +1,264 @@
+---
+title: "GraphQL Code Generation"
+---
+
+Code generation means the automatic generation of TypeScript types based on your GraphQL schema and your GraphQL operations.
+This is a very powerful feature that allows you to write your code in a type-safe manner, without you needing to manually
+write any types for your API calls.
+
+To do this, we will use [Graphql Code Generator](https://the-guild.dev/graphql/codegen).
+
+:::note
+This guide is for adding codegen to your Vendure plugins & Admin UI extensions. For a guide on adding codegen to your storefront, see the [Storefront Codegen](/guides/storefront/codegen/) guide.
+:::
+
+## Installation
+
+First, install the required dependencies:
+
+```bash
+npm install -D @graphql-codegen/cli @graphql-codegen/typescript
+```
+
+## Configuration
+
+Add a `codegen.ts` file to your project root with the following contents:
+
+```ts title="codegen.ts"
+import type {CodegenConfig} from '@graphql-codegen/cli';
+
+const config: CodegenConfig = {
+    overwrite: true,
+    // This assumes your server is running on the standard port
+    // and with the default admin API path. Adjust accordingly.
+    schema: 'http://localhost:3000/admin-api',
+    config: {
+        // This tells codegen that the `Money` scalar is a number
+        scalars: { Money: 'number' },
+        // This ensures generated enums do not conflict with the built-in types.
+        namingConvention: { enumValues: 'keep' },
+    },
+    generates: {
+        // The path to the generated type file in your
+        // plugin directory. Adjust accordingly.
+        'src/plugins/organization/gql/generated.ts': {
+            plugins: ['typescript'],
+        },
+    },
+};
+
+export default config;
+```
+
+This assumes that we have an "organization" plugin which adds support for grouping customers into organizations, e.g. for B2B use-cases.
+
+## Running codegen
+
+You can now add a script to your package.json to run codegen:
+
+```json title="package.json"
+{
+  "scripts": {
+    "codegen": "graphql-codegen --config codegen.ts"
+  }
+}
+```
+
+**Ensure your server is running**, then run the codegen script:
+
+```bash
+npm run codegen
+```
+
+This will generate a file at `src/plugins/organization/gql/generated.ts` which contains all the GraphQL types corresponding to your schema.
+
+## Using generated types in resolvers & services
+
+You would then use these types in your resolvers and service methods, for example:
+
+```ts title="src/plugins/organization/services/organization.service.ts"
+import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+import { Allow, Ctx, PaginatedList, RequestContext, Transaction } from '@vendure/core';
+
+import { organizationPermission } from '../constants';
+import { Organization } from '../entities/organization.entity';
+import { OrganizationService } from '../services/organization.service';
+// highlight-next-line
+import { QueryOrganizationArgs, MutationCreateOrganizationArgs } from '../gql/generated';
+
+@Resolver()
+export class AdminResolver {
+    constructor(private organizationService: OrganizationService) {}
+
+    @Query()
+    @Allow(organizationPermission.Read)
+    // highlight-next-line
+    organization(@Ctx() ctx: RequestContext, @Args() args: QueryOrganizationArgs): Promise<Organization> {
+        return this.organizationService.findOne(ctx, args.id);
+    }
+    
+    @Transaction()
+    @Mutation()
+    @Allow(organizationPermission.Create)
+    createOrganization(
+        @Ctx() ctx: RequestContext,
+        // highlight-next-line
+        @Args() args: MutationCreateOrganizationArgs,
+    ): Promise<Organization> {
+        return this.organizationService.create(ctx, args.input);
+    }
+
+    // ... etc
+}
+```
+
+In your service methods you can directly use any input types defined in your schema:
+
+```ts title="src/plugins/organization/services/organization.service.ts"
+import { Injectable } from '@nestjs/common';
+import { RequestContext, TransactionalConnection } from '@vendure/core';
+
+import { Organization } from '../entities/organization.entity';
+// highlight-next-line
+import { CreateOrganizationInput, UpdateOrganizationInput } from "../gql/generated";
+
+@Injectable()
+export class OrganizationService {
+    constructor(private connection: TransactionalConnection) {}
+
+    // highlight-next-line
+    async create(ctx: RequestContext, input: CreateOrganizationInput): Promise<Organization> {
+        return this.connection.getRepository(ctx, Organization).save(new Organization(input));
+    }
+
+    // highlight-next-line
+    async update(ctx: RequestContext, input: UpdateOrganizationInput): Promise<Organization> {
+        const example = await this.connection.getEntityOrThrow(ctx, Organization, input.id);
+        const updated = {...example, ...input};
+        return this.connection.getRepository(ctx, Organization).save(updated);
+    }
+}
+```
+
+## Codegen for Admin UI extensions
+
+When you create Admin UI extensions, very often those UI components will be making API calls to the Admin API. In this case, you can use codegen to generate the types for those API calls.
+
+To do this, we will use the ["client preset" plugin](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client). Assuming you have already completed the setup above, you'll need to install the following additional dependency:
+
+```bash
+npm install -D @graphql-codegen/client-preset
+```
+
+Then add the following to your `codegen.ts` file:
+
+```ts title="codegen.ts"
+import type { CodegenConfig } from '@graphql-codegen/cli';
+
+const config: CodegenConfig = {
+    overwrite: true,
+    schema: 'http://localhost:3000/admin-api',
+    config: {
+        scalars: { Money: 'number' },
+        namingConvention: { enumValues: 'keep' },
+    },
+    generates: {
+        // highlight-start
+        'apps/marketplace/src/plugins/marketplace/ui/gql/': {
+            preset: 'client',
+            documents: 'apps/marketplace/src/plugins/marketplace/ui/**/*.ts',
+            // This disables the "fragment masking" feature. Fragment masking
+            // can improve component isolation but introduces some additional
+            // complexity that we will avoid for now.
+            presetConfig: {
+                fragmentMasking: false,
+            },
+        },
+        // highlight-end
+        'apps/marketplace/src/plugins/marketplace/gql/generated.ts': {
+            plugins: ['typescript'],
+        },
+    },
+};
+
+export default config;
+```
+
+For the client preset plugin, we need to specify a _directory_ (`.../ui/gql/`) because a number of files will get generated.
+
+### Use the `graphql()` function
+
+In your Admin UI components, you can now use the `graphql()` function exported from the generated file to define your
+GraphQL operations. For example:
+
+```ts title="apps/marketplace/src/plugins/marketplace/ui/components/organization-list/organization-list.component.ts"
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { SharedModule, TypedBaseListComponent } from '@vendure/admin-ui/core';
+import { graphql } from '../../gql';
+
+// highlight-start
+const getOrganizationListDocument = graphql(`
+    query GetOrganizationList($options: OrganizationListOptions) {
+        organizations(options: $options) {
+            items {
+                id
+                createdAt
+                updatedAt
+                name
+                invoiceEmailAddresses
+            }
+            totalItems
+        }
+    }
+`);
+// highlight-end
+
+@Component({
+    selector: 'organization-list',
+    templateUrl: './organization-list.component.html',
+    styleUrls: ['./organization-list.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+    standalone: true,
+    imports: [SharedModule],
+})
+export class OrganizationListComponent extends TypedBaseListComponent<
+    // highlight-start
+    typeof getOrganizationListDocument,
+    'organizations'
+    // highlight-end
+> {
+    
+    // Sort & filter definitions omitted for brevity.
+    // For a complete ListComponent example, see the 
+    // "Creating List Views" guide.
+
+    constructor() {
+        super();
+        super.configure({
+            // highlight-next-line
+            document: getOrganizationListDocument,
+            getItems: (data) => data.organizations,
+            setVariables: (skip, take) => ({
+                options: {
+                    skip,
+                    take,
+                    filter: {
+                        name: {
+                            contains: this.searchTermControl.value,
+                        },
+                        ...this.filters.createFilterInput(),
+                    },
+                    sort: this.sorts.createSortInput(),
+                },
+            }),
+            refreshListOnChanges: [this.filters.valueChanges, this.sorts.valueChanges],
+        });
+    }
+}
+```
+
+Whenever you write a new GraphQL operation, or change an existing one, you will need to re-run the codegen script to generate the types for that operation.
+
+## Codegen watch mode
+
+You can also set up file watching as described in the [Graphql Code Generator watch mode docs](https://the-guild.dev/graphql/codegen/docs/getting-started/development-workflow#watch-mode).

+ 7 - 3
docs/docs/guides/storefront/codegen/index.mdx

@@ -1,5 +1,5 @@
 ---
-title: "GraphQL Code Generation"
+title: "Storefront GraphQL Code Generation"
 ---
 
 Code generation means the automatic generation of TypeScript types based on your GraphQL schema and your GraphQL operations.
@@ -9,7 +9,7 @@ write any types for your API calls.
 To do this, we will use [Graphql Code Generator](https://the-guild.dev/graphql/codegen).
 
 :::note
-Code generation can be used when building a storefront _as well as_ when building backend plugins.
+This guide is for adding codegen to your storefront. For a guide on adding codegen to your backend Vendure plugins or UI extensions, see the [Plugin Codegen](/guides/how-to/codegen/) guide.
 :::
 
 ## Installation
@@ -54,7 +54,11 @@ const config: CodegenConfig = {
         scalars: {
             // This tells codegen that the `Money` scalar is a number
             Money: 'number',
-        }
+        },
+        namingConvention: {
+            // This ensures generated enums do not conflict with the built-in types.
+            enumValues: 'keep',
+        },
       }
       // highlight-end
     },

+ 5 - 0
docs/docs/guides/storefront/connect-api/index.mdx

@@ -109,6 +109,11 @@ export async function request(query: string, variables: any) {
 
 There are some concrete examples of this approach in the examples later on in this guide.
 
+### Session duration
+
+The duration of a session is determined by the [AuthOptions.sessionDuration](/reference/typescript-api/auth/auth-options#sessionduration) config
+property. Sessions will automatically extend (or "refresh") when a user interacts with the API, so in effect the `sessionDuration` signifies the
+length of time that a session will stay valid since the last API call.
 
 ## Specifying a channel
 

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/create-product-variant-dialog/create-product-variant-dialog.component.ts

@@ -30,7 +30,7 @@ export class CreateProductVariantDialogComponent implements Dialog<CreateProduct
     constructor(private formBuilder: FormBuilder) {}
 
     ngOnInit() {
-        this.currencyCode = this.product.variants[0].currencyCode;
+        this.currencyCode = this.product.variants[0]?.currencyCode;
         for (const optionGroup of this.product.optionGroups) {
             (this.form.get('options') as FormRecord).addControl(
                 optionGroup.code,

+ 5 - 1
packages/admin-ui/src/lib/core/src/extension/add-nav-menu-item.ts

@@ -12,6 +12,8 @@ import { NavBuilderService } from '../providers/nav-builder/nav-builder.service'
  *
  * @example
  * ```ts title="providers.ts"
+ * import { addNavMenuSection } from '\@vendure/admin-ui/core';
+ *
  * export default [
  *     addNavMenuSection({
  *         id: 'reports',
@@ -48,6 +50,8 @@ export function addNavMenuSection(config: NavMenuSection, before?: string): Prov
  *
  * @example
  * ```ts title="providers.ts"
+ * import { addNavMenuItem } from '\@vendure/admin-ui/core';
+ *
  * export default [
  *     addNavMenuItem({
  *         id: 'reviews',
@@ -57,7 +61,7 @@ export function addNavMenuSection(config: NavMenuSection, before?: string): Prov
  *     },
  *     'marketing'),
  * ];
- * ``
+ * ```
  *
  * @docsCategory nav-menu
  */

+ 1 - 1
packages/admin-ui/src/lib/marketing/src/components/promotion-list/promotion-list.component.html

@@ -8,7 +8,7 @@
                ></vdr-language-selector>
         </vdr-ab-left>
         <vdr-ab-right>
-            <vdr-action-bar-items locationId="customer-list"></vdr-action-bar-items>
+            <vdr-action-bar-items locationId="promotion-list"></vdr-action-bar-items>
             <a class="btn btn-primary" [routerLink]="['./create']" *vdrIfPermissions="'CreatePromotion'">
                 <clr-icon shape="plus"></clr-icon>
                 {{ 'marketing.create-new-promotion' | translate }}

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

@@ -923,8 +923,8 @@ export class OrderService {
             });
         }
         const updatedOrder = await this.getOrderOrThrow(ctx, orderId);
-        await this.applyPriceAdjustments(ctx, order);
-        return this.connection.getRepository(ctx, Order).save(order);
+        await this.applyPriceAdjustments(ctx, updatedOrder);
+        return this.connection.getRepository(ctx, Order).save(updatedOrder);
     }
 
     /**

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

@@ -26,7 +26,7 @@
     "peerDependencies": {
         "@mollie/api-client": "3.x",
         "braintree": "3.x",
-        "stripe": "8.x"
+        "stripe": "13.x"
     },
     "dependencies": {
         "currency.js": "2.0.4"

+ 1 - 0
packages/testing/src/test-server.ts

@@ -115,6 +115,7 @@ export class TestServer {
             const app = await NestFactory.create(appModule.AppModule, {
                 cors: config.apiOptions.cors,
                 logger: new Logger(),
+                abortOnError: false,
             });
             const { tokenMethod } = config.authOptions;
             const usingCookie =