Przeglądaj źródła

docs: Add defining routes guide

Michael Bromley 2 lat temu
rodzic
commit
041ea842a7

+ 122 - 165
docs/docs/guides/extending-the-admin-ui/creating-detail-views/index.md

@@ -1,6 +1,5 @@
 ---
 title: 'Creating Detail Views'
-weight: 2
 ---
 
 # Creating Detail Views
@@ -9,24 +8,29 @@ The two most common type of components you'll be creating in your UI extensions
 
 In Vendure, we have standardized the way you write these components so that your ui extensions can be made to fit seamlessly into the rest of the app.
 
+:::note
+The specific pattern described here is for Angular-based components. It is also possible to create detail views using React components, but 
+in that case you won't be able to use the built-in Angular-specific components.
+:::
+
 ## Example: Creating a Product Detail View
 
-Let's say you have a plugin which adds a new entity to the database called `ProductReview`. You want to create a new list view in the Admin UI which displays all the reviews submitted.
+Let's say you have a plugin which adds a new entity to the database called `ProductReview`. You have already created a [list view](/guides/extending-the-admin-ui/creating-list-views/), and
+now you need a detail view which can be used to view and edit individual reviews.
 
 ### Extend the TypedBaseDetailComponent class
 
 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.
 
-```ts
+```ts title="src/plugins/reviews/ui/components/review-detail/review-detail.component.ts"
 import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core';
 import { FormBuilder } from '@angular/forms';
-import { TypedBaseDetailComponent, LanguageCode } from '@vendure/admin-ui/core';
-import { gql } from 'apollo-angular';
+import { TypedBaseDetailComponent, LanguageCode, SharedModule } from '@vendure/admin-ui/core';
 
 // This is the TypedDocumentNode & type generated by GraphQL Code Generator
-import { GetReviewDetailDocument, GetReviewDetailQuery } from './generated-types';
+import { graphql } from '../../gql';
 
