@vendure/dashboard 3.3.8-master-202507300243 → 3.3.8-master-202507310242

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vendure/dashboard",
3
3
  "private": false,
4
- "version": "3.3.8-master-202507300243",
4
+ "version": "3.3.8-master-202507310242",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -95,8 +95,8 @@
95
95
  "@types/react-dom": "^19.0.4",
96
96
  "@types/react-grid-layout": "^1.3.5",
97
97
  "@uidotdev/usehooks": "^2.4.1",
98
- "@vendure/common": "^3.3.8-master-202507300243",
99
- "@vendure/core": "^3.3.8-master-202507300243",
98
+ "@vendure/common": "^3.3.8-master-202507310242",
99
+ "@vendure/core": "^3.3.8-master-202507310242",
100
100
  "@vitejs/plugin-react": "^4.3.4",
101
101
  "acorn": "^8.11.3",
102
102
  "acorn-walk": "^8.3.2",
@@ -146,5 +146,5 @@
146
146
  "lightningcss-linux-arm64-musl": "^1.29.3",
147
147
  "lightningcss-linux-x64-musl": "^1.29.1"
148
148
  },
149
- "gitHead": "3679d74357c979aa9c60d8f8b575190723e2bb8f"
149
+ "gitHead": "146703259a0efc748d92322ed5713a15503a015d"
150
150
  }
@@ -6,6 +6,8 @@ import { Button } from '@/vdb/components/ui/button.js';
6
6
  import { Input } from '@/vdb/components/ui/input.js';
7
7
  import { Switch } from '@/vdb/components/ui/switch.js';
8
8
  import { NEW_ENTITY_PATH } from '@/vdb/constants.js';
