فهرست منبع

feat(dashboard): Global search quick actions API

David Höck 5 ماه پیش
والد
کامیت
5c686dc07f
22فایلهای تغییر یافته به همراه1674 افزوده شده و 310 حذف شده
  1. 4 0
      packages/dashboard/src/app/routes/_authenticated/_products/products.tsx
  2. 204 0
      packages/dashboard/src/lib/components/global-search/examples/custom-plugin-actions.tsx
  3. 237 0
      packages/dashboard/src/lib/components/global-search/examples/enhanced-product-list-integration.tsx
  4. 143 0
      packages/dashboard/src/lib/components/global-search/examples/list-route-actions.tsx
  5. 163 0
      packages/dashboard/src/lib/components/global-search/examples/product-route-actions.tsx
  6. 51 13
      packages/dashboard/src/lib/components/global-search/hooks/use-global-search.ts
  7. 288 0
      packages/dashboard/src/lib/components/global-search/hooks/use-product-list-actions.ts
  8. 11 223
      packages/dashboard/src/lib/components/global-search/hooks/use-quick-actions.ts
  9. 61 0
      packages/dashboard/src/lib/components/global-search/hooks/use-register-route-actions.ts
  10. 6 0
      packages/dashboard/src/lib/components/global-search/index.ts
  11. 3 7
      packages/dashboard/src/lib/components/global-search/quick-action-item.tsx
  12. 1 0
      packages/dashboard/src/lib/components/global-search/quick-actions-list.tsx
  13. 186 0
      packages/dashboard/src/lib/components/global-search/quick-actions-registry.ts
  14. 266 0
      packages/dashboard/src/lib/framework/defaults.ts
  15. 5 0
      packages/dashboard/src/lib/framework/extension-api/define-dashboard-extension.ts
  16. 7 0
      packages/dashboard/src/lib/framework/extension-api/extension-api-types.ts
  17. 1 0
      packages/dashboard/src/lib/framework/extension-api/logic/index.ts
  18. 9 0
      packages/dashboard/src/lib/framework/extension-api/logic/quick-actions.ts
  19. 2 0
      packages/dashboard/src/lib/framework/registry/registry-types.ts
  20. 24 64
      packages/dashboard/src/lib/graphql/global-search.ts
  21. 1 2
      packages/dashboard/src/lib/graphql/graphql-env.d.ts
  22. 1 1
      packages/dashboard/src/lib/providers/search-provider.tsx

+ 4 - 0
packages/dashboard/src/app/routes/_authenticated/_products/products.tsx

@@ -1,3 +1,4 @@
+// Global search actions are now registered centrally in defaults.ts
 import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
 import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
 import { Button } from '@/vdb/components/ui/button.js';
@@ -6,6 +7,7 @@ import { ListPage } from '@/vdb/framework/page/list-page.js';
 import { Trans } from '@/vdb/lib/trans.js';
 import { createFileRoute, Link } from '@tanstack/react-router';
 import { PlusIcon } from 'lucide-react';
