@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,297 @@
1
+ import { Copy, Edit, Globe, MoreHorizontal, Trash2 } from 'lucide-react';
2
+ import React, { useState } from 'react';
3
+ import { useSavedViews } from '../../hooks/use-saved-views.js';
4
+ import { useDataTableContext } from './data-table-context.js';
5
+ import { Button } from '../ui/button.js';
6
+ import { Input } from '../ui/input.js';
7
+ import {
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuTrigger,
12
+ } from '../ui/dropdown-menu.js';
13
+ import {
14
+ Sheet,
15
+ SheetContent,
16
+ SheetDescription,
17
+ SheetHeader,
18
+ SheetTitle,
19
+ } from '../ui/sheet.js';
20
+ import { SavedView } from '../../types/saved-views.js';
21
+ import { toast } from 'sonner';
22
+ import {
23
+ AlertDialog,
24
+ AlertDialogAction,
25
+ AlertDialogCancel,
26
+ AlertDialogContent,
27
+ AlertDialogDescription,
28
+ AlertDialogFooter,
29
+ AlertDialogHeader,
30
+ AlertDialogTitle,
31
+ } from '../ui/alert-dialog.js';
32
+ import { Trans, useLingui } from '@/vdb/lib/trans.js';
33
+
34
+ interface ViewsSheetProps {
35
+ open: boolean;
36
+ onOpenChange: (open: boolean) => void;
37
+ type: 'user' | 'global';
38
+ }
39
+
40
+ export const ViewsSheet: React.FC<ViewsSheetProps> = ({ open, onOpenChange, type }) => {
41
+ const { userViews, globalViews, deleteView, updateView, duplicateView, canManageGlobalViews } = useSavedViews();
42
+ const { handleApplyView } = useDataTableContext();
43
+ const { i18n } = useLingui();
44
+ const [editingId, setEditingId] = useState<string | null>(null);
45
+ const [editingName, setEditingName] = useState('');
46
+ const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
47
+
48
+ const views = type === 'global' ? globalViews : userViews;
49
+ const isGlobal = type === 'global';
50
+
51
+ const handleViewApply = (view: SavedView) => {
52
+ handleApplyView(view.filters, view.searchTerm);
53
+ const message = isGlobal
54
+ ? i18n.t(`Applied global view "${view.name}"`)
55
+ : i18n.t(`Applied view "${view.name}"`);
56
+ toast.success(message);
57
+ };
58
+
59
+ const handleStartEdit = (view: SavedView) => {
60
+ setEditingId(view.id);
61
+ setEditingName(view.name);
62
+ };
63
+
64
+ const handleSaveEdit = async () => {
65
+ if (!editingId || !editingName.trim()) return;
66
+
67
+ try {
68
+ await updateView({ id: editingId, name: editingName.trim() });
69
+ const message = isGlobal
70
+ ? i18n.t('Global view renamed successfully')
71
+ : i18n.t('View renamed successfully');
72
+ toast.success(message);
73
+ setEditingId(null);
74
+ setEditingName('');
75
+ } catch (error) {
76
+ const message = isGlobal
77
+ ? i18n.t('Failed to rename global view')
78
+ : i18n.t('Failed to rename view');
79
+ toast.error(message);
80
+ }
81
+ };
82
+
83
+ const handleCancelEdit = () => {
84
+ setEditingId(null);
85
+ setEditingName('');
86
+ };
87
+
88
+ const handleDelete = async () => {
89
+ if (!deleteConfirmId) return;
90
+
91
+ try {
92
+ await deleteView(deleteConfirmId);
93
+ const message = isGlobal
94
+ ? i18n.t('Global view deleted successfully')
95
+ : i18n.t('View deleted successfully');
96
+ toast.success(message);
97
+ setDeleteConfirmId(null);
98
+ } catch (error) {
99
+ const message = isGlobal
100
+ ? i18n.t('Failed to delete global view')
101
+ : i18n.t('Failed to delete view');
102
+ toast.error(message);
103
+ }
104
+ };
105
+
106
+ const handleDuplicate = async (view: SavedView) => {
107
+ try {
108
+ await duplicateView(view.id, type);
109
+ const message = isGlobal
110
+ ? i18n.t('Global view duplicated successfully')
111
+ : i18n.t('View duplicated successfully');
112
+ toast.success(message);
113
+ } catch (error) {
114
+ const message = isGlobal
115
+ ? i18n.t('Failed to duplicate global view')
116
+ : i18n.t('Failed to duplicate view');
117
+ toast.error(message);
118
+ }
119
+ };
120
+
121
+ const handleConvertToUser = async (view: SavedView) => {
122
+ try {
123
+ await duplicateView(view.id, 'user');
124
+ toast.success(i18n.t('Global view converted to personal view successfully'));
125
+ } catch (error) {
126
+ toast.error(i18n.t('Failed to convert global view to personal view'));
127
+ }
128
+ };
129
+
130
+ const handleConvertToGlobal = async (view: SavedView) => {
131
+ try {
132
+ await duplicateView(view.id, 'global');
133
+ await deleteView(view.id);
134
+ toast.success(i18n.t('View converted to global successfully'));
135
+ } catch (error) {
136
+ toast.error(i18n.t('Failed to convert view to global'));
137
+ }
138
+ };
139
+
140
+ const getTitle = () => {
141
+ return isGlobal ? <Trans>Manage Global Views</Trans> : <Trans>My Saved Views</Trans>;
142
+ };
143
+
144
+ const getDescription = () => {
145
+ return isGlobal
146
+ ? <Trans>Manage global saved views that are visible to all users</Trans>
147
+ : <Trans>Manage your personal saved views for this table</Trans>;
148
+ };
149
+
150
+ const getEmptyStateMessage = () => {
151
+ if (isGlobal) {
152
+ return (
153
+ <>
154
+ <p><Trans>No global views have been created yet.</Trans></p>
155
+ <p className="text-sm mt-2">
156
+ <Trans>Save a view as "Global" to make it available to all users.</Trans>
157
+ </p>
158
+ </>
159
+ );
160
+ } else {
161
+ return (
162
+ <>
163
+ <p><Trans>You haven't saved any views yet.</Trans></p>
164
+ <p className="text-sm mt-2">
165
+ <Trans>Apply filters to the table and click "Save View" to get started.</Trans>
166
+ </p>
167
+ </>
168
+ );
169
+ }
170
+ };
171
+
172
+ const getDeleteDialogTitle = () => {
173
+ return isGlobal ? <Trans>Delete Global View</Trans> : <Trans>Delete View</Trans>;
174
+ };
175
+
176
+ const getDeleteDialogDescription = () => {
177
+ return isGlobal
178
+ ? <Trans>Are you sure you want to delete this global view? This action cannot be undone and will affect all users.</Trans>
179
+ : <Trans>Are you sure you want to delete this view? This action cannot be undone.</Trans>;
180
+ };
181
+
182
+ return (
183
+ <>
184
+ <Sheet open={open} onOpenChange={onOpenChange}>
185
+ <SheetContent className="w-[400px] sm:w-[540px]">
186
+ <SheetHeader>
187
+ <SheetTitle>{getTitle()}</SheetTitle>
188
+ <SheetDescription>
189
+ {getDescription()}
190
+ </SheetDescription>
191
+ </SheetHeader>
192
+ <div className="mt-4">
193
+ {views.length === 0 ? (
194
+ <div className="text-center py-8 text-muted-foreground">
195
+ {getEmptyStateMessage()}
196
+ </div>
197
+ ) : (
198
+ <div className="divide-y">
199
+ {views.map(view => (
200
+ <div
201
+ key={view.id}
202
+ className="flex items-center justify-between py-3 first:pt-0 last:pb-0 hover:bg-accent/50 transition-colors rounded-md px-2"
203
+ >
204
+ {editingId === view.id ? (
205
+ <div className="flex items-center gap-2 flex-1">
206
+ <Input
207
+ value={editingName}
208
+ onChange={e => setEditingName(e.target.value)}
209
+ onKeyDown={e => {
210
+ if (e.key === 'Enter') handleSaveEdit();
211
+ if (e.key === 'Escape') handleCancelEdit();
212
+ }}
213
+ autoFocus
214
+ className="flex-1"
215
+ />
216
+ <Button size="sm" onClick={handleSaveEdit}>
217
+ <Trans>Save</Trans>
218
+ </Button>
219
+ <Button size="sm" variant="outline" onClick={handleCancelEdit}>
220
+ <Trans>Cancel</Trans>
221
+ </Button>
222
+ </div>
223
+ ) : (
224
+ <>
225
+ <span className="font-medium text-sm truncate flex-1">{view.name}</span>
226
+ <div className="flex items-center gap-1">
227
+ <Button
228
+ size="sm"
229
+ onClick={() => handleViewApply(view)}
230
+ >
231
+ <Trans>Apply</Trans>
232
+ </Button>
233
+ <DropdownMenu>
234
+ <DropdownMenuTrigger asChild>
235
+ <Button variant="ghost" size="sm">
236
+ <MoreHorizontal className="h-4 w-4" />
237
+ </Button>
238
+ </DropdownMenuTrigger>
239
+ <DropdownMenuContent align="end">
240
+ <DropdownMenuItem onClick={() => handleStartEdit(view)}>
241
+ <Edit className="h-4 w-4 mr-2" />
242
+ <Trans>Rename</Trans>
243
+ </DropdownMenuItem>
244
+ <DropdownMenuItem onClick={() => handleDuplicate(view)}>
245
+ <Copy className="h-4 w-4 mr-2" />
246
+ <Trans>Duplicate</Trans>
247
+ </DropdownMenuItem>
248
+ {isGlobal ? (
249
+ <DropdownMenuItem onClick={() => handleConvertToUser(view)}>
250
+ <Copy className="h-4 w-4 mr-2" />
251
+ <Trans>Copy to Personal</Trans>
252
+ </DropdownMenuItem>
253
+ ) : (
254
+ canManageGlobalViews && (
255
+ <DropdownMenuItem onClick={() => handleConvertToGlobal(view)}>
256
+ <Globe className="h-4 w-4 mr-2" />
257
+ <Trans>Make Global</Trans>
258
+ </DropdownMenuItem>
259
+ )
260
+ )}
261
+ <DropdownMenuItem
262
+ onClick={() => setDeleteConfirmId(view.id)}
263
+ className="text-destructive"
264
+ >
265
+ <Trash2 className="h-4 w-4 mr-2" />
266
+ <Trans>Delete</Trans>
267
+ </DropdownMenuItem>
268
+ </DropdownMenuContent>
269
+ </DropdownMenu>
270
+ </div>
271
+ </>
272
+ )}
273
+ </div>
274
+ ))}
275
+ </div>
276
+ )}
277
+ </div>
278
+ </SheetContent>
279
+ </Sheet>
280
+
281
+ <AlertDialog open={!!deleteConfirmId} onOpenChange={() => setDeleteConfirmId(null)}>
282
+ <AlertDialogContent>
283
+ <AlertDialogHeader>
284
+ <AlertDialogTitle>{getDeleteDialogTitle()}</AlertDialogTitle>
285
+ <AlertDialogDescription>
286
+ {getDeleteDialogDescription()}
287
+ </AlertDialogDescription>
288
+ </AlertDialogHeader>
289
+ <AlertDialogFooter>
290
+ <AlertDialogCancel><Trans>Cancel</Trans></AlertDialogCancel>
291
+ <AlertDialogAction onClick={handleDelete}><Trans>Delete</Trans></AlertDialogAction>
292
+ </AlertDialogFooter>
293
+ </AlertDialogContent>
294
+ </AlertDialog>
295
+ </>
296
+ );
297
+ };
@@ -0,0 +1,184 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/vdb/components/ui/button.js';
4
+ import { Calendar } from '@/vdb/components/ui/calendar.js';
5
+ import { Popover, PopoverContent, PopoverTrigger } from '@/vdb/components/ui/popover.js';
6
+ import { DefinedDateRange } from '@/vdb/framework/dashboard-widget/widget-filters-context.js';
7
+ import { cn } from '@/vdb/lib/utils.js';
8
+ import {
9
+ addDays,
10
+ endOfDay,
11
+ endOfMonth,
12
+ endOfWeek,
13
+ format,
14
+ startOfDay,
15
+ startOfMonth,
16
+ startOfWeek,
17
+ subDays
18
+ } from 'date-fns';
19
+ import { CalendarIcon } from 'lucide-react';
20
+ import * as React from 'react';
21
+ import { DateRange } from 'react-day-picker';
22
+
23
+ interface DateRangePickerProps {
24
+ className?: string;
25
+ dateRange: DefinedDateRange;
26
+ onDateRangeChange: (range: DefinedDateRange) => void;
27
+ }
28
+
29
+ const presets = [
30
+ {
31
+ label: 'Today',
32
+ getValue: () => ({
33
+ from: startOfDay(new Date()),
34
+ to: endOfDay(new Date()),
35
+ }),
36
+ },
37
+ {
38
+ label: 'Yesterday',
39
+ getValue: () => ({
40
+ from: startOfDay(subDays(new Date(), 1)),
41
+ to: endOfDay(subDays(new Date(), 1)),
42
+ }),
43
+ },
44
+ {
45
+ label: 'Last 7 days',
46
+ getValue: () => ({
47
+ from: startOfDay(subDays(new Date(), 6)),
48
+ to: endOfDay(new Date()),
49
+ }),
50
+ },
51
+ {
52
+ label: 'This week',
53
+ getValue: () => ({
54
+ from: startOfWeek(new Date(), { weekStartsOn: 1 }),
55
+ to: endOfWeek(new Date(), { weekStartsOn: 1 }),
56
+ }),
57
+ },
58
+ {
59
+ label: 'Last 30 days',
60
+ getValue: () => ({
61
+ from: startOfDay(subDays(new Date(), 29)),
62
+ to: endOfDay(new Date()),
63
+ }),
64
+ },
65
+ {
66
+ label: 'Month to date',
67
+ getValue: () => ({
68
+ from: startOfMonth(new Date()),
69
+ to: endOfDay(new Date()),
70
+ }),
71
+ },
72
+ {
73
+ label: 'This month',
74
+ getValue: () => ({
75
+ from: startOfMonth(new Date()),
76
+ to: endOfMonth(new Date()),
77
+ }),
78
+ },
79
+ {
80
+ label: 'Last month',
81
+ getValue: () => ({
82
+ from: startOfMonth(subDays(new Date(), 30)),
83
+ to: endOfMonth(subDays(new Date(), 30)),
84
+ }),
85
+ },
86
+ ];
87
+
88
+ export function DateRangePicker({ className, dateRange, onDateRangeChange }: DateRangePickerProps) {
89
+ const [open, setOpen] = React.useState(false);
90
+ // Internal state uses react-day-picker's DateRange type for Calendar compatibility
91
+ const [selectedRange, setSelectedRange] = React.useState<DateRange>({
92
+ from: dateRange.from,
93
+ to: dateRange.to
94
+ });
95
+
96
+ React.useEffect(() => {
97
+ setSelectedRange({
98
+ from: dateRange.from,
99
+ to: dateRange.to
100
+ });
101
+ }, [dateRange]);
102
+
103
+ const handleSelect = (range: DateRange | undefined) => {
104
+ if (range?.from) {
105
+ // If no end date is selected, use the from date as the end date
106
+ const to = range.to || range.from;
107
+ const finalRange: DefinedDateRange = {
108
+ from: range.from,
109
+ to: to
110
+ };
111
+ setSelectedRange({ from: range.from, to });
112
+ onDateRangeChange(finalRange);
113
+ }
114
+ };
115
+
116
+ const handlePresetClick = (preset: typeof presets[number]) => {
117
+ const range = preset.getValue();
118
+ handleSelect(range);
119
+ setOpen(false);
120
+ };
121
+
122
+ const formatDateRange = () => {
123
+ if (!selectedRange.from) {
124
+ return 'Select date range';
125
+ }
126
+ if (!selectedRange.to || selectedRange.from === selectedRange.to) {
127
+ return format(selectedRange.from, 'MMM dd, yyyy');
128
+ }
129
+ return `${format(selectedRange.from, 'MMM dd, yyyy')} - ${format(selectedRange.to, 'MMM dd, yyyy')}`;
130
+ };
131
+
132
+ const isPresetActive = (preset: typeof presets[number]) => {
133
+ if (!selectedRange.from || !selectedRange.to) return false;
134
+ const presetRange = preset.getValue();
135
+ return (
136
+ selectedRange.from.getTime() === presetRange.from.getTime() &&
137
+ selectedRange.to.getTime() === presetRange.to.getTime()
138
+ );
139
+ };
140
+
141
+ return (
142
+ <Popover open={open} onOpenChange={setOpen}>
143
+ <PopoverTrigger asChild>
144
+ <Button
145
+ variant="outline"
146
+ className={cn(
147
+ 'w-[280px] justify-start text-left font-normal',
148
+ className
149
+ )}
150
+ >
151
+ <CalendarIcon className="mr-2 h-4 w-4" />
152
+ {formatDateRange()}
153
+ </Button>
154
+ </PopoverTrigger>
155
+ <PopoverContent className="w-auto p-0" align="start">
156
+ <div className="flex">
157
+ <div className="border-r p-2 space-y-0.5 min-w-0 w-32">
158
+ {presets.map((preset) => (
159
+ <Button
160
+ key={preset.label}
161
+ variant={isPresetActive(preset) ? 'default' : 'ghost'}
162
+ size="sm"
163
+ className="w-full justify-start font-normal text-xs h-7 px-2"
164
+ onClick={() => handlePresetClick(preset)}
165
+ >
166
+ {preset.label}
167
+ </Button>
168
+ ))}
169
+ </div>
170
+ <div className="p-3">
171
+ <Calendar
172
+ mode="range"
173
+ defaultMonth={selectedRange?.from}
174
+ selected={selectedRange}
175
+ onSelect={handleSelect}
176
+ numberOfMonths={2}
177
+ showOutsideDays={false}
178
+ />
179
+ </div>
180
+ </div>
181
+ </PopoverContent>
182
+ </Popover>
183
+ );
184
+ }
@@ -1,13 +1,15 @@
1
- import { DataTable, FacetedFilter } from '\@/vdb/components/data-table/data-table.js';
2
- import { getObjectPathToPaginatedList } from '\@/vdb/framework/document-introspection/get-document-structure.js';
3
- import { useListQueryFields } from '\@/vdb/framework/document-introspection/hooks.js';
4
- import { api } from '\@/vdb/graphql/api.js';
1
+ import { DataTable, FacetedFilter } from '@/vdb/components/data-table/data-table.js';
2
+ import { getObjectPathToPaginatedList } from '@/vdb/framework/document-introspection/get-document-structure.js';
3
+ import { useListQueryFields } from '@/vdb/framework/document-introspection/hooks.js';
4
+ import { api } from '@/vdb/graphql/api.js';
5
5
  import { keepPreviousData, useQuery, useQueryClient } from '@tanstack/react-query';
