Преглед на файлове

feat(dashboard): Implement extensions for list pages

Michael Bromley преди 9 месеца
родител
ревизия
19267405b3

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

@@ -7,7 +7,6 @@ import { PageContext } from '../layout-engine/page-layout.js';
 
 export interface DashboardRouteDefinition {
     component: (route: AnyRoute) => React.ReactNode;
-    id: string;
     path: string;
     navMenuItem?: Partial<NavMenuItem> & { sectionId: string };
     loader?: RouteOptions['loader'];

+ 2 - 2
packages/dashboard/src/framework/layout-engine/location-wrapper.tsx

@@ -53,11 +53,11 @@ export function LocationWrapper({ children, blockId }: { children: React.ReactNo
                     onMouseLeave={() => setHoverId(parentId)}
                 >
                     <div
-                        className={`absolute top-0.5 right-0.5 transition-all delay-50 ${isHovered || isPopoverOpen ? 'visible' : 'invisible'}`}
+                        className={`absolute top-0.5 right-0.5 transition-all delay-50 z-10 ${isHovered || isPopoverOpen ? 'visible' : 'invisible'}`}
                     >
                         <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
                             <PopoverTrigger asChild>
-                                <Button variant="ghost" size="icon">
+                                <Button variant="ghost" size="icon" className="rounded-lg">
                                     <CodeXmlIcon className="text-dev-mode w-5 h-5" />
                                 </Button>
                             </PopoverTrigger>

+ 23 - 4
packages/dashboard/src/framework/layout-engine/page-layout.tsx

@@ -19,10 +19,10 @@ export interface PageProps extends ComponentProps<'div'> {
 
 export const PageProvider = createContext<PageContext | undefined>(undefined);
 
-export function Page({ children, ...props }: PageProps) {
+export function Page({ children, pageId, entity, ...props }: PageProps) {
     const [form, setForm] = useState<UseFormReturn<any> | undefined>(undefined);
     return (
-        <PageProvider value={{ pageId: props.pageId ?? '', form, setForm, entity: props.entity }}>
+        <PageProvider value={{ pageId, form, setForm, entity }}>
             <LocationWrapper>
                 <div className={cn('m-4', props.className)} {...props}>
                     {children}
@@ -38,7 +38,11 @@ export type PageLayoutProps = {
 };
 
 function isPageBlock(child: unknown): child is React.ReactElement<PageBlockProps> {
-    return React.isValidElement(child) && 'column' in (child as React.ReactElement<PageBlockProps>).props;
+    const props = (child as React.ReactElement<PageBlockProps>).props;
+    const isReactElement = React.isValidElement(child);
+    const hasColumn = 'column' in props;
+    const hasBlockId = 'blockId' in props;
+    return isReactElement && (hasColumn || hasBlockId);
 }
 
 export function PageLayout({ children, className }: PageLayoutProps) {
@@ -91,6 +95,7 @@ export function PageLayout({ children, className }: PageLayoutProps) {
         }
     }
 
+    const fullWidthBlocks = finalChildArray.filter(child => isPageBlock(child) && child.type === FullWidthPageBlock);
     const mainBlocks = finalChildArray.filter(child => isPageBlock(child) && child.props.column === 'main');
     const sideBlocks = finalChildArray.filter(child => isPageBlock(child) && child.props.column === 'side');
 
@@ -98,6 +103,9 @@ export function PageLayout({ children, className }: PageLayoutProps) {
         <div className={cn('w-full space-y-4', className)}>
             {isDesktop ? (
                 <div className="hidden md:grid md:grid-cols-5 lg:grid-cols-4 md:gap-4">
+                    {fullWidthBlocks.length > 0 && (
+                        <div className="md:col-span-5 space-y-4">{fullWidthBlocks}</div>
+                    )}
                     <div className="md:col-span-3 space-y-4">{mainBlocks}</div>
                     <div className="md:col-span-2 lg:col-span-1 space-y-4">{sideBlocks}</div>
                 </div>
@@ -199,7 +207,6 @@ export type PageBlockProps = {
     title?: React.ReactNode | string;
     description?: React.ReactNode | string;
     className?: string;
-    borderless?: boolean;
 };
 
 export function PageBlock({ children, title, description, borderless, className, blockId }: PageBlockProps) {
@@ -220,6 +227,18 @@ export function PageBlock({ children, title, description, borderless, className,
     );
 }
 
+export function FullWidthPageBlock({
+    children,
+    className,
+    blockId,
+}: Pick<PageBlockProps, 'children' | 'className' | 'blockId'>) {
+    return (
+        <LocationWrapper blockId={blockId}>
+            <div className={cn('w-full', className)}>{children}</div>
+        </LocationWrapper>
+    );
+}
+
 export function CustomFieldsPageBlock({
     column,
     entityType,

+ 4 - 2
packages/dashboard/src/framework/page/detail-page.tsx

@@ -30,6 +30,7 @@ export interface DetailPageProps<
     U extends TypedDocumentNode<any, any>,
     EntityField extends keyof ResultOf<T> = DetailEntityPath<T>,
 > {
+    pageId: string;
     route: AnyRoute;
     title: (entity: ResultOf<T>[EntityField]) => string;
     queryDocument: T;
@@ -43,6 +44,7 @@ export function DetailPage<
     C extends TypedDocumentNode<any, any>,
     U extends TypedDocumentNode<any, any>,
 >({
+    pageId,
     route,
     queryDocument,
     createDocument,
@@ -72,7 +74,7 @@ export function DetailPage<
     const updateFields = getOperationVariablesFields(updateDocument);
 
     return (
-        <Page>
+        <Page pageId={pageId}>
             <PageDetailForm form={form} submitHandler={submitHandler}>
                 <PageActionBar>
                     <PageActionBarLeft>
@@ -88,7 +90,7 @@ export function DetailPage<
                     </PageActionBarRight>
                 </PageActionBar>
                 <PageLayout>
-                    <PageBlock column="main">
+                    <PageBlock column="main" blockId="main-form">
                         <DetailFormGrid>
                             {updateFields.map(fieldInfo => {
                                 console.log(fieldInfo);

+ 47 - 37
packages/dashboard/src/framework/page/list-page.tsx

@@ -1,5 +1,3 @@
-import { PageProps } from '@/framework/page/page-types.js';
-
 import {
     AdditionalColumns,
     CustomizeColumnConfig,
@@ -7,13 +5,19 @@ import {
     ListQueryOptionsShape,
     ListQueryShape,
     PaginatedListDataTable,
-    RowAction
+    RowAction,
 } from '@/components/shared/paginated-list-data-table.js';
 import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 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';
+import {
+    FullWidthPageBlock,
+    Page,
+    PageActionBar,
+    PageLayout,
+    PageTitle,
+} from '../layout-engine/page-layout.js';
 import { addCustomFields } from '../document-introspection/add-custom-fields.js';
 
 type ListQueryFields<T extends TypedDocumentNode<any, any>> = {
@@ -24,13 +28,12 @@ type ListQueryFields<T extends TypedDocumentNode<any, any>> = {
         : never;
 }[keyof ResultOf<T>];
 
-
 export interface ListPageProps<
     T extends TypedDocumentNode<U, V>,
     U extends ListQueryShape,
     V extends ListQueryOptionsShape,
     AC extends AdditionalColumns<T>,
-> extends PageProps {
+> {
     pageId?: string;
     route: AnyRoute | (() => AnyRoute);
     title: string | React.ReactElement;
@@ -79,12 +82,15 @@ export function ListPage<
         itemsPerPage: routeSearch.perPage ? parseInt(routeSearch.perPage) : 10,
     };
 
-    const sorting: SortingState = (routeSearch.sort ?? '').split(',').filter(s => s.length).map((s: string) => {
-        return {
-            id: s.replace(/^-/, ''),
-            desc: s.startsWith('-'),
-        };
-    });
+    const sorting: SortingState = (routeSearch.sort ?? '')
+        .split(',')
+        .filter(s => s.length)
+        .map((s: string) => {
+            return {
+                id: s.replace(/^-/, ''),
+                desc: s.startsWith('-'),
+            };
+        });
 
     if (defaultSort && !sorting.length) {
         sorting.push(...defaultSort);
@@ -119,31 +125,35 @@ export function ListPage<
         <Page pageId={pageId}>
             <PageTitle>{title}</PageTitle>
             <PageActionBar>{children}</PageActionBar>
-            <PaginatedListDataTable
-                listQuery={listQueryWithCustomFields}
-                deleteMutation={deleteMutation}
-                transformVariables={transformVariables}
-                customizeColumns={customizeColumns}
-                additionalColumns={additionalColumns}
-                defaultColumnOrder={defaultColumnOrder}
-                defaultVisibility={defaultVisibility}
-                onSearchTermChange={onSearchTermChange}
-                page={pagination.page}
-                itemsPerPage={pagination.itemsPerPage}
-                sorting={sorting}
-                columnFilters={routeSearch.filters}
-                onPageChange={(table, page, perPage) => {
-                    persistListStateToUrl(table, { page, perPage });
-                }}
-                onSortChange={(table, sorting) => {
-                    persistListStateToUrl(table, { sort: sorting });
-                }}
-                onFilterChange={(table, filters) => {
-                    persistListStateToUrl(table, { filters });
-                }}
-                facetedFilters={facetedFilters}
-                rowActions={rowActions}
-            />
+            <PageLayout>
+                <FullWidthPageBlock blockId="list-table">
+                    <PaginatedListDataTable
+                        listQuery={listQueryWithCustomFields}
+                        deleteMutation={deleteMutation}
+                        transformVariables={transformVariables}
+                        customizeColumns={customizeColumns}
+                        additionalColumns={additionalColumns}
+                        defaultColumnOrder={defaultColumnOrder}
+                        defaultVisibility={defaultVisibility}
+                        onSearchTermChange={onSearchTermChange}
+                        page={pagination.page}
+                        itemsPerPage={pagination.itemsPerPage}
+                        sorting={sorting}
+                        columnFilters={routeSearch.filters}
+                        onPageChange={(table, page, perPage) => {
+                            persistListStateToUrl(table, { page, perPage });
+                        }}
+                        onSortChange={(table, sorting) => {
+                            persistListStateToUrl(table, { sort: sorting });
+                        }}
+                        onFilterChange={(table, filters) => {
+                            persistListStateToUrl(table, { filters });
+                        }}
+                        facetedFilters={facetedFilters}
+                        rowActions={rowActions}
+                    />
+                </FullWidthPageBlock>
+            </PageLayout>
         </Page>
     );
 }

+ 35 - 30
packages/dashboard/src/routes/_authenticated/index.tsx

@@ -1,13 +1,15 @@
 import { Button } from '@/components/ui/button.js';
-import { DashboardBaseWidgetProps } from '@/framework/dashboard-widget/base-widget.js';
-import { LatestOrdersWidget } from '@/framework/dashboard-widget/latest-orders-widget/index.js';
-import { MetricsWidget } from '@/framework/dashboard-widget/metrics-widget/index.js';
-import { OrdersSummaryWidget } from '@/framework/dashboard-widget/orders-summary/index.js';
 import { getDashboardWidget, getDashboardWidgetRegistry } from '@/framework/dashboard-widget/registry.js';
-import { DashboardWidgetInstance, WidgetDefinition } from '@/framework/dashboard-widget/types.js';
-import { Page, PageActionBar, PageActionBarRight, PageTitle } from '@/framework/layout-engine/page-layout.js';
+import { DashboardWidgetInstance } from '@/framework/dashboard-widget/types.js';
+import {
+    FullWidthPageBlock,
+    Page,
+    PageActionBar,
+    PageActionBarRight,
+    PageLayout,
+    PageTitle,
+} from '@/framework/layout-engine/page-layout.js';
 import { createFileRoute } from '@tanstack/react-router';
-import * as React from 'react';
 import { useEffect, useMemo, useState } from 'react';
 import { Responsive as ResponsiveGridLayout, WidthProvider } from 'react-grid-layout';
 import 'react-grid-layout/css/styles.css';
@@ -102,7 +104,6 @@ function DashboardPage() {
     }, []);
 
     const handleLayoutChange = (layout: ReactGridLayout.Layout[]) => {
-        console.log({ layout });
         setWidgets(prev =>
             prev.map((widget, i) => ({
                 ...widget,
@@ -114,7 +115,7 @@ function DashboardPage() {
     const ResponsiveReactGridLayout = useMemo(() => WidthProvider(ResponsiveGridLayout), []);
 
     return (
-        <Page className="min-h-dvh w-full">
+        <Page pageId="dashboard">
             <PageTitle>Dashboard</PageTitle>
             <PageActionBar>
                 <PageActionBarRight>
@@ -128,28 +129,32 @@ function DashboardPage() {
                     </Button>
                 </PageActionBarRight>
             </PageActionBar>
-            <ResponsiveReactGridLayout
-                className="h-full w-full"
-                layouts={{ lg: widgets.map(w => ({ ...w.layout, i: w.id })) }}
-                onLayoutChange={handleLayoutChange}
-                cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
-                rowHeight={100}
-                isDraggable={editMode}
-                isResizable={editMode}
-                autoSize={true}
-            >
-                {widgets.map(widget => {
-                    const definition = getDashboardWidget(widget.widgetId);
-                    if (!definition) return null;
-                    const WidgetComponent = definition.component;
+            <PageLayout>
+                <FullWidthPageBlock blockId="widgets">
+                    <ResponsiveReactGridLayout
+                        className="h-full w-full"
+                        layouts={{ lg: widgets.map(w => ({ ...w.layout, i: w.id })) }}
+                        onLayoutChange={handleLayoutChange}
+                        cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
+                        rowHeight={100}
+                        isDraggable={editMode}
+                        isResizable={editMode}
+                        autoSize={true}
+                    >
+                        {widgets.map(widget => {
+                            const definition = getDashboardWidget(widget.widgetId);
+                            if (!definition) return null;
+                            const WidgetComponent = definition.component;
 
-                    return (
-                        <div key={widget.id}>
-                            <WidgetComponent id={widget.id} config={widget.config} />
-                        </div>
-                    );
-                })}
-            </ResponsiveReactGridLayout>
+                            return (
+                                <div key={widget.id}>
+                                    <WidgetComponent id={widget.id} config={widget.config} />
+                                </div>
+                            );
+                        })}
+                    </ResponsiveReactGridLayout>
+                </FullWidthPageBlock>
+            </PageLayout>
         </Page>
     );
 }

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

@@ -43,7 +43,6 @@ const updateReviewDocument = graphql(`
 `);
 
 export const reviewDetail: DashboardRouteDefinition = {
-    id: 'review-detail',
     path: '/reviews/$id',
     loader: detailPageRouteLoader({
         queryDocument: reviewDetailDocument,
@@ -55,6 +54,7 @@ export const reviewDetail: DashboardRouteDefinition = {
     component: route => {
         return (
             <DetailPage
+                pageId="review-detail"
                 queryDocument={reviewDetailDocument}
                 updateDocument={updateReviewDocument}
                 route={route}

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

@@ -33,7 +33,6 @@ const getReviewList = graphql(`
 `);
 
 export const reviewList: DashboardRouteDefinition = {
-    id: 'review-list',
     navMenuItem: {
         sectionId: 'catalog',
         id: 'reviews',
@@ -46,6 +45,7 @@ export const reviewList: DashboardRouteDefinition = {
     }),
     component: route => (
         <ListPage
+            pageId="review-list"
             title="Product Reviews"
             listQuery={getReviewList}
             route={route}