+// No longer need hook-based action registration
 import {
     AssignFacetValuesToProductsBulkAction,
     AssignProductsToChannelBulkAction,
@@ -21,6 +23,8 @@ export const Route = createFileRoute('/_authenticated/_products/products')({
 });
 
 function ProductListPage() {
+    // Quick actions are now registered centrally in defaults.ts
+    // No need for hook-based registration here
     return (
         <ListPage
             pageId="product-list"

+ 204 - 0
packages/dashboard/src/lib/components/global-search/examples/custom-plugin-actions.tsx

@@ -0,0 +1,204 @@
+import { useEffect } from 'react';
+import { useSearchContext, QuickAction } from '@/vdb/providers/search-provider.js';
+
+/**
+ * Example of how plugin developers can register custom global and context-aware actions
+ * through the extension system (to be integrated with defineDashboardExtensions)
+ */
+
+/**
+ * Register global actions that are available everywhere
+ */
+export function registerGlobalPluginActions() {
+    // This would typically be called from within a plugin's dashboard extension
+    const { registerQuickAction } = useSearchContext();
+
+    const customGlobalActions: QuickAction[] = [
+        {
+            id: 'plugin-sync-inventory',
+            label: 'Sync Inventory',
+            description: 'Sync inventory with external system',
+            icon: 'refresh-ccw',
+            shortcut: 'ctrl+shift+i',
+            isContextAware: false,
+            handler: async (context) => {
+                context.showNotification('Starting inventory sync...', 'success');
+                try {
+                    // Custom plugin API call
+                    // await syncInventory();
+                    context.showNotification('Inventory sync completed', 'success');
+                } catch (error) {
+                    context.showNotification('Inventory sync failed', 'error');
+                }
+            }
+        },
+        {
+            id: 'plugin-generate-report',
+            label: 'Generate Custom Report',
+            description: 'Generate a custom sales report',
+            icon: 'file-text',
+            isContextAware: false,
+            handler: async (context) => {
+                // This could open a dialog for report configuration
+                // or navigate to a custom report page
+                context.navigate('/custom-reports/generate');
+            }
+        }
+    ];
+
+    // Register each action
+    customGlobalActions.forEach(action => {
+        registerQuickAction(action);
+    });
+}
+
+/**
+ * Hook for registering plugin-specific context actions
+ * This would be used in route components where the plugin wants to add actions
+ */
+export function useCustomPluginRouteActions(
+    routeType: 'product' | 'customer' | 'order',
+    entityId?: string
+) {
+    const { registerQuickAction, unregisterQuickAction } = useSearchContext();
+
+    useEffect(() => {
+        const customActions: QuickAction[] = [];
+
+        // Add actions based on route type
+        if (routeType === 'product' && entityId) {
+            customActions.push({
+                id: 'plugin-update-seo',
+                label: 'Update SEO Data',
+                description: 'Update SEO metadata for this product',
+                icon: 'search',
+                isContextAware: true,
+                handler: async (context) => {
+                    // Custom SEO plugin functionality
+                    context.showNotification('SEO data updated', 'success');
+                }
+            });
+
+            customActions.push({
+                id: 'plugin-check-competition',
+                label: 'Check Competition',
+                description: 'Check competitor pricing for this product',
+                icon: 'bar-chart',
+                isContextAware: true,
+                handler: async (context) => {
+                    // Open competition analysis dialog or navigate to page
+                    context.navigate(`/competition-analysis/${entityId}`);
+                }
+            });
+        }
+
+        if (routeType === 'customer' && entityId) {
+            customActions.push({
+                id: 'plugin-send-email',
+                label: 'Send Marketing Email',
+                description: 'Send a personalized marketing email',
+                icon: 'mail',
+                isContextAware: true,
+                handler: async (context) => {
+                    // Open email composer dialog
+                    context.showNotification('Email composer opened', 'success');
+                }
+            });
+        }
+
+        if (routeType === 'order' && entityId) {
+            customActions.push({
+                id: 'plugin-track-shipment',
+                label: 'Track Shipment',
+                description: 'Get real-time shipment tracking',
+                icon: 'truck',
+                isContextAware: true,
+                handler: async (context) => {
+                    // Integration with shipping provider API
+                    context.navigate(`/tracking/${entityId}`);
+                }
+            });
+        }
+
+        // Register all custom actions
+        customActions.forEach(action => {
+            registerQuickAction(action);
+        });
+
+        // Cleanup when component unmounts or entityId changes
+        return () => {
+            customActions.forEach(action => {
+                unregisterQuickAction(action.id);
+            });
+        };
+    }, [routeType, entityId, registerQuickAction, unregisterQuickAction]);
+}
+
+/**
+ * Integration with defineDashboardExtensions (future implementation)
+ * This shows how the extension API could be used to register actions
+ */
+
+export interface QuickActionExtension {
+    // Global actions (available everywhere)
+    globalActions?: QuickAction[];
+
+    // Context-aware actions (route/page specific)
+    contextActions?: {
+        [routePattern: string]: QuickAction[];
+    };
+
+    // Entity-specific actions (based on entity type)
+    entityActions?: {
+        [entityType: string]: QuickAction[];
+    };
+}
+
+export function registerQuickActionExtensions(extensions: QuickActionExtension) {
+    const { registerQuickAction } = useSearchContext();
+
+    // Register global actions
+    extensions.globalActions?.forEach(action => {
+        registerQuickAction({
+            ...action,
+            isContextAware: false
+        });
+    });
+
+    // Context actions would be registered by matching routes
+    // This would be handled by the router/extension system
+    
+    // Entity actions would be registered based on current entity type
+    // This would also be handled by the extension system
+}
+
+/**
+ * Example usage in a plugin's dashboard extension:
+ * 
+ * defineDashboardExtensions({
+ *     quickActions: {
+ *         globalActions: [
+ *             {
+ *                 id: 'my-plugin-sync',
+ *                 label: 'Sync with My Service',
+ *                 icon: 'refresh',
+ *                 handler: async (context) => {
+ *                     // Plugin logic here
+ *                 }
+ *             }
+ *         ],
+ *         contextActions: {
+ *             '/products/:id': [
+ *                 {
+ *                     id: 'my-plugin-product-action',
+ *                     label: 'Custom Product Action',
+ *                     icon: 'zap',
+ *                     handler: async (context) => {
+ *                         // Product-specific plugin logic
+ *                     }
+ *                 }
+ *             ]
+ *         }
+ *     }
+ * });
+ */

+ 237 - 0
packages/dashboard/src/lib/components/global-search/examples/enhanced-product-list-integration.tsx

@@ -0,0 +1,237 @@
+import { useCallback, useMemo } from 'react';
+import { useNavigate } from '@tanstack/react-router';
+import { toast } from 'sonner';
+import { useProductListActions } from '../hooks/use-product-list-actions.js';
+
+/**
+ * Enhanced example showing how to integrate product list actions with real data
+ * This would be used in a more sophisticated product list component
+ */
+
+interface Product {
+    id: string;
+    name: string;
+    enabled: boolean;
+    variants: Array<{
+        stockOnHand: number;
+    }>;
+}
+
+interface ProductListState {
+    products: Product[];
+    selectedProductIds: string[];
+    totalCount: number;
+    isLoading: boolean;
+    filters: {
+        enabled?: boolean;
+        outOfStock?: boolean;
+    };
+}
+
+export function useEnhancedProductListActions(
+    state: ProductListState,
+    actions: {
+        refetch: () => void;
+        setFilters: (filters: ProductListState['filters']) => void;
+        clearFilters: () => void;
+        deleteProducts: (ids: string[]) => Promise<void>;
+        duplicateProducts: (ids: string[]) => Promise<void>;
+        exportProducts: (ids?: string[]) => Promise<void>;
+        importProducts: () => Promise<void>;
+    }
+) {
+    const navigate = useNavigate();
+    const { products, selectedProductIds, totalCount, filters } = state;
+    const selectedCount = selectedProductIds.length;
+
+    // Enhanced handlers that work with real data
+    const handleCreateProduct = useCallback(() => {
+        navigate({ to: '/products/new' });
+    }, [navigate]);
+
+    const handleExport = useCallback(async () => {
+        try {
+            if (selectedCount > 0) {
+                await actions.exportProducts(selectedProductIds);
+                toast.success(`Exported ${selectedCount} selected products`);
+            } else {
+                await actions.exportProducts();
+                toast.success(`Exported all ${totalCount} products`);
+            }
+        } catch (error) {
+            toast.error('Export failed');
+        }
+    }, [actions, selectedProductIds, selectedCount, totalCount]);
+
+    const handleRefresh = useCallback(() => {
+        actions.refetch();
+        toast.success('Products list refreshed');
+    }, [actions]);
+
+    const handleBulkEdit = useCallback(() => {
+        if (selectedCount === 0) {
+            toast.warning('Please select products to edit');
+            return;
+        }
+        
+        // This could open a bulk edit dialog
+        toast.success(`Bulk edit mode activated for ${selectedCount} products`);
+        // In real implementation:
+        // setBulkEditDialogOpen(true);
+    }, [selectedCount]);
+
+    const handleImport = useCallback(async () => {
+        try {
+            await actions.importProducts();
+            toast.success('Product import completed');
+        } catch (error) {
+            toast.error('Import failed');
+        }
+    }, [actions]);
+
+    const handleBulkDelete = useCallback(async () => {
+        if (selectedCount === 0) {
+            toast.warning('Please select products to delete');
+            return;
+        }
+
+        const confirmed = window.confirm(
+            `Are you sure you want to delete ${selectedCount} selected products? This cannot be undone.`
+        );
+        
+        if (confirmed) {
+            try {
+                await actions.deleteProducts(selectedProductIds);
+                toast.success(`Deleted ${selectedCount} products`);
+            } catch (error) {
+                toast.error('Delete failed');
+            }
+        }
+    }, [actions, selectedProductIds, selectedCount]);
+
+    const handleBulkDuplicate = useCallback(async () => {
+        if (selectedCount === 0) {
+            toast.warning('Please select products to duplicate');
+            return;
+        }
+
+        try {
+            await actions.duplicateProducts(selectedProductIds);
+            toast.success(`Duplicated ${selectedCount} products`);
+        } catch (error) {
+            toast.error('Duplicate failed');
+        }
+    }, [actions, selectedProductIds, selectedCount]);
+
+    // Filter handlers
+    const handleFilterEnabled = useCallback(() => {
+        const newFilters = { ...filters, enabled: true };
+        actions.setFilters(newFilters);
+        toast.success('Showing enabled products only');
+    }, [actions, filters]);
+
+    const handleFilterDisabled = useCallback(() => {
+        const newFilters = { ...filters, enabled: false };
+        actions.setFilters(newFilters);
+        toast.success('Showing disabled products only');
+    }, [actions, filters]);
+
+    const handleFilterOutOfStock = useCallback(() => {
+        const newFilters = { ...filters, outOfStock: true };
+        actions.setFilters(newFilters);
+        toast.success('Showing out of stock products only');
+    }, [actions, filters]);
+
+    const handleClearFilters = useCallback(() => {
+        actions.clearFilters();
+        toast.success('All filters cleared');
+    }, [actions]);
+
+    // Dynamic action descriptions based on current state
+    const dynamicActions = useMemo(() => ({
+        export: {
+            label: selectedCount > 0 ? `Export Selected (${selectedCount})` : 'Export All Products',
+            description: selectedCount > 0 
+                ? `Export ${selectedCount} selected products to CSV`
+                : `Export all ${totalCount} products to CSV`
+        },
+        bulkEdit: {
+            label: selectedCount > 0 ? `Bulk Edit (${selectedCount})` : 'Bulk Edit',
+            description: selectedCount > 0
+                ? `Edit ${selectedCount} selected products at once`
+                : 'Select products to edit them in bulk'
+        }
+    }), [selectedCount, totalCount]);
+
+    // Register the actions with dynamic data
+    useProductListActions({
+        onCreateProduct: handleCreateProduct,
+        onExport: handleExport,
+        onRefresh: handleRefresh,
+        onBulkEdit: handleBulkEdit,
+        onImport: handleImport,
+        selectedCount,
+        totalCount,
+        // Additional dynamic handlers
+        onBulkDelete: handleBulkDelete,
+        onBulkDuplicate: handleBulkDuplicate,
+        onFilterEnabled: handleFilterEnabled,
+        onFilterDisabled: handleFilterDisabled,
+        onFilterOutOfStock: handleFilterOutOfStock,
+        onClearFilters: handleClearFilters,
+        // Pass dynamic descriptions
+        dynamicLabels: dynamicActions,
+    });
+
+    // Return handlers for use in the component if needed
+    return {
+        handleCreateProduct,
+        handleExport,
+        handleRefresh,
+        handleBulkEdit,
+        handleImport,
+        handleBulkDelete,
+        handleBulkDuplicate,
+        handleFilterEnabled,
+        handleFilterDisabled,
+        handleFilterOutOfStock,
+        handleClearFilters,
+    };
+}
+
+/**
+ * Usage in a sophisticated product list component:
+ * 
+ * function AdvancedProductListPage() {
+ *     const [products, setProducts] = useState<Product[]>([]);
+ *     const [selectedIds, setSelectedIds] = useState<string[]>([]);
+ *     const [filters, setFilters] = useState({});
+ *     
+ *     const state = {
+ *         products,
+ *         selectedProductIds: selectedIds,
+ *         totalCount: products.length,
+ *         isLoading: false,
+ *         filters,
+ *     };
+ *     
+ *     const actions = {
+ *         refetch: () => { /* refetch logic */ },
+ *         setFilters: (newFilters) => setFilters(newFilters),
+ *         clearFilters: () => setFilters({}),
+ *         deleteProducts: async (ids) => { /* delete logic */ },
+ *         duplicateProducts: async (ids) => { /* duplicate logic */ },
+ *         exportProducts: async (ids?) => { /* export logic */ },
+ *         importProducts: async () => { /* import logic */ },
+ *     };
+ *     
+ *     // Register enhanced actions with real data
+ *     const handlers = useEnhancedProductListActions(state, actions);
+ *     
+ *     return (
+ *         <ListPage>
+ *             {/* List implementation */}
+ *         </ListPage>
+ *     );
+ * }
+ */

+ 143 - 0
packages/dashboard/src/lib/components/global-search/examples/list-route-actions.tsx