9
+ import { extendDetailFormQuery } from '@/vdb/framework/document-extension/extend-detail-form-query.js';
10
+ import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
9
11
  import {
10
12
  CustomFieldsPageBlock,
11
13
  DetailFormGrid,
@@ -16,20 +18,25 @@ import {
16
18
  PageLayout,
17
19
  PageTitle,
18
20
  } from '@/vdb/framework/layout-engine/page-layout.js';
19
- import { useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
20
- import { api } from '@/vdb/graphql/api.js';
21
+ import { getDetailQueryOptions, useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
21
22
  import { Trans, useLingui } from '@/vdb/lib/trans.js';
22
23
  import { createFileRoute, useNavigate } from '@tanstack/react-router';
23
24
  import { toast } from 'sonner';
24
25
  import { globalSettingsDocument, updateGlobalSettingsDocument } from './global-settings.graphql.js';
25
26
 
27
+ const pageId = 'global-settings';
28
+
26
29
  export const Route = createFileRoute('/_authenticated/_global-settings/global-settings')({
27
30
  component: GlobalSettingsPage,
28
31
  loader: async ({ context }) => {
29
- await context.queryClient.ensureQueryData({
30
- queryFn: () => api.query(globalSettingsDocument),
31
- queryKey: ['DetailPage', 'globalSettings'],
32
- });
32
+ const { extendedQuery: extendedQueryDocument } = extendDetailFormQuery(
33
+ addCustomFields(globalSettingsDocument),
34
+ pageId,
35
+ );
36
+ await context.queryClient.ensureQueryData(
37
+ getDetailQueryOptions(extendedQueryDocument, { id: '' }),
38
+ {},
39
+ );
33
40
  return {
34
41
  breadcrumb: [{ path: '/global-settings', label: <Trans>Global settings</Trans> }],
35
42
  };
@@ -45,8 +52,9 @@ function GlobalSettingsPage() {
45
52
 
46
53
  const { form, submitHandler, entity, isPending } = useDetailPage({
47
54
  queryDocument: globalSettingsDocument,
48
- entityField: 'globalSettings',
55
+ entityName: 'GlobalSettings',
49
56
  updateDocument: updateGlobalSettingsDocument,
57
+ pageId,
50
58
  setValuesForUpdate: entity => {
51
59
  return {
52
60
  id: entity.id,
@@ -78,7 +86,7 @@ function GlobalSettingsPage() {
78
86
  });
79
87
 
80
88
  return (
81
- <Page pageId="global-settings" form={form} submitHandler={submitHandler} entity={entity}>
89
+ <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
82
90
  <PageTitle>
83
91
  <Trans>Global settings</Trans>
84
92
  </PageTitle>
@@ -26,7 +26,7 @@ export function PayloadDialog({ payload, trigger, title, description }: Readonly
26
26
  <DialogDescription>{description}</DialogDescription>
27
27
  </DialogHeader>
28
28
  <ScrollArea className="max-h-[600px]">
29
- <JsonEditor viewOnly data={payload} collapse />
29
+ <JsonEditor viewOnly data={payload} collapse={1} rootFontSize={12} />
30
30
  </ScrollArea>
31
31
  </DialogContent>
32
32
  </Dialog>
@@ -41,3 +41,14 @@ export const jobQueueListDocument = graphql(`
41
41
  }
42
42
  }
43
43
  `);
44
+
45
+ export const cancelJobDocument = graphql(
46
+ `
47
+ mutation CancelJob($jobId: ID!) {
48
+ cancelJob(jobId: $jobId) {
49
+ ...JobInfo
50
+ }
51
+ }
52
+ `,
53
+ [jobInfoFragment],
54
+ );
@@ -1,13 +1,32 @@
1
1
  import { Badge } from '@/vdb/components/ui/badge.js';
2
2
  import { Button } from '@/vdb/components/ui/button.js';
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuTrigger,
8
+ } from '@/vdb/components/ui/dropdown-menu.js';
9
+ import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
3
10
  import { ListPage } from '@/vdb/framework/page/list-page.js';
4
11
  import { api } from '@/vdb/graphql/api.js';
5
12
  import { Trans } from '@/vdb/lib/trans.js';
13
+ import { useMutation } from '@tanstack/react-query';
6
14
  import { createFileRoute } from '@tanstack/react-router';
7
15
  import { formatRelative } from 'date-fns';
8
- import { Ban, CheckCircle2Icon, CircleXIcon, ClockIcon, LoaderIcon, RotateCcw } from 'lucide-react';
16
+ import {
17
+ Ban,
18
+ CheckCircle2Icon,
19
+ ChevronDown,
20
+ CircleXIcon,
21
+ ClockIcon,
22
+ LoaderIcon,
23
+ MoreVertical,
24
+ RefreshCw,
25
+ RotateCcw,
26
+ } from 'lucide-react';
27
+ import { useEffect, useRef, useState } from 'react';
9
28
  import { PayloadDialog } from './components/payload-dialog.js';
10
- import { jobListDocument, jobQueueListDocument } from './job-queue.graphql.js';
29
+ import { cancelJobDocument, jobListDocument, jobQueueListDocument } from './job-queue.graphql.js';
11
30
 
12
31
  export const Route = createFileRoute('/_authenticated/_system/job-queue')({
13
32
  component: JobQueuePage,
@@ -47,7 +66,30 @@ const STATES = [
47
66
  },
48
67
  ];
49
68
 
69
+ const REFRESH_INTERVALS = [
70
+ { label: <Trans>Off</Trans>, value: 0 },
71
+ { label: <Trans>Every 5 seconds</Trans>, value: 5000 },
72
+ { label: <Trans>Every 10 seconds</Trans>, value: 10000 },
73
+ { label: <Trans>Every 30 seconds</Trans>, value: 30000 },
74
+ { label: <Trans>Every 60 seconds</Trans>, value: 60000 },
75
+ ];
76
+
50
77
  function JobQueuePage() {
78
+ const refreshRef = useRef<() => void>(() => {});
79
+ const [refreshInterval, setRefreshInterval] = useState(10000);
80
+
81
+ useEffect(() => {
82
+ if (refreshInterval === 0) return;
83
+
84
+ const interval = setInterval(() => {
85
+ refreshRef.current();
86
+ }, refreshInterval);
87
+
88
+ return () => clearInterval(interval);
89
+ }, [refreshInterval]);
90
+
91
+ const currentInterval = REFRESH_INTERVALS.find(i => i.value === refreshInterval);
92
+
51
93
  return (
52
94
  <ListPage
53
95
  pageId="job-queue-list"
@@ -105,9 +147,14 @@ function JobQueuePage() {
105
147
  },
106
148
  state: {
107
149
  header: 'State',
108
- cell: ({ row }) => {
150
+ cell: ({ row, table }) => {
151
+ const cancelJobMutation = useMutation({
152
+ mutationFn: (jobId: string) => api.mutate(cancelJobDocument, { jobId }),
153
+ onSuccess: () => {
154
+ refreshRef.current();
155
+ },
156
+ });
109
157
  const state = STATES.find(s => s.value === row.original.state);
110
-
111
158
  return (
112
159
  <Badge
113
160
  variant={
@@ -122,6 +169,27 @@ function JobQueuePage() {
122
169
  >
123
170
  {state && <state.icon />}
124
171
  {row.original.state}
172
+ {row.original.state === 'RUNNING' ? (
173
+ <div className="flex items-center gap-2">
174
+ <DropdownMenu>
175
+ <DropdownMenuTrigger asChild>
176
+ <Button variant="ghost" size="sm" className="h-6 w-6 p-0">
177
+ <MoreVertical className="h-4 w-4" />
178
+ </Button>
179
+ </DropdownMenuTrigger>
180
+ <DropdownMenuContent align="end">
181
+ <DropdownMenuItem
182
+ onClick={() => cancelJobMutation.mutate(row.original.id)}
183
+ disabled={cancelJobMutation.isPending}
184
+ className="text-destructive focus:text-destructive"
185
+ >
186
+ <Ban className="mr-2 h-4 w-4" />
187
+ <Trans>Cancel Job</Trans>
188
+ </DropdownMenuItem>
189
+ </DropdownMenuContent>
190
+ </DropdownMenu>
191
+ </div>
192
+ ) : null}
125
193
  </Badge>
126
194
  );
127
195
  },
@@ -159,6 +227,32 @@ function JobQueuePage() {
159
227
  options: STATES,
160
228
  },
161
229
  }}
162
- ></ListPage>
230
+ registerRefresher={refresher => {
231
+ refreshRef.current = refresher;
232
+ }}
233
+ >
234
+ <PageActionBarRight>
235
+ <DropdownMenu>
236
+ <DropdownMenuTrigger asChild>
237
+ <Button variant="outline" size="sm" className="gap-2">
238
+ <RefreshCw className="h-4 w-4" />
239
+ <span>Auto refresh: {currentInterval?.label}</span>
240
+ <ChevronDown className="h-4 w-4" />
241
+ </Button>
242
+ </DropdownMenuTrigger>
243
+ <DropdownMenuContent align="end">
244
+ {REFRESH_INTERVALS.map(interval => (
245
+ <DropdownMenuItem
246
+ key={interval.value}
247
+ onClick={() => setRefreshInterval(interval.value)}
248
+ className={refreshInterval === interval.value ? 'bg-accent' : ''}
249
+ >
250
+ {interval.label}
251
+ </DropdownMenuItem>
252
+ ))}
253
+ </DropdownMenuContent>
254
+ </DropdownMenu>
255
+ </PageActionBarRight>
256
+ </ListPage>
163
257
  );
164
258
  }
@@ -229,7 +229,7 @@ function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: Reado
229
229
  name={fieldName}
230
230
  render={({ field }) => (
231
231
  <FormItem>
232
- <FormLabel>{getTranslation(fieldDef.label) ?? field.name}</FormLabel>
232
+ <FormLabel>{getTranslation(fieldDef.label) ?? fieldDef.name}</FormLabel>
233
233
  <FormControl>
234
234
  <CustomFieldListInput
235
235
  field={field}
@@ -263,7 +263,7 @@ function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: Reado
263
263
  name={fieldName}
264
264
  render={({ field }) => (
265
265
  <FormItem>
266
- <FormLabel>{getTranslation(fieldDef.label) ?? field.name}</FormLabel>
266
+ <FormLabel>{getTranslation(fieldDef.label) ?? fieldDef.name}</FormLabel>
267
267
  <FormControl>
268
268
  <StructFormInput
269
269
  field={field}
@@ -291,7 +291,7 @@ function CustomFieldItem({ fieldDef, control, fieldName, getTranslation }: Reado
291
291
  <CustomFieldFormItem
292
292
  fieldDef={fieldDef}
293
293
  getTranslation={getTranslation}
294
- fieldName={field.name}
294
+ fieldName={fieldDef.name}
295
295
  >
296
296
  <FormInputForType fieldDef={fieldDef} field={field} />
297
297
  </CustomFieldFormItem>
@@ -7,6 +7,7 @@ import {
7
7
  ListQueryOptionsShape,
8
8
  ListQueryShape,
9
9
  PaginatedListDataTable,
10
+ PaginatedListRefresherRegisterFn,
10
11
  RowAction,
11
12
  } from '@/vdb/components/shared/paginated-list-data-table.js';
12
13
  import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
@@ -53,6 +54,12 @@ export interface ListPageProps<
53
54
  transformData?: (data: any[]) => any[];
54
55
  setTableOptions?: (table: TableOptions<any>) => TableOptions<any>;
55
56
  bulkActions?: BulkAction[];
57
+ /**
58
+ * Register a function that allows you to assign a refresh function for
59
+ * this list. The function can be assigned to a ref and then called when
60
+ * the list needs to be refreshed.
61
+ */
62
+ registerRefresher?: PaginatedListRefresherRegisterFn;
56
63
  }
57
64
 
58
65
  /**
@@ -90,6 +97,7 @@ export function ListPage<
90
97
  transformData,
91
98
  setTableOptions,
92
99
  bulkActions,
100
+ registerRefresher,
93
101
  }: Readonly<ListPageProps<T, U, V, AC>>) {
94
102
  const route = typeof routeOrFn === 'function' ? routeOrFn() : routeOrFn;
95
103
  const routeSearch = route.useSearch();
@@ -191,6 +199,7 @@ export function ListPage<
191
199
  bulkActions={bulkActions}
192
200
  setTableOptions={setTableOptions}
193
201
  transformData={transformData}
202
+ registerRefresher={registerRefresher}
194
203
  />
195
204
  </FullWidthPageBlock>
196
205
  </PageLayout>