-export const GET_REVIEW_DETAIL = gql`
+export const getReviewDetailDocument = graphql`
   query GetReviewDetail($id: ID!) {
     review(id: $id) {
       id
@@ -42,46 +46,48 @@ export const GET_REVIEW_DETAIL = gql`
 `;
 
 @Component({
-  selector: 'review-detail',
-  templateUrl: './review-detail.component.html',
-  styleUrls: ['./review-detail.component.scss'],
-  changeDetection: ChangeDetectionStrategy.OnPush,
+    selector: 'review-detail',
+    templateUrl: './review-detail.component.html',
+    styleUrls: ['./review-detail.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+    standalone: true,
+    imports: [SharedModule],
 })
-export class ReviewDetailComponent extends TypedBaseDetailComponent<typeof GetReviewDetailDocument, 'review'> implements OnInit, OnDestroy {
-  detailForm = this.formBuilder.group({
-    title: [''],
-    rating: [1],
-    authorName: [''],
-  });
-
-  constructor(private formBuilder: FormBuilder) {
-    super();
-  }
+export class ReviewDetailComponent extends TypedBaseDetailComponent<typeof getReviewDetailDocument, 'review'> implements OnInit, OnDestroy {
+    detailForm = this.formBuilder.group({
+        title: [''],
+        rating: [1],
+        authorName: [''],
+    });
 
-  ngOnInit() {
-    this.init();
-  }
+    constructor(private formBuilder: FormBuilder) {
+        super();
+    }
 
-  ngOnDestroy() {
-    this.destroy();
-  }
+    ngOnInit() {
+        this.init();
+    }
 
-  create() {
-    // Logic to save a Review
-  }
+    ngOnDestroy() {
+        this.destroy();
+    }
 
-  update() {
-    // Logic to update a Review
-  }
+    create() {
+        // Logic to save a Review
+    }
 
-  protected setFormValues(entity: NonNullable<GetReviewDetailQuery['review']>, languageCode: LanguageCode): void {
-    this.detailForm.patchValue({
-      title: entity.name,
-      rating: entity.rating,
-      authorName: entity.authorName,
-      productId: entity.productId,
-    });
-  }
+    update() {
+        // Logic to update a Review
+    }
+
+    protected setFormValues(entity: NonNullable<(typeof getReviewDetailDocument)['review']>, languageCode: LanguageCode): void {
+        this.detailForm.patchValue({
+            title: entity.name,
+            rating: entity.rating,
+            authorName: entity.authorName,
+            productId: entity.productId,
+        });
+    }
 }
 ```
 
@@ -90,146 +96,97 @@ export class ReviewDetailComponent extends TypedBaseDetailComponent<typeof GetRe
 Here is the standard layout for detail views:
 
 ```html
-<vdr-page-header>
-  <vdr-page-title></vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
-  <vdr-page-block>
+<vdr-page-block>
     <vdr-action-bar>
-      <vdr-ab-left></vdr-ab-left>
-      <vdr-ab-right>
-        <button
-          class="button primary"
-          *ngIf="isNew$ | async; else updateButton"
-          (click)="create()"
-          [disabled]="detailForm.pristine || detailForm.invalid"
-        >
-          {{ 'common.create' | translate }}
-        </button>
-        <ng-template #updateButton>
-          <button
-            class="btn btn-primary"
-            (click)="update()"
-            [disabled]="detailForm.pristine || detailForm.invalid"
-          >
-            {{ 'common.update' | translate }}
-          </button>
-        </ng-template>
-      </vdr-ab-right>
+        <vdr-ab-left></vdr-ab-left>
+        <vdr-ab-right>
+            <button
+                class="button primary"
+                *ngIf="isNew$ | async; else updateButton"
+                (click)="create()"
+                [disabled]="detailForm.pristine || detailForm.invalid"
+            >
+                {{ 'common.create' | translate }}
+            </button>
+            <ng-template #updateButton>
+                <button
+                    class="btn btn-primary"
+                    (click)="update()"
+                    [disabled]="detailForm.pristine || detailForm.invalid"
+                >
+                    {{ 'common.update' | translate }}
+                </button>
+            </ng-template>
+        </vdr-ab-right>
     </vdr-action-bar>
-  </vdr-page-block>
+</vdr-page-block>
 
-  <form class="form" [formGroup]="detailForm">
+<form class="form" [formGroup]="detailForm">
     <vdr-page-detail-layout>
-      <!-- The sidebar is used for displaying "metadata" type information about the entity -->
-      <vdr-page-detail-sidebar>
-        <vdr-card *ngIf="entity$ | async as entity">
-          <vdr-page-entity-info [entity]="entity" />
-        </vdr-card>
-      </vdr-page-detail-sidebar>
-
-      <!-- The main content area is used for displaying the entity's fields -->
-      <vdr-page-block>
-        <!-- The vdr-card is the container for grouping items together on a page -->
-        <!-- it can also take an optional [title] property to display a title -->
-        <vdr-card>
-          <!-- the form-grid class is used to lay out the form fields -->
-          <div class="form-grid">
-            <vdr-form-field label="Title" for="title">
-              <input id="title" type="text" formControlName="title" />
-            </vdr-form-field>
-            <vdr-form-field label="Rating" for="rating">
-              <input id="rating" type="number" min="1" max="5" formControlName="rating" />
-            </vdr-form-field>
-
-            <!-- etc -->
-          </div>
-        </vdr-card>
-      </vdr-page-block>
+        <!-- The sidebar is used for displaying "metadata" type information about the entity -->
+        <vdr-page-detail-sidebar>
+            <vdr-card *ngIf="entity$ | async as entity">
+                <vdr-page-entity-info [entity]="entity" />
+            </vdr-card>
+        </vdr-page-detail-sidebar>
+
+        <!-- The main content area is used for displaying the entity's fields -->
+        <vdr-page-block>
+            <!-- The vdr-card is the container for grouping items together on a page -->
+            <!-- it can also take an optional [title] property to display a title -->
+            <vdr-card>
+                <!-- the form-grid class is used to lay out the form fields -->
+                <div class="form-grid">
+                    <vdr-form-field label="Title" for="title">
+                        <input id="title" type="text" formControlName="title" />
+                    </vdr-form-field>
+                    <vdr-form-field label="Rating" for="rating">
+                        <input id="rating" type="number" min="1" max="5" formControlName="rating" />
+                    </vdr-form-field>
+
+                    <!-- etc -->
+                </div>
+            </vdr-card>
+        </vdr-page-block>
     </vdr-page-detail-layout>
-  </form>
-</vdr-page-body>
+</form>
 ```
 
 ### Route config
 
-The `TypedBaseDetailComponent` expects that the entity detail data is resolved as part of loading the route. The data needs to be loaded in a very specific object shape:
-
-```ts
-interface DetailResolveData {
-    detail: {
-        entity: Observable<Entity>;
-    };
-}
-```
-
 Here's how the routing would look for a typical list & detail view:
 
-```ts
-import { inject, NgModule } from '@angular/core';
-import { RouterModule } from '@angular/router';
-import { DataService, SharedModule } from '@vendure/admin-ui/core';
-import { Observable, of } from "rxjs";
-import { map } from 'rxjs/operators';
+```ts title="src/plugins/reviews/ui/routes.ts"
+import { registerRouteComponent } from '@vendure/admin-ui/core';
 
-import { ReviewDetailComponent } from './components/review-detail/review-detail.component';
+import { ReviewDetailComponent, getReviewDetailDocument } from './components/review-detail/review-detail.component';
 import { ReviewListComponent } from './components/review-list/review-list.component';
-import { GetReviewDocument, GetReviewDetailQuery } from './generated-types';
-
-@NgModule({
-  imports: [
-    SharedModule,
-    RouterModule.forChild([
-      // This defines the route for the list view  
-      {
+
+export default [
+    // List view
+    registerRouteComponent({
         path: '',
-        pathMatch: 'full',
         component: ReviewListComponent,
-        data: {
-          breadcrumb: [
-            {
-              label: 'Reviews',
-              link: [],
-            },
-          ],
-        },
-      },
-        
-      // This defines the route for the detail view  
-      {
+        breadcrumb: 'Product reviews',
+    }),
+    // highlight-start
+    // Detail view
+    registerRouteComponent({
         path: ':id',
         component: ReviewDetailComponent,
-        resolve: {
-          detail: route => {
-            // Here we are using the DataService to load the detail data
-            // from the API. The `GetReviewDocument` is a generated GraphQL
-            // TypedDocumentNode.  
-            const review$ = inject(DataService)
-              .query(GetReviewDocument, { id: route.paramMap.get('id') })
-              .mapStream(data => data.review);
-            return of({ entity: review$ });
-          },
-        },
-        data: {
-          breadcrumb: (
-            data: { detail: { entity: Observable<NonNullable<GetReviewDetailQuery['review']>> } },
-          ) => data.detail.entity.pipe(
-            map((entity) => [
-              {
-                label: 'Reviews',
-                link: ['/extensions', 'reviews'],
-              },
-              {
-                label: `${entity?.title ?? 'New Review'}`,
+        query: getReviewDetailDocument,
+        entityKey: 'productReview',
+        getBreadcrumbs: entity => [
+            {
+                label: 'Product reviews',
+                link: ['/extensions', 'product-reviews'],
+            },
+            {
+                label: `#${entity?.id} (${entity?.product.name})`,
                 link: [],
-              },
-            ]),
-          ),
-        },
-      },
-    ]),
-  ],
-  declarations: [ReviewListComponent, ReviewDetailComponent],
-})
-export class ReviewsUiLazyModule {}
+            },
+        ],
+    }),
+    // highlight-end
+]
 ```

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

@@ -1,14 +1,16 @@
 ---
 title: 'Creating List Views'
-weight: 2
 ---
 
-# Creating List Views
-
 The two most common type of components you'll be creating in your UI extensions are list components and detail components.
 
 In Vendure, we have standardized the way you write these components so that your ui extensions can be made to fit seamlessly into the rest of the app.
 
+:::note
+The specific pattern described here is for Angular-based components. It is also possible to create list views using React components, but 
+in that case you won't be able to use the built-in data table & other Angular-specific components.
+:::
+
 ## Example: Creating a Product Reviews List
 
 Let's say you have a plugin which adds a new entity to the database called `ProductReview`. You want to create a new list view in the Admin UI which displays all the reviews submitted.
@@ -36,7 +38,9 @@ type ProductReviewList implements PaginatedList {
 }
 ```
 
