@vendure/dashboard 3.5.3-master-202601300300 → 3.5.3

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 (25) hide show
  1. package/dist/vite/utils/plugin-discovery.js +3 -3
  2. package/dist/vite/vite-plugin-lingui-babel.d.ts +15 -2
  3. package/dist/vite/vite-plugin-lingui-babel.js +90 -8
  4. package/dist/vite/vite-plugin-translations.js +2 -2
  5. package/package.json +3 -3
  6. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +22 -3
  7. package/src/app/routes/_authenticated/_customers/customers.graphql.ts +1 -0
  8. package/src/app/routes/_authenticated/_customers/customers.tsx +3 -0
  9. package/src/app/routes/_authenticated/_orders/components/draft-order-status.tsx +48 -0
  10. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +22 -6
  11. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +1 -0
  12. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +9 -3
  13. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +49 -30
  14. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +1 -0
  15. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +1 -0
  16. package/src/i18n/locales/ar.po +58 -5
  17. package/src/i18n/locales/en.po +58 -5
  18. package/src/lib/components/data-input/index.ts +1 -0
  19. package/src/lib/components/ui/alert.tsx +2 -0
  20. package/src/lib/framework/extension-api/input-component-extensions.tsx +2 -0
  21. package/src/lib/framework/form-engine/form-schema-tools.ts +4 -1
  22. package/src/lib/framework/page/detail-page-route-loader.tsx +6 -4
  23. package/src/lib/framework/page/detail-page.tsx +22 -37
  24. package/src/lib/graphql/graphql-env.d.ts +30 -13
  25. package/src/lib/hooks/use-job-queue-polling.ts +160 -0
