skills/vendure-dashboard-migration/04-list-pages.md
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { TypedBaseListComponent, SharedModule } from '@vendure/admin-ui/core';
// This is the TypedDocumentNode generated by GraphQL Code Generator
import { graphql } from '../../gql';
const getReviewListDocument = graphql(`
query GetReviewList($options: ReviewListOptions) {
reviews(options: $options) {
items {
id
createdAt
updatedAt
title
rating
text
authorName
productId
}
totalItems
}
}
`);
@Component({
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()
.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],
});
}
}
<!-- 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-action-bar>
</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"
/>
<!-- Adds a search bar -->
<vdr-dt2-search
[searchTermControl]="searchTermControl"
searchTermPlaceholder="Filter by title"
/>
<!-- Here we define all the available columns -->
<vdr-dt2-column id="id" [heading]="'common.id' | translate" [hiddenByDefault]="true">
<ng-template let-review="item">
{{ review.id }}
</ng-template>
</vdr-dt2-column>
<vdr-dt2-column
id="created-at"
[heading]="'common.created-at' | translate"
[hiddenByDefault]="true"
[sort]="sorts.get('createdAt')"
>
<ng-template let-review="item">
{{ review.createdAt | localeDate : 'short' }}
</ng-template>
</vdr-dt2-column>
<vdr-dt2-column
id="updated-at"
[heading]="'common.updated-at' | translate"
[hiddenByDefault]="true"
[sort]="sorts.get('updatedAt')"
>
<ng-template let-review="item">
{{ review.updatedAt | localeDate : 'short' }}
</ng-template>
</vdr-dt2-column>
<vdr-dt2-column id="title" 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>
</vdr-dt2-column>
<vdr-dt2-column id="rating" heading="Rating" [sort]="sorts.get('rating')">
<ng-template let-review="item"><my-star-rating-component [rating]="review.rating" /></ng-template>
</vdr-dt2-column>
<vdr-dt2-column id="author" heading="Author" [sort]="sorts.get('authorName')">
<ng-template let-review="item">{{ review.authorName }}</ng-template>
</vdr-dt2-column>
</vdr-data-table-2>
import { registerRouteComponent } from '@vendure/admin-ui/core';
import { ReviewListComponent } from './components/review-list/review-list.component';
export default [
registerRouteComponent({
path: '',
component: ReviewListComponent,
breadcrumb: 'Product reviews',
}),
]
import {
Button,
DashboardRouteDefinition,
ListPage,
PageActionBarRight,
DetailPageButton,
} from '@vendure/dashboard';
import { Link } from '@tanstack/react-router';
import { PlusIcon } from 'lucide-react';
// This function is generated for you by the `vendureDashboardPlugin` in your Vite config.
// It uses gql-tada to generate TypeScript types which give you type safety as you write
// your queries and mutations.
import { graphql } from '@/gql';
// The fields you select here will be automatically used to generate the appropriate columns in the
// data table below.
const getArticleList = graphql(`
query GetArticles($options: ArticleListOptions) {
articles(options: $options) {
items {
id
createdAt
updatedAt
isPublished
title
slug
body
customFields
}
totalItems
}
}
`);
const deleteArticleDocument = graphql(`
mutation DeleteArticle($id: ID!) {
deleteArticle(id: $id) {
result
}
}
`);
export const articleList: DashboardRouteDefinition = {
navMenuItem: {
sectionId: 'catalog',
id: 'articles',
url: '/articles',
title: 'CMS Articles',
},
path: '/articles',
loader: () => ({
breadcrumb: 'Articles',
}),
component: route => (
<ListPage
pageId="article-list"
title="Articles"
listQuery={getArticleList}
deleteMutation={deleteArticleDocument}
route={route}
customizeColumns={{
title: {
cell: ({ row }) => {
const post = row.original;
return <DetailPageButton id={post.id} label={post.title} />;
},
},
}}
defaultVisibility={{
type: true,
summary: true,
state: true,
rating: true,
authorName: true,
authorLocation: true,
}}
defaultColumnOrder={[
'type',
'summary',
'authorName',
'authorLocation',
'rating',
]}
>
<PageActionBarRight>
<Button render={<Link to="./new" />}>
<PlusIcon className="mr-2 h-4 w-4" />
New article
</Button>
</PageActionBarRight>
</ListPage>
),
};
Important:
- When using defaultVisibility, specify the specific visible ones with true. Do not mix
true and false values. It is implicit that any not specified will default to false.
- The id, createdAt and updatedAt never need to be specified in customizeColumns, defaultVisibilityordefaultColumnOrder. They are handled correctly by default. - By default the DataTable will handle column names based on the field name, e.g. authorName->Author Name, rating->Rating, so an explicit cell header is not needed unless the column header title must significantly differ from the field name. - If a custom cellfunction needs to access fields _other_ than the one being rendered, those other fields *must* be declared as dependencies: ```tsx customizeColumns={{ name: { // Note, we DO NOT need to declare "name" as a dependency here, // since we are handling thename` column already.
meta: { dependencies: ['reviewCount'] },
cell: ({ row }) => {
const { name, reviewCount } = row.original;
return <Badge variant="outline">{name} ({reviewCount})</Badge>
},
},
}}
```