@vendure/dashboard 3.4.3-master-202509260228 → 3.5.0-minor-202509261210

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/plugin/api/api-extensions.js +11 -14
  2. package/dist/plugin/api/metrics.resolver.d.ts +2 -2
  3. package/dist/plugin/api/metrics.resolver.js +2 -2
  4. package/dist/plugin/config/metrics-strategies.d.ts +9 -9
  5. package/dist/plugin/config/metrics-strategies.js +6 -6
  6. package/dist/plugin/constants.d.ts +2 -0
  7. package/dist/plugin/constants.js +3 -1
  8. package/dist/plugin/dashboard.plugin.js +13 -0
  9. package/dist/plugin/service/metrics.service.d.ts +3 -3
  10. package/dist/plugin/service/metrics.service.js +37 -53
  11. package/dist/plugin/types.d.ts +9 -12
  12. package/dist/plugin/types.js +7 -11
  13. package/dist/vite/vite-plugin-vendure-dashboard.js +2 -2
  14. package/package.json +4 -4
  15. package/src/app/routes/_authenticated/_collections/collections.tsx +7 -2
  16. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +15 -2
  17. package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +14 -2
  18. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +10 -0
  19. package/src/app/routes/_authenticated/_products/components/product-option-group-badge.tsx +19 -0
  20. package/src/app/routes/_authenticated/_products/components/product-options-table.tsx +111 -0
  21. package/src/app/routes/_authenticated/_products/product-option-groups.graphql.ts +103 -0
  22. package/src/app/routes/_authenticated/_products/products.graphql.ts +13 -1
  23. package/src/app/routes/_authenticated/_products/products.tsx +27 -3
  24. package/src/app/routes/_authenticated/_products/products_.$id.tsx +26 -9
  25. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +181 -0
  26. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +208 -0
  27. package/src/app/routes/_authenticated/_zones/components/zone-countries-sheet.tsx +4 -1
  28. package/src/app/routes/_authenticated/index.tsx +41 -24
  29. package/src/lib/components/data-display/json.tsx +16 -1
  30. package/src/lib/components/data-input/index.ts +3 -0
  31. package/src/lib/components/data-input/slug-input.tsx +296 -0
  32. package/src/lib/components/data-table/add-filter-menu.tsx +13 -6
  33. package/src/lib/components/data-table/data-table-bulk-action-item.tsx +38 -1
  34. package/src/lib/components/data-table/data-table-context.tsx +91 -0
  35. package/src/lib/components/data-table/data-table-filter-badge.tsx +9 -5
  36. package/src/lib/components/data-table/data-table-view-options.tsx +17 -8
  37. package/src/lib/components/data-table/data-table.tsx +146 -94
  38. package/src/lib/components/data-table/global-views-bar.tsx +97 -0
  39. package/src/lib/components/data-table/global-views-sheet.tsx +11 -0
  40. package/src/lib/components/data-table/manage-global-views-button.tsx +26 -0
  41. package/src/lib/components/data-table/my-views-button.tsx +47 -0
  42. package/src/lib/components/data-table/refresh-button.tsx +12 -3
  43. package/src/lib/components/data-table/save-view-button.tsx +45 -0
  44. package/src/lib/components/data-table/save-view-dialog.tsx +113 -0
  45. package/src/lib/components/data-table/use-generated-columns.tsx +3 -1
  46. package/src/lib/components/data-table/user-views-sheet.tsx +11 -0
  47. package/src/lib/components/data-table/views-sheet.tsx +297 -0
  48. package/src/lib/components/date-range-picker.tsx +184 -0
  49. package/src/lib/components/shared/paginated-list-data-table.tsx +59 -32
  50. package/src/lib/components/ui/button.tsx +1 -1
  51. package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +29 -2
  52. package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +10 -7
  53. package/src/lib/framework/dashboard-widget/metrics-widget/metrics-widget.graphql.ts +9 -3
  54. package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +19 -75
  55. package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +33 -0
  56. package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +319 -9
  57. package/src/lib/framework/document-introspection/add-custom-fields.ts +60 -31
  58. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +1 -159
  59. package/src/lib/framework/document-introspection/include-only-selected-list-fields.spec.ts +1840 -0
  60. package/src/lib/framework/document-introspection/include-only-selected-list-fields.ts +940 -0
  61. package/src/lib/framework/document-introspection/testing-utils.ts +161 -0
  62. package/src/lib/framework/extension-api/display-component-extensions.tsx +2 -0
  63. package/src/lib/framework/extension-api/types/data-table.ts +62 -4
  64. package/src/lib/framework/extension-api/types/navigation.ts +16 -0
  65. package/src/lib/framework/form-engine/utils.ts +34 -0
  66. package/src/lib/framework/page/list-page.tsx +289 -4
  67. package/src/lib/framework/page/use-extended-router.tsx +59 -17
  68. package/src/lib/graphql/api.ts +4 -2
  69. package/src/lib/graphql/graphql-env.d.ts +13 -10
  70. package/src/lib/hooks/use-extended-list-query.ts +5 -0
  71. package/src/lib/hooks/use-saved-views.ts +230 -0
  72. package/src/lib/index.ts +15 -0
  73. package/src/lib/types/saved-views.ts +39 -0
  74. package/src/lib/utils/saved-views-utils.ts +40 -0
