Browse Source

feat(dashboard): Improve extensions API for list/detail views

Michael Bromley 10 months ago
parent
commit
662b21eb89

+ 66 - 0
packages/dashboard/generate-index.js

@@ -0,0 +1,66 @@
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const TARGET_DIRS = ['components', 'framework', 'hooks', 'lib'];
+const SRC_DIR = path.join(__dirname, 'src');
+const INDEX_FILE = path.join(SRC_DIR, 'index.ts');
+
+function getAllFiles(dir, fileList = []) {
+    const files = fs.readdirSync(dir);
+    
+    files.forEach(file => {
+        const filePath = path.join(dir, file);
+        const stat = fs.statSync(filePath);
+        
+        if (stat.isDirectory()) {
+            getAllFiles(filePath, fileList);
+        } else if (file.match(/\.(ts|tsx|js|jsx)$/) && !file.endsWith('.d.ts') && !file.endsWith('.spec.ts')) {
+            fileList.push(filePath);
+        }
+    });
+    
+    return fileList;
+}
+
+function generateExports() {
+    let exportStatements = [];
+    
+    TARGET_DIRS.forEach(dir => {
+        const dirPath = path.join(SRC_DIR, dir);
+        if (!fs.existsSync(dirPath)) {
+            console.warn(`Directory ${dirPath} does not exist`);
+            return;
+        }
+        
+        const files = getAllFiles(dirPath);
+        files.forEach(file => {
+            const relativePath = path.relative(SRC_DIR, file);
+            const exportPath = relativePath.replace(/\\/g, '/');
+            // replace the tsx with js in the export path
+            const exportPathJs = exportPath.replace(/\.tsx?/, '.js');
+            
+            // Generate both named and default exports
+            exportStatements.push(`export * from './${exportPathJs}';`);
+        });
+    });
+    
+    return exportStatements.join('\n');
+}
+
+function generateIndexFile() {
+    const exports = generateExports();
+    const content = `// This file is auto-generated. Do not edit manually.
+// Generated on: ${new Date().toISOString()}
+
+${exports}
+`;
+
+    fs.writeFileSync(INDEX_FILE, content);
+    console.log(`Generated ${INDEX_FILE} successfully!`);
+}
+
+generateIndexFile();

+ 2 - 1
packages/dashboard/package.json

@@ -9,7 +9,8 @@
     "build:plugin": "tsc --project tsconfig.plugin.json",
     "watch:plugin": "tsc --project tsconfig.plugin.json --watch",
     "lint": "eslint .",
-    "preview": "vite preview"
+    "preview": "vite preview",
+    "generate-index": "node ./generate-index.js"
   },
   "module": "./src/index.ts",
   "main": "./src/index.ts",

+ 3 - 3
packages/dashboard/src/components/layout/generated-breadcrumbs.tsx

@@ -9,18 +9,18 @@ import { Link, useRouterState } from '@tanstack/react-router';
 import * as React from 'react';
 import { Fragment } from 'react';
 
