Просмотр исходного кода

docs: Add guide to new Dashboard

Michael Bromley 8 месяцев назад
Родитель
Сommit
6eb7071cfb

BIN
docs/docs/guides/extending-the-dashboard/getting-started/detail-view.webp


BIN
docs/docs/guides/extending-the-dashboard/getting-started/dev-mode.webp


+ 567 - 0
docs/docs/guides/extending-the-dashboard/getting-started/index.md

@@ -0,0 +1,567 @@
+---
+title: 'Getting Started'
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+:::warning
+The `@vendure/dashboard` package is currently **beta** and is not yet recommended for production use. The API may change in future releases.
+:::
+
+Our new React-based dashboard is currently in beta, and you can try it out now!
+
+The goal of the new dashboard:
+
+- Improve the developer experience to make it significantly easier and faster to build customizations
+- Reduce boilerplate (repetitive code) by using schema-driven UI generation
+- Modern, AI-ready stack using React, Tailwind & Shadcn.
+
+Because the dashboard is in beta, not all planned features are available yet. However, enough has been implemented that
+you can try it out and give us feedback.
+
+## Installation & Setup
+
+:::note
+This guide assumes an existing project based on the `@vendure/create` folder structure.
+If you have a different setup (e.g. an Nx monorepo), you may need to adapt the instructions accordingly.
+:::
+
+First install the `@vendure/dashboard` package:
+
+```bash
+npm install @vendure/dashboard
+```
+
+Then create a `vite.config.mts` file in the root of your project with the following content:
+
+```ts title="vite.config.mts"
+import { vendureDashboardPlugin } from '@vendure/dashboard/plugin';
+import { pathToFileURL } from 'url';
+import { defineConfig } from 'vite';
+import { resolve, join } from 'path';
+
+export default defineConfig({
+    build: {
+        outDir: join(__dirname, 'dist/dashboard')
+    },
+    plugins: [
+        vendureDashboardPlugin({
+            vendureConfigPath: pathToFileURL('./src/vendure-config.ts'),
+            adminUiConfig: { vapiHost: 'http://localhost', apiPort: 3000 },
+            gqlTadaOutputPath: './src/gql',
+        }),
+    ],
+    resolve: {
+        alias: {
+            // This allows all plugins to reference a shared set of
+            // GraphQL types.
+            '@/gql': resolve(__dirname, './src/gql/graphql.ts'),
+        }
+    }
+});
+```
+
+You should also add the following to your `tsconfig.json` file to allow your IDE
+to correctly resolve imports of GraphQL types & interpret JSX in your dashboard extensions:
+
+```json title="tsconfig.json"
+{
+    "compilerOptions": {
+        // ... existing options
+        // highlight-start
+        "jsx": "react-jsx",
+        "paths": {
+            "@/gql": ["./src/gql/graphql.ts"]
+        }
+        // highlight-end
+    }
+}
+```
+
+## Running the Dashboard
+
+Now you can run the dashboard in development mode with:
+
+```bash
+npx vite
+```
+
+To stop the running dashboard, type `q` and hit enter.
+
+## Dev Mode
+
+Once you have logged in to the dashboard, you can toggle on "Dev Mode" using the user menu in the bottom left:
+
+![Dev Mode](./dev-mode.webp)
+
+In Dev Mode, hovering any block in the dashboard will allow you to find the corresponding `pageId` and `blockId` values,
+which you can later use when customizing the dashboard.
+
+![Finding the location ids](./location-id.webp)
+
+## Extending the Dashboard
+
+Follow this guide to see how to extend the dashboard with custom pages, blocks, and components.
+
+We will create a brand new `CmsPlugin` that implements a simple content management system (CMS) for Vendure in
+order to demonstrate how to extend the dashboard.
+
+### Creating the plugin
+
+Let's create the plugin:
+
+```bash
+npx vendure add
+```
+- `Which feature would you like to add?`: `[Plugin] Create a new Vendure plugin`
+- `What is the name of the plugin?`: `cms`
+- `Add features to cms?`: `[Plugin: Entity] Add a new entity to a plugin`
+- `What is the name of the custom entity?`: `Article`
+- `Entity features`: (hit enter to keep defaults)
+- `[Finish] No, I'm done!`
+
+You now have you `CmsPlugin` created with a new `Article` entity. You can find the plugin in the `./src/plugins/cms` directory.
+
+Let's edit the entity to add the appropriate fields:
+
+```ts title="src/plugins/cms/entities/article.entity.ts"
+import {
+    DeepPartial,
+    HasCustomFields,
+    VendureEntity,
+} from '@vendure/core';
+import { Column, Entity } from 'typeorm';
+
+export class ArticleCustomFields {}
+
+@Entity()
+export class Article extends VendureEntity implements HasCustomFields {
+    constructor(input?: DeepPartial<Article>) {
+        super(input);
+    }
+
+    @Column()
+    slug: string;
+
+    @Column()
+    title: string;
+
+    @Column('text')
+    body: string;
+
+    @Column()
+    isPublished: boolean;
+
+    @Column(type => ArticleCustomFields)
+    customFields: ArticleCustomFields;
+}
+```
+
+Now let's create a new `ArticleService` to handle the business logic of our new entity:
+
+```bash
+npx vendure add
+```
+
+- `Which feature would you like to add?`: `[Plugin: Service] Add a new service to a plugin`
+- `To which plugin would you like to add the feature?`: `CmsPlugin`
+- `What type of service would you like to add?`: `Service to perform CRUD operations on an entity`
+- `Select an entity`: `Article`
+
+The service will be created in the `./src/plugins/cms/services` directory.
+
+Finally, we'll extend the GraphQL API to expose those CRUD operations:
+
+```bash
+npx vendure add
+```
+
+- `Which feature would you like to add?`: `[Plugin: API] Adds GraphQL API extensions to a plugin`
+- `To which plugin would you like to add the feature?`: `CmsPlugin`
+- `Which service contains the business logic for this API extension?`: `ArticleService: (CRUD service for Article)`
+
+Now the api extensions and resolver has been created in the `./src/plugins/cms/api-extensions` directory.
+
+The last step is to create a migration for our newly-created entity:
+
+```bash
+npx vendure migrate
+```
+
+- `What would you like to do?`: `Generate a new migration`
+- `Enter a meaningful name for the migration`: `article`
+- `Migration file location`: (pick the first option in the `src/migrations` dir)
+
+
+### Setting up Dashboard extensions
+
+Dashboard extensions are declared directly on the plugin metadata. Unlike the old AdminUiPlugin, you do not need to separately
+declare ui extensions anywhere except on the plugin itself.
+
+```ts title="src/plugins/cms/cms.plugin.ts"
+@VendurePlugin({
+    // ...
+    entities: [Article],
+    adminApiExtensions: {
+        schema: adminApiExtensions,
+        resolvers: [ArticleAdminResolver]
+    },
+    // highlight-next-line
+    dashboard: './dashboard/index.tsx',
+})
+export class CmsPlugin {
+  // ...
+}
+```
+
+Now we'll create the entry point of our dashboard extension:
+
+```tsx title="src/plugins/cms/dashboard/index.tsx"
+import { defineDashboardExtension } from '@vendure/dashboard';
+
+export default defineDashboardExtension({
+    // Let's add a simple test page to check things are working
+    routes: [
+        {
+            component: () => <div>Test Page Works!</div>,
+            path: '/test',
+            navMenuItem: {
+                id: 'test',
+                title: 'Test Page',
+                sectionId: 'catalog'
+            }
+        }
+    ]
+})
+```
+
+Restart the Vite server (`q, enter` to quit if still running), and then you should be able to see your new test page!
+
+![Test Page](./test-page.webp)
+
+
+
+### Creating a list page
+
+Now that the test page is working, let's create a list page for our `Article` entity.
+
+First we'll create a new `article-list.tsx` file in the `./src/plugins/cms/dashboard` directory:
+
+```tsx title="src/plugins/cms/dashboard/article-list.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
+            }
+        }
+    }
+`);
+
+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}/>;
+                    },
+                }
+            }}
+        >
+            <PageActionBarRight>
+                <Button asChild>
+                    <Link to="./new">
+                        <PlusIcon className="mr-2 h-4 w-4"/>
+                        New article
+                    </Link>
+                </Button>
+            </PageActionBarRight>
+        </ListPage>
+    ),
+};
+```
+
+Let's register this route (and we can also remove the test page) in our `index.tsx` file:
+
+```tsx title="src/plugins/cms/dashboard/index.tsx"
+import { defineDashboardExtension } from '@vendure/dashboard';
+
+// highlight-next-line
+import { articleList } from "./article-list";
+
+export default defineDashboardExtension({
+    routes: [
+        // highlight-next-line
+        articleList,
+    ],
+});
+```
+
+You should now be able to see the list view, which will be empty:
+
+![Empty List](./list-view-empty.webp)
+
+### Creating a detail page
+
+Now let's create a detail page so we can start adding articles.
+
+We'll begin with the simplest approach, where the form will be auto-generated for us based on the GraphQL schema. 
+This is useful for quickly getting started, but you will probably want to customize the form later on.
+
+Create a new file called `article-detail.tsx` in the `./src/plugins/cms/dashboard` directory:
+
+```tsx title="src/plugins/cms/dashboard/article-detail.tsx"
+import {
+    DashboardRouteDefinition,
+    DetailPage,
+    detailPageRouteLoader
+} from '@vendure/dashboard';
+import { graphql } from '@/gql';
+
+const articleDetailDocument = graphql(`
+    query GetArticleDetail($id: ID!) {
+        article(id: $id) {
+            id
+            createdAt
+            updatedAt
+            isPublished
+            title
+            slug
+            body
+            customFields
+        }
+    }
+`);
+
+const createArticleDocument = graphql(`
+    mutation CreateArticle($input: CreateArticleInput!) {
+        createArticle(input: $input) {
+            id
+        }
+    }
+`);
+
+const updateArticleDocument = graphql(`
+    mutation UpdateArticle($input: UpdateArticleInput!) {
+        updateArticle(input: $input) {
+            id
+        }
+    }
+`);
+
+export const articleDetail: DashboardRouteDefinition = {
+    path: '/articles/$id',
+    loader: detailPageRouteLoader({
+        queryDocument: articleDetailDocument,
+        breadcrumb: (isNew, entity) => [
+            { path: '/articles', label: 'Articles' },
+            isNew ? 'New article' : entity?.title,
+        ],
+    }),
+    component: route => {
+        return (
+            <DetailPage
+                pageId="article-detail"
+                queryDocument={articleDetailDocument}
+                createDocument={createArticleDocument}
+                updateDocument={updateArticleDocument}
+                route={route}
+                title={article => article?.title ?? 'New article'}
+                setValuesForUpdate={article => {
+                    return {
+                        id: article?.id ?? '',
+                        isPublished: article?.isPublished ?? false,
+                        title: article?.title ?? '',
+                        slug: article?.slug ?? '',
+                        body: article?.body ?? '',
+                    };
+                }}
+            />
+        );
+    },
+};
+```
+
+Now we can register this route in our `index.tsx` file:
+
+```tsx title="src/plugins/cms/dashboard/index.tsx"
+import {defineDashboardExtension} from '@vendure/dashboard';
+
+import {articleList} from "./article-list";
+// highlight-next-line
+import {articleDetail} from "./article-detail";
+
+export default defineDashboardExtension({
+    routes: [
+        articleList,
+        // highlight-next-line
+        articleDetail,
+    ]
+});
+```
+
+You should now be able to click on the "New article" button in the list view, and see the detail page:
+![Detail Page](./detail-view.webp)
+
+Congratulations! You can now add, edit and delete articles in the dashboard.
+
+![List view with entries](./list-view-full.webp)
+
+### Defining page blocks
+
+In the Dashboard, all pages are build from blocks. Every block has a `pageId` and a `blockId` which uniquely locates it in the
+app (see earlier section on Dev Mode).
+
+You can also define your own blocks, which can be added to any page and can even replace the default blocks. 
+
+Here's an example which you can add to the existing `index.tsx` file:
+
+```tsx title="src/plugins/cms/dashboard/index.tsx"
+import { defineDashboardExtension } from '@vendure/dashboard';
+
+import { articleList } from "./article-list";
+import { articleDetail } from "./article-detail";
+
+export default defineDashboardExtension({
+    routes: [
+        articleList,
+        articleDetail,
+    ],
+    // highlight-start
+    pageBlocks: [
+        {
+            id: 'related-articles',
+            title: 'Related Articles',
+            location: {
+                // This is the pageId of the page where this block will be
+                pageId: 'product-detail',
+                // can be "main" or "side"
+                column: 'side',
+                position: {
+                    // Blocks are positioned relative to existing blocks on
+                    // the page.
+                    blockId: 'facet-values',
+                    // Can be "before", "after" or "replace"
+                    // Here we'll place it after the `facet-values` block.
+                    order: 'after'
+                }
+            },
+            component: ({ context }) => {
+                // In the component, you can use the `context` prop to
+                // access the entity and the form instance.
+                return (
+                    <div className="text-sm">
+                        Articles related to {context.entity.name}
+                    </div>
+                );
+            }
+        }
+    ],
+    // highlight-end
+});
+```
+
+This will look this:
+
+![Related Articles page block](./page-block.webp)
+
+### Adding action bar items
+
+Like in the old Admin UI, we have the concept of the "action bar", which is the bar at the top of the page where you can add
+buttons and other actions.
+
+Currently, we only support adding buttons, but dropdown menu support is coming soon.
+
+```tsx title="src/plugins/cms/dashboard/index.tsx"
+import { Button, defineDashboardExtension } from '@vendure/dashboard';
+import { useState } from 'react';
+
+export default defineDashboardExtension({
+    actionBarItems: [
+        {
+            pageId: 'product-detail',
+            component: ({ context }) => {
+                const [count, setCount] = useState(0);
+                return (<Button type="button" 
+                               variant="secondary" 
+                               onClick={() => setCount(x => x + 1)}>
+                    Counter: { count }
+                </Button>);
+            }
+        }
+    ],
+});
+```
+
+## Tech Stack
+
+In your custom components, you also have access to the following libraries and tools which the Dashboard is built upon:
+
+- Tailwind CSS
+- Shadcn UI
+- React Query
+- Tanstack Router
+- React Hook Form
+
+## API Reference
+
+A partial API reference of the new Dashboard API can be found here:
+
+- [Dashboard API Reference](/reference/dashboard/extensions/)
+
+## Still to come
+
+We hope this gives you a taste of what is possible with the new dashboard.
+
+We're still working to bring feature-parity with the existing Admin UI - so support for things like:
+
+- bulk actions, 
+- history timeline components
+- theming & branding
+- translations
+
+The final release (expected Q3 2025) will also include much more extensive documentation & guides.

BIN
docs/docs/guides/extending-the-dashboard/getting-started/list-view-empty.webp


BIN
docs/docs/guides/extending-the-dashboard/getting-started/list-view-full.webp


BIN
docs/docs/guides/extending-the-dashboard/getting-started/location-id.webp


BIN
docs/docs/guides/extending-the-dashboard/getting-started/page-block.webp


BIN
docs/docs/guides/extending-the-dashboard/getting-started/test-page.webp


+ 27 - 0
docs/sidebars.js

@@ -22,6 +22,15 @@ const icon = {
     cloudArrowUp: `<path fill-rule="evenodd" d="M5.5 17a4.5 4.5 0 01-1.44-8.765 4.5 4.5 0 018.302-3.046 3.5 3.5 0 014.504 4.272A4 4 0 0115 17H5.5zm3.75-2.75a.75.75 0 001.5 0V9.66l1.95 2.1a.75.75 0 101.1-1.02l-3.25-3.5a.75.75 0 00-1.1 0l-3.25 3.5a.75.75 0 101.1 1.02l1.95-2.1v4.59z" clip-rule="evenodd" />`,
     tsLogo: `<rect fill="#3178c6" height="128" rx="6" width="128"></rect><path clip-rule="evenodd" d="m74.2622 99.468v14.026c2.2724 1.168 4.9598 2.045 8.0625 2.629 3.1027.585 6.3728.877 9.8105.877 3.3503 0 6.533-.321 9.5478-.964 3.016-.643 5.659-1.702 7.932-3.178 2.272-1.476 4.071-3.404 5.397-5.786 1.325-2.381 1.988-5.325 1.988-8.8313 0-2.5421-.379-4.7701-1.136-6.6841-.758-1.9139-1.85-3.6159-3.278-5.1062-1.427-1.4902-3.139-2.827-5.134-4.0104-1.996-1.1834-4.246-2.3011-6.752-3.353-1.8352-.7597-3.4812-1.4975-4.9378-2.2134-1.4567-.7159-2.6948-1.4464-3.7144-2.1915-1.0197-.7452-1.8063-1.5341-2.3598-2.3669-.5535-.8327-.8303-1.7751-.8303-2.827 0-.9643.2476-1.8336.7429-2.6079s1.1945-1.4391 2.0976-1.9943c.9031-.5551 2.0101-.9861 3.3211-1.2929 1.311-.3069 2.7676-.4603 4.3699-.4603 1.1658 0 2.3958.0877 3.6928.263 1.296.1753 2.6.4456 3.911.8109 1.311.3652 2.585.8254 3.824 1.3806 1.238.5552 2.381 1.198 3.43 1.9285v-13.1051c-2.127-.8182-4.45-1.4245-6.97-1.819s-5.411-.5917-8.6744-.5917c-3.3211 0-6.4674.3579-9.439 1.0738-2.9715.7159-5.5862 1.8336-7.844 3.353-2.2578 1.5195-4.0422 3.4553-5.3531 5.8075-1.311 2.3522-1.9665 5.1646-1.9665 8.4373 0 4.1785 1.2017 7.7433 3.6052 10.6945 2.4035 2.9513 6.0523 5.4496 10.9466 7.495 1.9228.7889 3.7145 1.5633 5.375 2.323 1.6606.7597 3.0954 1.5486 4.3044 2.3668s2.1628 1.7094 2.8618 2.6736c.7.9643 1.049 2.06 1.049 3.2873 0 .9062-.218 1.7462-.655 2.5202s-1.1 1.446-1.9885 2.016c-.8886.57-1.9956 1.016-3.3212 1.337-1.3255.321-2.8768.482-4.6539.482-3.0299 0-6.0305-.533-9.0021-1.6-2.9715-1.066-5.7245-2.666-8.2591-4.799zm-23.5596-34.9136h18.2974v-11.5544h-51v11.5544h18.2079v51.4456h14.4947z" fill="#fff" fill-rule="evenodd"></path>`,
     graphqlLogo: `<path fill-rule="evenodd" fill="#e10098" clip-rule="evenodd" d="M50 6.90308L87.323 28.4515V71.5484L50 93.0968L12.677 71.5484V28.4515L50 6.90308ZM16.8647 30.8693V62.5251L44.2795 15.0414L16.8647 30.8693ZM50 13.5086L18.3975 68.2457H81.6025L50 13.5086ZM77.4148 72.4334H22.5852L50 88.2613L77.4148 72.4334ZM83.1353 62.5251L55.7205 15.0414L83.1353 30.8693V62.5251Z"></path><circle fill="#e10098"  cx="50" cy="9.3209" r="8.82"></circle><circle fill="#e10098"  cx="85.2292" cy="29.6605" r="8.82"></circle><circle fill="#e10098"  cx="85.2292" cy="70.3396" r="8.82"></circle><circle fill="#e10098"  cx="50" cy="90.6791" r="8.82"></circle><circle fill="#e10098"  cx="14.7659" cy="70.3396" r="8.82"></circle><circle fill="#e10098"  cx="14.7659" cy="29.6605" r="8.82"></circle>`,
+    reactLogo: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-11.5 -10.23174 23 20.46348">
+  <title>React Logo</title>
+  <circle cx="0" cy="0" r="2.05" fill="currentColor"/>
+  <g stroke="currentColor" stroke-width="1" fill="none">
+    <ellipse rx="11" ry="4.2"/>
+    <ellipse rx="11" ry="4.2" transform="rotate(60)"/>
+    <ellipse rx="11" ry="4.2" transform="rotate(120)"/>
+  </g>
+</svg>`
 };
 
 /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
@@ -158,6 +167,16 @@ const sidebars = {
                 'guides/extending-the-admin-ui/creating-detail-views/index',
             ],
         },
+        {
+            type: 'category',
+            label: 'Dashboard (BETA)',
+            customProps: {
+                icon: icon.reactLogo,
+            },
+            items: [
+                'guides/extending-the-dashboard/getting-started/index',
+            ]
+        },
         {
             type: 'category',
             label: 'Building a Storefront',
@@ -332,6 +351,14 @@ const sidebars = {
                 icon: icon.computer,
             },
         },
+        {
+            type: 'category',
+            label: 'Dashboard API',
+            items: [{ type: 'autogenerated', dirName: 'reference/dashboard' }],
+            customProps: {
+                icon: icon.reactLogo,
+            },
+        },
     ],
     userGuideSidebar: [
         {