6
6
  import { useDebounce } from '@uidotdev/usehooks';
7
7
 
8
- import { BulkAction } from '\@/vdb/framework/extension-api/types/index.js';
9
- import { ResultOf } from '\@/vdb/graphql/graphql.js';
10
- import { useExtendedListQuery } from '\@/vdb/hooks/use-extended-list-query.js';
8
+ import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
9
+ import { includeOnlySelectedListFields } from '@/vdb/framework/document-introspection/include-only-selected-list-fields.js';
10
+ import { BulkAction } from '@/vdb/framework/extension-api/types/index.js';
11
+ import { ResultOf } from '@/vdb/graphql/graphql.js';
12
+ import { useExtendedListQuery } from '@/vdb/hooks/use-extended-list-query.js';
11
13
  import { TypedDocumentNode } from '@graphql-typed-document-node/core';
12
14
  import { ColumnFiltersState, ColumnSort, SortingState, Table } from '@tanstack/react-table';
13
15
  import { ColumnDef, Row, TableOptions, VisibilityState } from '@tanstack/table-core';
@@ -88,7 +90,18 @@ export type AllItemFieldKeys<T extends TypedDocumentNode<any, any>> =
88
90
  | CustomFieldKeysOfItem<PaginatedListItemFields<T>>;
89
91
 
