@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.
- package/dist/vite/utils/plugin-discovery.js +3 -3
- package/dist/vite/vite-plugin-lingui-babel.d.ts +15 -2
- package/dist/vite/vite-plugin-lingui-babel.js +90 -8
- package/dist/vite/vite-plugin-translations.js +2 -2
- package/package.json +3 -3
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +22 -3
- package/src/app/routes/_authenticated/_customers/customers.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_customers/customers.tsx +3 -0
- package/src/app/routes/_authenticated/_orders/components/draft-order-status.tsx +48 -0
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +22 -6
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +1 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +9 -3
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +49 -30
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +1 -0
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +1 -0
- package/src/i18n/locales/ar.po +58 -5
- package/src/i18n/locales/en.po +58 -5
- package/src/lib/components/data-input/index.ts +1 -0
- package/src/lib/components/ui/alert.tsx +2 -0
- package/src/lib/framework/extension-api/input-component-extensions.tsx +2 -0
- package/src/lib/framework/form-engine/form-schema-tools.ts +4 -1
- package/src/lib/framework/page/detail-page-route-loader.tsx +6 -4
- package/src/lib/framework/page/detail-page.tsx +22 -37
- package/src/lib/graphql/graphql-env.d.ts +30 -13
- 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
|
+
}
|