## List Pages

### Old (Angular)
```ts
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],
        });
    }
}
```

```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-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>
```

```ts
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',
    }),
]
```

### New (React Dashboard)

```tsx
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 asChild>
                    <Link to="./new">
                        <PlusIcon className="mr-2 h-4 w-4" />
                        New article
                    </Link>
                </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`, defaultVisibility` or `defaultColumnOrder`.
      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 `cell` function 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 the `name` column already.
          meta: { dependencies: ['reviewCount'] },
          cell: ({ row }) => {
            const { name, reviewCount } = row.original;
            return <Badge variant="outline">{name} ({reviewCount})</Badge>
          },
        },
      }}
      ```