90
92
  export type CustomizeColumnConfig<T extends TypedDocumentNode<any, any>> = {
91
- [Key in AllItemFieldKeys<T>]?: Partial<ColumnDef<PaginatedListItemFields<T>, any>>;
93
+ [Key in AllItemFieldKeys<T>]?: Partial<ColumnDef<PaginatedListItemFields<T>, any>> & {
94
+ meta?: {
95
+ /**
96
+ * @description
97
+ * Columns that rely on _other_ columns in order to correctly render,
98
+ * can declare those other columns as dependencies in order to ensure that
99
+ * those columns are always fetched, even when those columns are not explicitly
100
+ * included in the visible table columns.
101
+ */
102
+ dependencies?: Array<AllItemFieldKeys<T>>;
103
+ };
104
+ };
92
105
  };
93
106
 
94
107
  export type FacetedFilterConfig<T extends TypedDocumentNode<any, any>> = {
@@ -365,7 +378,7 @@ export function PaginatedListDataTable<
365
378
  const [searchTerm, setSearchTerm] = React.useState<string>('');
366
379
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
367
380
  const queryClient = useQueryClient();
368
- const extendedListQuery = useExtendedListQuery(listQuery);
381
+ const extendedListQuery = useExtendedListQuery(addCustomFields(listQuery));
369
382
 
370
383
  const sort = sorting?.reduce((acc: any, sort: ColumnSort) => {
371
384
  const direction = sort.desc ? 'DESC' : 'ASC';
@@ -388,9 +401,44 @@ export function PaginatedListDataTable<
388
401
  }
389
402
  : undefined;
390
403
 
404
+ function refetchPaginatedList() {
405
+ queryClient.invalidateQueries({ queryKey });
406
+ }
407
+
408
+ registerRefresher?.(refetchPaginatedList);
409
+
410
+ // First we get info on _all_ the fields, including all custom fields, for the
411
+ // purpose of configuring the table columns.
412
+ const fields = useListQueryFields(extendedListQuery);
413
+ const paginatedListObjectPath = getObjectPathToPaginatedList(extendedListQuery);
414
+
415
+ const { columns, customFieldColumnNames } = useGeneratedColumns({
416
+ fields,
417
+ customizeColumns,
418
+ rowActions,
419
+ bulkActions,
420
+ deleteMutation,
421
+ additionalColumns,
422
+ defaultColumnOrder,
423
+ });
424
+
425
+ const columnVisibility = getColumnVisibility(fields, defaultVisibility, customFieldColumnNames);
426
+ // Get the actual visible columns and only fetch those
427
+ const visibleColumns = columns
428
+ // Filter out invisible columns, but _always_ select "id"
429
+ // because it is usually needed.
430
+ .filter(c => columnVisibility[c.id as string] || c.id === 'id')
431
+ .map(c => ({
432
+ name: c.id as string,
433
+ isCustomField: (c.meta as any)?.isCustomField ?? false,
434
+ dependencies: (c.meta as any)?.dependencies ?? [],
435
+ }));
436
+ const minimalListQuery = includeOnlySelectedListFields(extendedListQuery, visibleColumns);
437
+
391
438
  const defaultQueryKey = [
392
439
  PaginatedListDataTableKey,
393
- extendedListQuery,
440
+ minimalListQuery,
441
+ visibleColumns,
394
442
  page,
395
443
  itemsPerPage,
396
444
  sorting,
@@ -399,12 +447,6 @@ export function PaginatedListDataTable<
399
447
  ];
400
448
  const queryKey = transformQueryKey ? transformQueryKey(defaultQueryKey) : defaultQueryKey;
401
449
 
402
- function refetchPaginatedList() {
403
- queryClient.invalidateQueries({ queryKey });
404
- }
405
-
406
- registerRefresher?.(refetchPaginatedList);
407
-
408
450
  const { data, isFetching } = useQuery({
409
451
  queryFn: () => {
410
452
  const searchFilter = onSearchTermChange ? onSearchTermChange(debouncedSearchTerm) : {};
@@ -419,31 +461,16 @@ export function PaginatedListDataTable<
419
461
  } as V;
420
462
 
421
463
  const transformedVariables = transformVariables ? transformVariables(variables) : variables;
422
- return api.query(extendedListQuery, transformedVariables);
464
+ return api.query(minimalListQuery, transformedVariables);
423
465
  },
424
466
  queryKey,
425
467
  placeholderData: keepPreviousData,
426
468
  });
427
-
428
- const fields = useListQueryFields(extendedListQuery);
429
- const paginatedListObjectPath = getObjectPathToPaginatedList(extendedListQuery);
430
-
431
469
  let listData = data as any;
432
470
  for (const path of paginatedListObjectPath) {
433
471
  listData = listData?.[path];
434
472
  }
435
473
 
436
- const { columns, customFieldColumnNames } = useGeneratedColumns({
437
- fields,
438
- customizeColumns,
439
- rowActions,
440
- bulkActions,
441
- deleteMutation,
442
- additionalColumns,
443
- defaultColumnOrder,
444
- });
445
-
446
- const columnVisibility = getColumnVisibility(fields, defaultVisibility, customFieldColumnNames);
447
474
  const transformedData =
448
475
  typeof transformData === 'function' ? transformData(listData?.items ?? []) : (listData?.items ?? []);
449
476
  return (
@@ -23,7 +23,7 @@ const buttonVariants = cva(
23
23
  lg: 'h-10 rounded-md px-8',
24
24
  icon: 'h-9 w-9',
25
25
  xs: 'h-5 rounded-md px-2 text-xs',
26
- 'icon-sm': 'h-7 w-7 text-xs',
26
+ 'icon-sm': 'h-8 w-8 text-xs',
27
27
  'icon-xs': 'h-5 w-5 text-xs',
28
28
  },
29
29
  },