@@ -0,0 +1,143 @@
+import { useRegisterRouteActions } from '../hooks/use-register-route-actions.js';
+import { QuickAction, QuickActionContext } from '@/vdb/providers/search-provider.js';
+
+/**
+ * Example hook for registering quick actions on list pages (e.g., /products, /customers, /orders)
+ * These actions are common to most list views.
+ */
+export function useListRouteActions(options: {
+    entityType: string;
+    onExport?: () => void;
+    onBulkEdit?: () => void;
+    canCreate?: boolean;
+    canExport?: boolean;
+    canBulkEdit?: boolean;
+} = { entityType: 'item' }) {
+    
+    const { entityType, onExport, onBulkEdit, canCreate = true, canExport = true, canBulkEdit = true } = options;
+    
+    const listActions: QuickAction[] = [
+        // Create new item action
+        ...(canCreate ? [{
+            id: 'create-new-item',
+            label: `Create New ${entityType.charAt(0).toUpperCase() + entityType.slice(1)}`,
+            description: `Create a new ${entityType}`,
+            icon: 'plus',
+            shortcut: 'ctrl+n',
+            isContextAware: true,
+            handler: (context: QuickActionContext) => {
+                context.navigate(`/${entityType}s/new`);
+            }
+        }] : []),
+
+        // Export data action
+        ...(canExport ? [{
+            id: 'export-data',
+            label: 'Export Data',
+            description: 'Export current filtered data to CSV',
+            icon: 'download',
+            shortcut: 'ctrl+e',
+            isContextAware: true,
+            handler: async (context: QuickActionContext) => {
+                if (onExport) {
+                    onExport();
+                } else {
+                    // Default export behavior
+                    context.showNotification('Export started', 'success');
+                    // In real implementation, this would trigger the actual export
+                }
+            }
+        }] : []),
+
+        // Bulk edit action
+        ...(canBulkEdit ? [{
+            id: 'bulk-edit',
+            label: 'Bulk Edit',
+            description: 'Edit multiple items at once',
+            icon: 'edit',
+            shortcut: 'ctrl+b',
+            isContextAware: true,
+            handler: async (context: QuickActionContext) => {
+                if (onBulkEdit) {
+                    onBulkEdit();
+                } else {
+                    // Default bulk edit behavior - could open a dialog
+                    context.showNotification('Bulk edit mode activated', 'success');
+                }
+            }
+        }] : []),
+
+        // Refresh list action
+        {
+            id: 'refresh-list',
+            label: 'Refresh List',
+            description: 'Reload the current list',
+            icon: 'refresh-cw',
+            shortcut: 'ctrl+r',
+            isContextAware: true,
+            handler: async (context: QuickActionContext) => {
+                // This could trigger a refetch of the list data
+                context.showNotification('List refreshed', 'success');
+                // In real implementation: refetch();
+            }
+        }
+    ];
+
+    useRegisterRouteActions(listActions);
+}
+
+/**
+ * Specific implementations for common list pages
+ */
+
+export function useProductsListActions(options?: {
+    onExport?: () => void;
+    onBulkEdit?: () => void;
+}) {
+    useListRouteActions({
+        entityType: 'product',
+        ...options,
+    });
+}
+
+export function useCustomersListActions(options?: {
+    onExport?: () => void;
+    onBulkEdit?: () => void;
+}) {
+    useListRouteActions({
+        entityType: 'customer',
+        ...options,
+    });
+}
+
+export function useOrdersListActions(options?: {
+    onExport?: () => void;
+    onBulkEdit?: () => void;
+}) {
+    useListRouteActions({
+        entityType: 'order',
+        ...options,
+    });
+}
+
+/**
+ * Usage in a list page component:
+ * 
+ * function ProductsListPage() {
+ *     const handleExport = useCallback(() => {
+ *         // Custom export logic
+ *     }, []);
+ *     
+ *     const handleBulkEdit = useCallback(() => {
+ *         // Custom bulk edit logic
+ *     }, []);
+ *     
+ *     // Register list-specific actions
+ *     useProductsListActions({
+ *         onExport: handleExport,
+ *         onBulkEdit: handleBulkEdit
+ *     });
+ *     
+ *     // ... rest of component logic
+ * }
+ */

+ 163 - 0
packages/dashboard/src/lib/components/global-search/examples/product-route-actions.tsx

@@ -0,0 +1,163 @@
+import { QuickAction } from '@/vdb/providers/search-provider.js';
+import { useNavigate } from '@tanstack/react-router';
+import { useRegisterRouteActions, useRouteActionHelpers } from '../hooks/use-register-route-actions.js';
+
+/**
+ * Example component showing how to register context-aware quick actions
+ * for a product detail route. This would be used inside the ProductDetailPage component.
+ */
+export function useProductRouteActions() {
+    const navigate = useNavigate();
+    const { entityId, entityType } = useRouteActionHelpers();
+
+    // Define the actions specific to this route
+    const productActions: QuickAction[] = [
+        {
+            id: 'duplicate-product',
+            label: 'Duplicate Product',
+            description: 'Create a copy of this product',
+            icon: 'copy',
+            shortcut: 'ctrl+d',
+            isContextAware: true,
+            handler: async context => {
+                const confirmed = await context.confirm('Duplicate this product?');
+                if (confirmed) {
+                    // In a real implementation, this would call the duplicate API
+                    context.showNotification('Product duplicated successfully');
+                    // Navigate to the new product or stay on current
+                    // context.navigate(`/products/${newProductId}`);
+                }
+            },
+        },
+        {
+            id: 'add-product-variant',
+            label: 'Add Variant',
+            description: 'Add a new variant to this product',
+            icon: 'plus-square',
+            shortcut: 'ctrl+shift+v',
+            isContextAware: true,
+            handler: context => {
+                // This could open a dialog or navigate to variant creation
+                context.navigate(`/products/${entityId}/variants/create`);
+            },
+        },
+        {
+            id: 'save-product',
+            label: 'Save Product',
+            description: 'Save changes to this product',
+            icon: 'save',
+            shortcut: 'ctrl+s',
+            isContextAware: true,
+            handler: async context => {
+                // In a real implementation, this would trigger form submission
+                // You might access form state through a callback or context
+                context.showNotification('Product saved');
+            },
+        },
+        {
+            id: 'delete-product',
+            label: 'Delete Product',
+            description: 'Delete this product permanently',
+            icon: 'trash',
+            shortcut: 'ctrl+shift+delete',
+            isContextAware: true,
+            requiredPermissions: ['DeleteProduct'],
+            handler: async context => {
+                const confirmed = await context.confirm(
+                    'Are you sure you want to delete this product? This action cannot be undone.',
+                );
+                if (confirmed) {
+                    // In a real implementation, this would call the delete API
+                    context.showNotification('Product deleted', 'success');
+                    context.navigate('/products');
+                }
+            },
+        },
+    ];
+
+    // Register these actions when the component mounts
+    useRegisterRouteActions(productActions);
+}
+
+/**
+ * Alternative approach: Create actions dynamically based on product state
+ */
+export function useProductRouteActionsWithState(product: any, isDirty: boolean, onSave: () => void) {
+    const { entityId } = useRouteActionHelpers();
+
+    const productActions: QuickAction[] = [
+        // Basic actions that always exist
+        {
+            id: 'duplicate-product',
+            label: 'Duplicate Product',
+            description: 'Create a copy of this product',
+            icon: 'copy',
+            shortcut: 'ctrl+d',
+            isContextAware: true,
+            handler: async context => {
+                const confirmed = await context.confirm('Duplicate this product?');
+                if (confirmed) {
+                    context.showNotification('Product duplicated successfully');
+                }
+            },
+        },
+        // Conditional actions based on state
+        ...(isDirty
+            ? [
+                  {
+                      id: 'save-product',
+                      label: 'Save Product',
+                      description: 'Save changes to this product',
+                      icon: 'save',
+                      shortcut: 'ctrl+s',
+                      isContextAware: true,
+                      handler: async context => {
+                          onSave();
+                          context.showNotification('Product saved');
+                      },
+                  },
+              ]
+            : []),
+        // Actions based on product properties
+        ...(product?.enabled
+            ? [
+                  {
+                      id: 'disable-product',
+                      label: 'Disable Product',
+                      description: 'Mark this product as disabled',
+                      icon: 'eye-off',
+                      isContextAware: true,
+                      handler: async context => {
+                          // Implementation here
+                          context.showNotification('Product disabled');
+                      },
+                  },
+              ]
+            : [
+                  {
+                      id: 'enable-product',
+                      label: 'Enable Product',
+                      description: 'Mark this product as enabled',
+                      icon: 'eye',
+                      isContextAware: true,
+                      handler: async context => {
+                          // Implementation here
+                          context.showNotification('Product enabled');
+                      },
+                  },
+              ]),
+    ];
+
+    useRegisterRouteActions(productActions);
+}
+
+/**
+ * Usage in a route component:
+ *
+ * function ProductDetailPage() {
+ *     // Register product-specific actions
+ *     useProductRouteActions();
+ *
+ *     // ... rest of component logic
+ * }
+ */

+ 51 - 13
packages/dashboard/src/lib/components/global-search/hooks/use-global-search.ts

@@ -1,5 +1,4 @@
-import { api } from '@/vdb/graphql/api.js';
-import { GLOBAL_SEARCH_QUERY } from '@/vdb/graphql/global-search.js';
+import { GlobalSearchResult } from '@/vdb/graphql/global-search.js';
 import { useDebounce } from '@/vdb/hooks/use-debounce.js';
 import { useSearchContext } from '@/vdb/providers/search-provider.js';
 import { useQuery } from '@tanstack/react-query';