@@ -0,0 +1,97 @@
1
+ import { ChevronDown } from 'lucide-react';
2
+ import React from 'react';
3
+ import { useSavedViews } from '../../hooks/use-saved-views.js';
4
+ import { Trans } from '../../lib/trans.js';
5
+ import { SavedView } from '../../types/saved-views.js';
6
+ import { findMatchingSavedView } from '../../utils/saved-views-utils.js';
7
+ import { PermissionGuard } from '../shared/permission-guard.js';
8
+ import { Button } from '../ui/button.js';
9
+ import {
10
+ DropdownMenu,
11
+ DropdownMenuContent,
12
+ DropdownMenuItem,
13
+ DropdownMenuTrigger,
14
+ } from '../ui/dropdown-menu.js';
15
+ import { useDataTableContext } from './data-table-context.js';
16
+ import { ManageGlobalViewsButton } from './manage-global-views-button.js';
17
+
18
+ export const GlobalViewsBar: React.FC = () => {
19
+ const { globalViews, canManageGlobalViews } = useSavedViews();
20
+ const { columnFilters, searchTerm, handleApplyView } = useDataTableContext();
21
+
22
+ if (globalViews.length === 0) {
23
+ return null;
24
+ }
25
+
26
+ const sortedViews = [...globalViews].sort(
27
+ (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
28
+ );
29
+
30
+ const handleViewClick = (view: SavedView) => {
31
+ handleApplyView(view.filters, view.searchTerm);
32
+ };
33
+
34
+ const isViewActive = (view: SavedView) => {
35
+ return findMatchingSavedView(columnFilters, searchTerm, [view]) !== undefined;
36
+ };
37
+
38
+ if (sortedViews.length <= 3) {
39
+ // Show all views as buttons
40
+ return (
41
+ <div className="flex items-center gap-1">
42
+ {sortedViews.map(view => (
43
+ <Button
44
+ key={view.id}
45
+ variant={isViewActive(view) ? 'default' : 'outline'}
46
+ size="sm"
47
+ onClick={() => handleViewClick(view)}
48
+ >
49
+ {view.name}
50
+ </Button>
51
+ ))}
52
+ {canManageGlobalViews && <ManageGlobalViewsButton />}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ // Show first 3 as buttons, rest in dropdown
58
+ const visibleViews = sortedViews.slice(0, 3);
59
+ const dropdownViews = sortedViews.slice(3);
60
+
61
+ return (
62
+ <div className="flex items-center gap-1">
63
+ {visibleViews.map(view => (
64
+ <Button
65
+ key={view.id}
66
+ variant={isViewActive(view) ? 'default' : 'outline'}
67
+ size="sm"
68
+ onClick={() => handleViewClick(view)}
69
+ >
70
+ {view.name}
71
+ </Button>
72
+ ))}
73
+ <DropdownMenu>
74
+ <DropdownMenuTrigger asChild>
75
+ <Button variant="outline" size="sm">
76
+ <Trans>More views</Trans>
77
+ <ChevronDown className="h-3 w-3" />
78
+ </Button>
79
+ </DropdownMenuTrigger>
80
+ <DropdownMenuContent align="start">
81
+ {dropdownViews.map(view => (
82
+ <DropdownMenuItem
83
+ key={view.id}
84
+ onClick={() => handleViewClick(view)}
85
+ className={isViewActive(view) ? 'bg-primary' : ''}
86
+ >
87
+ {view.name}
88
+ </DropdownMenuItem>
89
+ ))}
90
+ </DropdownMenuContent>
91
+ </DropdownMenu>
92
+ <PermissionGuard requires={['WriteDashboardGlobalViews']}>
93
+ {canManageGlobalViews && <ManageGlobalViewsButton />}
94
+ </PermissionGuard>
95
+ </div>
96
+ );
97
+ };
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { ViewsSheet } from './views-sheet.js';
3
+
4
+ interface GlobalViewsSheetProps {
5
+ open: boolean;
6
+ onOpenChange: (open: boolean) => void;
7
+ }
8
+
9
+ export const GlobalViewsSheet: React.FC<GlobalViewsSheetProps> = ({ open, onOpenChange }) => {
10
+ return <ViewsSheet open={open} onOpenChange={onOpenChange} type="global" />;
11
+ };
@@ -0,0 +1,26 @@
1
+ import { Settings } from 'lucide-react';
2
+ import React, { useState } from 'react';
3
+ import { Button } from '../ui/button.js';
4
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip.js';
5
+ import { Trans } from '@/vdb/lib/trans.js';
6
+ import { GlobalViewsSheet } from './global-views-sheet.js';
7
+
8
+ export const ManageGlobalViewsButton: React.FC = () => {
9
+ const [sheetOpen, setSheetOpen] = useState(false);
10
+
11
+ return (
12
+ <>
13
+ <Tooltip>
14
+ <TooltipTrigger asChild>
15
+ <Button variant="outline" size="icon-sm" onClick={() => setSheetOpen(true)}>
16
+ <Settings />
17
+ </Button>
18
+ </TooltipTrigger>
19
+ <TooltipContent>
20
+ <Trans>Manage global views</Trans>
21
+ </TooltipContent>
22
+ </Tooltip>
23
+ <GlobalViewsSheet open={sheetOpen} onOpenChange={setSheetOpen} />
24
+ </>
25
+ );
26
+ };
@@ -0,0 +1,47 @@
1
+ import { Bookmark } from 'lucide-react';
2
+ import React, { useState, useMemo } from 'react';
3
+ import { Button } from '../ui/button.js';
4
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip.js';
5
+ import { Trans } from '@/vdb/lib/trans.js';
6
+ import { UserViewsSheet } from './user-views-sheet.js';
7
+ import { useSavedViews } from '../../hooks/use-saved-views.js';
8
+ import { findMatchingSavedView } from '../../utils/saved-views-utils.js';
9
+ import { useDataTableContext } from './data-table-context.js';
10
+
11
+ export const MyViewsButton: React.FC = () => {
12
+ const [sheetOpen, setSheetOpen] = useState(false);
13
+ const { userViews } = useSavedViews();
14
+ const { columnFilters, searchTerm, handleApplyView } = useDataTableContext();
15
+
16
+ // Find the active view using centralized utility
17
+ const activeView = useMemo(() => {
18
+ return findMatchingSavedView(columnFilters, searchTerm, userViews);
19
+ }, [userViews, columnFilters, searchTerm]);
20
+
21
+ return (
22
+ <>
23
+ <div className="flex items-center gap-2">
24
+ <Tooltip>
25
+ <TooltipTrigger asChild>
26
+ <Button
27
+ variant={activeView ? "default" : "outline"}
28
+ size="icon"
29
+ onClick={() => setSheetOpen(true)}
30
+ >
31
+ <Bookmark />
32
+ </Button>
33
+ </TooltipTrigger>
34
+ <TooltipContent>
35
+ <Trans>My saved views</Trans>
36
+ </TooltipContent>
37
+ </Tooltip>
38
+ {activeView && (
39
+ <span className="text-sm text-muted-foreground">
40
+ {activeView.name}
41
+ </span>
42
+ )}
43
+ </div>
44
+ <UserViewsSheet open={sheetOpen} onOpenChange={setSheetOpen} />
45
+ </>
46
+ );
47
+ };
@@ -1,4 +1,6 @@
1
1
  import { Button } from '@/vdb/components/ui/button.js';
2
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/vdb/components/ui/tooltip.js';
3
+ import { Trans } from '@/vdb/lib/trans.js';
2
4
  import { RefreshCw } from 'lucide-react';
3
5
  import { useEffect, useState } from 'react';
4
6
 
@@ -33,8 +35,15 @@ export function RefreshButton({
33
35
  };
34
36
 
35
37
  return (
36
- <Button variant="ghost" size="sm" onClick={handleClick} disabled={delayedLoading}>
37
- <RefreshCw className={delayedLoading ? 'animate-rotate' : ''} />
38
- </Button>
38
+ <Tooltip>
39
+ <TooltipTrigger asChild>
40
+ <Button variant="outline" size="sm" onClick={handleClick} disabled={delayedLoading}>
41
+ <RefreshCw className={delayedLoading ? 'animate-rotate' : ''} />
42
+ </Button>
43
+ </TooltipTrigger>
44
+ <TooltipContent>
45
+ <Trans>Refresh data</Trans>
46
+ </TooltipContent>
47
+ </Tooltip>
39
48
  );
40
49
  }
@@ -0,0 +1,45 @@
1
+ import { BookmarkPlus } from 'lucide-react';
2
+ import React, { useState } from 'react';
3
+ import { Button } from '../ui/button.js';
4
+ import { SaveViewDialog } from './save-view-dialog.js';
5
+ import { useSavedViews } from '../../hooks/use-saved-views.js';
6
+ import { isMatchingSavedView } from '../../utils/saved-views-utils.js';
7
+ import { useDataTableContext } from './data-table-context.js';
8
+
9
+ interface SaveViewButtonProps {
10
+ disabled?: boolean;
11
+ }
12
+
13
+ export const SaveViewButton: React.FC<SaveViewButtonProps> = ({ disabled }) => {
14
+ const [dialogOpen, setDialogOpen] = useState(false);
15
+ const { userViews, globalViews } = useSavedViews();
16
+ const { columnFilters, searchTerm } = useDataTableContext();
17
+
18
+ const hasFilters = columnFilters.length > 0 || (searchTerm && searchTerm.length > 0);
19
+ const matchesExistingView = isMatchingSavedView(columnFilters, searchTerm || '', userViews, globalViews);
20
+
21
+ // Don't show the button if there are no filters or if filters match an existing saved view
22
+ if (!hasFilters || matchesExistingView) {
23
+ return null;
24
+ }
25
+
26
+ return (
27
+ <>
28
+ <Button
29
+ variant="outline"
30
+ size="sm"
31
+ onClick={() => setDialogOpen(true)}
32
+ disabled={disabled}
33
+ >
34
+ <BookmarkPlus className="h-4 w-4 mr-1" />
35
+ Save View
36
+ </Button>
37
+ <SaveViewDialog
38
+ open={dialogOpen}
39
+ onOpenChange={setDialogOpen}
40
+ filters={columnFilters}
41
+ searchTerm={searchTerm}
42
+ />
43
+ </>
44
+ );
45
+ };
@@ -0,0 +1,113 @@
1
+ import { ColumnFiltersState } from '@tanstack/react-table';
2
+ import React, { useState } from 'react';
3
+ import { useSavedViews } from '../../hooks/use-saved-views.js';
4
+ import { Button } from '../ui/button.js';
5
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog.js';
6
+ import { Input } from '../ui/input.js';
7
+ import { Label } from '../ui/label.js';
8
+ import { RadioGroup, RadioGroupItem } from '../ui/radio-group.js';
9
+ import { toast } from 'sonner';
10
+
11
+ interface SaveViewDialogProps {
12
+ open: boolean;
13
+ onOpenChange: (open: boolean) => void;
14
+ filters: ColumnFiltersState;
15
+ searchTerm?: string;
16
+ }
17
+
18
+ export const SaveViewDialog: React.FC<SaveViewDialogProps> = ({
19
+ open,
20
+ onOpenChange,
21
+ filters,
22
+ searchTerm,
23
+ }) => {
24
+ const [name, setName] = useState('');
25
+ const [scope, setScope] = useState<'user' | 'global'>('user');
26
+ const [saving, setSaving] = useState(false);
27
+ const { saveView, userViews, globalViews, canManageGlobalViews } = useSavedViews();
28
+
29
+ const handleSave = async () => {
30
+ if (!name.trim()) {
31
+ toast.error('Please enter a name for the view');
32
+ return;
33
+ }
34
+
35
+ // Check for duplicate names
36
+ const existingViews = scope === 'user' ? userViews : globalViews;
37
+ if (existingViews.some(v => v.name === name.trim())) {
38
+ toast.error(`A ${scope} view with this name already exists`);
39
+ return;
40
+ }
41
+
42
+ setSaving(true);
43
+ try {
44
+ await saveView({
45
+ name: name.trim(),
46
+ scope,
47
+ filters,
48
+ searchTerm,
49
+ });
50
+ toast.success(`View "${name}" saved successfully`);
51
+ onOpenChange(false);
52
+ setName('');
53
+ setScope('user');
54
+ } catch (error) {
55
+ toast.error('Failed to save view');
56
+ console.error('Failed to save view:', error);
57
+ } finally {
58
+ setSaving(false);
59
+ }
60
+ };
61
+
62
+ return (
63
+ <Dialog open={open} onOpenChange={onOpenChange}>
64
+ <DialogContent>
65
+ <DialogHeader>
66
+ <DialogTitle>Save Current View</DialogTitle>
67
+ <DialogDescription>
68
+ Save the current filters and search term as a reusable view.
69
+ </DialogDescription>
70
+ </DialogHeader>
71
+ <div className="space-y-4 py-4">
72
+ <div className="space-y-2">
73
+ <Label htmlFor="view-name">View Name</Label>
74
+ <Input
75
+ id="view-name"
76
+ value={name}
77
+ onChange={e => setName(e.target.value)}
78
+ placeholder="Enter a name for this view"
79
+ autoFocus
80
+ />
81
+ </div>
82
+ {canManageGlobalViews && (
83
+ <div className="space-y-2">
84
+ <Label>View Scope</Label>
85
+ <RadioGroup value={scope} onValueChange={value => setScope(value as 'user' | 'global')}>
86
+ <div className="flex items-center space-x-2">
87
+ <RadioGroupItem value="user" id="scope-user" />
88
+ <Label htmlFor="scope-user" className="font-normal">
89
+ Personal View (only visible to you)
90
+ </Label>
91
+ </div>
92
+ <div className="flex items-center space-x-2">
93
+ <RadioGroupItem value="global" id="scope-global" />
94
+ <Label htmlFor="scope-global" className="font-normal">
95
+ Global View (visible to all users)
96
+ </Label>
97
+ </div>
98
+ </RadioGroup>
99
+ </div>
100
+ )}
101
+ </div>
102
+ <DialogFooter>
103
+ <Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
104
+ Cancel
105
+ </Button>
106
+ <Button onClick={handleSave} disabled={saving || !name.trim()}>
107
+ {saving ? 'Saving...' : 'Save View'}
108
+ </Button>
109
+ </DialogFooter>
110
+ </DialogContent>
111
+ </Dialog>
112
+ );
113
+ };
@@ -136,7 +136,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
136
136
  return <DisplayComponent id="vendure:asset" value={value} />;
137
137
  }
138
138
  if (value !== null && typeof value === 'object') {
139
- return JSON.stringify(value);
139
+ return <DisplayComponent id="vendure:json" value={value} />;
140
140
  }
141
141
  return value;
142
142
  },
@@ -196,6 +196,7 @@ export function useGeneratedColumns<T extends TypedDocumentNode<any, any>>({
196
196
  />
197
197
  ),
198
198
  enableColumnFilter: false,
199
+ enableHiding: false,
199
200
  cell: ({ row }) => {
200
201
  return (
201
202
  <Checkbox
@@ -224,6 +225,7 @@ function getRowActions(
224
225
  accessorKey: 'actions',
225
226
  header: () => <Trans>Actions</Trans>,
226
227
  enableColumnFilter: false,
228
+ enableHiding: false,
227
229
  cell: ({ row, table }) => {
228
230
  return (
229
231
  <DropdownMenu>
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { ViewsSheet } from './views-sheet.js';
3
+
4
+ interface UserViewsSheetProps {
5
+ open: boolean;
6
+ onOpenChange: (open: boolean) => void;
7
+ }
8
+
9
+ export const UserViewsSheet: React.FC<UserViewsSheetProps> = ({ open, onOpenChange }) => {
10
+ return <ViewsSheet open={open} onOpenChange={onOpenChange} type="user" />;
11
+ };