@@ -0,0 +1,160 @@
1
+ import { api } from '@/vdb/graphql/api.js';
2
+ import { graphql } from '@/vdb/graphql/graphql.js';
3
+ import { useQuery } from '@tanstack/react-query';
4
+ import { useCallback, useEffect, useRef, useState } from 'react';
5
+
6
+ const JOB_LOOKBACK_MS = 5000; // Look back 5 seconds to catch jobs created before mutation returned
7
+ const MAX_POLLING_TIMEOUT_MS = 30000;
8
+ const INITIAL_POLL_INTERVAL_MS = 500;
9
+ const MAX_POLL_INTERVAL_MS = 4000;
10
+ const STORAGE_KEY_PREFIX = 'job-queue-polling:';
11
+
12
+ interface StoredPollingState {
13
+ startTime: string;
14
+ expiresAt: number;
15
+ }
16
+
17
+ const jobListForPollingDocument = graphql(`
18
+ query JobListForPolling($options: JobListOptions) {
19
+ jobs(options: $options) {
20
+ items {
21
+ id
22
+ createdAt
23
+ state
24
+ }
25
+ totalItems
26
+ }
27
+ }
28
+ `);
29
+
30
+ const getStorageKey = (queueName: string) => `${STORAGE_KEY_PREFIX}${queueName}`;
31
+ const getStoredState = (queueName: string) => {
32
+ try {
33
+ const stored = sessionStorage.getItem(getStorageKey(queueName));
34
+ if (stored) {
35
+ return JSON.parse(stored) as StoredPollingState;
36
+ }
37
+ } catch {
38
+ // Ignore parsing errors
39
+ }
40
+ return null;
41
+ };
42
+ const setStoredState = (queueName: string, state: StoredPollingState) =>
43
+ sessionStorage.setItem(getStorageKey(queueName), JSON.stringify(state));
44
+ const clearStoredState = (queueName: string) => sessionStorage.removeItem(getStorageKey(queueName));
45
+
46
+ /**
47
+ * Hook to poll a job queue until jobs complete.
48
+ * Waits for jobs created after polling starts to settle before calling onComplete.
49
+ *
50
+ * Polling state is persisted in sessionStorage, allowing it to survive navigation
51
+ * (e.g., after creating an entity) and page refresh while maintaining the correct
52
+ * time window for finding relevant jobs.
53
+ */
54
+ export function useJobQueuePolling(queueName: string, onComplete: () => void) {
55
+ const [isPolling, setIsPolling] = useState(false);
56
+ const [pollCount, setPollCount] = useState(0);
57
+ const startTimeRef = useRef<string | null>(null);
58
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
59
+ const onCompleteRef = useRef(onComplete);
60
+ const hasResumedRef = useRef(false);
61
+
62
+ useEffect(() => {
63
+ onCompleteRef.current = onComplete;
64
+ }, [onComplete]);
65
+
66
+ // On mount, check for pending polling state
67
+ useEffect(() => {
68
+ if (hasResumedRef.current) return;
69
+ hasResumedRef.current = true;
70
+
71
+ const stored = getStoredState(queueName);
72
+ if (stored && Date.now() < stored.expiresAt) {
73
+ startTimeRef.current = stored.startTime;
74
+ setPollCount(0);
75
+ setIsPolling(true);
76
+
77
+ const remainingTime = stored.expiresAt - Date.now();
78
+ timeoutRef.current = setTimeout(() => {
79
+ setIsPolling(false);
80
+ startTimeRef.current = null;
81
+ clearStoredState(queueName);
82
+ onCompleteRef.current();
83
+ }, remainingTime);
84
+ } else if (stored) {
85
+ clearStoredState(queueName);
86
+ }
87
+ }, [queueName]);
88
+
89
+ // Calculate exponential backoff interval
90
+ const pollInterval = isPolling
91
+ ? Math.min(INITIAL_POLL_INTERVAL_MS * Math.pow(1.75, pollCount), MAX_POLL_INTERVAL_MS)
92
+ : false;
93
+
94
+ const { data: jobsData } = useQuery({
95
+ queryKey: ['jobQueuePolling', queueName],
96
+ queryFn: () => {
97
+ setPollCount(c => c + 1);
98
+ return api.query(jobListForPollingDocument, {
99
+ options: {
100
+ filter: { queueName: { eq: queueName } },
101
+ sort: { createdAt: 'DESC' as const },
102
+ take: 10,
103
+ },
104
+ });
105
+ },
106
+ enabled: isPolling,
107
+ refetchInterval: pollInterval,
108
+ });
109
+
110
+ // Detect job completion
111
+ useEffect(() => {
112
+ const startTime = startTimeRef.current;
113
+ if (!isPolling || !startTime) return;
114
+
115
+ const relevantJobs = jobsData?.jobs.items.filter(j => j.createdAt >= startTime) ?? [];
116
+ const hasSettledJob =
117
+ relevantJobs.length > 0 &&
118
+ relevantJobs.every(j => j.state !== 'PENDING' && j.state !== 'RUNNING' && j.state !== 'RETRYING');
119
+
120
+ if (hasSettledJob) {
121
+ setIsPolling(false);
122
+ startTimeRef.current = null;
123
+ clearStoredState(queueName);
124
+ if (timeoutRef.current) {
125
+ clearTimeout(timeoutRef.current);
126
+ timeoutRef.current = null;
127
+ }
128
+ onCompleteRef.current();
129
+ }
130
+ }, [jobsData, isPolling, queueName]);
131
+
132
+ useEffect(() => {
133
+ return () => {
134
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
135
+ };
136
+ }, []);
137
+
138
+ const startPolling = useCallback(() => {
139
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
140
+
141
+ const startTime = new Date(Date.now() - JOB_LOOKBACK_MS).toISOString();
142
+ const expiresAt = Date.now() + MAX_POLLING_TIMEOUT_MS;
143
+
144
+ // Store in sessionStorage so polling can resume after navigation
145
+ setStoredState(queueName, { startTime, expiresAt });
146
+
147
+ startTimeRef.current = startTime;
148
+ setPollCount(0);
149
+ setIsPolling(true);
150
+
151
+ timeoutRef.current = setTimeout(() => {
152
+ setIsPolling(false);
153
+ startTimeRef.current = null;
154
+ clearStoredState(queueName);
155
+ onCompleteRef.current();
156
+ }, MAX_POLLING_TIMEOUT_MS);
157
+ }, [queueName]);
158
+
159
+ return { isPolling, startPolling };
160
+ }