@@ -13,25 +12,64 @@ export const useGlobalSearch = () => {
 
     const { data, isLoading, error } = useQuery({
         queryKey: ['globalSearch', debouncedQuery, selectedTypes],
-        queryFn: () => {
-            return api.query(GLOBAL_SEARCH_QUERY, {
-                input: {
-                    query: debouncedQuery,
-                    types: selectedTypes.length > 0 ? selectedTypes : undefined,
-                    limit: 20,
-                    skip: 0,
-                },
-            });
+        queryFn: async (): Promise<{ globalSearch: GlobalSearchResult[] }> => {
+            // For now, return mock data until backend is implemented
+            // In the future, this will be: return api.query(GLOBAL_SEARCH_QUERY, { input: ... });
+            if (!debouncedQuery || debouncedQuery.length < 2) {
+                return { globalSearch: [] };
+            }
+
+            // TODO: Replace with actual API call once backend is implemented
+            // return api.query(GLOBAL_SEARCH_QUERY, {
+            //     input: {
+            //         query: debouncedQuery,
+            //         types: selectedTypes.length > 0 ? selectedTypes : undefined,
+            //         limit: 20,
+            //         skip: 0,
+            //     },
+            // });
+
+            // Mock response for development
+            await new Promise(resolve => setTimeout(resolve, 300)); // Simulate network delay
+            return {
+                globalSearch: [
+                    {
+                        id: 'mock-product-1',
+                        type: 'PRODUCT',
+                        title: `Mock Product matching "${debouncedQuery}"`,
+                        subtitle: 'SKU: MOCK-001',
+                        description: 'This is a mock product for testing the search interface',
+                        url: '/products/1',
+                        relevanceScore: 0.9,
+                        lastModified: new Date().toISOString(),
+                    },
+                    {
+                        id: 'mock-customer-1',
+                        type: 'CUSTOMER',
+                        title: `Mock Customer "${debouncedQuery}"`,
+                        subtitle: 'customer@example.com',
+                        description: 'This is a mock customer for testing',
+                        url: '/customers/1',
+                        relevanceScore: 0.8,
+                        lastModified: new Date().toISOString(),
+                    },
+                ],
+            };
         },
-        enabled: !debouncedQuery || debouncedQuery.length < 2,
+        enabled: debouncedQuery.length >= 2,
     });
 
     useEffect(() => {
         if (data?.globalSearch) {
             setSearchResults(data.globalSearch);
             setIsSearching(false);
+
+            // Add to recent searches if query was successful and returned results
+            if (debouncedQuery && data.globalSearch.length > 0) {
+                addRecentSearch(debouncedQuery);
+            }
         }
-    }, [data, setSearchResults, setIsSearching]);
+    }, [data, setSearchResults, setIsSearching, debouncedQuery, addRecentSearch]);
 
     // Update loading state
     useEffect(() => {

+ 288 - 0
packages/dashboard/src/lib/components/global-search/hooks/use-product-list-actions.ts

@@ -0,0 +1,288 @@
+import { QuickAction, QuickActionContext } from '@/vdb/providers/search-provider.js';
+
+import { useRegisterRouteActions } from './use-register-route-actions.js';
+
+interface ProductListActionsOptions {
+    onCreateProduct?: () => void;
+    onExport?: () => void;
+    onRefresh?: () => void;
+    onBulkEdit?: () => void;
+    onImport?: () => void;
+    onBulkDelete?: () => void;
+    onBulkDuplicate?: () => void;
+    onFilterEnabled?: () => void;
+    onFilterDisabled?: () => void;
+    onFilterOutOfStock?: () => void;
+    onClearFilters?: () => void;
+    selectedCount?: number;
+    totalCount?: number;
+    dynamicLabels?: {
+        export?: { label: string; description: string };
+        bulkEdit?: { label: string; description: string };
+    };
+}
+
+/**
+ * Hook for registering context-aware quick actions specific to the product list page.
+ * This provides actions that are relevant when viewing the products list.
+ */
+export function useProductListActions(options: ProductListActionsOptions = {}) {
+    const {
+        onCreateProduct,
+        onExport,
+        onRefresh,
+        onBulkEdit,
+        onImport,
+        onBulkDelete,
+        onBulkDuplicate,
+        onFilterEnabled,
+        onFilterDisabled,
+        onFilterOutOfStock,
+        onClearFilters,
+        selectedCount = 0,
+        totalCount = 0,
+        dynamicLabels,
+    } = options;
+
+    const productListActions: QuickAction[] = [
+        // Create new product action
+        {
+            id: 'create-new-product',
+            label: 'Create New Product',
+            description: 'Add a new product to the catalog',
+            icon: 'plus',
+            shortcut: 'ctrl+n',
+            isContextAware: true,
+            requiredPermissions: ['CreateProduct', 'CreateCatalog'],
+            handler: (context: QuickActionContext) => {
+                if (onCreateProduct) {
+                    onCreateProduct();
+                } else {
+                    context.navigate('/products/new');
+                }
+            },
+        },
+
+        // Export products action
+        {
+            id: 'export-products',
+            label: dynamicLabels?.export?.label || 'Export Products',
+            description:
+                dynamicLabels?.export?.description ||
+                `Export ${totalCount > 0 ? `${totalCount} products` : 'products'} to CSV`,
+            icon: 'download',
+            shortcut: 'ctrl+e',
+            isContextAware: true,
+            handler: async (context: QuickActionContext) => {
+                if (onExport) {
+                    onExport();
+                } else {
+                    context.showNotification('Product export started', 'success');
+                    // Default export implementation would go here
+                    // This could trigger a download or navigate to an export page
+                }
+            },
+        },
+
+        // Import products action
+        {
+            id: 'import-products',
+            label: 'Import Products',
+            description: 'Import products from CSV file',
+            icon: 'upload',
+            shortcut: 'ctrl+i',
+            isContextAware: true,
+            requiredPermissions: ['CreateProduct', 'UpdateProduct'],
+            handler: (context: QuickActionContext) => {
+                if (onImport) {
+                    onImport();
+                } else {
+                    // Could open an import dialog or navigate to import page
+                    context.navigate('/products/import');
+                    // Or trigger a file picker dialog
+                    // document.createElement('input').click();
+                }
+            },
+        },
+
+        // Refresh products list action
+        {
+            id: 'refresh-products',
+            label: 'Refresh Products',
+            description: 'Reload the products list',
+            icon: 'refresh-cw',
+            shortcut: 'ctrl+r',
+            isContextAware: true,
+            handler: async (context: QuickActionContext) => {
+                if (onRefresh) {
+                    onRefresh();
+                } else {
+                    context.showNotification('Products list refreshed', 'success');
+                }
+            },
+        },
+
+        // Bulk operations action (only show if items are selected)
+        ...(selectedCount > 0
+            ? [
+                  {
+                      id: 'bulk-edit-products',
+                      label: dynamicLabels?.bulkEdit?.label || `Bulk Edit (${selectedCount} selected)`,
+                      description:
+                          dynamicLabels?.bulkEdit?.description ||
+                          `Edit ${selectedCount} selected products at once`,
+                      icon: 'edit',
+                      shortcut: 'ctrl+b',
+                      isContextAware: true,
+                      handler: async (context: QuickActionContext) => {
+                          if (onBulkEdit) {
+                              onBulkEdit();
+                          } else {
+                              context.showNotification(
+                                  `Bulk edit mode activated for ${selectedCount} products`,
+                                  'success',
+                              );
+                              // This could open a bulk edit dialog or enter a special mode
+                          }
+                      },
+                  },
+              ]
+            : []),
+
+        // Additional bulk actions (only show if items are selected)
+        ...(selectedCount > 0 && onBulkDelete
+            ? [
+                  {
+                      id: 'bulk-delete-products',
+                      label: `Delete Selected (${selectedCount})`,
+                      description: `Delete ${selectedCount} selected products permanently`,
+                      icon: 'trash',
+                      shortcut: 'ctrl+shift+delete',
+                      isContextAware: true,
+                      requiredPermissions: ['DeleteProduct'],
+                      handler: async (context: QuickActionContext) => {
+                          if (onBulkDelete) {
+                              onBulkDelete();
+                          } else {
+                              const confirmed = await context.confirm(
+                                  `Are you sure you want to delete ${selectedCount} products? This cannot be undone.`,
+                              );
+                              if (confirmed) {
+                                  context.showNotification(`${selectedCount} products deleted`, 'success');
+                              }
+                          }
+                      },
+                  },
+              ]
+            : []),
+
+        ...(selectedCount > 0 && onBulkDuplicate
+            ? [
+                  {
+                      id: 'bulk-duplicate-products',
+                      label: `Duplicate Selected (${selectedCount})`,
+                      description: `Create copies of ${selectedCount} selected products`,
+                      icon: 'copy',
+                      shortcut: 'ctrl+shift+d',
+                      isContextAware: true,
+                      requiredPermissions: ['CreateProduct'],
+                      handler: async (context: QuickActionContext) => {
+                          if (onBulkDuplicate) {
+                              onBulkDuplicate();
+                          } else {
+                              context.showNotification(`${selectedCount} products duplicated`, 'success');
+                          }
+                      },
+                  },
+              ]
+            : []),
+
+        // Quick filters/views
+        {
+            id: 'filter-enabled-products',
+            label: 'Show Enabled Products',
+            description: 'Filter to show only enabled products',
+            icon: 'eye',
+            isContextAware: true,
+            handler: (context: QuickActionContext) => {
+                if (onFilterEnabled) {
+                    onFilterEnabled();
+                } else {
+                    context.showNotification('Showing enabled products only', 'success');
+                }
+            },
+        },
+
+        {
+            id: 'filter-disabled-products',
+            label: 'Show Disabled Products',
+            description: 'Filter to show only disabled products',
+            icon: 'eye-off',
+            isContextAware: true,
+            handler: (context: QuickActionContext) => {
+                if (onFilterDisabled) {
+                    onFilterDisabled();
+                } else {
+                    context.showNotification('Showing disabled products only', 'success');
+                }
+            },
+        },
+
+        {
+            id: 'filter-out-of-stock',
+            label: 'Show Out of Stock',
+            description: 'Filter to show products that are out of stock',
+            icon: 'alert-triangle',
+            isContextAware: true,
+            handler: (context: QuickActionContext) => {
+                if (onFilterOutOfStock) {
+                    onFilterOutOfStock();
+                } else {
+                    context.showNotification('Showing out of stock products only', 'success');
+                }
+            },
+        },
+
+        // Clear all filters
+        {
+            id: 'clear-filters',
+            label: 'Clear All Filters',
+            description: 'Remove all applied filters and show all products',
+            icon: 'filter-x',
+            shortcut: 'ctrl+shift+x',
+            isContextAware: true,
+            handler: (context: QuickActionContext) => {
+                if (onClearFilters) {
+                    onClearFilters();
+                } else {
+                    context.showNotification('All filters cleared', 'success');
+                }
+            },
+        },
+
+        // Product management actions
+        {
+            id: 'manage-facets',
+            label: 'Manage Facets',
+            description: 'Navigate to facet management',
+            icon: 'tags',
+            isContextAware: true,
+            handler: (context: QuickActionContext) => {
+                context.navigate('/facets');
+            },
+        },
+
+        {
+            id: 'manage-collections',
+            label: 'Manage Collections',
+            description: 'Navigate to collection management',
+            icon: 'folder',
+            isContextAware: true,
+            handler: (context: QuickActionContext) => {
+                context.navigate('/collections');
+            },
+        },
+    ];
+
+    useRegisterRouteActions(productListActions);
+}

+ 11 - 223
packages/dashboard/src/lib/components/global-search/hooks/use-quick-actions.ts

@@ -1,10 +1,10 @@
-import { QuickAction, QuickActionContext, useSearchContext } from '@/vdb/providers/search-provider.js';
 import { useLocation, useNavigate } from '@tanstack/react-router';
-import { useCallback, useEffect, useMemo } from 'react';
+import { useCallback } from 'react';
 import { toast } from 'sonner';
 
+import { QuickActionContext, useVisibleQuickActions } from '../quick-actions-registry.js';
+
 export const useQuickActions = () => {
-    const { quickActions, registerQuickAction } = useSearchContext();
     const location = useLocation();
     const navigate = useNavigate();
 
@@ -17,7 +17,7 @@ export const useQuickActions = () => {
         );
 
         return {
-            currentRoute: location.pathname,
+            currentRoute: location,
             currentEntityType,
             currentEntityId,
             navigate: (path: string) => navigate({ to: path }),
@@ -25,234 +25,21 @@ export const useQuickActions = () => {
                 toast[type](message);
             },
             confirm: async (message: string) => {
+                // TODO: Replace with our internal confirm dialog
                 return window.confirm(message);
             },
-            executeGraphQL: async (query: string, variables?: any) => {
-                // perform API operation
-            },
         };
     }, [location, navigate]);
 
-    // Built-in global actions
-    const builtInGlobalActions: QuickAction[] = useMemo(
-        () => [
-            {
-                id: 'create-product',
-                label: 'Create New Product',
-                description: 'Create a new product',
-                icon: 'plus',
-                shortcut: 'ctrl+shift+p',
-                isContextAware: false,
-                handler: context => context.navigate('/products/create'),
-            },
-            {
-                id: 'create-customer',
-                label: 'Create New Customer',
-                description: 'Create a new customer',
-                icon: 'user-plus',
-                shortcut: 'ctrl+shift+c',
-                isContextAware: false,
-                handler: context => context.navigate('/customers/create'),
-            },
-            {
-                id: 'create-order',
-                label: 'Create New Order',
-                description: 'Create a new order',
-                icon: 'shopping-cart',
-                shortcut: 'ctrl+shift+o',
-                isContextAware: false,
-                handler: context => context.navigate('/orders/create'),
-            },
-            {
-                id: 'go-to-products',
-                label: 'Go to Products',
-                description: 'Navigate to products list',
-                icon: 'package',
-                isContextAware: false,
-                handler: context => context.navigate('/products'),
-            },
-            {
-                id: 'go-to-orders',
-                label: 'Go to Orders',
-                description: 'Navigate to orders list',
-                icon: 'shopping-cart',
-                isContextAware: false,
-                handler: context => context.navigate('/orders'),
-            },
-            {
-                id: 'go-to-customers',
-                label: 'Go to Customers',
-                description: 'Navigate to customers list',
-                icon: 'users',
-                isContextAware: false,
-                handler: context => context.navigate('/customers'),
-            },
-            {
-                id: 'go-to-profile',
-                label: 'Go to Profile',
-                description: 'Navigate to user profile',
-                icon: 'user',
-                isContextAware: false,
-                handler: context => context.navigate('/profile'),
-            },
-        ],
-        [],
-    );
-
-    // Context-aware actions based on current route
-    const getContextActions = useCallback(
-        (route: string, entityType?: string, entityId?: string): QuickAction[] => {
-            const actions: QuickAction[] = [];
-
-            // Product detail page actions
-            if (route.includes('/products/') && entityId) {
-                actions.push({
-                    id: 'duplicate-product',
-                    label: 'Duplicate Product',
-                    description: 'Duplicate current product',
-                    icon: 'copy',
-                    shortcut: 'ctrl+d',
-                    isContextAware: true,
-                    handler: async context => {
-                        const confirmed = await context.confirm('Duplicate this product?');
-                        if (confirmed) {
-                            // Implementation would go here
-                            context.showNotification('Product duplicated');
-                            context.navigate(`/products/${entityId}/duplicate`);
-                        }
-                    },
-                });
-
-                actions.push({
-                    id: 'add-variant',
-                    label: 'Add Product Variant',
-                    description: 'Add new variant to current product',
-                    icon: 'plus-square',
-                    shortcut: 'ctrl+shift+v',
-                    isContextAware: true,
-                    handler: context => {
-                        context.navigate(`/products/${entityId}/variants/create`);
-                    },
-                });
-            }
-
-            // Order detail page actions
-            if (route.includes('/orders/') && entityId) {
-                actions.push({
-                    id: 'fulfill-order',
-                    label: 'Fulfill Order',
-                    description: 'Fulfill current order',
-                    icon: 'truck',
-                    shortcut: 'ctrl+f',
-                    isContextAware: true,
-                    handler: context => {
-                        context.navigate(`/orders/${entityId}/fulfill`);
-                    },
-                });
-
-                actions.push({
-                    id: 'cancel-order',
-                    label: 'Cancel Order',
-                    description: 'Cancel current order',
-                    icon: 'x-circle',
-                    shortcut: 'ctrl+shift+x',
-                    isContextAware: true,
-                    handler: async context => {
-                        const confirmed = await context.confirm(
-                            'Are you sure you want to cancel this order?',
-                        );
-                        if (confirmed) {
-                            // Implementation would go here
-                            context.showNotification('Order cancelled');
-                        }
-                    },
-                });
-            }
-
-            // Customer detail page actions
-            if (route.includes('/customers/') && entityId) {
-                actions.push({
-                    id: 'view-customer-orders',
-                    label: 'View Customer Orders',
-                    description: 'View orders for current customer',
-                    icon: 'list',
-                    isContextAware: true,
-                    handler: context => {
-                        context.navigate(`/customers/${entityId}/orders`);
-                    },
-                });
-            }
-
-            // Any list page actions
-            if (route.includes('/list') || route.match(/\/(products|orders|customers|collections)$/)) {
-                actions.push({
-                    id: 'export-data',
-                    label: 'Export Data',
-                    description: 'Export current filtered data',
-                    icon: 'download',
-                    shortcut: 'ctrl+e',
-                    isContextAware: true,
-                    handler: async context => {
-                        context.showNotification('Export started', 'success');
-                        // Implementation would go here
-                    },
-                });
-
-                if (entityType) {
-                    actions.push({
-                        id: 'create-new-item',
-                        label: `Create New ${entityType.charAt(0).toUpperCase() + entityType.slice(1)}`,
-                        description: `Create new ${entityType}`,
-                        icon: 'plus',
-                        shortcut: 'ctrl+n',
-                        isContextAware: true,
-                        handler: context => {
-                            context.navigate(`/${entityType}/new`);
-                        },
-                    });
-                }
-            }
-
-            return actions;
-        },
-        [],
-    );
-
-    // Register built-in actions
-    useEffect(() => {
-        builtInGlobalActions.forEach(action => {
-            registerQuickAction(action);
-        });
-    }, [builtInGlobalActions, registerQuickAction]);
-
-    // Get all available actions for current context
-    const availableActions = useMemo(() => {
-        const pathSegments = location.pathname.split('/').filter(Boolean);
-        const entityType = pathSegments[0];
-        const entityId = pathSegments.find(
-            segment => segment.match(/^[a-f0-9-]{36}$/i) || segment.match(/^\d+$/),
-        );
-
-        const contextActions = getContextActions(location.pathname, entityType, entityId);
-
-        // Combine built-in global actions, context actions, and registered custom actions
-        const allActions = [
-            ...builtInGlobalActions,
-            ...contextActions,
-            ...quickActions.filter(
-                action =>
-                    !builtInGlobalActions.some(builtin => builtin.id === action.id) &&
-                    !contextActions.some(context => context.id === action.id),
-            ),
-        ];
-
-        return allActions;
-    }, [location.pathname, builtInGlobalActions, getContextActions, quickActions]);
+    // Get all actions visible for the current location from the registry
+    const availableActions = useVisibleQuickActions(location);
 
     const executeAction = useCallback(
         async (actionId: string) => {
             const action = availableActions.find(a => a.id === actionId);
             if (!action) {
+                // eslint-disable-next-line
+                console.warn(`Quick action with id "${actionId}" not found`);
                 return;
             }
 
@@ -260,6 +47,8 @@ export const useQuickActions = () => {
                 const context = createActionContext();
                 await action.handler(context);
             } catch (error) {
+                // eslint-disable-next-line
+                console.error(`Error executing quick action "${actionId}":`, error);
                 toast.error(`Failed to execute action: ${action.label}`);
             }
         },
@@ -269,7 +58,6 @@ export const useQuickActions = () => {
     return {
         actions: availableActions,
         executeAction,
-        registerAction: registerQuickAction,
         createContext: createActionContext,
     };
 };

+ 61 - 0
packages/dashboard/src/lib/components/global-search/hooks/use-register-route-actions.ts

@@ -0,0 +1,61 @@
+import { QuickAction, useSearchContext } from '@/vdb/providers/search-provider.js';
+import { useLocation, useParams } from '@tanstack/react-router';
+import { useEffect } from 'react';
+
+/**
+ * Hook for registering context-aware quick actions that are specific to a route.
+ * This should be used within route components to register actions that are only
+ * available when that specific route is active.
+ */
+export const useRegisterRouteActions = (actions: QuickAction[]) => {
+    const { registerQuickAction, unregisterQuickAction } = useSearchContext();
+    const location = useLocation();
+    const params = useParams({ strict: false });
+
+    useEffect(() => {
+        // Register actions when component mounts or location changes
+        actions.forEach(action => {
+            registerQuickAction({
+                ...action,
+                isContextAware: true, // Mark all route actions as context-aware
+                // Store route info in metadata for cleanup
+                params: {
+                    ...action.params,
+                    __routePath: location.pathname,
+                    __routeParams: params,
+                },
+            });
+        });
+
+        // Cleanup function to unregister actions when component unmounts or route changes
+        return () => {
+            actions.forEach(action => {
+                unregisterQuickAction(action.id);
+            });
+        };
+    }, [actions, registerQuickAction, unregisterQuickAction, location.pathname, params]);
+};
+
+/**
+ * Convenience hook for creating route-specific actions with access to current route context.
+ * Provides common utilities like navigate, showNotification, etc.
+ */
+export const useRouteActionHelpers = () => {
+    const location = useLocation();
+    const params = useParams({ strict: false });
+
+    // Extract entity info from current path
+    const pathSegments = location.pathname.split('/').filter(Boolean);
+    const entityType = pathSegments[0];
+    const entityId =
+        pathSegments.find(segment => segment.match(/^[a-f0-9-]{36}$/i) || segment.match(/^\d+$/)) ||
+        params.id;
+
+    return {
+        currentRoute: location.pathname,
+        entityType,
+        entityId,
+        params,
+        pathSegments,
+    };
+};

+ 6 - 0
packages/dashboard/src/lib/components/global-search/index.ts

@@ -17,3 +17,9 @@ export { RecentSearches } from './recent-searches.js';
 export { useGlobalSearch } from './hooks/use-global-search.js';
 export { useKeyboardShortcuts } from './hooks/use-keyboard-shortcuts.js';
 export { useQuickActions } from './hooks/use-quick-actions.js';
+
+// Registry (for centralized action management)
+export * from './quick-actions-registry.js';
+
+// Legacy context exports (now from registry)
+export type { QuickActionDefinition as QuickAction, QuickActionContext } from './quick-actions-registry.js';

+ 3 - 7
packages/dashboard/src/lib/components/global-search/quick-action-item.tsx

@@ -1,10 +1,10 @@
 import { CommandItem, CommandShortcut } from '@/vdb/components/ui/command.js';
 import { Badge } from '@/vdb/components/ui/badge.js';
-import { QuickAction } from '@/vdb/providers/search-provider.js';
+import { QuickActionDefinition } from './quick-actions-registry.js';
 import * as Icons from 'lucide-react';
 
 interface QuickActionItemProps {
-    action: QuickAction;
+    action: QuickActionDefinition;
     onSelect: (actionId: string) => void;
 }
 
@@ -28,11 +28,7 @@ export function QuickActionItem({ action, onSelect }: QuickActionItemProps) {
             <div className="flex-1 space-y-1">
                 <div className="flex items-center gap-2">
                     <span className="font-medium">{action.label}</span>
-                    {action.isContextAware && (
-                        <Badge variant="secondary" className="text-xs">
-                            Context
-                        </Badge>
-                    )}
+                    {/* Context badge removed since visibility is now determined by the visible function */}
                 </div>
                 {action.description && (
                     <p className="text-xs text-muted-foreground">{action.description}</p>

+ 1 - 0
packages/dashboard/src/lib/components/global-search/quick-actions-list.tsx

@@ -2,6 +2,7 @@ import { CommandGroup } from '@/vdb/components/ui/command.js';
 import { QuickActionItem } from './quick-action-item.js';
 import { useQuickActions } from './hooks/use-quick-actions.js';
 import { useMemo } from 'react';
+import { QuickActionDefinition } from './quick-actions-registry.js';
 
 interface QuickActionsListProps {
     searchQuery: string;

+ 186 - 0
packages/dashboard/src/lib/components/global-search/quick-actions-registry.ts

@@ -0,0 +1,186 @@
+import { ParsedLocation, useRouter } from '@tanstack/react-router';
+import { useMemo } from 'react';
+
+import { globalRegistry } from '../../framework/registry/global-registry.js';
+
+export interface QuickActionDefinition {
+    id: string;
+    label: string;
+    description?: string;
+    icon?: string;
+    shortcut?: string;
+    requiredPermissions?: string[];
+    handler: QuickActionHandler;
+    params?: Record<string, any>;
+    /**
+     * Function that determines if this action should be visible based on the current route.
+     * Replaces the isContextAware flag with more flexible visibility logic.
+     */
+    visible?: (location: ParsedLocation, basepath?: string) => boolean;
+}
+
+export type QuickActionHandler = (context: QuickActionContext) => void | Promise<void>;
+
+export interface QuickActionContext {
+    currentRoute: ParsedLocation<any>;
+    currentEntityType?: string;
+    currentEntityId?: string;
+    navigate: (path: string) => void;
+    showNotification: (message: string, type?: 'success' | 'error' | 'warning') => void;
+    confirm: (message: string) => Promise<boolean>;
+}
+
+/**
+ * Initialize the quick actions registry in the global registry if it doesn't exist
+ */
+function initializeRegistry(): Map<string, QuickActionDefinition> {
+    if (!globalRegistry.has('searchQuickActions')) {
+        globalRegistry.register('searchQuickActions', new Map<string, QuickActionDefinition>());
+    }
+    return globalRegistry.get('searchQuickActions');
+}
+
+/**
+ * Register a quick action in the global registry
+ */
+export function registerQuickAction(action: QuickActionDefinition) {
+    const registry = initializeRegistry();
+    registry.set(action.id, action);
+}
+
+/**
+ * Get all registered quick actions
+ */
+export function getRegisteredQuickActions(): QuickActionDefinition[] {
+    const registry = initializeRegistry();
+    return Array.from(registry.values());
+}
+
+/**
+ * Get quick actions that are visible for the given location
+ */
+export function getVisibleQuickActions(location: ParsedLocation, basepath?: string): QuickActionDefinition[] {
+    const registry = initializeRegistry();
+    return Array.from(registry.values()).filter(action => action.visible?.(location, basepath) ?? true);
+}
+
+/**
+ * Hook to get quick actions that are visible for the current location, taking router basepath into account
+ */
+export function useVisibleQuickActions(location: ParsedLocation): QuickActionDefinition[] {
+    const router = useRouter();
+    const basepath = router.basepath || '';
+    return useMemo(() => getVisibleQuickActions(location, basepath), [basepath, location]);
+}
+
+/**
+ * Clear all registered quick actions (mainly for testing)
+ */
+export function clearQuickActionsRegistry() {
+    const registry = initializeRegistry();
+    registry.clear();
+}
+
+// Utility functions for common visibility patterns
+
+/**
+ * Normalizes a pathname by removing the basepath if present
+ */
+function normalizePathname(pathname: string, basepath?: string): string {
+    if (!basepath || basepath === '/') {
+        return pathname;
+    }
+
+    // Ensure basepath doesn't end with a slash (unless it's just "/")
+    const normalizedBasepath = basepath.endsWith('/') ? basepath.slice(0, -1) : basepath;
+
+    if (pathname.startsWith(normalizedBasepath)) {
+        const pathWithoutBase = pathname.slice(normalizedBasepath.length);
+        return pathWithoutBase.startsWith('/') ? pathWithoutBase : '/' + pathWithoutBase;
+    }
+
+    return pathname;
+}
+
+/**
+ * Always visible (global actions)
+ */
+export const alwaysVisible = (_location: ParsedLocation, _basepath?: string) => true;
+
+/**
+ * Visible only on specific routes
+ */
+export const visibleOnRoutes =
+    (...routes: string[]) =>
+    (location: ParsedLocation, basepath?: string) => {
+        const normalizedPath = normalizePathname(location.pathname, basepath);
+        return routes.some(route => {
+            // Support exact match and pattern matching
+            if (route.includes('*')) {
+                const pattern = route.replace('*', '.*');
+                const regex = new RegExp(`^${pattern}$`);
+                return regex.test(normalizedPath);
+            }
+            return normalizedPath === route || normalizedPath.startsWith(route + '/');
+        });
+    };
+
+/**
+ * Visible only on list pages
+ */
+export const visibleOnListPages = (entityType?: string) => (location: ParsedLocation, basepath?: string) => {
+    const path = normalizePathname(location.pathname, basepath);
+    if (entityType) {
+        return path === `/${entityType}` || path === `/${entityType}/`;
+    }
+    // Generic list page detection
+    // eslint-disable-next-line
+    return /^\/(products|customers|orders|collections|facets|assets|promotions|administrators|roles|channels|countries|zones|sellers|stock-locations|customer-groups|payment-methods|shipping-methods|tax-categories|tax-rates)$/.test(
+        path,
+    );
+};
+
+/**
+ * Visible only on detail pages
+ */
+export const visibleOnDetailPages =
+    (entityType?: string) => (location: ParsedLocation, basepath?: string) => {
+        const normalizedPath = normalizePathname(location.pathname, basepath);
+        const pathSegments = normalizedPath.split('/').filter(Boolean);
+
+        if (entityType) {
+            return pathSegments[0] === entityType && pathSegments.length >= 2 && pathSegments[1] !== 'new';
+        }
+
+        // Generic detail page detection (has an ID in the URL)
+        return (
+            pathSegments.length >= 2 &&
+            pathSegments[1] !== 'new' &&
+            (!!pathSegments[1].match(/^[a-f0-9-]{36}$/i) || !!pathSegments[1].match(/^\d+$/))
+        );
+    };
+
+/**
+ * Visible only on create/new pages
+ */
+export const visibleOnCreatePages =
+    (entityType?: string) => (location: ParsedLocation, basepath?: string) => {
+        const normalizedPath = normalizePathname(location.pathname, basepath);
+        const pathSegments = normalizedPath.split('/').filter(Boolean);
+
+        if (entityType) {
+            return pathSegments[0] === entityType && pathSegments[1] === 'new';
+        }
+
+        // Generic create page detection
+        return pathSegments.length >= 2 && pathSegments[1] === 'new';
+    };
+
+/**
+ * Visible when selection exists (requires additional state, used with dynamic actions)
+ */
+export const visibleWithSelection =
+    (getSelectionCount?: () => number) => (location: ParsedLocation, basepath?: string) => {
+        const isListPage = visibleOnListPages()(location, basepath);
+        return isListPage && (getSelectionCount ? getSelectionCount() > 0 : true);
+    };

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

@@ -1,3 +1,9 @@
+import {
+    alwaysVisible,
+    registerQuickAction,
+    visibleOnDetailPages,
+    visibleOnListPages,
+} from '@/vdb/components/global-search/quick-actions-registry.js';
 import { setNavMenuConfig } from '@/vdb/framework/nav-menu/nav-menu-extensions.js';
 import {
     LayoutDashboardIcon,
@@ -275,4 +281,264 @@ export function registerDefaults() {
     //         },
     //     ],
     // });
+
+    // Register default quick actions
+    registerDefaultQuickActions();
+}
+
+function registerDefaultQuickActions() {
+    // Global actions - always visible
+    registerQuickAction({
+        id: 'create-product',
+        label: 'Create New Product',
+        description: 'Create a new product',
+        icon: 'plus',
+        shortcut: 'ctrl+shift+p',
+        visible: alwaysVisible,
+        requiredPermissions: ['CreateProduct', 'CreateCatalog'],
+        handler: context => context.navigate('/products/new'),
+    });
+
+    registerQuickAction({
+        id: 'create-customer',
+        label: 'Create New Customer',
+        description: 'Create a new customer',
+        icon: 'user-plus',
+        shortcut: 'ctrl+shift+c',
+        visible: alwaysVisible,
+        requiredPermissions: ['CreateCustomer'],
+        handler: context => context.navigate('/customers/new'),
+    });
+
+    registerQuickAction({
+        id: 'create-order',
+        label: 'Create New Order',
+        description: 'Create a new order',
+        icon: 'shopping-cart',
+        shortcut: 'ctrl+shift+o',
+        visible: alwaysVisible,
+        requiredPermissions: ['CreateOrder'],
+        handler: context => context.navigate('/orders/new'),
+    });
+
+    registerQuickAction({
+        id: 'go-to-products',
+        label: 'Go to Products',
+        description: 'Navigate to products list',
+        icon: 'package',
+        visible: alwaysVisible,
+        handler: context => context.navigate('/products'),
+    });
+
+    registerQuickAction({
+        id: 'go-to-orders',
+        label: 'Go to Orders',
+        description: 'Navigate to orders list',
+        icon: 'shopping-cart',
+        visible: alwaysVisible,
+        handler: context => context.navigate('/orders'),
+    });
+
+    registerQuickAction({
+        id: 'go-to-customers',
+        label: 'Go to Customers',
+        description: 'Navigate to customers list',
+        icon: 'users',
+        visible: alwaysVisible,
+        handler: context => context.navigate('/customers'),
+    });
+
+    // Product list actions
+    registerQuickAction({
+        id: 'export-products',
+        label: 'Export Products',
+        description: 'Export products to CSV',
+        icon: 'download',
+        shortcut: 'ctrl+e',
+        visible: visibleOnListPages('products'),
+        handler: async context => {
+            context.showNotification('Product export started', 'success');
+        },
+    });
+
+    registerQuickAction({
+        id: 'import-products',
+        label: 'Import Products',
+        description: 'Import products from CSV',
+        icon: 'upload',
+        shortcut: 'ctrl+i',
+        visible: visibleOnListPages('products'),
+        requiredPermissions: ['CreateProduct', 'UpdateProduct'],
+        handler: context => {
+            context.navigate('/products/import');
+        },
+    });
+
+    registerQuickAction({
+        id: 'refresh-products',
+        label: 'Refresh Products',
+        description: 'Reload the products list',
+        icon: 'refresh-cw',
+        shortcut: 'ctrl+r',
+        visible: visibleOnListPages('products'),
+        handler: async context => {
+            context.showNotification('Products list refreshed', 'success');
+            // In real implementation, this would trigger a refetch
+        },
+    });
+
+    // Product detail actions
+    registerQuickAction({
+        id: 'duplicate-product',
+        label: 'Duplicate Product',
+        description: 'Create a copy of this product',
+        icon: 'copy',
+        shortcut: 'ctrl+d',
+        visible: visibleOnDetailPages('products'),
+        requiredPermissions: ['CreateProduct'],
+        handler: async context => {
+            const confirmed = await context.confirm('Duplicate this product?');
+            if (confirmed) {
+                context.showNotification('Product duplicated successfully');
+                // In real implementation, this would duplicate the product
+            }
+        },
+    });
+
+    registerQuickAction({
+        id: 'add-product-variant',
+        label: 'Add Variant',
+        description: 'Add a new variant to this product',
+        icon: 'plus-square',
+        shortcut: 'ctrl+shift+v',
+        visible: visibleOnDetailPages('products'),
+        requiredPermissions: ['CreateProduct', 'UpdateProduct'],
+        handler: context => {
+            const entityId = context.currentEntityId;
+            if (entityId) {
+                context.navigate(`/products/${entityId}/variants/create`);
+            }
+        },
+    });
+
+    registerQuickAction({
+        id: 'delete-product',
+        label: 'Delete Product',
+        description: 'Delete this product permanently',
+        icon: 'trash',
+        shortcut: 'ctrl+shift+delete',
+        visible: visibleOnDetailPages('products'),
+        requiredPermissions: ['DeleteProduct'],
+        handler: async context => {
+            const confirmed = await context.confirm(
+                'Are you sure you want to delete this product? This action cannot be undone.',
+            );
+            if (confirmed) {
+                context.showNotification('Product deleted', 'success');
+                context.navigate('/products');
+            }
+        },
+    });
+
+    // Customer list actions
+    registerQuickAction({
+        id: 'export-customers',
+        label: 'Export Customers',
+        description: 'Export customers to CSV',
+        icon: 'download',
+        shortcut: 'ctrl+e',
+        visible: visibleOnListPages('customers'),
+        handler: async context => {
+            context.showNotification('Customer export started', 'success');
+        },
+    });
+
+    // Customer detail actions
+    registerQuickAction({
+        id: 'view-customer-orders',
+        label: 'View Customer Orders',
+        description: 'View orders for this customer',
+        icon: 'shopping-cart',
+        visible: visibleOnDetailPages('customers'),
+        handler: context => {
+            const entityId = context.currentEntityId;
+            if (entityId) {
+                context.navigate(`/customers/${entityId}/orders`);
+            }
+        },
+    });
+
+    // Order list actions
+    registerQuickAction({
+        id: 'export-orders',
+        label: 'Export Orders',
+        description: 'Export orders to CSV',
+        icon: 'download',
+        shortcut: 'ctrl+e',
+        visible: visibleOnListPages('orders'),
+        handler: async context => {
+            context.showNotification('Order export started', 'success');
+        },
+    });
+
+    // Order detail actions
+    registerQuickAction({
+        id: 'fulfill-order',
+        label: 'Fulfill Order',
+        description: 'Fulfill this order',
+        icon: 'truck',
+        shortcut: 'ctrl+f',
+        visible: visibleOnDetailPages('orders'),
+        requiredPermissions: ['UpdateOrder'],
+        handler: context => {
+            const entityId = context.currentEntityId;
+            if (entityId) {
+                context.navigate(`/orders/${entityId}/fulfill`);
+            }
+        },
+    });
+
+    registerQuickAction({
+        id: 'cancel-order',
+        label: 'Cancel Order',
+        description: 'Cancel this order',
+        icon: 'x-circle',
+        shortcut: 'ctrl+shift+x',
+        visible: visibleOnDetailPages('orders'),
+        requiredPermissions: ['UpdateOrder'],
+        handler: async context => {
+            const confirmed = await context.confirm('Are you sure you want to cancel this order?');
+            if (confirmed) {
+                context.showNotification('Order cancelled', 'success');
+            }
+        },
+    });
+
+    // Navigation actions
+    registerQuickAction({
+        id: 'go-to-profile',
+        label: 'Go to Profile',
+        description: 'Navigate to user profile',
+        icon: 'user',
+        visible: alwaysVisible,
+        handler: context => context.navigate('/profile'),
+    });
+
+    registerQuickAction({
+        id: 'manage-facets',
+        label: 'Manage Facets',
+        description: 'Navigate to facet management',
+        icon: 'tags',
+        visible: visibleOnListPages('products'),
+        handler: context => context.navigate('/facets'),
+    });
+
+    registerQuickAction({
+        id: 'manage-collections',
+        label: 'Manage Collections',
+        description: 'Navigate to collection management',
+        icon: 'folder',
+        visible: visibleOnListPages('products'),
+        handler: context => context.navigate('/collections'),
+    });
 }

+ 5 - 0
packages/dashboard/src/lib/framework/extension-api/define-dashboard-extension.ts

@@ -9,6 +9,7 @@ import {
     registerLayoutExtensions,
     registerLoginExtensions,
     registerNavigationExtensions,
+    registerQuickActionsExtensions,
     registerWidgetExtensions,
 } from './logic/index.js';
 
@@ -61,8 +62,12 @@ export function defineDashboardExtension(extension: DashboardExtension) {
         // Register login extensions
         registerLoginExtensions(extension.login);
 
+        // Register quick actions extensions
+        registerQuickActionsExtensions(extension.quickActions);
+
         // Execute extension source change callbacks
         const callbacks = globalRegistry.get('extensionSourceChangeCallbacks');
+
         if (callbacks.size) {
             for (const callback of callbacks) {
                 callback();

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

@@ -1,4 +1,6 @@
 // Import types for the main interface
+import { QuickActionDefinition } from '../../components/global-search/quick-actions-registry.js';
+
 import {
     DashboardActionBarItem,
     DashboardAlertDefinition,
@@ -73,4 +75,9 @@ export interface DashboardExtension {
      * Allows you to customize the login page with custom components.
      */
     login?: DashboardLoginExtensions;
+    /**
+     * @description
+     * Allows you to define custom quick actions that can be accessed via the global search command palette.
+     */
+    quickActions?: QuickActionDefinition[];
 }

+ 1 - 0
packages/dashboard/src/lib/framework/extension-api/logic/index.ts

@@ -6,4 +6,5 @@ export * from './form-components.js';
 export * from './layout.js';
 export * from './login.js';
 export * from './navigation.js';
+export * from './quick-actions.js';
 export * from './widgets.js';

+ 9 - 0
packages/dashboard/src/lib/framework/extension-api/logic/quick-actions.ts

@@ -0,0 +1,9 @@
+import { QuickActionDefinition, registerQuickAction } from '@/vdb/components/global-search/index.js';
+
+export function registerQuickActionsExtensions(quickActions?: QuickActionDefinition[]) {
+    if (quickActions && quickActions.length > 0) {
+        for (const action of quickActions) {
+            registerQuickAction(action);
+        }
+    }
+}

+ 2 - 0
packages/dashboard/src/lib/framework/registry/registry-types.ts

@@ -8,6 +8,7 @@ import {
 import { DocumentNode } from 'graphql';
 import React from 'react';
 
+import { QuickActionDefinition } from '../../components/global-search/quick-actions-registry.js';
 import { DataDisplayComponent, DataInputComponent } from '../component-registry/component-registry.js';
 import { DashboardAlertDefinition } from '../extension-api/types/alerts.js';
 import { CustomFormComponentInputProps } from '../form-engine/custom-form-component.js';
@@ -28,4 +29,5 @@ export interface GlobalRegistryContents {
     listQueryDocumentRegistry: Map<string, DocumentNode[]>;
     detailQueryDocumentRegistry: Map<string, DocumentNode[]>;
     loginExtensions: DashboardLoginExtensions;
+    searchQuickActions: Map<string, QuickActionDefinition>;
 }

+ 24 - 64
packages/dashboard/src/lib/graphql/global-search.ts

@@ -1,5 +1,9 @@
 import { gql } from 'graphql-tag';
 
+// Note: These GraphQL definitions are placeholders for the frontend implementation.
+// The actual backend GraphQL schema will be implemented separately according to
+// the GLOBAL_SEARCH_IMPLEMENTATION_PLAN.md
+
 export const GLOBAL_SEARCH_QUERY = gql`
     query GlobalSearch($input: GlobalSearchInput!) {
         globalSearch(input: $input) {
@@ -17,68 +21,24 @@ export const GLOBAL_SEARCH_QUERY = gql`
     }
 `;
 
-export const GLOBAL_SEARCH_INPUT_FRAGMENT = gql`
-    input GlobalSearchInput {
-        query: String!
-        types: [SearchResultType!]
-        limit: Int = 20
-        skip: Int = 0
-        channelId: String
-    }
-`;
-
-export const SEARCH_RESULT_TYPE_ENUM = gql`
-    enum SearchResultType {
-        # Core Entities
-        PRODUCT
-        PRODUCT_VARIANT
-        CUSTOMER
-        ORDER
-        COLLECTION
-        ADMINISTRATOR
-        CHANNEL
-        ASSET
-        FACET
-        FACET_VALUE
-        PROMOTION
-        PAYMENT_METHOD
-        SHIPPING_METHOD
-        TAX_CATEGORY
-        TAX_RATE
-        COUNTRY
-        ZONE
-        ROLE
-        CUSTOMER_GROUP
-        STOCK_LOCATION
-        TAG
-
-        # Custom/Plugin Entities
-        CUSTOM_ENTITY
-
-        # Dashboard Content
-        NAVIGATION
-        SETTINGS
-        QUICK_ACTION
+// TypeScript types that match the expected GraphQL schema
+export interface GlobalSearchInput {
+    query: string;
+    types?: string[];
+    limit?: number;
+    skip?: number;
+    channelId?: string;
+}
 
-        # External Content
-        DOCUMENTATION
-        BLOG_POST
-        PLUGIN
-        WEBSITE_CONTENT
-    }
-`;
-
-export const GLOBAL_SEARCH_RESULT_FRAGMENT = gql`
-    fragment GlobalSearchResult on GlobalSearchResult {
-        id
-        type
-        title
-        subtitle
-        description
-        url
-        thumbnailUrl
-        metadata
-        relevanceScore
-        lastModified
-    }
-`;
+export interface GlobalSearchResult {
+    id: string;
+    type: string;
+    title: string;
+    subtitle?: string;
+    description?: string;
+    url: string;
+    thumbnailUrl?: string;
+    metadata?: Record<string, any>;
+    relevanceScore?: number;
+    lastModified?: string;
+}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 2
packages/dashboard/src/lib/graphql/graphql-env.d.ts


+ 1 - 1
packages/dashboard/src/lib/providers/search-provider.tsx

@@ -2,7 +2,7 @@ import { createContext, useContext, useState, ReactNode, useCallback } from 'rea
 
 export interface SearchResult {
     id: string;
-    type: SearchResultType;
+    type: SearchResultType | string; // Allow both enum and string for flexibility
     title: string;
     subtitle?: string;
     description?: string;

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است