Преглед изворни кода

feat(dashboard): Custom nav sections (#3598)

David Höck пре 7 месеци
родитељ
комит
cbc04092cf

+ 2 - 1
packages/dashboard/src/lib/components/layout/app-sidebar.tsx

@@ -7,14 +7,15 @@ import {
     SidebarHeader,
     SidebarRail,
 } from '@/components/ui/sidebar.js';
-import { getNavMenuConfig } from '@/framework/nav-menu/nav-menu-extensions.js';
 import { useDashboardExtensions } from '@/framework/extension-api/use-dashboard-extensions.js';
+import { getNavMenuConfig } from '@/framework/nav-menu/nav-menu-extensions.js';
 import * as React from 'react';
 import { ChannelSwitcher } from './channel-switcher.js';
 
 export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
     const { extensionsLoaded } = useDashboardExtensions();
     const { sections } = getNavMenuConfig();
+
     return (
         extensionsLoaded && (
             <Sidebar collapsible="icon" {...props}>

+ 35 - 6
packages/dashboard/src/lib/components/layout/nav-main.tsx

@@ -9,19 +9,48 @@ import {
     SidebarMenuSubButton,
     SidebarMenuSubItem,
 } from '@/components/ui/sidebar.js';
-import { NavMenuSection, NavMenuItem } from '@/framework/nav-menu/nav-menu-extensions.js';
-import { Link, rootRouteId, useLocation, useMatch } from '@tanstack/react-router';
+import {
+    NavMenuItem,
+    NavMenuSection,
+    NavMenuSectionPlacement,
+} from '@/framework/nav-menu/nav-menu-extensions.js';
+import { Link, useLocation } from '@tanstack/react-router';
 import { ChevronRight } from 'lucide-react';
 import * as React from 'react';
 
+// Utility to sort items & sections by the optional `order` prop (ascending) and then alphabetically by title
+function sortByOrder<T extends { order?: number; title: string }>(a: T, b: T) {
+    const orderA = a.order ?? Number.MAX_SAFE_INTEGER;
+    const orderB = b.order ?? Number.MAX_SAFE_INTEGER;
+    if (orderA === orderB) {
+        return a.title.localeCompare(b.title);
+    }
+    return orderA - orderB;
+}
+
 export function NavMain({ items }: { items: Array<NavMenuSection | NavMenuItem> }) {
     const location = useLocation();
     // State to track which bottom section is currently open
     const [openBottomSectionId, setOpenBottomSectionId] = React.useState<string | null>(null);
 
-    // Split sections into top and bottom groups based on placement property
-    const topSections = items.filter(item => item.placement === 'top');
-    const bottomSections = items.filter(item => item.placement === 'bottom');
+    // Helper to build a sorted list of sections for a given placement, memoized for stability
+    const getSortedSections = React.useCallback(
+        (placement: NavMenuSectionPlacement) => {
+            return items
+                .filter(item => item.placement === placement)
+                .slice()
+                .sort(sortByOrder)
+                .map(section =>
+                    'items' in section
+                        ? { ...section, items: section.items?.slice().sort(sortByOrder) }
+                        : section,
+                );
+        },
+        [items],
+    );
+
+    const topSections = React.useMemo(() => getSortedSections('top'), [getSortedSections]);
+    const bottomSections = React.useMemo(() => getSortedSections('bottom'), [getSortedSections]);
 
     // Handle bottom section open/close
     const handleBottomSectionToggle = (sectionId: string, isOpen: boolean) => {
@@ -50,7 +79,7 @@ export function NavMain({ items }: { items: Array<NavMenuSection | NavMenuItem>
                 return;
             }
         }
-    }, [location.pathname]);
+    }, [location.pathname, bottomSections]);
 
     // Render a top navigation section
     const renderTopSection = (item: NavMenuSection | NavMenuItem) => {

+ 7 - 0
packages/dashboard/src/lib/framework/defaults.ts

@@ -23,6 +23,7 @@ export function registerDefaults() {
                 placement: 'top',
                 icon: LayoutDashboardIcon,
                 url: '/',
+                order: 100,
             },
             {
                 id: 'catalog',
@@ -30,6 +31,7 @@ export function registerDefaults() {
                 icon: SquareTerminal,
                 defaultOpen: true,
                 placement: 'top',
+                order: 200,
                 items: [
                     {
                         id: 'products',
@@ -64,6 +66,7 @@ export function registerDefaults() {
                 icon: ShoppingCart,
                 defaultOpen: true,
                 placement: 'top',
+                order: 300,
                 items: [
                     {
                         id: 'orders',
@@ -78,6 +81,7 @@ export function registerDefaults() {
                 icon: Users,
                 defaultOpen: false,
                 placement: 'top',
+                order: 400,
                 items: [
                     {
                         id: 'customers',
@@ -97,6 +101,7 @@ export function registerDefaults() {
                 icon: Mail,
                 defaultOpen: false,
                 placement: 'top',
+                order: 500,
                 items: [
                     {
                         id: 'promotions',
@@ -111,6 +116,7 @@ export function registerDefaults() {
                 icon: Terminal,
                 defaultOpen: false,
                 placement: 'bottom',
+                order: 100,
                 items: [
                     {
                         id: 'job-queue',
@@ -135,6 +141,7 @@ export function registerDefaults() {
                 icon: Settings2,
                 defaultOpen: false,
                 placement: 'bottom',
+                order: 200,
                 items: [
                     {
                         id: 'sellers',

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

@@ -3,7 +3,7 @@ import {
     registerDashboardActionBarItem,
     registerDashboardPageBlock,
 } from '../layout-engine/layout-extensions.js';
-import { addNavMenuItem, NavMenuItem } from '../nav-menu/nav-menu-extensions.js';
+import { addNavMenuItem, addNavMenuSection, NavMenuItem } from '../nav-menu/nav-menu-extensions.js';
 import { registerRoute } from '../page/page-api.js';
 import { globalRegistry } from '../registry/global-registry.js';
 
@@ -34,6 +34,16 @@ export function executeDashboardExtensionCallbacks() {
  */
 export function defineDashboardExtension(extension: DashboardExtension) {
     globalRegistry.get('registerDashboardExtensionCallbacks').add(() => {
+        if (extension.navSections) {
+            for (const section of extension.navSections) {
+                addNavMenuSection({
+                    ...section,
+                    placement: 'top',
+                    order: section.order ?? 999,
+                    items: [],
+                });
+            }
+        }
         if (extension.routes) {
             for (const route of extension.routes) {
                 if (route.navMenuItem) {

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

@@ -1,5 +1,6 @@
 import { PageContextValue } from '@/framework/layout-engine/page-provider.js';
 import { AnyRoute, RouteOptions } from '@tanstack/react-router';
+import { LucideIcon } from 'lucide-react';
 import type React from 'react';
 
 import { DashboardAlertDefinition } from '../alert/types.js';
@@ -18,6 +19,13 @@ export interface ActionBarButtonState {
     visible: boolean;
 }
 
+export interface DashboardNavSectionDefinition {
+    id: string;
+    title: string;
+    icon?: LucideIcon;
+    order?: number;
+}
+
 /**
  * @description
  * **Status: Developer Preview**
@@ -103,6 +111,11 @@ export interface DashboardExtension {
      * Allows you to define custom routes such as list or detail views.
      */
     routes?: DashboardRouteDefinition[];
+    /**
+     * @description
+     * Allows you to define custom nav sections for the dashboard.
+     */
+    navSections?: DashboardNavSectionDefinition[];
     /**
      * @description
      * Allows you to define custom page blocks for any page in the dashboard.

+ 7 - 0
packages/dashboard/src/lib/framework/nav-menu/nav-menu-extensions.ts

@@ -9,6 +9,7 @@ interface NavMenuBaseItem {
     id: string;
     title: string;
     icon?: LucideIcon;
+    order?: number;
     placement?: NavMenuSectionPlacement;
 }
 
@@ -64,3 +65,9 @@ export function addNavMenuItem(item: NavMenuItem, sectionId: string) {
         }
     }
 }
+
+export function addNavMenuSection(section: NavMenuSection) {
+    const navMenuConfig = getNavMenuConfig();
+    navMenuConfig.sections = [...navMenuConfig.sections];
+    navMenuConfig.sections.push(section);
+}