-See the [ListQueryBuilder docs](/reference/typescript-api/data-access/list-query-builder/) for more information on how to implement this in your server plugin code.
+:::info
+See the [Paginated Lists guide](/guides/how-to/paginated-list/) for details on how to implement this in your server plugin code.
+:::
 
 ### Create the list component
 
@@ -44,15 +48,13 @@ The list component itself is an Angular component which extends the [BaseListCom
 
 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).
 
-```ts
+```ts title="src/plugins/reviews/ui/components/review-list/review-list.component.ts"
 import { ChangeDetectionStrategy, Component } from '@angular/core';
-import { TypedBaseListComponent } from '@vendure/admin-ui/core';
-import { gql } from 'apollo-angular';
-
+import { TypedBaseListComponent, SharedModule } from '@vendure/admin-ui/core';
 // This is the TypedDocumentNode generated by GraphQL Code Generator
-import { GetReviewListDocument } from './generated-types';
+import { graphql } from '../../gql';
 
-const GET_REVIEW_LIST = gql`
+const getReviewListDocument = graphql`
   query GetReviewList($options: ReviewListOptions) {
     reviews(options: $options) {
       items {
@@ -71,69 +73,72 @@ const GET_REVIEW_LIST = gql`
 `;
 
 @Component({
-  selector: 'review-list',
-  templateUrl: './review-list.component.html',
-  styleUrls: ['./review-list.component.scss'],
-  changeDetection: ChangeDetectionStrategy.OnPush,
+    selector: 'review-list',
+    templateUrl: './review-list.component.html',
+    styleUrls: ['./review-list.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+    standalone: true,
+    imports: [SharedModule],
 })
-export class ReviewListComponent extends TypedBaseListComponent<typeof GetReviewListDocument, 'reviews'> {
-  
-  // Here we set up the filters that will be available
-  // to use in the data table
-  readonly filters = this.createFilterCollection()
-    .addDateFilters()
-    .addFilter({
-      name: 'title',
-      type: { kind: 'text' },
-      label: 'Title',
-      filterField: 'title',
-    })
-    .addFilter({
-      name: 'rating',
-      type: { kind: 'number' },
-      label: 'Rating',
-      filterField: 'rating',
-    })
-    .addFilter({
-      name: 'authorName',
-      type: { kind: 'text' },
-      label: 'Author',
-      filterField: 'authorName',
-    })
-    .connectToRoute(this.route);
-
-  // Here we set up the sorting options that will be available
-  // to use in the data table
-  readonly sorts = this.createSortCollection()
-    .defaultSort('createdAt', 'DESC')
-    .addSort({ name: 'createdAt' })
-    .addSort({ name: 'updatedAt' })
-    .addSort({ name: 'title' })
-    .addSort({ name: 'rating' })
-    .addSort({ name: 'authorName' })
-    .connectToRoute(this.route);
-
-  constructor() {
-    super();
-    super.configure({
-      document: GetReviewListDocument,
-      getItems: data => data.reviews,
-      setVariables: (skip, take) => ({
-        options: {
-          skip,
-          take,
-          filter: {
-            title: {
-              contains: this.searchTermControl.value,
-            },
-            ...this.filters.createFilterInput(),
-          },
-          sort: this.sorts.createSortInput(),
-        },
-      }),
-      refreshListOnChanges: [this.filters.valueChanges, this.sorts.valueChanges],
-    });
-  }
+export class ReviewListComponent extends TypedBaseListComponent<typeof getReviewListDocument, 'reviews'> {
+
+    // Here we set up the filters that will be available
+    // to use in the data table
+    readonly filters = this.createFilterCollection()
+        .addIdFilter()
+        .addDateFilters()
+        .addFilter({
+            name: 'title',
+            type: {kind: 'text'},
+            label: 'Title',
+            filterField: 'title',
+        })
+        .addFilter({
+            name: 'rating',
+            type: {kind: 'number'},
+            label: 'Rating',
+            filterField: 'rating',
+        })
+        .addFilter({
+            name: 'authorName',
+            type: {kind: 'text'},
+            label: 'Author',
+            filterField: 'authorName',
+        })
+        .connectToRoute(this.route);
+
+    // Here we set up the sorting options that will be available
+    // to use in the data table
+    readonly sorts = this.createSortCollection()
+        .defaultSort('createdAt', 'DESC')
+        .addSort({name: 'createdAt'})
+        .addSort({name: 'updatedAt'})
+        .addSort({name: 'title'})
+        .addSort({name: 'rating'})
+        .addSort({name: 'authorName'})
+        .connectToRoute(this.route);
+
+    constructor() {
+        super();
+        super.configure({
+            document: getReviewListDocument,
+            getItems: data => data.reviews,
+            setVariables: (skip, take) => ({
+                options: {
+                    skip,
+                    take,
+                    filter: {
+                        title: {
+                            contains: this.searchTermControl.value,
+                        },
+                        ...this.filters.createFilterInput(),
+                    },
+                    sort: this.sorts.createSortInput(),
+                },
+            }),
+            refreshListOnChanges: [this.filters.valueChanges, this.sorts.valueChanges],
+        });
+    }
 }
 ```
 
@@ -141,95 +146,99 @@ export class ReviewListComponent extends TypedBaseListComponent<typeof GetReview
 
 This is the standard layout for any list view. The main functionality is provided by the [DataTable2Component](/reference/admin-ui-api/components/data-table2component/).
 
-```html
-<vdr-page-header>
-  <vdr-page-title></vdr-page-title>
-</vdr-page-header>
-<vdr-page-body>
-
-  <!-- optional if you want some button at the top -->
-  <vdr-page-block>
+```html title="src/plugins/reviews/ui/components/review-list/review-list.component.html"
+<!-- optional if you want some buttons at the top -->
+<vdr-page-block>
     <vdr-action-bar>
-      <vdr-ab-left></vdr-ab-left>
-      <vdr-ab-right>
-        <a
-            class="btn btn-primary"
-            *vdrIfPermissions="['CreateReview']"
-            [routerLink]="['./', 'create']"
-        >
-          <clr-icon shape="plus"></clr-icon>
-          Create a review
-        </a>
-      </vdr-ab-right>
+        <vdr-ab-left></vdr-ab-left>
+        <vdr-ab-right>
+            <a class="btn btn-primary" *vdrIfPermissions="['CreateReview']" [routerLink]="['./', 'create']">
+                <clr-icon shape="plus"></clr-icon>
+                Create a review
+            </a>
+        </vdr-ab-right>
     </vdr-action-bar>
-  </vdr-page-block>
-  
-  <vdr-data-table-2
-      id="review-list"
-      [items]="items$ | async"
-      [itemsPerPage]="itemsPerPage$ | async"
-      [totalItems]="totalItems$ | async"
-      [currentPage]="currentPage$ | async"
-      [filters]="filters"
-      (pageChange)="setPageNumber($event)"
-      (itemsPerPageChange)="setItemsPerPage($event)"
-  >
+</vdr-page-block>
+
+<!-- The data table -->
+<vdr-data-table-2
+        id="review-list"
+        [items]="items$ | async"
+        [itemsPerPage]="itemsPerPage$ | async"
+        [totalItems]="totalItems$ | async"
+        [currentPage]="currentPage$ | async"
+        [filters]="filters"
+        (pageChange)="setPageNumber($event)"
+        (itemsPerPageChange)="setItemsPerPage($event)"
+>
     <!-- optional if you want to support bulk actions -->
     <vdr-bulk-action-menu
-        locationId="review-list"
-        [hostComponent]="this"
-        [selectionManager]="selectionManager"
+            locationId="review-list"
+            [hostComponent]="this"
+            [selectionManager]="selectionManager"
     />
     
     <!-- Adds a search bar -->
     <vdr-dt2-search
-        [searchTermControl]="searchTermControl"
-        searchTermPlaceholder="Filter by title"
+            [searchTermControl]="searchTermControl"
+            searchTermPlaceholder="Filter by title"
     />
     
     <!-- Here we define all the available columns -->
     <vdr-dt2-column [heading]="'common.id' | translate" [hiddenByDefault]="true">
-      <ng-template let-review="item">
-        {{ review.id }}
-      </ng-template>
+        <ng-template let-review="item">
+            {{ review.id }}
+        </ng-template>
     </vdr-dt2-column>
     <vdr-dt2-column
-        [heading]="'common.created-at' | translate"
-        [hiddenByDefault]="true"
-        [sort]="sorts.get('createdAt')"
+            [heading]="'common.created-at' | translate"
+            [hiddenByDefault]="true"
+            [sort]="sorts.get('createdAt')"
     >
-      <ng-template let-review="item">
-        {{ review.createdAt | localeDate : 'short' }}
-      </ng-template>
+        <ng-template let-review="item">
+            {{ review.createdAt | localeDate : 'short' }}
+        </ng-template>
     </vdr-dt2-column>
     <vdr-dt2-column
-        [heading]="'common.updated-at' | translate"
-        [hiddenByDefault]="true"
-        [sort]="sorts.get('updatedAt')"
+            [heading]="'common.updated-at' | translate"
+            [hiddenByDefault]="true"
+            [sort]="sorts.get('updatedAt')"
     >
-      <ng-template let-review="item">
-        {{ review.updatedAt | localeDate : 'short' }}
-      </ng-template>
+        <ng-template let-review="item">
+            {{ review.updatedAt | localeDate : 'short' }}
+        </ng-template>
     </vdr-dt2-column>
     <vdr-dt2-column heading="Title" [optional]="false" [sort]="sorts.get('title')">
-      <ng-template let-review="item">
-        <a class="button-ghost" [routerLink]="['./', review.id]"
-        ><span>{{ review.title }}</span>
-          <clr-icon shape="arrow right"></clr-icon>
-        </a>
-      </ng-template>
+        <ng-template let-review="item">
+            <a class="button-ghost" [routerLink]="['./', review.id]"
+            ><span>{{ review.title }}</span>
+                <clr-icon shape="arrow right"></clr-icon>
+            </a>
+        </ng-template>
     </vdr-dt2-column>
     <vdr-dt2-column heading="Rating" [sort]="sorts.get('rating')">
-      <ng-template let-review="item"><my-star-rating-component [rating]="review.rating"  /></ng-template>
+        <ng-template let-review="item"><my-star-rating-component [rating]="review.rating"    /></ng-template>
     </vdr-dt2-column>
     <vdr-dt2-column heading="Author" [sort]="sorts.get('authorName')">
-      <ng-template let-review="item">{{ review.authorName }}</ng-template>
+        <ng-template let-review="item">{{ review.authorName }}</ng-template>
     </vdr-dt2-column>
-  </vdr-data-table-2>
-
-</vdr-page-body>
+</vdr-data-table-2>
 ```
 
 ### Route config
 
-For an example of how the route config would look for this list view component, see the full example in the [Creating detail views guide](/guides/extending-the-admin-ui/creating-detail-views/#route-config).
+```ts title="src/plugins/reviews/ui/routes.ts"
+import { registerRouteComponent } from '@vendure/admin-ui/core';
+
+import { ReviewListComponent } from './components/review-list/review-list.component';
+
+export default [
+    // highlight-start
+    registerRouteComponent({
+        path: '',
+        component: ReviewListComponent,
+        breadcrumb: 'Product reviews',
+    }),
+    // highlight-end
+]
+```

+ 598 - 0
docs/docs/guides/extending-the-admin-ui/defining-routes/index.md

@@ -0,0 +1,598 @@
+---
+title: 'Defining routes'
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+Routes allow you to mount entirely custom components at a given URL in the Admin UI. New routes will appear in this area of the Admin UI:
+
+![Route area](./route-area.webp)
+
+Routes can be defined natively using either **Angular** or **React**. It is also possible to [use other frameworks](/guides/extending-the-admin-ui/using-other-frameworks/) in a more limited capacity.
+
+## Example: Creating a "Greeter" route
+
+### 1. Create the route component
+
+First we need to create the component which will be mounted at the route. This component can be either an Angular component or a React component.
+
+<Tabs groupId="framework">
+<TabItem value="Angular" label="Angular" default>
+
+
+```ts title="src/plugins/greeter/ui/components/greeter/greeter.component.ts"
+import { SharedModule } from '@vendure/admin-ui/core';
+import { Component } from '@angular/core';
+
+@Component({
+    selector: 'greeter',
+    template: `
+        <vdr-page-block>
+            <h2>{{ greeting }}</h2>
+        </vdr-page-block>`,
+    standalone: true,
+    imports: [SharedModule],
+})
+export class GreeterComponent {
+    greeting = 'Hello!';
+}
+```
+
+</TabItem>
+<TabItem value="React" label="React">
+
+```ts title="src/plugins/greeter/ui/components/Greeter.tsx"
+import React from 'react';
+
+export function Greeter() {
+    const greeting = 'Hello!';
+    return (
+        <div className="page-block">
+            <h2>{greeting}</h2>
+        </div>
+    );
+}
+```
+
+</TabItem>
+</Tabs>
+
+:::note
+The `<vdr-page-block>` (Angular) and `<div className="page-block">` (React) is a wrapper that sets the layout and max width of your component to match the rest of the Admin UI. You should usually wrap your component in this element.
+:::
+
+
+### 2. Define the route
+
+Next we need to define a route in our `routes.ts` file. Note that this file can have any name, but "routes.ts" is a convention.
+
+<Tabs groupId="framework">
+<TabItem value="Angular" label="Angular" default>
+
+Using [`registerRouteComponent`](/reference/admin-ui-api/routes/register-route-component) you can define a new route based on an Angular component.
+
+```ts title="src/plugins/greeter/ui/routes.ts"
+import { registerRouteComponent } from '@vendure/admin-ui/core';
+import { GreeterComponent } from './components/greeter/greeter.component';
+
+export default [
+    registerRouteComponent({
+        component: GreeterComponent,
+        path: '',
+        title: 'Greeter Page',
+        breadcrumb: 'Greeter',
+    }),
+];
+```
+
+</TabItem>
+<TabItem value="React" label="React">
+
+Here's the equivalent example using React and [`registerReactRouteComponent`](/reference/admin-ui-api/react-extensions/register-react-route-component):
+
+```ts title="src/plugins/greeter/ui/routes.ts"
+import { registerReactRouteComponent } from '@vendure/admin-ui/react';
+import { Greeter } from './components/Greeter';
+
+export default [
+    registerReactRouteComponent({
+        component: Greeter,
+        path: '',
+        title: 'Greeter Page',
+        breadcrumb: 'Greeter',
+    }),
+];
+```
+
+</TabItem>
+</Tabs>
+
+The `path: ''` is actually optional, since `''` is the default value. But this is included here to show that you can mount different components at different paths. See the section on route parameters below.
+
+### 3. Add the route to the extension config
+
+Now we need to add this routes file to our extension definition:
+
+```ts title="src/vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
+import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
+import * as path from 'path';
+
+export const config: VendureConfig = {
+    // ...
+    plugins: [
+        AdminUiPlugin.init({
+            port: 3002,
+            app: compileUiExtensions({
+                outputPath: path.join(__dirname, '../admin-ui'),
+                extensions: [
+                    {
+                        id: 'greeter',
+                        extensionPath: path.join(__dirname, 'plugins/greeter/ui'),
+                        // highlight-start
+                        routes: [{ route: 'greet', filePath: 'routes.ts' }],
+                        // highlight-end
+                    },
+                ],
+            }),
+        }),
+    ],
+};
+```
+
+Note that by specifying `route: 'greet'`, we are "mounting" the routes at the `/extensions/greet` path.
+
+The `filePath` property is relative to the directory specified in the `extensionPath` property. In this case, the `routes.ts` file is located at `src/plugins/greeter/ui/routes.ts`.
+
+Now go to the Admin UI app in your browser and log in. You should now be able to manually enter the URL `http://localhost:3000/admin/extensions/greet` and you should see the component with the "Hello!" header:
+
+![./ui-extensions-greeter.webp](./ui-extensions-greeter.webp)
+
+## Route parameters
+
+The `path` property is used to specify the path to a specific component. This path can contain parameters, which will then be made available to the component. Parameters are defined using the `:` prefix. For example:
+
+<Tabs groupId="framework">
+<TabItem value="Angular" label="Angular" default>
+
+```ts title="src/plugins/my-plugin/ui/routes.ts"
+import { registerRouteComponent } from '@vendure/admin-ui/core';
+import { TestComponent } from './components/test/test.component';
+
+export default [
+    registerRouteComponent({
+        component: TestComponent,
+        // highlight-next-line
+        path: ':id',
+        title: 'Test',
+        breadcrumb: 'Test',
+    }),
+];
+```
+
+</TabItem>
+<TabItem value="React" label="React">
+
+```ts title="src/plugins/my-plugin/ui/routes.ts"
+import { registerReactRouteComponent } from '@vendure/admin-ui/react';
+import { Test } from './components/Test';
+
+export default [
+    registerReactRouteComponent({
+        component: Test,
+        // highlight-next-line
+        path: ':id',
+        title: 'Test',
+        breadcrumb: 'Test',
+    }),
+];
+```
+
+</TabItem>
+</Tabs>
+
+The `id` parameter will then be available in the component:
+
+<Tabs groupId="framework">
+<TabItem value="Angular" label="Angular" default>
+
+```ts title="src/plugins/my-plugin/ui/components/test/test.component.ts"
+import { SharedModule } from '@vendure/admin-ui/core';
+import { Component } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+
+@Component({
+    selector: 'test',
+    template: `
+        <vdr-page-block>
+            // highlight-next-line
+            <p>id: {{ id }}</p>
+        </vdr-page-block>`,
+    standalone: true,
+    imports: [SharedModule],
+})
+export class TestComponent {
+    id: string;
+
+    constructor(private route: ActivatedRoute) {
+        // highlight-next-line
+        this.id = this.route.snapshot.paramMap.get('id');
+    }
+}
+```
+
+</TabItem>
+<TabItem value="React" label="React">
+
+```tsx title="src/plugins/my-plugin/ui/components/Test.tsx"
+import React from 'react';
+import { useRouteParams } from '@vendure/admin-ui/react';
+
+export function Test() {
+    // highlight-next-line
+    const { params } = useRouteParams();
+    return (
+        <div className="page-block">
+            // highlight-next-line
+            <p>id: {params.id}</p>
+        </div>
+    );
+}
+```
+
+</TabItem>
+</Tabs>
+
+Loading the route `/extensions/test/123` will then display the id "123".
+
+## Injecting services
+
+It is possible to inject services into your components. This includes both the [built-in services](/reference/admin-ui-api/services/) for things like data fetching, notifications and modals, as well as any custom services you have defined in your UI extension.
+
+Here's an example of injecting the built-in `NotificationService` into a component to display a toast notification:
+
+
+<Tabs groupId="framework">
+<TabItem value="Angular" label="Angular" default>
+
+In Angular, we can use either the constructor to inject the service (as shown below), or the `inject()` function. See the [Angular dependency injection guide](https://angular.io/guide/dependency-injection#injecting-a-dependency) for more information.
+
+```ts title="src/plugins/my-plugin/ui/components/test/test.component.ts"
+import { SharedModule, NotificationService } from '@vendure/admin-ui/core';
+import { Component } from '@angular/core';
+
+@Component({
+    selector: 'test',
+    template: `
+        <vdr-page-block>
+            <button class="button primary" (click)="showNotification()">Click me</button>
+        </vdr-page-block>`,
+    standalone: true,
+    imports: [SharedModule],
+})
+export class TestComponent {
+    // highlight-next-line
+    constructor(private notificationService: NotificationService) {}
+    
+    showNotification() {
+        // highlight-next-line
+        this.notificationService.success('Hello!');
+    }
+}
+```
+
+</TabItem>
+<TabItem value="React" label="React">
+
+In React, we use the [`useInjector()`](/reference/admin-ui-api/react-hooks/use-injector) hook to inject the service:
+
+```tsx title="src/plugins/my-plugin/ui/components/Test.tsx"
+import { NotificationService } from '@vendure/admin-ui/core';
+// highlight-next-line
+import { useInjector } from '@vendure/admin-ui/react';
+import React from 'react';
+
+export function Test() {
+    // highlight-next-line
+    const notificationService = useInjector(NotificationService);
+    
+    function showNotification() {
+        // highlight-next-line
+        notificationService.success('Hello!');
+    }
+    return (
+        <div className="page-block">
+            <button className="button primary" onClick={showNotification}>Click me</button>
+        </div>
+    );
+}
+```
+
+</TabItem>
+</Tabs>
+
+## Setting page title
+
+The `title` property is used to set the page title. This is displayed in the browser tab as well as in the page header.
+
+### In the route definition
+
+The page title can be set in the route definition:
+
+<Tabs groupId="framework">
+<TabItem value="Angular" label="Angular" default>
+
+```ts title="src/plugins/my-plugin/ui/routes.ts"
+import { registerRouteComponent } from '@vendure/admin-ui/core';
+import { TestComponent } from './components/test/test.component';
+
+export default [
+    registerRouteComponent({
+        component: TestComponent,
+        // highlight-next-line
+        title: 'Test',
+        breadcrumb: 'Test',
+    }),
+];
+```
+
+</TabItem>
+<TabItem value="React" label="React">
+
+```ts title="src/plugins/my-plugin/ui/routes.ts"
+import { registerReactRouteComponent } from '@vendure/admin-ui/react';
+import { Test } from './components/Test';
+
+export default [
+    registerReactRouteComponent({
+        component: Test,
+        // highlight-next-line
+        title: 'Test',
+        breadcrumb: 'Test',
+    }),
+];
+```
+
+</TabItem>
+</Tabs>
+
+### Dynamically from the component
+
+It is also possible to update the page title dynamically from the route component itself:
+
+<Tabs groupId="framework">
+<TabItem value="Angular" label="Angular" default>
+
+```ts title="src/plugins/my-plugin/ui/components/test/test.component.ts"
+import { PageMetadataService, SharedModule } from '@vendure/admin-ui/core';
+import { Component } from '@angular/core';
+
+@Component({
+    selector: 'test',
+    template: `
+        <vdr-page-block>
+            <vdr-card>
+                // highlight-next-line
+                <button class="button primary" (click)="handleClick()">Update title</button>
+            </vdr-card>
+        </vdr-page-block>`,
+    standalone: true,
+    imports: [SharedModule],
+})
+export class TestComponent {
+    // highlight-next-line
+    constructor(private pageMetadataService: PageMetadataService) {}
+
+    handleClick() {
+        // highlight-next-line
+        pageMetadataService.setTitle('New title');
+    }
+}
+```
+
+</TabItem>
+<TabItem value="React" label="React">
+
+```tsx title="src/plugins/my-plugin/ui/components/Test.tsx"
+import { Card, usePageMetadata } from '@vendure/admin-ui/react';
+import React from 'react';
+
+export function Test() {
+    // highlight-next-line
+    const { setTitle } = usePageMetadata();
+
+    function handleClick() {
+        // highlight-next-line
+        setTitle('New title');
+    }
+    return (
+        <div className="page-block">
+            <Card>
+                <button className="button primary" onClick={handleClick}>
+                    Update title
+                </button>
+            </Card>
+        </div>
+    );
+}
+```
+
+</TabItem>
+</Tabs>
+
+## Setting breadcrumbs
+
+### In the route definition
+
+The page breadcumbs can be set in the route definition in a couple of ways. The simplest is to specify the `breadcumb` property:
+
+
+<Tabs groupId="framework">
+<TabItem value="Angular" label="Angular" default>
+
+```ts title="src/plugins/my-plugin/ui/routes.ts"
+import { registerRouteComponent } from '@vendure/admin-ui/core';
+import { TestComponent } from './components/test/test.component';
+
+export default [
+    registerRouteComponent({
+        component: TestComponent,
+        title: 'Test',
+        // highlight-next-line
+        breadcrumb: 'Test',
+    }),
+];
+```
+
+</TabItem>
+<TabItem value="React" label="React">
+
+```ts title="src/plugins/my-plugin/ui/routes.ts"
+import { registerReactRouteComponent } from '@vendure/admin-ui/react';
+import { Test } from './components/Test';
+
+export default [
+    registerReactRouteComponent({
+        component: Test,
+        title: 'Test',
+        // highlight-next-line
+        breadcrumb: 'Test',
+    }),
+];
+```
+
+</TabItem>
+</Tabs>
+
+This can be a string (as above), a link/label pair, or an array of link/label pairs:
+
+```ts title="src/plugins/my-plugin/ui/routes.ts"
+import { registerRouteComponent } from '@vendure/admin-ui/core';
+import { TestComponent } from './components/test/test.component';
+
+export default [
+    registerRouteComponent({
+        component: TestComponent,
+        path: 'test-1',
+        title: 'Test 1',
+        // highlight-start
+        breadcrumb: { label: 'Test', link: '/extensions/test' },
+        // highlight-end
+    }),
+    registerRouteComponent({
+        component: TestComponent,
+        path: 'test-2',
+        title: 'Test 2',
+        // highlight-start
+        breadcrumb: [
+            { label: 'Parent', link: '/extensions/test' },
+            { label: 'Child', link: '/extensions/test/test-2' },
+        ],
+        // highlight-end
+    }),
+];
+```
+
+A more powerful way to set the breadcrumbs is by using the `getBreadcrumbs` property. This is a function that receives any resolved detail data and returns an array of link/label pairs. An example of its use can be seen in the [Creating detail views guide](/guides/extending-the-admin-ui/creating-detail-views/#route-config).
+
+### Dynamically from the component
+
+Similar to setting the title, the breadcrumbs can also be updated dynamically from the route component itself:
+
+<Tabs groupId="framework">
+<TabItem value="Angular" label="Angular" default>
+
+```ts title="src/plugins/my-plugin/ui/components/test/test.component.ts"
+import { PageMetadataService, SharedModule } from '@vendure/admin-ui/core';
+import { Component } from '@angular/core';
+
+@Component({
+    selector: 'test',
+    template: `
+        <vdr-page-block>
+            <vdr-card>
+                // highlight-next-line
+                <button class="button primary" (click)="handleClick()">Update breadcrumb</button>
+            </vdr-card>
+        </vdr-page-block>`,
+    standalone: true,
+    imports: [SharedModule],
+})
+export class TestComponent {
+    // highlight-next-line
+    constructor(private pageMetadataService: PageMetadataService) {}
+
+    handleClick() {
+        // highlight-next-line
+        pageMetadataService.setBreadcrumb('New breadcrumb');
+    }
+}
+```
+
+</TabItem>
+<TabItem value="React" label="React">
+
+```tsx title="src/plugins/my-plugin/ui/components/Test.tsx"
+import { Card, usePageMetadata } from '@vendure/admin-ui/react';
+import React from 'react';
+
+export function Test() {
+    // highlight-next-line
+    const { setBreadcrumb } = usePageMetadata();
+
+    function handleClick() {
+        // highlight-next-line
+        setBreadcrumb('New breadcrumb');
+    }
+    return (
+        <div className="page-block">
+            <Card>
+                <button className="button primary" onClick={handleClick}>
+                    Update title
+                </button>
+            </Card>
+        </div>
+    );
+}
+```
+
+</TabItem>
+</Tabs>
+
+## Advanced configuration
+
+The Admin UI app routing is built on top of the [Angular router](https://angular.io/guide/routing-overview) - a very advanced and robust router. As such, you are able to tap into all the advanced features it provides by using the `routeConfig` property, which takes an Angular [`Route` definition object](https://angular.io/api/router/Route) and passes it directly to the router.
+
+```ts title="src/plugins/my-plugin/ui/routes.ts"
+import { registerRouteComponent } from '@vendure/admin-ui/core';
+import { inject } from '@angular/core';
+import { ActivatedRouteSnapshot } from '@angular/router';
+import { TestComponent } from './components/test/test.component';
+import { PermissionsService } from './services';
+
+export default [
+    registerRouteComponent({
+        component: TestComponent,
+        path: ':id',
+        title: 'Test',
+        breadcrumb: 'Test',
+        // highlight-start
+        routeConfig: {
+            pathMatch: 'full',
+            canActivate: [(route: ActivatedRouteSnapshot) => {
+                return inject(PermissionsService).canActivate(route.params.id);
+            }],
+        },
+        // highlight-end
+    }),
+];
+```
+
+This allows you to leverage advanced features such as:
+
+- [Route guards](https://angular.io/api/router/CanActivateFn)
+- [Data resolvers](https://angular.io/api/router/ResolveFn)
+- [Nested routes](https://angular.io/guide/router#nesting-routes)
+- [Redirects](https://angular.io/guide/router#setting-up-redirects)

BIN
docs/docs/guides/extending-the-admin-ui/defining-routes/route-area.webp


+ 0 - 0
docs/docs/guides/extending-the-admin-ui/getting-started/ui-extensions-greeter.webp → docs/docs/guides/extending-the-admin-ui/defining-routes/ui-extensions-greeter.webp


+ 48 - 101
docs/docs/guides/extending-the-admin-ui/getting-started/index.md

@@ -52,7 +52,7 @@ export default [
 ];
 ```
 
-You can then use the `compileUiExtensions` function to compile your UI extensions and add them to the Admin UI app bundle.
+You can then use the [`compileUiExtensions` function](/reference/admin-ui-api/ui-devkit/compile-ui-extensions/) to compile your UI extensions and add them to the Admin UI app bundle.
 
 ```ts title="src/vendure-config.ts"
 import { VendureConfig } from '@vendure/core';
@@ -118,7 +118,9 @@ You can exclude them in your main `tsconfig.json` by adding a line to the "exclu
 
 ## Providers
 
-Your `providers.ts` file exports an array of objects known as "providers" in Angular terminology. These providers are passed to the application on startup to configure new functionality. With providers you can:
+Your `providers.ts` file exports an array of objects known as "providers" in Angular terminology. These providers are passed to the application on startup to configure new functionality.
+
+With providers you can:
 
 -   Add new buttons to the action bar of existing pages (the top bar containing the primary actions for a page) using [`addActionBarItem`](/reference/admin-ui-api/action-bar/add-action-bar-item).
 -   Add new menu items to the left-hand navigation menu using [`addNavMenuItem`](/reference/admin-ui-api/nav-menu/add-nav-menu-item) and [`addNavMenuSection`](/reference/admin-ui-api/nav-menu/add-nav-menu-section).
@@ -129,6 +131,46 @@ Your `providers.ts` file exports an array of objects known as "providers" in Ang
 -   Define custom form input components for custom fields and configurable operation arguments using [`registerFormInputComponent`](/reference/admin-ui-api/custom-input-components/register-form-input-component) or [`registerReactFormInputComponent`](/reference/admin-ui-api/react-extensions/register-react-form-input-component)
 -   Define custom components to render customer/order history timeline entries using [`registerHistoryEntryComponent`](/reference/admin-ui-api/custom-history-entry-components/register-history-entry-component)
 
+### Providers format 
+
+A providers file should have a **default export** which is an array of providers:
+
+```ts title="src/plugins/my-plugin/ui/providers.ts"
+import { addActionBarItem } from '@vendure/admin-ui/core';
+
+export default [
+    addActionBarItem({
+        id: 'test-button',
+        label: 'Test Button',
+        locationId: 'order-list',
+    }),
+];
+```
+
+### Specifying providers
+
+When defining UI extensions in the `compileUiExtensions()` function, you must specify at least one providers file. This is done by passing an array of file paths, where each file path is relative to the directory specified by the `extensionPath` option.
+
+```ts title="src/vendure-config.ts"
+import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
+import * as path from 'path';
+
+// ... omitted for brevity
+
+compileUiExtensions({
+    outputPath: path.join(__dirname, '../admin-ui'),
+    extensions: [
+        {
+            id: 'test-extension',
+            extensionPath: path.join(__dirname, 'plugins/my-plugin/ui'),
+            // highlight-next-line
+            providers: ['providers.ts'],
+        },
+    ],
+    devMode: true,
+});
+```
+
 :::info
 When running the Admin UI in dev mode, you can use the `ctrl + u` keyboard shortcut to see the location of all UI extension points.
 
@@ -170,109 +212,14 @@ export default [
 
 ## Routes
 
-Your `routes.ts` file exports an array of objects which define new routes in the Admin UI. For example, imagine you have created a plugin which implements a simple content management system. You can define a route for the list of articles, and another for the detail view of an article.
-
-<Tabs groupId="framework">
-<TabItem value="Angular" label="Angular" default>
+Routes allow you to define completely custom views in the Admin UI.
 
-Using [`registerRouteComponent`](/reference/admin-ui-api/routes/register-route-component) you can define a new route based on an Angular component. Here's a simple example:
-
-```ts title="src/plugins/greeter/ui/routes.ts"
-import { registerRouteComponent, SharedModule } from '@vendure/admin-ui/core';
-import { Component } from '@angular/core';
+![Custom route](../defining-routes/route-area.webp)
 
-@Component({
-    selector: 'greeter',
-    template: ` <vdr-page-block>
-        <h2>{{ greeting }}</h2>
-    </vdr-page-block>`,
-    standalone: true,
-    imports: [SharedModule],
-})
-export class GreeterComponent {
-    greeting = 'Hello!';
-}
-
-export default [
-    registerRouteComponent({
-        component: GreeterComponent,
-        path: '',
-        title: 'Greeter Page',
-        breadcrumb: 'Greeter',
-    }),
-];
-```
-
-</TabItem>
-<TabItem value="React" label="React">
-
-Here's the equivalent example using React and [`registerReactRouteComponent`](/reference/admin-ui-api/react-extensions/register-react-route-component):
-
-```ts title="src/plugins/greeter/ui/routes.ts"
-import { registerReactRouteComponent } from '@vendure/admin-ui/react';
-import React from 'react';
-
-function Greeter() {
-    const greeting = 'Hello!';
-    return (
-        <div className="page-block">
-            <h2>{greeting}</h2>
-        </div>
-    );
-}
-
-export default [
-    registerReactRouteComponent({
-        component: Greeter,
-        path: '',
-        title: 'Greeter Page',
-        breadcrumb: 'Greeter',
-    }),
-];
-```
-
-</TabItem>
-</Tabs>
-
-:::note
-The `<vdr-page-block>` (Angular) and `<div className="page-block">` (React) is a wrapper that sets the layout and max width of your component to match the rest of the Admin UI. You should usually wrap your component in this element.
-:::
-
-Now we need to add this routes file to our extension definition:
-
-```ts title="src/vendure-config.ts"
-import { VendureConfig } from '@vendure/core';
-import { AdminUiPlugin } from '@vendure/admin-ui-plugin';
-import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
-import * as path from 'path';
-
-export const config: VendureConfig = {
-    // ...
-    plugins: [
-        AdminUiPlugin.init({
-            port: 3002,
-            app: compileUiExtensions({
-                outputPath: path.join(__dirname, '../admin-ui'),
-                extensions: [
-                    {
-                        id: 'greeter',
-                        extensionPath: path.join(__dirname, 'plugins/greeter/ui'),
-                        // highlight-start
-                        routes: [{ route: 'greet', filePath: 'routes.ts' }],
-                        // highlight-end
-                    },
-                ],
-            }),
-        }),
-    ],
-};
-```
-
-Note that by specifying `route: 'greet'`, we are "mounting" the routes at the `/extensions/greet` path.
+Your `routes.ts` file exports an array of objects which define new routes in the Admin UI. For example, imagine you have created a plugin which implements a simple content management system. You can define a route for the list of articles, and another for the detail view of an article.
 
-Now go to the Admin UI app in your browser and log in. You should now be able to manually enter the URL `http://localhost:3000/admin/extensions/greet` and you should see the component with the "Hello!" header:
+For a detailed instructions, see the [Defining Routes guide](/guides/extending-the-admin-ui/defining-routes/).
 
-![./ui-extensions-greeter.webp](./ui-extensions-greeter.webp)
 
 ## Dev vs Prod mode
 

+ 2 - 1
docs/sidebars.js

@@ -143,8 +143,9 @@ const sidebars = {
                     value: 'Routes',
                     className: 'sidebar-section-header',
                 },
-                'guides/extending-the-admin-ui/creating-detail-views/index',
+                'guides/extending-the-admin-ui/defining-routes/index',
                 'guides/extending-the-admin-ui/creating-list-views/index',
+                'guides/extending-the-admin-ui/creating-detail-views/index',
             ],
         },
         {