|
@@ -1,10 +1,10 @@
|
|
|
-import { QuickAction, QuickActionContext, useSearchContext } from '@/vdb/providers/search-provider.js';
|
|
|
|
|
import { useLocation, useNavigate } from '@tanstack/react-router';
|
|
import { useLocation, useNavigate } from '@tanstack/react-router';
|
|
|
-import { useCallback, useEffect, useMemo } from 'react';
|
|
|
|
|
|
|
+import { useCallback } from 'react';
|
|
|
import { toast } from 'sonner';
|
|
import { toast } from 'sonner';
|
|
|
|
|
|
|
|
|
|
+import { QuickActionContext, useVisibleQuickActions } from '../quick-actions-registry.js';
|
|
|
|
|
+
|
|
|
export const useQuickActions = () => {
|
|
export const useQuickActions = () => {
|
|
|
- const { quickActions, registerQuickAction } = useSearchContext();
|
|
|
|
|
const location = useLocation();
|
|
const location = useLocation();
|
|
|
const navigate = useNavigate();
|
|
const navigate = useNavigate();
|
|
|
|
|
|
|
@@ -17,7 +17,7 @@ export const useQuickActions = () => {
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
- currentRoute: location.pathname,
|
|
|
|
|
|
|
+ currentRoute: location,
|
|
|
currentEntityType,
|
|
currentEntityType,
|
|
|
currentEntityId,
|
|
currentEntityId,
|
|
|
navigate: (path: string) => navigate({ to: path }),
|
|
navigate: (path: string) => navigate({ to: path }),
|
|
@@ -25,234 +25,21 @@ export const useQuickActions = () => {
|
|
|
toast[type](message);
|
|
toast[type](message);
|
|
|
},
|
|
},
|
|
|
confirm: async (message: string) => {
|
|
confirm: async (message: string) => {
|
|
|
|
|
+ // TODO: Replace with our internal confirm dialog
|
|
|
return window.confirm(message);
|
|
return window.confirm(message);
|
|
|
},
|
|
},
|
|
|
- executeGraphQL: async (query: string, variables?: any) => {
|
|
|
|
|
- // perform API operation
|
|
|
|
|
- },
|
|
|
|
|
};
|
|
};
|
|
|
}, [location, navigate]);
|
|
}, [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(
|
|
const executeAction = useCallback(
|
|
|
async (actionId: string) => {
|
|
async (actionId: string) => {
|
|
|
const action = availableActions.find(a => a.id === actionId);
|
|
const action = availableActions.find(a => a.id === actionId);
|
|
|
if (!action) {
|
|
if (!action) {
|
|
|
|
|
+ // eslint-disable-next-line
|
|
|
|
|
+ console.warn(`Quick action with id "${actionId}" not found`);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -260,6 +47,8 @@ export const useQuickActions = () => {
|
|
|
const context = createActionContext();
|
|
const context = createActionContext();
|
|
|
await action.handler(context);
|
|
await action.handler(context);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
|
|
+ // eslint-disable-next-line
|
|
|
|
|
+ console.error(`Error executing quick action "${actionId}":`, error);
|
|
|
toast.error(`Failed to execute action: ${action.label}`);
|
|
toast.error(`Failed to execute action: ${action.label}`);
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
@@ -269,7 +58,6 @@ export const useQuickActions = () => {
|
|
|
return {
|
|
return {
|
|
|
actions: availableActions,
|
|
actions: availableActions,
|
|
|
executeAction,
|
|
executeAction,
|
|
|
- registerAction: registerQuickAction,
|
|
|
|
|
createContext: createActionContext,
|
|
createContext: createActionContext,
|
|
|
};
|
|
};
|
|
|
};
|
|
};
|