-export interface BreadcrumbItem {
+export interface BreadcrumbPair {
     label: string | React.ReactElement;
     path: string;
 }
 
 export type BreadcrumbShorthand = string | React.ReactElement;
 
-export type PageBreadcrumb = BreadcrumbItem | BreadcrumbShorthand;
+export type PageBreadcrumb = BreadcrumbPair | BreadcrumbShorthand;
 
 export function GeneratedBreadcrumbs() {
     const matches = useRouterState({ select: s => s.matches });
-    const breadcrumbs: BreadcrumbItem[] = matches
+    const breadcrumbs: BreadcrumbPair[] = matches
         .filter(match => match.loaderData?.breadcrumb)
         .map(({ pathname, loaderData }) => {
             if (typeof loaderData.breadcrumb === 'string') {

+ 5 - 6
packages/dashboard/src/components/shared/asset-preview-dialog.tsx

@@ -1,21 +1,20 @@
 import {
     Dialog,
     DialogContent,
+    DialogDescription,
     DialogHeader,
     DialogTitle,
-    DialogDescription,
 } from '@/components/ui/dialog.js';
-import { Asset, AssetPreview } from './asset-preview.js';
-import { SheetDescription } from '../ui/sheet.js';
+import { AssetWithTags, AssetPreview } from './asset-preview.js';
 
 interface AssetPreviewDialogProps {
     open: boolean;
     onOpenChange: (open: boolean) => void;
-    asset: Asset;
-    assets?: Asset[];
+    asset: AssetWithTags;
+    assets?: AssetWithTags[];
     editable?: boolean;
     customFields?: any[];
-    onAssetChange?: (asset: Partial<Asset>) => void;
+    onAssetChange?: (asset: Partial<AssetWithTags>) => void;
 }
 
 export function AssetPreviewDialog({

+ 4 - 4
packages/dashboard/src/components/shared/asset-preview.tsx

@@ -19,14 +19,14 @@ interface Point {
     y: number;
 }
 
-export type Asset = AssetFragment & { tags?: { value: string }[] };
+export type AssetWithTags = AssetFragment & { tags?: { value: string }[] };
 
 interface AssetPreviewProps {
-    asset: Asset;
-    assets?: Asset[];
+    asset: AssetWithTags;
+    assets?: AssetWithTags[];
     editable?: boolean;
     customFields?: any[];
-    onAssetChange?: (asset: Partial<Asset>) => void;
+    onAssetChange?: (asset: Partial<AssetWithTags>) => void;
     onEditClick?: () => void;
 }
 

+ 1 - 2
packages/dashboard/src/components/shared/customer-group-chip.tsx

@@ -1,12 +1,11 @@
 import { X } from 'lucide-react';
-import { CustomerGroup } from './customer-group-selector.js';
 import { Badge } from '../ui/badge.js';
 
 export function CustomerGroupChip({
     group,
     onRemove,
 }: {
-    group: CustomerGroup;
+    group: { id: string; name: string };
     onRemove?: (id: string) => void;
 }) {
     return (

+ 1 - 6
packages/dashboard/src/components/shared/customer-group-selector.tsx

@@ -19,13 +19,8 @@ const customerGroupsDocument = graphql(`
     }
 `);
 
-export interface CustomerGroup {
-    id: string;
-    name: string;
-}
-
 export interface CustomerGroupSelectorProps {
-    onSelect: (value: CustomerGroup) => void;
+    onSelect: (value: { id: string; name: string }) => void;
     readOnly?: boolean;
 }
 

+ 0 - 56
packages/dashboard/src/components/shared/order-addresses.tsx

@@ -1,56 +0,0 @@
-import { FC } from 'react';
-import { AddressDisplay } from './address-display';
-import type { OrderAddress } from '@/graphql/types';
-
-interface OrderAddressesProps {
-  shippingAddress?: OrderAddress | null;
-  billingAddress?: OrderAddress | null;
-  customerName?: string;
-  showBothAddresses?: boolean;
-}
-
-export const OrderAddresses: FC<OrderAddressesProps> = ({
-  shippingAddress,
-  billingAddress,
-  customerName,
-  showBothAddresses = true,
-}) => {
-  const title = customerName ? `${customerName}'s Address` : undefined;
-
-  return (
-    <AddressDisplay
-      shippingAddress={shippingAddress || undefined}
-      billingAddress={billingAddress || undefined}
-      showBothAddresses={showBothAddresses}
-      title={title}
-    />
-  );
-};
-
-// Example usage with the GraphQL fragment
-/*
-import { orderDetailDocument } from '@/routes/_authenticated/_orders/orders.graphql';
-
-const OrderDetails: FC<{ orderId: string }> = ({ orderId }) => {
-  const { data } = useSuspenseQuery(orderDetailDocument, { variables: { id: orderId } });
-  
-  return (
-    <div>
-      <h2 className="text-xl font-semibold mb-4">Order {data.order.code}</h2>
-      
-      <div className="my-6">
-        <h3 className="text-lg font-medium mb-3">Addresses</h3>
-        <OrderAddresses
-          shippingAddress={data.order.shippingAddress}
-          billingAddress={data.order.billingAddress}
-          customerName={`${data.order.customer.firstName} ${data.order.customer.lastName}`}
-        />
-      </div>
-      
-      {/* Other order details *//*}
-    </div>
-  );
-};
-*/
-
-export default OrderAddresses; 

+ 3 - 3
packages/dashboard/src/framework/extension-api/define-dashboard-extension.ts

@@ -1,6 +1,6 @@
 import { DashboardExtension } from '@/framework/extension-api/extension-api-types.js';
 import { addNavMenuItem, NavMenuItem } from '@/framework/nav-menu/nav-menu.js';
-import { registerListView } from '@/framework/page/page-api.js';
+import { registerRoute } from '@/framework/page/page-api.js';
 
 const extensionSourceChangeCallbacks = new Set<() => void>();
 
@@ -20,9 +20,9 @@ export function defineDashboardExtension(extension: DashboardExtension) {
                 };
                 addNavMenuItem(item, route.navMenuItem.sectionId);
             }
-            if (route.listQuery) {
+            if (route.path) {
                 // Configure a list page
-                registerListView(route);
+                registerRoute(route);
             }
         }
     }

+ 4 - 12
packages/dashboard/src/framework/extension-api/extension-api-types.ts

@@ -1,24 +1,16 @@
 import { NavMenuItem } from '@/framework/nav-menu/nav-menu.js';
-import { ListPageProps, ListQueryOptionsShape, ListQueryShape } from '@/framework/page/list-page.js';
-import { TypedDocumentNode } from '@graphql-typed-document-node/core';
+import { AnyRoute } from '@tanstack/react-router';
 import React from 'react';
 
 export interface DashboardBaseRouteDefinition {
     id: string;
     navMenuItem?: Partial<NavMenuItem> & { sectionId: string };
-    title: string | React.ReactElement;
-}
-
-export interface DashboardListRouteDefinition<
-    T extends TypedDocumentNode<U, V> = TypedDocumentNode<any, any>,
-    U extends ListQueryShape = any,
-    V extends ListQueryOptionsShape = any,
-> extends DashboardBaseRouteDefinition,
-        Omit<ListPageProps<T, U, V>, 'route'> {
     path: string;
 }
 
-export type DashboardRouteDefinition = DashboardListRouteDefinition;
+export interface DashboardRouteDefinition extends DashboardBaseRouteDefinition {
+    component: (route: AnyRoute) => React.ReactNode;
+}
 
 export interface DashboardExtension {
     routes: DashboardRouteDefinition[];

+ 1 - 1
packages/dashboard/src/framework/nav-menu/nav-menu.ts

@@ -5,7 +5,7 @@ export type NavMenuSectionPlacement = 'top' | 'bottom';
 
 export interface NavMenuItem {
     id: string;
-    title: string;
+    title: React.ReactNode;
     url: string;
 }
 

+ 4 - 4
packages/dashboard/src/framework/page/page-api.ts

@@ -1,9 +1,9 @@
-import { DashboardListRouteDefinition } from '@/framework/extension-api/extension-api-types.js';
+import { DashboardRouteDefinition } from '@/framework/extension-api/extension-api-types.js';
 
-export const listViewExtensionRoutes = new Map<string, DashboardListRouteDefinition>();
+export const extensionRoutes = new Map<string, DashboardRouteDefinition>();
 
-export function registerListView(config: DashboardListRouteDefinition) {
+export function registerRoute(config: DashboardRouteDefinition) {
     if (config.path) {
-        listViewExtensionRoutes.set(config.path, config);
+        extensionRoutes.set(config.path, config);
     }
 }

+ 3 - 13
packages/dashboard/src/framework/page/use-extended-router.tsx

@@ -1,6 +1,6 @@
 import { useDashboardExtensions } from '@/framework/extension-api/use-dashboard-extensions.js';
 import { ListPage } from '@/framework/page/list-page.js';
-import { listViewExtensionRoutes } from '@/framework/page/page-api.js';
+import { extensionRoutes } from '@/framework/page/page-api.js';
 import { AUTHENTICATED_ROUTE_PREFIX } from '@/routes/_authenticated.js';
 import { AnyRoute, createRoute, Router } from '@tanstack/react-router';
 import { useMemo } from 'react';
@@ -29,7 +29,7 @@ export const useExtendedRouter = (router: Router<AnyRoute, any, any>) => {
 
         const newRoutes: AnyRoute[] = [];
         // Create new routes for each extension
-        for (const [path, config] of listViewExtensionRoutes.entries()) {
+        for (const [path, config] of extensionRoutes.entries()) {
             const pathWithoutLeadingSlash = path.startsWith('/') ? path.slice(1) : path;
             if (
                 authenticatedRoute.children.findIndex((r: AnyRoute) => r.path === pathWithoutLeadingSlash) >
@@ -45,17 +45,7 @@ export const useExtendedRouter = (router: Router<AnyRoute, any, any>) => {
                 loader: () => ({
                     breadcrumb: config.title,
                 }),
-                component: () => (
-                    <ListPage
-                        title={config.title}
-                        listQuery={config.listQuery}
-                        defaultVisibility={config.defaultVisibility}
-                        customizeColumns={config.customizeColumns}
-                        onSearchTermChange={config.onSearchTermChange}
-                        defaultColumnOrder={config.defaultColumnOrder}
-                        route={() => newRoute}
-                    />
-                ),
+                component: () => config.component(newRoute),
             });
             newRoutes.push(newRoute);
         }

+ 132 - 2
packages/dashboard/src/index.ts

@@ -1,2 +1,132 @@
-export * from '@/framework/extension-api/define-dashboard-extension.js';
-export * from '@/framework/extension-api/extension-api-types.js';
+// This file is auto-generated. Do not edit manually.
+// Generated on: 2025-03-26T10:58:17.182Z
+
+export * from './components/data-display/boolean.js';
+export * from './components/data-display/date-time.js';
+export * from './components/data-display/money.js';
+export * from './components/data-input/affixed-input.js';
+export * from './components/data-input/customer-group-input.js';
+export * from './components/data-input/datetime-input.js';
+export * from './components/data-input/facet-value-input.js';
+export * from './components/data-input/money-input.js';
+export * from './components/data-table/data-table-column-header.js';
+export * from './components/data-table/data-table-faceted-filter.js';
+export * from './components/data-table/data-table-filter-dialog.js';
+export * from './components/data-table/data-table-pagination.js';
+export * from './components/data-table/data-table-view-options.js';
+export * from './components/data-table/data-table.js';
+export * from './components/layout/app-layout.js';
+export * from './components/layout/app-sidebar.js';
+export * from './components/layout/channel-switcher.js';
+export * from './components/layout/content-language-selector.js';
+export * from './components/layout/generated-breadcrumbs.js';
+export * from './components/layout/language-dialog.js';
+export * from './components/layout/nav-main.js';
+export * from './components/layout/nav-projects.js';
+export * from './components/layout/nav-user.js';
+export * from './components/login/login-form.js';
+export * from './components/shared/asset-gallery.js';
+export * from './components/shared/asset-picker-dialog.js';
+export * from './components/shared/asset-preview-dialog.js';
+export * from './components/shared/asset-preview.js';
+export * from './components/shared/assigned-facet-values.js';
+export * from './components/shared/channel-code-label.js';
+export * from './components/shared/channel-selector.js';
+export * from './components/shared/configurable-operation-arg-input.js';
+export * from './components/shared/configurable-operation-input.js';
+export * from './components/shared/confirmation-dialog.js';
+export * from './components/shared/country-selector.js';
+export * from './components/shared/currency-selector.js';
+export * from './components/shared/custom-fields-form.js';
+export * from './components/shared/customer-group-chip.js';
+export * from './components/shared/customer-group-selector.js';
+export * from './components/shared/customer-selector.js';
+export * from './components/shared/detail-page-button.js';
+export * from './components/shared/entity-assets.js';
+export * from './components/shared/error-page.js';
+export * from './components/shared/facet-value-chip.js';
+export * from './components/shared/facet-value-selector.js';
+export * from './components/shared/focal-point-control.js';
+export * from './components/shared/form-field-wrapper.js';
+export * from './components/shared/history-timeline/history-entry.js';
+export * from './components/shared/history-timeline/history-note-checkbox.js';
+export * from './components/shared/history-timeline/history-note-editor.js';
+export * from './components/shared/history-timeline/history-note-input.js';
+export * from './components/shared/history-timeline/history-timeline.js';
+export * from './components/shared/icon-mark.js';
+export * from './components/shared/language-selector.js';
+export * from './components/shared/logo-mark.js';
+export * from './components/shared/multi-select.js';
+export * from './components/shared/paginated-list-data-table.js';
+export * from './components/shared/permission-guard.js';
+export * from './components/shared/rich-text-editor.js';
+export * from './components/shared/role-code-label.js';
+export * from './components/shared/role-selector.js';
+export * from './components/shared/seller-selector.js';
+export * from './components/shared/tax-category-selector.js';
+export * from './components/shared/translatable-form-field.js';
+export * from './components/shared/vendure-image.js';
+export * from './components/shared/zone-selector.js';
+export * from './components/ui/accordion.js';
+export * from './components/ui/alert-dialog.js';
+export * from './components/ui/alert.js';
+export * from './components/ui/avatar.js';
+export * from './components/ui/badge.js';
+export * from './components/ui/breadcrumb.js';
+export * from './components/ui/button.js';
+export * from './components/ui/calendar.js';
+export * from './components/ui/card.js';
+export * from './components/ui/checkbox.js';
+export * from './components/ui/collapsible.js';
+export * from './components/ui/command.js';
+export * from './components/ui/dialog.js';
+export * from './components/ui/dropdown-menu.js';
+export * from './components/ui/form.js';
+export * from './components/ui/input.js';
+export * from './components/ui/label.js';
+export * from './components/ui/pagination.js';
+export * from './components/ui/popover.js';
+export * from './components/ui/scroll-area.js';
+export * from './components/ui/select.js';
+export * from './components/ui/separator.js';
+export * from './components/ui/sheet.js';
+export * from './components/ui/sidebar.js';
+export * from './components/ui/skeleton.js';
+export * from './components/ui/sonner.js';
+export * from './components/ui/switch.js';
+export * from './components/ui/table.js';
+export * from './components/ui/tabs.js';
+export * from './components/ui/textarea.js';
+export * from './components/ui/tooltip.js';
+export * from './framework/component-registry/component-registry.js';
+export * from './framework/component-registry/dynamic-component.js';
+export * from './framework/defaults.js';
+export * from './framework/document-introspection/add-custom-fields.js';
+export * from './framework/document-introspection/get-document-structure.js';
+export * from './framework/document-introspection/hooks.js';
+export * from './framework/extension-api/define-dashboard-extension.js';
+export * from './framework/extension-api/extension-api-types.js';
+export * from './framework/extension-api/use-dashboard-extensions.js';
+export * from './framework/form-engine/form-schema-tools.js';
+export * from './framework/form-engine/use-generated-form.js';
+export * from './framework/layout-engine/page-layout.js';
+export * from './framework/nav-menu/nav-menu.js';
+export * from './framework/page/detail-page-route-loader.js';
+export * from './framework/page/list-page.js';
+export * from './framework/page/page-api.js';
+export * from './framework/page/page-types.js';
+export * from './framework/page/use-detail-page.js';
+export * from './framework/page/use-extended-router.js';
+export * from './hooks/use-auth.js';
+export * from './hooks/use-channel.js';
+export * from './hooks/use-custom-field-config.js';
+export * from './hooks/use-debounce.js';
+export * from './hooks/use-grouped-permissions.js';
+export * from './hooks/use-local-format.js';
+export * from './hooks/use-mobile.js';
+export * from './hooks/use-permissions.js';
+export * from './hooks/use-server-config.js';
+export * from './hooks/use-theme.js';
+export * from './hooks/use-user-settings.js';
+export * from './lib/locale-utils.js';
+export * from './lib/utils.js';

+ 3 - 3
packages/dashboard/src/routes/_authenticated/_orders/orders_.$id.tsx

@@ -98,7 +98,7 @@ export function FacetDetailPage() {
                             </Link>
                         </Button>
                         <div className="mt-4 divide-y">
-                            {entity.shippingAddress && (
+                            {entity?.shippingAddress && (
                                 <div className="pb-6">
                                     <div className="font-medium">
                                         <Trans>Shipping address</Trans>
@@ -106,7 +106,7 @@ export function FacetDetailPage() {
                                     <OrderAddress address={entity.shippingAddress} />
                                 </div>
                             )}
-                            {entity.billingAddress && (
+                            {entity?.billingAddress && (
                                 <div className="pt-4">
                                     <div className="font-medium">
                                         <Trans>Billing address</Trans>
@@ -117,7 +117,7 @@ export function FacetDetailPage() {
                         </div>
                     </PageBlock>
                     <PageBlock column="side" title={<Trans>Payment details</Trans>}>
-                        {entity.payments?.map(payment => (
+                        {entity?.payments?.map(payment => (
                             <PaymentDetails
                                 key={payment.id}
                                 payment={payment}

+ 2 - 1
packages/dev-server/test-plugins/reviews/dashboard/index.tsx

@@ -1,7 +1,8 @@
 import { defineDashboardExtension } from '@vendure/dashboard';
 
+import { reviewDetail } from './review-detail';
 import { reviewList } from './review-list';
 
 export default defineDashboardExtension({
-    routes: [reviewList],
+    routes: [reviewList, reviewDetail],
 });

+ 137 - 0
packages/dev-server/test-plugins/reviews/dashboard/review-detail.tsx

@@ -0,0 +1,137 @@
+import {
+    DashboardRouteDefinition,
+    FormFieldWrapper,
+    PageBlock,
+    PageLayout,
+    PageTitle,
+    Page,
+    PageDetailForm,
+    PageActionBar,
+    PageActionBarRight,
+    PermissionGuard,
+    Button,
+    DetailFormGrid,
+    useDetailPage,
+    CustomFieldsPageBlock,
+    Switch,
+    Input,
+} from '@vendure/dashboard';
+import { Trans } from '@lingui/react/macro';
+
+import gql from 'graphql-tag';
+
+const reviewDetailDocument = gql`
+    query GetReviewDetail($id: ID!) {
+        productReview(id: $id) {
+            id
+            createdAt
+            updatedAt
+            product {
+                id
+                name
+            }
+            productVariant {
+                id
+                name
+                sku
+            }
+            summary
+            body
+            rating
+            authorName
+            authorLocation
+            upvotes
+            downvotes
+            state
+            response
+            responseCreatedAt
+        }
+    }
+`;
+
+const updateReviewDocument = gql`
+    mutation UpdateReview($input: UpdateProductReviewInput!) {
+        updateProductReview(input: $input) {
+            id
+        }
+    }
+`;
+
+
+export const reviewDetail: DashboardRouteDefinition = {
+    id: 'review-detail',
+    title: 'Product Reviews',
+    navMenuItem: {
+        sectionId: 'catalog',
+        id: 'reviews',
+        url: '/reviews',
+        title: 'Product Reviews',
+    },
+    path: '/reviews/$id',
+    component: route => {
+        const params = route.useParams();
+
+        const { form, submitHandler, entity, isPending } = useDetailPage({
+            queryDocument: reviewDetailDocument,
+            updateDocument: updateReviewDocument,
+            params: { id: params.id },
+            setValuesForUpdate: entity => {
+                return {
+                    id: entity.id,
+                    summary: entity.summary,
+                    body: entity.body,
+                    rating: entity.rating,
+                    authorName: entity.authorName,
+                    authorLocation: entity.authorLocation,
+                    upvotes: entity.upvotes,
+                    downvotes: entity.downvotes,
+                };
+            },
+        });
+
+        return (
+            <Page>
+                <PageTitle>
+                    {!entity ? <Trans>New tax category</Trans> : (entity.name)}
+                </PageTitle>
+                <PageDetailForm form={form} submitHandler={submitHandler}>
+                    <PageActionBar>
+                        <PageActionBarRight>
+                            <PermissionGuard requires={['UpdateTaxCategory']}>
+                                <Button
+                                    type="submit"
+                                    disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                                >
+                                    <Trans>Update</Trans>
+                                </Button>
+                            </PermissionGuard>
+                        </PageActionBarRight>
+                    </PageActionBar>
+                    <PageLayout>
+                        <PageBlock column="main">
+                            <DetailFormGrid>
+                                <FormFieldWrapper
+                                    control={form.control}
+                                    name="summary"
+                                    label={<Trans>Summary</Trans>}
+                                    render={({ field }) => <Input {...field} />}
+                                />
+                                <FormFieldWrapper
+                                    control={form.control}
+                                    name="body"
+                                    label={<Trans>Is default tax category</Trans>}
+                                    render={({ field }) => <Input {...field} />}
+                                />
+                            </DetailFormGrid>
+                        </PageBlock>
+                        <CustomFieldsPageBlock
+                            column="main"
+                            entityType="TaxCategory"
+                            control={form.control}
+                        />
+                    </PageLayout>
+                </PageDetailForm>
+            </Page>
+        );
+    },
+};

+ 23 - 8
packages/dev-server/test-plugins/reviews/dashboard/review-list.tsx

@@ -1,4 +1,4 @@
-import { DashboardListRouteDefinition } from '@vendure/dashboard';
+import { DashboardRouteDefinition, DetailPageButton, ListPage } from '@vendure/dashboard';
 import gql from 'graphql-tag';
 
 const getReviewList = gql`
@@ -19,7 +19,7 @@ const getReviewList = gql`
                 }
                 summary
                 body
-                rating
+                rating  
                 authorName
                 authorLocation
                 upvotes
@@ -32,16 +32,31 @@ const getReviewList = gql`
     }
 `;
 
-export const reviewList: DashboardListRouteDefinition = {
+export const reviewList: DashboardRouteDefinition = {
     id: 'review-list',
-    title: 'Product Reviews!',
+    navMenuItem: {
+        sectionId: 'catalog',
+        id: 'reviews',
+        url: '/reviews',
+        title: 'Product Reviews',
+    },
     path: '/reviews',
-    navMenuItem: { sectionId: 'catalog' },
-    defaultVisibility: {
+    component: (route) => <ListPage title="Product Reviews" listQuery={getReviewList} route={route} defaultVisibility={{
         product: true,
         summary: true,
         rating: true,
         authorName: true,
-    },
-    listQuery: getReviewList,
+    }}
+    customizeColumns={{
+        product: {
+            header: 'Product',
+            cell: ({ row }) => {
+                return (
+                    <DetailPageButton id={row.original.id} label={row.original.product.name} />
+                );
+            },
+        },
+        
+    }}
+    />,
 };

+ 1 - 0
packages/dev-server/test-plugins/reviews/dashboard/tsconfig.json

@@ -1,5 +1,6 @@
 {
   "compilerOptions": {
     "module": "nodenext",
+    "jsx": "react-jsx"
   }
 }