Browse Source

feat(dashboard): Add vite plugin to generate gql.tada output

Michael Bromley 10 months ago
parent
commit
a76d9053fb

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

@@ -16,7 +16,7 @@ export function defineDashboardExtension(extension: DashboardExtension) {
                 const item: NavMenuItem = {
                     url: route.navMenuItem.url ?? route.path,
                     id: route.navMenuItem.id ?? route.id,
-                    title: route.navMenuItem.title ?? route.title,
+                    title: route.navMenuItem.title,
                 };
                 addNavMenuItem(item, route.navMenuItem.sectionId);
             }

+ 6 - 7
packages/dashboard/src/framework/extension-api/extension-api-types.ts

@@ -1,15 +1,14 @@
+import { PageBreadcrumb } from '@/components/layout/generated-breadcrumbs.js';
 import { NavMenuItem } from '@/framework/nav-menu/nav-menu.js';
-import { AnyRoute } from '@tanstack/react-router';
+import { AnyRoute, RouteOptions } from '@tanstack/react-router';
 import React from 'react';
 
-export interface DashboardBaseRouteDefinition {
+export interface DashboardRouteDefinition {
+    component: (route: AnyRoute) => React.ReactNode;
     id: string;
-    navMenuItem?: Partial<NavMenuItem> & { sectionId: string };
     path: string;
-}
-
-export interface DashboardRouteDefinition extends DashboardBaseRouteDefinition {
-    component: (route: AnyRoute) => React.ReactNode;
+    navMenuItem?: Partial<NavMenuItem> & { sectionId: string };
+    loader?: RouteOptions['loader'];
 }
 
 export interface DashboardExtension {

+ 135 - 0
packages/dashboard/src/framework/page/detail-page.tsx

@@ -0,0 +1,135 @@
+import { FormFieldWrapper } from '@/components/shared/form-field-wrapper.js';
+import { Input } from '@/components/ui/input.js';
+import { useDetailPage } from '@/framework/page/use-detail-page.js';
+import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
+import { Trans } from '@lingui/react/macro';
+import { AnyRoute } from '@tanstack/react-router';
+import { ResultOf, VariablesOf } from 'gql.tada';
+
+import { getOperationVariablesFields } from '../document-introspection/get-document-structure.js';
+import {
+    DetailFormGrid,
+    Page,
+    PageActionBar,
+    PageActionBarLeft,
+    PageActionBarRight,
+    PageBlock,
+    PageDetailForm,
+    PageLayout,
+    PageTitle,
+} from '../layout-engine/page-layout.js';
+import { DetailEntityPath } from './page-types.js';
+import { toast } from 'sonner';
+import { Button } from '@/components/ui/button.js';
+import { Checkbox } from '@/components/ui/checkbox.js';
+import { DateTimeInput } from '@/components/data-input/datetime-input.js';
+
+export interface DetailPageProps<
+    T extends TypedDocumentNode<any, any>,
+    C extends TypedDocumentNode<any, any>,
+    U extends TypedDocumentNode<any, any>,
+    EntityField extends keyof ResultOf<T> = DetailEntityPath<T>,
+> {
+    route: AnyRoute;
+    title: (entity: ResultOf<T>[EntityField]) => string;
+    queryDocument: T;
+    createDocument?: C;
+    updateDocument: U;
+    setValuesForUpdate: (entity: ResultOf<T>[EntityField]) => VariablesOf<U>['input'];
+}
+
+export function DetailPage<
+    T extends TypedDocumentNode<any, any>,
+    C extends TypedDocumentNode<any, any>,
+    U extends TypedDocumentNode<any, any>,
+>({
+    route,
+    queryDocument,
+    createDocument,
+    updateDocument,
+    setValuesForUpdate,
+    title,
+}: DetailPageProps<T, C, U>) {
+    const params = route.useParams();
+
+    const { form, submitHandler, entity, isPending, resetForm } = useDetailPage<any, any, any>({
+        queryDocument,
+        updateDocument,
+        createDocument,
+        params: { id: params.id },
+        setValuesForUpdate,
+        onSuccess: () => {
+            toast.success('Updated successfully');
+            resetForm();
+        },
+        onError: error => {
+            toast.error('Failed to update', {
+                description: error instanceof Error ? error.message : 'Unknown error',
+            });
+        },
+    });
+
+    const updateFields = getOperationVariablesFields(updateDocument);
+
+    return (
+        <Page>
+            <PageDetailForm form={form} submitHandler={submitHandler}>
+                <PageActionBar>
+                    <PageActionBarLeft>
+                        <PageTitle>{title(entity)}</PageTitle>
+                    </PageActionBarLeft>
+                    <PageActionBarRight>
+                        <Button
+                            type="submit"
+                            disabled={!form.formState.isDirty || !form.formState.isValid || isPending}
+                        >
+                            <Trans>Update</Trans>
+                        </Button>
+                    </PageActionBarRight>
+                </PageActionBar>
+                <PageLayout>
+                    <PageBlock column="main">
+                        <DetailFormGrid>
+                            {updateFields.map(fieldInfo => {
+                                console.log(fieldInfo);
+                                if (fieldInfo.name === 'id' && fieldInfo.type === 'ID') {
+                                    return null;
+                                }
+                                return (
+                                    <FormFieldWrapper
+                                        key={fieldInfo.name}
+                                        control={form.control}
+                                        name={fieldInfo.name as never}
+                                        label={fieldInfo.name}
+                                        render={({ field }) => {
+                                            switch (fieldInfo.type) {
+                                                case 'Int':
+                                                case 'Float':
+                                                    return (
+                                                        <Input
+                                                            type="number"
+                                                            value={field.value}
+                                                            onChange={e =>
+                                                                field.onChange(e.target.valueAsNumber)
+                                                            }
+                                                        />
+                                                    );
+                                                case 'DateTime':
+                                                    return <DateTimeInput {...field} />;
+                                                case 'Boolean':
+                                                    return <Checkbox {...field} />;
+                                                case 'String':
+                                                default:
+                                                    return <Input {...field} />;
+                                            }
+                                        }}
+                                    />
+                                );
+                            })}
+                        </DetailFormGrid>
+                    </PageBlock>
+                </PageLayout>
+            </PageDetailForm>
+        </Page>
+    );
+}

+ 3 - 1
packages/dashboard/src/framework/page/list-page.tsx

@@ -10,7 +10,7 @@ import {
     RowAction
 } from '@/components/shared/paginated-list-data-table.js';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
-import { AnyRouter, useNavigate } from '@tanstack/react-router';
+import { AnyRoute, AnyRouter, useNavigate } from '@tanstack/react-router';
 import { ColumnFiltersState, SortingState, Table } from '@tanstack/react-table';
 import { ResultOf } from 'gql.tada';
 import { Page, PageActionBar, PageTitle } from '../layout-engine/page-layout.js';
@@ -31,6 +31,8 @@ export interface ListPageProps<
     V extends ListQueryOptionsShape,
     AC extends AdditionalColumns<T>,
 > extends PageProps {
+    route: AnyRoute | (() => AnyRoute);
+    title: string | React.ReactElement;
     listQuery: T;
     deleteMutation?: TypedDocumentNode<any, { id: string }>;
     transformVariables?: (variables: V) => V;

+ 1 - 8
packages/dashboard/src/framework/page/page-types.ts

@@ -1,11 +1,4 @@
-import { TypedDocumentNode, ResultOf } from '@graphql-typed-document-node/core';
-import { AnyRoute } from '@tanstack/react-router';
-import React from 'react';
-
-export interface PageProps {
-    title: string | React.ReactElement;
-    route: AnyRoute | (() => AnyRoute);
-}
+import { ResultOf, TypedDocumentNode } from '@graphql-typed-document-node/core';
 
 // Type that identifies a paginated list structure (has items array and totalItems)
 type IsEntity<T> = T extends { id: string } ? true : false;

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

@@ -1,3 +1,4 @@
+import { ErrorPage } from '@/components/shared/error-page.js';
 import { useDashboardExtensions } from '@/framework/extension-api/use-dashboard-extensions.js';
 import { ListPage } from '@/framework/page/list-page.js';
 import { extensionRoutes } from '@/framework/page/page-api.js';
@@ -42,10 +43,9 @@ export const useExtendedRouter = (router: Router<AnyRoute, any, any>) => {
             const newRoute = createRoute({
                 path: `/${pathWithoutLeadingSlash}`,
                 getParentRoute: () => authenticatedRoute,
-                loader: () => ({
-                    breadcrumb: config.title,
-                }),
+                loader: config.loader,
                 component: () => config.component(newRoute),
+                errorComponent: ({ error }) => <ErrorPage message={error.message} />,
             });
             newRoutes.push(newRoute);
         }

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

@@ -1,5 +1,5 @@
 // This file is auto-generated. Do not edit manually.
-// Generated on: 2025-03-26T10:58:17.182Z
+// Generated on: 2025-03-26T14:10:22.200Z
 
 export * from './components/data-display/boolean.js';
 export * from './components/data-display/date-time.js';
@@ -112,6 +112,7 @@ 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/detail-page.js';
 export * from './framework/page/list-page.js';
 export * from './framework/page/page-api.js';
 export * from './framework/page/page-types.js';

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

@@ -59,6 +59,10 @@ export function FacetDetailPage() {
             });
         },
     });
+    
+    if (!entity) {
+        return null;
+    }
 
     return (
         <Page>

+ 1 - 1
packages/dashboard/vite/config-loader.ts

@@ -34,7 +34,7 @@ export async function loadVendureConfig(
     const outputPath = tempDir;
     const configFileName = path.basename(vendureConfigPath);
     await fs.remove(outputPath);
-    await compileFile(vendureConfigPath, path.join(import.meta.dirname, './.vendure-dashboard-temp'));
+    await compileFile(vendureConfigPath, outputPath);
     const compiledConfigFilePath = pathToFileURL(path.join(outputPath, configFileName)).href.replace(
         /.ts$/,
         '.js',

+ 40 - 0
packages/dashboard/vite/schema-generator.ts

@@ -0,0 +1,40 @@
+import { GraphQLTypesLoader } from '@nestjs/graphql';
+import {
+    resetConfig,
+    setConfig,
+    getConfig,
+    runPluginConfigurations,
+    getFinalVendureSchema,
+    VENDURE_ADMIN_API_TYPE_PATHS,
+    VendureConfig,
+} from '@vendure/core';
+import { buildSchema } from 'graphql';
+import { GraphQLSchema } from 'graphql';
+
+let schemaPromise: Promise<GraphQLSchema>;
+
+export async function generateSchema({
+    vendureConfig,
+}: {
+    vendureConfig: VendureConfig;
+}): Promise<GraphQLSchema> {
+    if (!schemaPromise) {
+        schemaPromise = new Promise(async (resolve, reject) => {
+            resetConfig();
+            await setConfig(vendureConfig ?? {});
+
+            const runtimeConfig = await runPluginConfigurations(getConfig() as any);
+            const typesLoader = new GraphQLTypesLoader();
+            const finalSchema = await getFinalVendureSchema({
+                config: runtimeConfig,
+                typePaths: VENDURE_ADMIN_API_TYPE_PATHS,
+                typesLoader,
+                apiType: 'admin',
+                output: 'sdl',
+            });
+            const safeSchema = buildSchema(finalSchema);
+            resolve(safeSchema);
+        });
+    }
+    return schemaPromise;
+}

+ 2 - 14
packages/dashboard/vite/vite-plugin-admin-api-schema.ts

@@ -21,6 +21,7 @@ import {
 } from 'graphql';
 import { Plugin } from 'vite';
 
+import { generateSchema } from './schema-generator.js';
 import { ConfigLoaderApi, getConfigLoaderApi } from './vite-plugin-config-loader.js';
 
 export type FieldInfoTuple = readonly [
@@ -62,20 +63,7 @@ export function adminApiSchemaPlugin(): Plugin {
         async buildStart() {
             const vendureConfig = await configLoaderApi.getVendureConfig();
             if (!schemaInfo) {
-                this.info(`Constructing Admin API schema...`);
-                resetConfig();
-                await setConfig(vendureConfig ?? {});
-
-                const runtimeConfig = await runPluginConfigurations(getConfig() as any);
-                const typesLoader = new GraphQLTypesLoader();
-                const finalSchema = await getFinalVendureSchema({
-                    config: runtimeConfig,
-                    typePaths: VENDURE_ADMIN_API_TYPE_PATHS,
-                    typesLoader,
-                    apiType: 'admin',
-                    output: 'sdl',
-                });
-                const safeSchema = buildSchema(finalSchema);
+                const safeSchema = await generateSchema({ vendureConfig });
                 schemaInfo = generateSchemaInfo(safeSchema);
             }
         },

+ 4 - 6
packages/dashboard/vite/vite-plugin-config-loader.ts

@@ -15,9 +15,7 @@ export const configLoaderName = 'vendure:config-loader';
  */
 export function configLoaderPlugin(options: ConfigLoaderOptions): Plugin {
     let vendureConfig: VendureConfig;
-    let onConfigLoaded = () => {
-        /* */
-    };
+    const onConfigLoaded: Array<() => void> = [];
     return {
         name: configLoaderName,
         async buildStart() {
@@ -35,7 +33,7 @@ export function configLoaderPlugin(options: ConfigLoaderOptions): Plugin {
                     this.error(`Error loading Vendure config: ${e.message}`);
                 }
             }
-            onConfigLoaded();
+            onConfigLoaded.forEach(fn => fn());
         },
         api: {
             getVendureConfig(): Promise<VendureConfig> {
@@ -43,9 +41,9 @@ export function configLoaderPlugin(options: ConfigLoaderOptions): Plugin {
                     return Promise.resolve(vendureConfig);
                 } else {
                     return new Promise<VendureConfig>(resolve => {
-                        onConfigLoaded = () => {
+                        onConfigLoaded.push(() => {
                             resolve(vendureConfig);
-                        };
+                        });
                     });
                 }
             },

+ 62 - 0
packages/dashboard/vite/vite-plugin-gql-tada.ts

@@ -0,0 +1,62 @@
+import { generateOutput } from '@gql.tada/cli-utils';
+import * as fs from 'fs/promises';
+import { printSchema } from 'graphql';
+import * as path from 'path';
+import { Plugin } from 'vite';
+
+import { generateSchema } from './schema-generator.js';
+import { ConfigLoaderApi, getConfigLoaderApi } from './vite-plugin-config-loader.js';
+
+export function gqlTadaPlugin(options: {
+    gqlTadaOutputPath: string;
+    tempDir: string;
+    packageRoot: string;
+}): Plugin {
+    let configLoaderApi: ConfigLoaderApi;
+
+    return {
+        name: 'vendure:gql-tada',
+        configResolved({ plugins }) {
+            configLoaderApi = getConfigLoaderApi(plugins);
+        },
+        async buildStart() {
+            const vendureConfig = await configLoaderApi.getVendureConfig();
+            const safeSchema = await generateSchema({ vendureConfig });
+
+            const tsConfigContent = {
+                compilerOptions: {
+                    plugins: [
+                        {
+                            name: 'gql.tada/ts-plugin',
+                            schema: './schema.graphql',
+                        },
+                    ],
+                },
+            };
+
+            const tsConfigPath = path.join(options.tempDir, 'tsconfig.json');
+            await fs.writeFile(tsConfigPath, JSON.stringify(tsConfigContent, null, 2));
+
+            const schemaPath = path.join(options.tempDir, 'schema.graphql');
+            await fs.writeFile(schemaPath, printSchema(safeSchema));
+
+            await generateOutput({
+                output: path.join(options.gqlTadaOutputPath, 'graphql-env.d.ts'),
+                tsconfig: tsConfigPath,
+            });
+
+            // Copy the graphql.ts file to the output path
+            const graphqlTsPath = path.join(options.packageRoot, 'src/graphql/graphql.ts');
+            try {
+                await fs.copyFile(graphqlTsPath, path.join(options.gqlTadaOutputPath, 'graphql.ts'));
+            } catch (error) {
+                if (error instanceof Error) {
+                    this.error(error.message);
+                } else {
+                    this.error('Failed to copy graphql.ts file');
+                }
+            }
+            this.info('graphql introspection files output to ' + options.gqlTadaOutputPath);
+        },
+    };
+}

+ 1 - 3
packages/dashboard/vite/vite-plugin-ui-config.ts

@@ -1,6 +1,4 @@
-import { AdminUiPlugin, AdminUiPluginOptions } from '@vendure/admin-ui-plugin';
-import { AdminUiConfig, Type, VendureConfig } from '@vendure/core';
-import { getPluginDashboardExtensions } from '@vendure/core';
+import { AdminUiConfig, VendureConfig } from '@vendure/core';
 import path from 'path';
 import { Plugin } from 'vite';
 

+ 5 - 0
packages/dashboard/vite/vite-plugin-vendure-dashboard.ts

@@ -9,6 +9,7 @@ import { PluginOption } from 'vite';
 import { adminApiSchemaPlugin } from './vite-plugin-admin-api-schema.js';
 import { configLoaderPlugin } from './vite-plugin-config-loader.js';
 import { dashboardMetadataPlugin } from './vite-plugin-dashboard-metadata.js';
+import { gqlTadaPlugin } from './vite-plugin-gql-tada.js';
 import { setRootPlugin } from './vite-plugin-set-root.js';
 import { UiConfigPluginOptions, uiConfigPlugin } from './vite-plugin-ui-config.js';
 
@@ -28,6 +29,7 @@ export type VitePluginVendureDashboardOptions = {
      * This is only required if the plugin is unable to auto-detect the name of the exported variable.
      */
     vendureConfigExport?: string;
+    gqlTadaOutputPath?: string;
 } & UiConfigPluginOptions;
 
 /**
@@ -55,6 +57,9 @@ export function vendureDashboardPlugin(options: VitePluginVendureDashboardOption
         adminApiSchemaPlugin(),
         dashboardMetadataPlugin({ rootDir: tempDir }),
         uiConfigPlugin({ adminUiConfig: options.adminUiConfig }),
+        ...(options.gqlTadaOutputPath
+            ? [gqlTadaPlugin({ gqlTadaOutputPath: options.gqlTadaOutputPath, tempDir, packageRoot })]
+            : []),
     ];
 }
 

File diff suppressed because it is too large
+ 53 - 0
packages/dev-server/graphql/graphql-env.d.ts


+ 15 - 0
packages/dev-server/graphql/graphql.ts

@@ -0,0 +1,15 @@
+import type { introspection } from './graphql-env.d.ts';
+import { initGraphQLTada } from 'gql.tada';
+
+export const graphql = initGraphQLTada<{
+    disableMasking: true;
+    introspection: introspection;
+    scalars: {
+        DateTime: string;
+        JSON: any;
+        Money: number;
+    };
+}>();
+
+export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada';
+export { readFragment } from 'gql.tada';

+ 32 - 92
packages/dev-server/test-plugins/reviews/dashboard/review-detail.tsx

@@ -1,26 +1,11 @@
+import { graphql } from '@/graphql/graphql';
 import {
     DashboardRouteDefinition,
-    FormFieldWrapper,
-    PageBlock,
-    PageLayout,
-    PageTitle,
-    Page,
-    PageDetailForm,
-    PageActionBar,
-    PageActionBarRight,
-    PermissionGuard,
-    Button,
-    DetailFormGrid,
-    useDetailPage,
-    CustomFieldsPageBlock,
-    Switch,
-    Input,
+    DetailPage,
+    detailPageRouteLoader
 } from '@vendure/dashboard';
-import { Trans } from '@lingui/react/macro';
 
-import gql from 'graphql-tag';
-
-const reviewDetailDocument = gql`
+const reviewDetailDocument = graphql(`
     query GetReviewDetail($id: ID!) {
         productReview(id: $id) {
             id
@@ -47,91 +32,46 @@ const reviewDetailDocument = gql`
             responseCreatedAt
         }
     }
-`;
+`);
 
-const updateReviewDocument = gql`
+const updateReviewDocument = graphql(`
     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',
+    loader: detailPageRouteLoader({
+        queryDocument: reviewDetailDocument,
+        breadcrumb: (isNew, entity) => [
+            { path: '/reviews', label: 'Reviews' },
+            isNew ? 'New review' : entity?.summary,
+        ],
+    }),
     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>
+            <DetailPage
+                queryDocument={reviewDetailDocument}
+                updateDocument={updateReviewDocument}
+                route={route}
+                title={review => review.summary}
+                setValuesForUpdate={review => {
+                    return {
+                        id: review.id,
+                        summary: review.summary,
+                        body: review.body,
+                        rating: review.rating,
+                        authorName: review.authorName,
+                        authorLocation: review.authorLocation,
+                        upvotes: review.upvotes,
+                        downvotes: review.downvotes,
+                    };
+                }}
+            />
         );
     },
 };

+ 30 - 24
packages/dev-server/test-plugins/reviews/dashboard/review-list.tsx

@@ -1,9 +1,9 @@
+import { graphql } from '@/graphql/graphql';
 import { DashboardRouteDefinition, DetailPageButton, ListPage } from '@vendure/dashboard';
-import gql from 'graphql-tag';
 
-const getReviewList = gql`
-    query GetProductReviews {
-        productReviews {
+const getReviewList = graphql(`
+    query GetProductReviews($options: ProductReviewListOptions) {
+        productReviews(options: $options) {
             items {
                 id
                 createdAt
@@ -19,7 +19,7 @@ const getReviewList = gql`
                 }
                 summary
                 body
-                rating  
+                rating
                 authorName
                 authorLocation
                 upvotes
@@ -30,7 +30,7 @@ const getReviewList = gql`
             }
         }
     }
-`;
+`);
 
 export const reviewList: DashboardRouteDefinition = {
     id: 'review-list',
@@ -41,22 +41,28 @@ export const reviewList: DashboardRouteDefinition = {
         title: 'Product Reviews',
     },
     path: '/reviews',
-    component: (route) => <ListPage title="Product Reviews" listQuery={getReviewList} route={route} defaultVisibility={{
-        product: true,
-        summary: true,
-        rating: true,
-        authorName: true,
-    }}
-    customizeColumns={{
-        product: {
-            header: 'Product',
-            cell: ({ row }) => {
-                return (
-                    <DetailPageButton id={row.original.id} label={row.original.product.name} />
-                );
-            },
-        },
-        
-    }}
-    />,
+    loader: () => ({
+        breadcrumb: 'Reviews',
+    }),
+    component: route => (
+        <ListPage
+            title="Product Reviews"
+            listQuery={getReviewList}
+            route={route}
+            defaultVisibility={{
+                product: true,
+                summary: true,
+                rating: true,
+                authorName: true,
+            }}
+            customizeColumns={{
+                product: {
+                    header: 'Product',
+                    cell: ({ row }) => {
+                        return <DetailPageButton id={row.original.id} label={row.original.product.name} />;
+                    },
+                },
+            }}
+        />
+    ),
 };

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

@@ -1,6 +1,9 @@
 {
   "compilerOptions": {
     "module": "nodenext",
-    "jsx": "react-jsx"
+    "jsx": "react-jsx",
+    "paths": {
+      "@/graphql/*": ["../../../graphql/*"]
+    }
   }
 }

+ 1 - 0
packages/dev-server/vite.config.mts

@@ -13,6 +13,7 @@ export default defineConfig({
         vendureDashboardPlugin({
             vendureConfigPath: pathToFileURL('./dev-config.ts'),
             adminUiConfig: { apiHost: 'http://localhost', apiPort: 3000 },
+            gqlTadaOutputPath: path.resolve(__dirname, './graphql/'),
         }) as any,
     ],
 });

Some files were not shown because too many files changed in this diff