@inoo-ch/payload-image-optimizer 1.8.0 → 1.9.0
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/components/RegenerationButton.js +85 -21
- package/dist/components/RegenerationButton.js.map +1 -1
- package/dist/defaults.js +14 -2
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/regenerate.d.ts +1 -0
- package/dist/endpoints/regenerate.js +77 -1
- package/dist/endpoints/regenerate.js.map +1 -1
- package/dist/hooks/beforeChange.js +16 -9
- package/dist/hooks/beforeChange.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +32 -5
- package/dist/index.js.map +1 -1
- package/dist/tasks/regenerateDocument.js +27 -4
- package/dist/tasks/regenerateDocument.js.map +1 -1
- package/dist/types.d.ts +49 -2
- package/dist/types.js.map +1 -1
- package/dist/utilities/filenameStrategies.d.ts +25 -0
- package/dist/utilities/filenameStrategies.js +46 -0
- package/dist/utilities/filenameStrategies.js.map +1 -0
- package/dist/utilities/stripDiacritics.d.ts +9 -0
- package/dist/utilities/stripDiacritics.js +10 -0
- package/dist/utilities/stripDiacritics.js.map +1 -0
- package/dist/utilities/toKebabCase.d.ts +10 -0
- package/dist/utilities/toKebabCase.js +11 -0
- package/dist/utilities/toKebabCase.js.map +1 -0
- package/package.json +1 -1
- package/src/components/RegenerationButton.tsx +92 -24
- package/src/defaults.ts +15 -1
- package/src/endpoints/regenerate.ts +68 -0
- package/src/hooks/beforeChange.ts +16 -9
- package/src/index.ts +27 -6
- package/src/tasks/regenerateDocument.ts +24 -4
- package/src/types.ts +51 -2
- package/src/utilities/filenameStrategies.ts +61 -0
- package/src/utilities/stripDiacritics.ts +10 -0
- package/src/utilities/toKebabCase.ts +16 -0
|
@@ -16,6 +16,7 @@ export const RegenerationButton = ()=>{
|
|
|
16
16
|
const [force, setForce] = useState(false);
|
|
17
17
|
const [error, setError] = useState(null);
|
|
18
18
|
const [stalled, setStalled] = useState(false);
|
|
19
|
+
const [cancelled, setCancelled] = useState(false);
|
|
19
20
|
const [collectionSlug, setCollectionSlug] = useState(null);
|
|
20
21
|
const [stats, setStats] = useState(null);
|
|
21
22
|
const [confirming, setConfirming] = useState(false);
|
|
@@ -24,6 +25,9 @@ export const RegenerationButton = ()=>{
|
|
|
24
25
|
lastProcessed: 0,
|
|
25
26
|
stallCount: 0
|
|
26
27
|
});
|
|
28
|
+
// Snapshot of complete+errored at the moment regeneration starts,
|
|
29
|
+
// so we can compute batch-relative progress for selective regeneration.
|
|
30
|
+
const baselineRef = useRef(null);
|
|
27
31
|
const prevIsRunningRef = useRef(false);
|
|
28
32
|
// Extract collection slug from URL after mount to avoid hydration mismatch
|
|
29
33
|
useEffect(()=>{
|
|
@@ -65,6 +69,15 @@ export const RegenerationButton = ()=>{
|
|
|
65
69
|
if (res.ok) {
|
|
66
70
|
const data = await res.json();
|
|
67
71
|
setProgress(data);
|
|
72
|
+
// Stop polling if server reports cancellation
|
|
73
|
+
if (data.cancelled) {
|
|
74
|
+
setCancelled(true);
|
|
75
|
+
setIsRunning(false);
|
|
76
|
+
setStalled(false);
|
|
77
|
+
stopPolling();
|
|
78
|
+
sessionStorage.removeItem(SESSION_KEY);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
68
81
|
// Stop polling when no more pending
|
|
69
82
|
if (data.pending <= 0) {
|
|
70
83
|
setIsRunning(false);
|
|
@@ -158,12 +171,35 @@ export const RegenerationButton = ()=>{
|
|
|
158
171
|
const handleCancel = ()=>{
|
|
159
172
|
setConfirming(false);
|
|
160
173
|
};
|
|
174
|
+
const handleStop = async ()=>{
|
|
175
|
+
if (!collectionSlug) return;
|
|
176
|
+
try {
|
|
177
|
+
await fetch('/api/image-optimizer/regenerate', {
|
|
178
|
+
method: 'DELETE',
|
|
179
|
+
headers: {
|
|
180
|
+
'Content-Type': 'application/json'
|
|
181
|
+
},
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
collectionSlug
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
setCancelled(true);
|
|
187
|
+
setIsRunning(false);
|
|
188
|
+
setStalled(false);
|
|
189
|
+
stopPolling();
|
|
190
|
+
sessionStorage.removeItem(SESSION_KEY);
|
|
191
|
+
fetchStats();
|
|
192
|
+
} catch {
|
|
193
|
+
// ignore cancel errors
|
|
194
|
+
}
|
|
195
|
+
};
|
|
161
196
|
// Phase 2: Actually start regeneration (after user confirms)
|
|
162
197
|
const handleConfirm = async ()=>{
|
|
163
198
|
if (!collectionSlug) return;
|
|
164
199
|
setConfirming(false);
|
|
165
200
|
setError(null);
|
|
166
201
|
setStalled(false);
|
|
202
|
+
setCancelled(false);
|
|
167
203
|
setIsRunning(true);
|
|
168
204
|
setQueued(null);
|
|
169
205
|
setProgress(null);
|
|
@@ -171,6 +207,8 @@ export const RegenerationButton = ()=>{
|
|
|
171
207
|
lastProcessed: 0,
|
|
172
208
|
stallCount: 0
|
|
173
209
|
};
|
|
210
|
+
// Capture current complete+errored as baseline before new jobs run
|
|
211
|
+
baselineRef.current = stats ? stats.complete + stats.errored : 0;
|
|
174
212
|
try {
|
|
175
213
|
const requestBody = {
|
|
176
214
|
collectionSlug,
|
|
@@ -212,7 +250,13 @@ export const RegenerationButton = ()=>{
|
|
|
212
250
|
stopPolling
|
|
213
251
|
]);
|
|
214
252
|
if (!collectionSlug) return null;
|
|
215
|
-
|
|
253
|
+
// When a batch is running, compute progress relative to the queued count
|
|
254
|
+
// (not the total collection) so selective regeneration shows e.g. 1/2, not 1/167.
|
|
255
|
+
const batchTotal = queued ?? progress?.total ?? 0;
|
|
256
|
+
const batchProcessed = progress ? progress.complete + progress.errored - (baselineRef.current ?? 0) : 0;
|
|
257
|
+
const batchComplete = progress ? progress.complete - Math.max((baselineRef.current ?? 0) - progress.errored, 0) : 0;
|
|
258
|
+
const batchErrored = progress ? Math.max(batchProcessed - Math.max(batchComplete, 0), 0) : 0;
|
|
259
|
+
const progressPercent = batchTotal > 0 ? Math.min(Math.round(batchProcessed / batchTotal * 100), 100) : 0;
|
|
216
260
|
const showProgressBar = isRunning && progress || stalled && progress;
|
|
217
261
|
// Stats computations
|
|
218
262
|
const statsPercent = stats && stats.total > 0 ? Math.round(stats.complete / stats.total * 100) : 0;
|
|
@@ -227,20 +271,33 @@ export const RegenerationButton = ()=>{
|
|
|
227
271
|
flexWrap: 'wrap'
|
|
228
272
|
},
|
|
229
273
|
children: [
|
|
230
|
-
!confirming && /*#__PURE__*/ _jsx("button", {
|
|
274
|
+
!confirming && !isRunning && /*#__PURE__*/ _jsx("button", {
|
|
231
275
|
onClick: handlePreflight,
|
|
232
|
-
disabled: isRunning,
|
|
233
276
|
style: {
|
|
234
|
-
backgroundColor:
|
|
277
|
+
backgroundColor: '#4f46e5',
|
|
278
|
+
color: '#fff',
|
|
279
|
+
border: 'none',
|
|
280
|
+
borderRadius: '6px',
|
|
281
|
+
padding: '8px 16px',
|
|
282
|
+
fontSize: '14px',
|
|
283
|
+
fontWeight: 500,
|
|
284
|
+
cursor: 'pointer'
|
|
285
|
+
},
|
|
286
|
+
children: hasSelection ? `Regenerate ${selectionCount} Selected` : 'Regenerate All Images'
|
|
287
|
+
}),
|
|
288
|
+
!confirming && isRunning && /*#__PURE__*/ _jsx("button", {
|
|
289
|
+
onClick: handleStop,
|
|
290
|
+
style: {
|
|
291
|
+
backgroundColor: '#ef4444',
|
|
235
292
|
color: '#fff',
|
|
236
293
|
border: 'none',
|
|
237
294
|
borderRadius: '6px',
|
|
238
295
|
padding: '8px 16px',
|
|
239
296
|
fontSize: '14px',
|
|
240
297
|
fontWeight: 500,
|
|
241
|
-
cursor:
|
|
298
|
+
cursor: 'pointer'
|
|
242
299
|
},
|
|
243
|
-
children:
|
|
300
|
+
children: "Stop Processing"
|
|
244
301
|
}),
|
|
245
302
|
confirming && stats && /*#__PURE__*/ _jsxs("div", {
|
|
246
303
|
style: {
|
|
@@ -323,13 +380,20 @@ export const RegenerationButton = ()=>{
|
|
|
323
380
|
" for processing"
|
|
324
381
|
]
|
|
325
382
|
}),
|
|
326
|
-
queued === 0 && !isRunning && !stalled && !confirming && /*#__PURE__*/ _jsx("span", {
|
|
383
|
+
queued === 0 && !isRunning && !stalled && !confirming && !cancelled && /*#__PURE__*/ _jsx("span", {
|
|
327
384
|
style: {
|
|
328
385
|
color: '#10b981',
|
|
329
386
|
fontSize: '13px'
|
|
330
387
|
},
|
|
331
388
|
children: "All images already optimized."
|
|
332
389
|
}),
|
|
390
|
+
cancelled && !isRunning && !confirming && /*#__PURE__*/ _jsx("span", {
|
|
391
|
+
style: {
|
|
392
|
+
color: '#f59e0b',
|
|
393
|
+
fontSize: '13px'
|
|
394
|
+
},
|
|
395
|
+
children: "Processing cancelled."
|
|
396
|
+
}),
|
|
333
397
|
stalled && progress && /*#__PURE__*/ _jsxs("span", {
|
|
334
398
|
style: {
|
|
335
399
|
color: '#f59e0b',
|
|
@@ -359,18 +423,18 @@ export const RegenerationButton = ()=>{
|
|
|
359
423
|
children: [
|
|
360
424
|
/*#__PURE__*/ _jsxs("span", {
|
|
361
425
|
children: [
|
|
362
|
-
|
|
426
|
+
Math.max(batchProcessed, 0),
|
|
363
427
|
" / ",
|
|
364
|
-
|
|
428
|
+
batchTotal,
|
|
365
429
|
" complete"
|
|
366
430
|
]
|
|
367
431
|
}),
|
|
368
|
-
|
|
432
|
+
batchErrored > 0 && /*#__PURE__*/ _jsxs("span", {
|
|
369
433
|
style: {
|
|
370
434
|
color: '#ef4444'
|
|
371
435
|
},
|
|
372
436
|
children: [
|
|
373
|
-
|
|
437
|
+
batchErrored,
|
|
374
438
|
" errors"
|
|
375
439
|
]
|
|
376
440
|
}),
|
|
@@ -394,15 +458,15 @@ export const RegenerationButton = ()=>{
|
|
|
394
458
|
/*#__PURE__*/ _jsx("div", {
|
|
395
459
|
style: {
|
|
396
460
|
height: '100%',
|
|
397
|
-
width: `${
|
|
461
|
+
width: `${batchTotal > 0 ? Math.min(Math.round((batchProcessed - batchErrored) / batchTotal * 100), 100) : 0}%`,
|
|
398
462
|
backgroundColor: '#10b981',
|
|
399
463
|
transition: 'width 0.3s ease'
|
|
400
464
|
}
|
|
401
465
|
}),
|
|
402
|
-
|
|
466
|
+
batchErrored > 0 && /*#__PURE__*/ _jsx("div", {
|
|
403
467
|
style: {
|
|
404
468
|
height: '100%',
|
|
405
|
-
width: `${
|
|
469
|
+
width: `${batchTotal > 0 ? Math.round(batchErrored / batchTotal * 100) : 0}%`,
|
|
406
470
|
backgroundColor: '#ef4444',
|
|
407
471
|
transition: 'width 0.3s ease'
|
|
408
472
|
}
|
|
@@ -411,30 +475,30 @@ export const RegenerationButton = ()=>{
|
|
|
411
475
|
})
|
|
412
476
|
]
|
|
413
477
|
}),
|
|
414
|
-
!isRunning && !stalled &&
|
|
478
|
+
!isRunning && !stalled && !cancelled && progress && batchProcessed > 0 && queued !== 0 && !confirming && /*#__PURE__*/ _jsxs("span", {
|
|
415
479
|
style: {
|
|
416
480
|
fontSize: '13px'
|
|
417
481
|
},
|
|
418
482
|
children: [
|
|
419
483
|
/*#__PURE__*/ _jsxs("span", {
|
|
420
484
|
style: {
|
|
421
|
-
color:
|
|
485
|
+
color: batchErrored > 0 ? '#f59e0b' : '#10b981'
|
|
422
486
|
},
|
|
423
487
|
children: [
|
|
424
488
|
"Done! ",
|
|
425
|
-
|
|
489
|
+
Math.max(batchProcessed - batchErrored, 0),
|
|
426
490
|
"/",
|
|
427
|
-
|
|
428
|
-
" optimized
|
|
491
|
+
batchTotal,
|
|
492
|
+
" optimized."
|
|
429
493
|
]
|
|
430
494
|
}),
|
|
431
|
-
|
|
495
|
+
batchErrored > 0 && /*#__PURE__*/ _jsxs("span", {
|
|
432
496
|
style: {
|
|
433
497
|
color: '#ef4444'
|
|
434
498
|
},
|
|
435
499
|
children: [
|
|
436
500
|
' ',
|
|
437
|
-
|
|
501
|
+
batchErrored,
|
|
438
502
|
" failed."
|
|
439
503
|
]
|
|
440
504
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/RegenerationButton.tsx"],"sourcesContent":["'use client'\n\nimport React, { useState, useEffect, useCallback, useRef } from 'react'\nimport { useSelection } from '@payloadcms/ui'\n\ntype RegenerationProgress = {\n total: number\n complete: number\n errored: number\n pending: number\n}\n\nconst POLL_INTERVAL_MS = 2000\n// With sequential processing each image takes ~4-5s, so no progress for 30s\n// (15 polls) strongly suggests a real stall rather than slow processing.\nconst STALL_THRESHOLD = 15\nconst SESSION_KEY = 'imageOptimizer_running'\n\nexport const RegenerationButton: React.FC = () => {\n const { count: selectionCount, getSelectedIds } = useSelection()\n const hasSelection = selectionCount > 0\n const [isRunning, setIsRunning] = useState(false)\n const [progress, setProgress] = useState<RegenerationProgress | null>(null)\n const [queued, setQueued] = useState<number | null>(null)\n const [force, setForce] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [stalled, setStalled] = useState(false)\n const [collectionSlug, setCollectionSlug] = useState<string | null>(null)\n const [stats, setStats] = useState<RegenerationProgress | null>(null)\n const [confirming, setConfirming] = useState(false)\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n const stallRef = useRef({ lastProcessed: 0, stallCount: 0 })\n const prevIsRunningRef = useRef(false)\n\n // Extract collection slug from URL after mount to avoid hydration mismatch\n useEffect(() => {\n const slug = window.location.pathname.split('/collections/')[1]?.split('/')[0] ?? null\n setCollectionSlug(slug)\n }, [])\n\n // Fetch optimization stats (independent of regeneration)\n const fetchStats = useCallback(async () => {\n if (!collectionSlug) return\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (res.ok) {\n const data: RegenerationProgress = await res.json()\n setStats(data)\n }\n } catch {\n // ignore stats fetch errors\n }\n }, [collectionSlug])\n\n const stopPolling = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n }, [])\n\n const startPolling = useCallback(\n (pollFn: () => void) => {\n // Prevent duplicate intervals\n stopPolling()\n intervalRef.current = setInterval(pollFn, POLL_INTERVAL_MS)\n },\n [stopPolling],\n )\n\n const pollProgress = useCallback(async () => {\n if (!collectionSlug) return\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (res.ok) {\n const data: RegenerationProgress = await res.json()\n setProgress(data)\n\n // Stop polling when no more pending\n if (data.pending <= 0) {\n setIsRunning(false)\n setStalled(false)\n stopPolling()\n sessionStorage.removeItem(SESSION_KEY)\n return\n }\n\n // Stall detection — warn but keep polling so we detect when jobs resume\n const processed = data.complete + data.errored\n if (processed === stallRef.current.lastProcessed) {\n stallRef.current.stallCount += 1\n } else {\n stallRef.current.stallCount = 0\n stallRef.current.lastProcessed = processed\n // Clear stall warning when progress resumes\n setStalled(false)\n }\n\n if (stallRef.current.stallCount >= STALL_THRESHOLD) {\n setStalled(true)\n // Keep polling — jobs may still be running server-side\n }\n }\n } catch {\n // ignore polling errors\n }\n }, [collectionSlug, stopPolling])\n\n // On mount: fetch stats for the counter display. If the user previously\n // triggered regeneration (sessionStorage flag) and there are still pending\n // images, resume polling so the UI reconnects after page navigation.\n useEffect(() => {\n if (!collectionSlug) return\n let cancelled = false\n const loadStats = async () => {\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (!res.ok || cancelled) return\n const data: RegenerationProgress = await res.json()\n setStats(data)\n\n // Resume polling only if the user triggered regeneration in this session\n const wasRunning = sessionStorage.getItem(SESSION_KEY) === collectionSlug\n if (wasRunning && data.pending > 0) {\n setProgress(data)\n setIsRunning(true)\n setStalled(false)\n stallRef.current = { lastProcessed: data.complete + data.errored, stallCount: 0 }\n startPolling(pollProgress)\n } else if (wasRunning && data.pending <= 0) {\n // Jobs finished while we were away — clear the flag\n sessionStorage.removeItem(SESSION_KEY)\n }\n } catch {\n // ignore\n }\n }\n loadStats()\n return () => {\n cancelled = true\n stopPolling()\n }\n }, [collectionSlug, pollProgress, startPolling, stopPolling])\n\n // Refresh stats when regeneration finishes (isRunning transitions from true to false)\n useEffect(() => {\n if (prevIsRunningRef.current && !isRunning) {\n fetchStats()\n }\n prevIsRunningRef.current = isRunning\n }, [isRunning, fetchStats])\n\n // Phase 1: Show confirmation with counts\n const handlePreflight = async () => {\n if (!collectionSlug) return\n setError(null)\n // Refresh stats to get the latest counts before confirming\n await fetchStats()\n setConfirming(true)\n }\n\n const handleCancel = () => {\n setConfirming(false)\n }\n\n // Phase 2: Actually start regeneration (after user confirms)\n const handleConfirm = async () => {\n if (!collectionSlug) return\n setConfirming(false)\n setError(null)\n setStalled(false)\n setIsRunning(true)\n setQueued(null)\n setProgress(null)\n stallRef.current = { lastProcessed: 0, stallCount: 0 }\n\n try {\n const requestBody: Record<string, unknown> = { collectionSlug, force }\n if (hasSelection) {\n requestBody.docIds = getSelectedIds().map(String)\n }\n\n const res = await fetch('/api/image-optimizer/regenerate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(requestBody),\n })\n\n if (!res.ok) {\n const data = await res.json()\n throw new Error(data.error || 'Failed to start regeneration')\n }\n\n const data = await res.json()\n setQueued(data.queued)\n\n if (data.queued === 0) {\n setIsRunning(false)\n return\n }\n\n // Persist running state so we can resume after page navigation\n sessionStorage.setItem(SESSION_KEY, collectionSlug)\n // Start polling\n startPolling(pollProgress)\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err))\n setIsRunning(false)\n }\n }\n\n // Cleanup interval on unmount\n useEffect(() => {\n return () => stopPolling()\n }, [stopPolling])\n\n if (!collectionSlug) return null\n\n const progressPercent =\n progress && progress.total > 0\n ? Math.round(((progress.complete + progress.errored) / progress.total) * 100)\n : 0\n\n const showProgressBar = (isRunning && progress) || (stalled && progress)\n\n // Stats computations\n const statsPercent =\n stats && stats.total > 0\n ? Math.round((stats.complete / stats.total) * 100)\n : 0\n const allOptimized = stats && stats.total > 0 && stats.complete === stats.total\n\n return (\n <div\n style={{\n padding: '16px 24px',\n borderBottom: '1px solid #e5e7eb',\n display: 'flex',\n alignItems: 'center',\n gap: '16px',\n flexWrap: 'wrap',\n }}\n >\n {!confirming && (\n <button\n onClick={handlePreflight}\n disabled={isRunning}\n style={{\n backgroundColor: isRunning ? '#9ca3af' : '#4f46e5',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '8px 16px',\n fontSize: '14px',\n fontWeight: 500,\n cursor: isRunning ? 'not-allowed' : 'pointer',\n }}\n >\n {isRunning\n ? 'Processing images...'\n : hasSelection\n ? `Regenerate ${selectionCount} Selected`\n : 'Regenerate All Images'}\n </button>\n )}\n\n {confirming && stats && (\n <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>\n <span style={{ fontSize: '13px', color: '#374151' }}>\n {hasSelection\n ? `Regenerate ${selectionCount} selected image${selectionCount !== 1 ? 's' : ''}?`\n : force\n ? `Re-process all ${stats.total} images across the entire collection?`\n : `Regenerate ${stats.pending} unoptimized image${stats.pending !== 1 ? 's' : ''} across the entire collection?`}\n </span>\n <button\n onClick={handleConfirm}\n style={{\n backgroundColor: '#4f46e5',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '6px 14px',\n fontSize: '13px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Confirm\n </button>\n <button\n onClick={handleCancel}\n style={{\n backgroundColor: 'transparent',\n color: '#6b7280',\n border: '1px solid #d1d5db',\n borderRadius: '6px',\n padding: '6px 14px',\n fontSize: '13px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Cancel\n </button>\n </div>\n )}\n\n {!confirming && (\n <label\n style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '13px' }}\n >\n <input\n type=\"checkbox\"\n checked={force}\n onChange={(e) => setForce(e.target.checked)}\n disabled={isRunning}\n />\n Force re-process all\n </label>\n )}\n\n {error && (\n <span style={{ color: '#ef4444', fontSize: '13px' }}>{error}</span>\n )}\n\n {queued !== null && queued > 0 && isRunning && !confirming && (\n <span style={{ color: '#4f46e5', fontSize: '13px' }}>\n Queued {queued} image{queued !== 1 ? 's' : ''} for processing\n </span>\n )}\n\n {queued === 0 && !isRunning && !stalled && !confirming && (\n <span style={{ color: '#10b981', fontSize: '13px' }}>\n All images already optimized.\n </span>\n )}\n\n {stalled && progress && (\n <span style={{ color: '#f59e0b', fontSize: '13px' }}>\n Processing appears slow — {progress.pending} image{progress.pending !== 1 ? 's' : ''} still pending.\n Jobs may still be running server-side.\n </span>\n )}\n\n {showProgressBar && (\n <div style={{ flex: 1, minWidth: '200px' }}>\n <div\n style={{\n display: 'flex',\n justifyContent: 'space-between',\n fontSize: '12px',\n marginBottom: '4px',\n }}\n >\n <span>\n {progress.complete} / {progress.total} complete\n </span>\n {progress.errored > 0 && (\n <span style={{ color: '#ef4444' }}>{progress.errored} errors</span>\n )}\n <span>{progressPercent}%</span>\n </div>\n <div\n style={{\n height: '6px',\n backgroundColor: '#e5e7eb',\n borderRadius: '3px',\n overflow: 'hidden',\n display: 'flex',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${progress.total > 0 ? Math.round((progress.complete / progress.total) * 100) : 0}%`,\n backgroundColor: '#10b981',\n transition: 'width 0.3s ease',\n }}\n />\n {progress.errored > 0 && (\n <div\n style={{\n height: '100%',\n width: `${progress.total > 0 ? Math.round((progress.errored / progress.total) * 100) : 0}%`,\n backgroundColor: '#ef4444',\n transition: 'width 0.3s ease',\n }}\n />\n )}\n </div>\n </div>\n )}\n\n {!isRunning && !stalled && progress && progress.complete > 0 && queued !== 0 && !confirming && (\n <span style={{ fontSize: '13px' }}>\n <span style={{ color: progress.errored > 0 ? '#f59e0b' : '#10b981' }}>\n Done! {progress.complete}/{progress.total} optimized (across entire collection).\n </span>\n {progress.errored > 0 && (\n <span style={{ color: '#ef4444' }}>\n {' '}{progress.errored} failed.\n </span>\n )}\n </span>\n )}\n\n {/* Persistent optimization stats — always visible when not actively regenerating */}\n {!isRunning && !stalled && stats && stats.total > 0 && (\n <div\n style={{\n marginLeft: 'auto',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'flex-end',\n gap: '4px',\n minWidth: '180px',\n }}\n >\n <div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }}>\n {allOptimized ? (\n <span style={{ color: '#10b981' }}>\n ✓ All {stats.total} images optimized\n </span>\n ) : (\n <>\n <span style={{ color: '#6b7280' }}>\n {stats.complete}/{stats.total} optimized\n </span>\n {stats.errored > 0 && (\n <>\n <span style={{ color: '#d1d5db' }}>·</span>\n <span style={{ color: '#ef4444' }}>{stats.errored} errors</span>\n </>\n )}\n </>\n )}\n </div>\n {!allOptimized && (\n <div\n style={{\n width: '100%',\n height: '3px',\n backgroundColor: '#e5e7eb',\n borderRadius: '2px',\n overflow: 'hidden',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${statsPercent}%`,\n backgroundColor: stats.errored > 0 ? '#f59e0b' : '#10b981',\n borderRadius: '2px',\n transition: 'width 0.3s ease',\n }}\n />\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],"names":["React","useState","useEffect","useCallback","useRef","useSelection","POLL_INTERVAL_MS","STALL_THRESHOLD","SESSION_KEY","RegenerationButton","count","selectionCount","getSelectedIds","hasSelection","isRunning","setIsRunning","progress","setProgress","queued","setQueued","force","setForce","error","setError","stalled","setStalled","collectionSlug","setCollectionSlug","stats","setStats","confirming","setConfirming","intervalRef","stallRef","lastProcessed","stallCount","prevIsRunningRef","slug","window","location","pathname","split","fetchStats","res","fetch","ok","data","json","stopPolling","current","clearInterval","startPolling","pollFn","setInterval","pollProgress","pending","sessionStorage","removeItem","processed","complete","errored","cancelled","loadStats","wasRunning","getItem","handlePreflight","handleCancel","handleConfirm","requestBody","docIds","map","String","method","headers","body","JSON","stringify","Error","setItem","err","message","progressPercent","total","Math","round","showProgressBar","statsPercent","allOptimized","div","style","padding","borderBottom","display","alignItems","gap","flexWrap","button","onClick","disabled","backgroundColor","color","border","borderRadius","fontSize","fontWeight","cursor","span","label","input","type","checked","onChange","e","target","flex","minWidth","justifyContent","marginBottom","height","overflow","width","transition","marginLeft","flexDirection"],"mappings":"AAAA;;AAEA,OAAOA,SAASC,QAAQ,EAAEC,SAAS,EAAEC,WAAW,EAAEC,MAAM,QAAQ,QAAO;AACvE,SAASC,YAAY,QAAQ,iBAAgB;AAS7C,MAAMC,mBAAmB;AACzB,4EAA4E;AAC5E,yEAAyE;AACzE,MAAMC,kBAAkB;AACxB,MAAMC,cAAc;AAEpB,OAAO,MAAMC,qBAA+B;IAC1C,MAAM,EAAEC,OAAOC,cAAc,EAAEC,cAAc,EAAE,GAAGP;IAClD,MAAMQ,eAAeF,iBAAiB;IACtC,MAAM,CAACG,WAAWC,aAAa,GAAGd,SAAS;IAC3C,MAAM,CAACe,UAAUC,YAAY,GAAGhB,SAAsC;IACtE,MAAM,CAACiB,QAAQC,UAAU,GAAGlB,SAAwB;IACpD,MAAM,CAACmB,OAAOC,SAAS,GAAGpB,SAAS;IACnC,MAAM,CAACqB,OAAOC,SAAS,GAAGtB,SAAwB;IAClD,MAAM,CAACuB,SAASC,WAAW,GAAGxB,SAAS;IACvC,MAAM,CAACyB,gBAAgBC,kBAAkB,GAAG1B,SAAwB;IACpE,MAAM,CAAC2B,OAAOC,SAAS,GAAG5B,SAAsC;IAChE,MAAM,CAAC6B,YAAYC,cAAc,GAAG9B,SAAS;IAC7C,MAAM+B,cAAc5B,OAA8C;IAClE,MAAM6B,WAAW7B,OAAO;QAAE8B,eAAe;QAAGC,YAAY;IAAE;IAC1D,MAAMC,mBAAmBhC,OAAO;IAEhC,2EAA2E;IAC3EF,UAAU;QACR,MAAMmC,OAAOC,OAAOC,QAAQ,CAACC,QAAQ,CAACC,KAAK,CAAC,gBAAgB,CAAC,EAAE,EAAEA,MAAM,IAAI,CAAC,EAAE,IAAI;QAClFd,kBAAkBU;IACpB,GAAG,EAAE;IAEL,yDAAyD;IACzD,MAAMK,aAAavC,YAAY;QAC7B,IAAI,CAACuB,gBAAgB;QACrB,IAAI;YACF,MAAMiB,MAAM,MAAMC,MAChB,CAAC,2CAA2C,EAAElB,gBAAgB;YAEhE,IAAIiB,IAAIE,EAAE,EAAE;gBACV,MAAMC,OAA6B,MAAMH,IAAII,IAAI;gBACjDlB,SAASiB;YACX;QACF,EAAE,OAAM;QACN,4BAA4B;QAC9B;IACF,GAAG;QAACpB;KAAe;IAEnB,MAAMsB,cAAc7C,YAAY;QAC9B,IAAI6B,YAAYiB,OAAO,EAAE;YACvBC,cAAclB,YAAYiB,OAAO;YACjCjB,YAAYiB,OAAO,GAAG;QACxB;IACF,GAAG,EAAE;IAEL,MAAME,eAAehD,YACnB,CAACiD;QACC,8BAA8B;QAC9BJ;QACAhB,YAAYiB,OAAO,GAAGI,YAAYD,QAAQ9C;IAC5C,GACA;QAAC0C;KAAY;IAGf,MAAMM,eAAenD,YAAY;QAC/B,IAAI,CAACuB,gBAAgB;QACrB,IAAI;YACF,MAAMiB,MAAM,MAAMC,MAChB,CAAC,2CAA2C,EAAElB,gBAAgB;YAEhE,IAAIiB,IAAIE,EAAE,EAAE;gBACV,MAAMC,OAA6B,MAAMH,IAAII,IAAI;gBACjD9B,YAAY6B;gBAEZ,oCAAoC;gBACpC,IAAIA,KAAKS,OAAO,IAAI,GAAG;oBACrBxC,aAAa;oBACbU,WAAW;oBACXuB;oBACAQ,eAAeC,UAAU,CAACjD;oBAC1B;gBACF;gBAEA,wEAAwE;gBACxE,MAAMkD,YAAYZ,KAAKa,QAAQ,GAAGb,KAAKc,OAAO;gBAC9C,IAAIF,cAAczB,SAASgB,OAAO,CAACf,aAAa,EAAE;oBAChDD,SAASgB,OAAO,CAACd,UAAU,IAAI;gBACjC,OAAO;oBACLF,SAASgB,OAAO,CAACd,UAAU,GAAG;oBAC9BF,SAASgB,OAAO,CAACf,aAAa,GAAGwB;oBACjC,4CAA4C;oBAC5CjC,WAAW;gBACb;gBAEA,IAAIQ,SAASgB,OAAO,CAACd,UAAU,IAAI5B,iBAAiB;oBAClDkB,WAAW;gBACX,uDAAuD;gBACzD;YACF;QACF,EAAE,OAAM;QACN,wBAAwB;QAC1B;IACF,GAAG;QAACC;QAAgBsB;KAAY;IAEhC,wEAAwE;IACxE,2EAA2E;IAC3E,qEAAqE;IACrE9C,UAAU;QACR,IAAI,CAACwB,gBAAgB;QACrB,IAAImC,YAAY;QAChB,MAAMC,YAAY;YAChB,IAAI;gBACF,MAAMnB,MAAM,MAAMC,MAChB,CAAC,2CAA2C,EAAElB,gBAAgB;gBAEhE,IAAI,CAACiB,IAAIE,EAAE,IAAIgB,WAAW;gBAC1B,MAAMf,OAA6B,MAAMH,IAAII,IAAI;gBACjDlB,SAASiB;gBAET,yEAAyE;gBACzE,MAAMiB,aAAaP,eAAeQ,OAAO,CAACxD,iBAAiBkB;gBAC3D,IAAIqC,cAAcjB,KAAKS,OAAO,GAAG,GAAG;oBAClCtC,YAAY6B;oBACZ/B,aAAa;oBACbU,WAAW;oBACXQ,SAASgB,OAAO,GAAG;wBAAEf,eAAeY,KAAKa,QAAQ,GAAGb,KAAKc,OAAO;wBAAEzB,YAAY;oBAAE;oBAChFgB,aAAaG;gBACf,OAAO,IAAIS,cAAcjB,KAAKS,OAAO,IAAI,GAAG;oBAC1C,oDAAoD;oBACpDC,eAAeC,UAAU,CAACjD;gBAC5B;YACF,EAAE,OAAM;YACN,SAAS;YACX;QACF;QACAsD;QACA,OAAO;YACLD,YAAY;YACZb;QACF;IACF,GAAG;QAACtB;QAAgB4B;QAAcH;QAAcH;KAAY;IAE5D,sFAAsF;IACtF9C,UAAU;QACR,IAAIkC,iBAAiBa,OAAO,IAAI,CAACnC,WAAW;YAC1C4B;QACF;QACAN,iBAAiBa,OAAO,GAAGnC;IAC7B,GAAG;QAACA;QAAW4B;KAAW;IAE1B,yCAAyC;IACzC,MAAMuB,kBAAkB;QACtB,IAAI,CAACvC,gBAAgB;QACrBH,SAAS;QACT,2DAA2D;QAC3D,MAAMmB;QACNX,cAAc;IAChB;IAEA,MAAMmC,eAAe;QACnBnC,cAAc;IAChB;IAEA,6DAA6D;IAC7D,MAAMoC,gBAAgB;QACpB,IAAI,CAACzC,gBAAgB;QACrBK,cAAc;QACdR,SAAS;QACTE,WAAW;QACXV,aAAa;QACbI,UAAU;QACVF,YAAY;QACZgB,SAASgB,OAAO,GAAG;YAAEf,eAAe;YAAGC,YAAY;QAAE;QAErD,IAAI;YACF,MAAMiC,cAAuC;gBAAE1C;gBAAgBN;YAAM;YACrE,IAAIP,cAAc;gBAChBuD,YAAYC,MAAM,GAAGzD,iBAAiB0D,GAAG,CAACC;YAC5C;YAEA,MAAM5B,MAAM,MAAMC,MAAM,mCAAmC;gBACzD4B,QAAQ;gBACRC,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CC,MAAMC,KAAKC,SAAS,CAACR;YACvB;YAEA,IAAI,CAACzB,IAAIE,EAAE,EAAE;gBACX,MAAMC,OAAO,MAAMH,IAAII,IAAI;gBAC3B,MAAM,IAAI8B,MAAM/B,KAAKxB,KAAK,IAAI;YAChC;YAEA,MAAMwB,OAAO,MAAMH,IAAII,IAAI;YAC3B5B,UAAU2B,KAAK5B,MAAM;YAErB,IAAI4B,KAAK5B,MAAM,KAAK,GAAG;gBACrBH,aAAa;gBACb;YACF;YAEA,+DAA+D;YAC/DyC,eAAesB,OAAO,CAACtE,aAAakB;YACpC,gBAAgB;YAChByB,aAAaG;QACf,EAAE,OAAOyB,KAAK;YACZxD,SAASwD,eAAeF,QAAQE,IAAIC,OAAO,GAAGT,OAAOQ;YACrDhE,aAAa;QACf;IACF;IAEA,8BAA8B;IAC9Bb,UAAU;QACR,OAAO,IAAM8C;IACf,GAAG;QAACA;KAAY;IAEhB,IAAI,CAACtB,gBAAgB,OAAO;IAE5B,MAAMuD,kBACJjE,YAAYA,SAASkE,KAAK,GAAG,IACzBC,KAAKC,KAAK,CAAC,AAAEpE,CAAAA,SAAS2C,QAAQ,GAAG3C,SAAS4C,OAAO,AAAD,IAAK5C,SAASkE,KAAK,GAAI,OACvE;IAEN,MAAMG,kBAAkB,AAACvE,aAAaE,YAAcQ,WAAWR;IAE/D,qBAAqB;IACrB,MAAMsE,eACJ1D,SAASA,MAAMsD,KAAK,GAAG,IACnBC,KAAKC,KAAK,CAAC,AAACxD,MAAM+B,QAAQ,GAAG/B,MAAMsD,KAAK,GAAI,OAC5C;IACN,MAAMK,eAAe3D,SAASA,MAAMsD,KAAK,GAAG,KAAKtD,MAAM+B,QAAQ,KAAK/B,MAAMsD,KAAK;IAE/E,qBACE,MAACM;QACCC,OAAO;YACLC,SAAS;YACTC,cAAc;YACdC,SAAS;YACTC,YAAY;YACZC,KAAK;YACLC,UAAU;QACZ;;YAEC,CAACjE,4BACA,KAACkE;gBACCC,SAAShC;gBACTiC,UAAUpF;gBACV2E,OAAO;oBACLU,iBAAiBrF,YAAY,YAAY;oBACzCsF,OAAO;oBACPC,QAAQ;oBACRC,cAAc;oBACdZ,SAAS;oBACTa,UAAU;oBACVC,YAAY;oBACZC,QAAQ3F,YAAY,gBAAgB;gBACtC;0BAECA,YACG,yBACAD,eACE,CAAC,WAAW,EAAEF,eAAe,SAAS,CAAC,GACvC;;YAITmB,cAAcF,uBACb,MAAC4D;gBAAIC,OAAO;oBAAEG,SAAS;oBAAQC,YAAY;oBAAUC,KAAK;gBAAO;;kCAC/D,KAACY;wBAAKjB,OAAO;4BAAEc,UAAU;4BAAQH,OAAO;wBAAU;kCAC/CvF,eACG,CAAC,WAAW,EAAEF,eAAe,eAAe,EAAEA,mBAAmB,IAAI,MAAM,GAAG,CAAC,CAAC,GAChFS,QACE,CAAC,eAAe,EAAEQ,MAAMsD,KAAK,CAAC,qCAAqC,CAAC,GACpE,CAAC,WAAW,EAAEtD,MAAM2B,OAAO,CAAC,kBAAkB,EAAE3B,MAAM2B,OAAO,KAAK,IAAI,MAAM,GAAG,8BAA8B,CAAC;;kCAEtH,KAACyC;wBACCC,SAAS9B;wBACTsB,OAAO;4BACLU,iBAAiB;4BACjBC,OAAO;4BACPC,QAAQ;4BACRC,cAAc;4BACdZ,SAAS;4BACTa,UAAU;4BACVC,YAAY;4BACZC,QAAQ;wBACV;kCACD;;kCAGD,KAACT;wBACCC,SAAS/B;wBACTuB,OAAO;4BACLU,iBAAiB;4BACjBC,OAAO;4BACPC,QAAQ;4BACRC,cAAc;4BACdZ,SAAS;4BACTa,UAAU;4BACVC,YAAY;4BACZC,QAAQ;wBACV;kCACD;;;;YAMJ,CAAC3E,4BACA,MAAC6E;gBACClB,OAAO;oBAAEG,SAAS;oBAAQC,YAAY;oBAAUC,KAAK;oBAAOS,UAAU;gBAAO;;kCAE7E,KAACK;wBACCC,MAAK;wBACLC,SAAS1F;wBACT2F,UAAU,CAACC,IAAM3F,SAAS2F,EAAEC,MAAM,CAACH,OAAO;wBAC1CZ,UAAUpF;;oBACV;;;YAKLQ,uBACC,KAACoF;gBAAKjB,OAAO;oBAAEW,OAAO;oBAAWG,UAAU;gBAAO;0BAAIjF;;YAGvDJ,WAAW,QAAQA,SAAS,KAAKJ,aAAa,CAACgB,4BAC9C,MAAC4E;gBAAKjB,OAAO;oBAAEW,OAAO;oBAAWG,UAAU;gBAAO;;oBAAG;oBAC3CrF;oBAAO;oBAAOA,WAAW,IAAI,MAAM;oBAAG;;;YAIjDA,WAAW,KAAK,CAACJ,aAAa,CAACU,WAAW,CAACM,4BAC1C,KAAC4E;gBAAKjB,OAAO;oBAAEW,OAAO;oBAAWG,UAAU;gBAAO;0BAAG;;YAKtD/E,WAAWR,0BACV,MAAC0F;gBAAKjB,OAAO;oBAAEW,OAAO;oBAAWG,UAAU;gBAAO;;oBAAG;oBACxBvF,SAASuC,OAAO;oBAAC;oBAAOvC,SAASuC,OAAO,KAAK,IAAI,MAAM;oBAAG;;;YAKxF8B,iCACC,MAACG;gBAAIC,OAAO;oBAAEyB,MAAM;oBAAGC,UAAU;gBAAQ;;kCACvC,MAAC3B;wBACCC,OAAO;4BACLG,SAAS;4BACTwB,gBAAgB;4BAChBb,UAAU;4BACVc,cAAc;wBAChB;;0CAEA,MAACX;;oCACE1F,SAAS2C,QAAQ;oCAAC;oCAAI3C,SAASkE,KAAK;oCAAC;;;4BAEvClE,SAAS4C,OAAO,GAAG,mBAClB,MAAC8C;gCAAKjB,OAAO;oCAAEW,OAAO;gCAAU;;oCAAIpF,SAAS4C,OAAO;oCAAC;;;0CAEvD,MAAC8C;;oCAAMzB;oCAAgB;;;;;kCAEzB,MAACO;wBACCC,OAAO;4BACL6B,QAAQ;4BACRnB,iBAAiB;4BACjBG,cAAc;4BACdiB,UAAU;4BACV3B,SAAS;wBACX;;0CAEA,KAACJ;gCACCC,OAAO;oCACL6B,QAAQ;oCACRE,OAAO,GAAGxG,SAASkE,KAAK,GAAG,IAAIC,KAAKC,KAAK,CAAC,AAACpE,SAAS2C,QAAQ,GAAG3C,SAASkE,KAAK,GAAI,OAAO,EAAE,CAAC,CAAC;oCAC5FiB,iBAAiB;oCACjBsB,YAAY;gCACd;;4BAEDzG,SAAS4C,OAAO,GAAG,mBAClB,KAAC4B;gCACCC,OAAO;oCACL6B,QAAQ;oCACRE,OAAO,GAAGxG,SAASkE,KAAK,GAAG,IAAIC,KAAKC,KAAK,CAAC,AAACpE,SAAS4C,OAAO,GAAG5C,SAASkE,KAAK,GAAI,OAAO,EAAE,CAAC,CAAC;oCAC3FiB,iBAAiB;oCACjBsB,YAAY;gCACd;;;;;;YAOT,CAAC3G,aAAa,CAACU,WAAWR,YAAYA,SAAS2C,QAAQ,GAAG,KAAKzC,WAAW,KAAK,CAACY,4BAC/E,MAAC4E;gBAAKjB,OAAO;oBAAEc,UAAU;gBAAO;;kCAC9B,MAACG;wBAAKjB,OAAO;4BAAEW,OAAOpF,SAAS4C,OAAO,GAAG,IAAI,YAAY;wBAAU;;4BAAG;4BAC7D5C,SAAS2C,QAAQ;4BAAC;4BAAE3C,SAASkE,KAAK;4BAAC;;;oBAE3ClE,SAAS4C,OAAO,GAAG,mBAClB,MAAC8C;wBAAKjB,OAAO;4BAAEW,OAAO;wBAAU;;4BAC7B;4BAAKpF,SAAS4C,OAAO;4BAAC;;;;;YAO9B,CAAC9C,aAAa,CAACU,WAAWI,SAASA,MAAMsD,KAAK,GAAG,mBAChD,MAACM;gBACCC,OAAO;oBACLiC,YAAY;oBACZ9B,SAAS;oBACT+B,eAAe;oBACf9B,YAAY;oBACZC,KAAK;oBACLqB,UAAU;gBACZ;;kCAEA,KAAC3B;wBAAIC,OAAO;4BAAEG,SAAS;4BAAQC,YAAY;4BAAUC,KAAK;4BAAOS,UAAU;wBAAO;kCAC/EhB,6BACC,MAACmB;4BAAKjB,OAAO;gCAAEW,OAAO;4BAAU;;gCAAG;gCACnBxE,MAAMsD,KAAK;gCAAC;;2CAG5B;;8CACE,MAACwB;oCAAKjB,OAAO;wCAAEW,OAAO;oCAAU;;wCAC7BxE,MAAM+B,QAAQ;wCAAC;wCAAE/B,MAAMsD,KAAK;wCAAC;;;gCAE/BtD,MAAMgC,OAAO,GAAG,mBACf;;sDACE,KAAC8C;4CAAKjB,OAAO;gDAAEW,OAAO;4CAAU;sDAAG;;sDACnC,MAACM;4CAAKjB,OAAO;gDAAEW,OAAO;4CAAU;;gDAAIxE,MAAMgC,OAAO;gDAAC;;;;;;;;oBAM3D,CAAC2B,8BACA,KAACC;wBACCC,OAAO;4BACL+B,OAAO;4BACPF,QAAQ;4BACRnB,iBAAiB;4BACjBG,cAAc;4BACdiB,UAAU;wBACZ;kCAEA,cAAA,KAAC/B;4BACCC,OAAO;gCACL6B,QAAQ;gCACRE,OAAO,GAAGlC,aAAa,CAAC,CAAC;gCACzBa,iBAAiBvE,MAAMgC,OAAO,GAAG,IAAI,YAAY;gCACjD0C,cAAc;gCACdmB,YAAY;4BACd;;;;;;;AAQhB,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../src/components/RegenerationButton.tsx"],"sourcesContent":["'use client'\n\nimport React, { useState, useEffect, useCallback, useRef } from 'react'\nimport { useSelection } from '@payloadcms/ui'\n\ntype RegenerationProgress = {\n total: number\n complete: number\n errored: number\n pending: number\n}\n\nconst POLL_INTERVAL_MS = 2000\n// With sequential processing each image takes ~4-5s, so no progress for 30s\n// (15 polls) strongly suggests a real stall rather than slow processing.\nconst STALL_THRESHOLD = 15\nconst SESSION_KEY = 'imageOptimizer_running'\n\nexport const RegenerationButton: React.FC = () => {\n const { count: selectionCount, getSelectedIds } = useSelection()\n const hasSelection = selectionCount > 0\n const [isRunning, setIsRunning] = useState(false)\n const [progress, setProgress] = useState<RegenerationProgress | null>(null)\n const [queued, setQueued] = useState<number | null>(null)\n const [force, setForce] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [stalled, setStalled] = useState(false)\n const [cancelled, setCancelled] = useState(false)\n const [collectionSlug, setCollectionSlug] = useState<string | null>(null)\n const [stats, setStats] = useState<RegenerationProgress | null>(null)\n const [confirming, setConfirming] = useState(false)\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)\n const stallRef = useRef({ lastProcessed: 0, stallCount: 0 })\n // Snapshot of complete+errored at the moment regeneration starts,\n // so we can compute batch-relative progress for selective regeneration.\n const baselineRef = useRef<number | null>(null)\n const prevIsRunningRef = useRef(false)\n\n // Extract collection slug from URL after mount to avoid hydration mismatch\n useEffect(() => {\n const slug = window.location.pathname.split('/collections/')[1]?.split('/')[0] ?? null\n setCollectionSlug(slug)\n }, [])\n\n // Fetch optimization stats (independent of regeneration)\n const fetchStats = useCallback(async () => {\n if (!collectionSlug) return\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (res.ok) {\n const data: RegenerationProgress = await res.json()\n setStats(data)\n }\n } catch {\n // ignore stats fetch errors\n }\n }, [collectionSlug])\n\n const stopPolling = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current)\n intervalRef.current = null\n }\n }, [])\n\n const startPolling = useCallback(\n (pollFn: () => void) => {\n // Prevent duplicate intervals\n stopPolling()\n intervalRef.current = setInterval(pollFn, POLL_INTERVAL_MS)\n },\n [stopPolling],\n )\n\n const pollProgress = useCallback(async () => {\n if (!collectionSlug) return\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (res.ok) {\n const data = await res.json()\n setProgress(data)\n\n // Stop polling if server reports cancellation\n if (data.cancelled) {\n setCancelled(true)\n setIsRunning(false)\n setStalled(false)\n stopPolling()\n sessionStorage.removeItem(SESSION_KEY)\n return\n }\n\n // Stop polling when no more pending\n if (data.pending <= 0) {\n setIsRunning(false)\n setStalled(false)\n stopPolling()\n sessionStorage.removeItem(SESSION_KEY)\n return\n }\n\n // Stall detection — warn but keep polling so we detect when jobs resume\n const processed = data.complete + data.errored\n if (processed === stallRef.current.lastProcessed) {\n stallRef.current.stallCount += 1\n } else {\n stallRef.current.stallCount = 0\n stallRef.current.lastProcessed = processed\n // Clear stall warning when progress resumes\n setStalled(false)\n }\n\n if (stallRef.current.stallCount >= STALL_THRESHOLD) {\n setStalled(true)\n // Keep polling — jobs may still be running server-side\n }\n }\n } catch {\n // ignore polling errors\n }\n }, [collectionSlug, stopPolling])\n\n // On mount: fetch stats for the counter display. If the user previously\n // triggered regeneration (sessionStorage flag) and there are still pending\n // images, resume polling so the UI reconnects after page navigation.\n useEffect(() => {\n if (!collectionSlug) return\n let cancelled = false\n const loadStats = async () => {\n try {\n const res = await fetch(\n `/api/image-optimizer/regenerate?collection=${collectionSlug}`,\n )\n if (!res.ok || cancelled) return\n const data: RegenerationProgress = await res.json()\n setStats(data)\n\n // Resume polling only if the user triggered regeneration in this session\n const wasRunning = sessionStorage.getItem(SESSION_KEY) === collectionSlug\n if (wasRunning && data.pending > 0) {\n setProgress(data)\n setIsRunning(true)\n setStalled(false)\n stallRef.current = { lastProcessed: data.complete + data.errored, stallCount: 0 }\n startPolling(pollProgress)\n } else if (wasRunning && data.pending <= 0) {\n // Jobs finished while we were away — clear the flag\n sessionStorage.removeItem(SESSION_KEY)\n }\n } catch {\n // ignore\n }\n }\n loadStats()\n return () => {\n cancelled = true\n stopPolling()\n }\n }, [collectionSlug, pollProgress, startPolling, stopPolling])\n\n // Refresh stats when regeneration finishes (isRunning transitions from true to false)\n useEffect(() => {\n if (prevIsRunningRef.current && !isRunning) {\n fetchStats()\n }\n prevIsRunningRef.current = isRunning\n }, [isRunning, fetchStats])\n\n // Phase 1: Show confirmation with counts\n const handlePreflight = async () => {\n if (!collectionSlug) return\n setError(null)\n // Refresh stats to get the latest counts before confirming\n await fetchStats()\n setConfirming(true)\n }\n\n const handleCancel = () => {\n setConfirming(false)\n }\n\n const handleStop = async () => {\n if (!collectionSlug) return\n try {\n await fetch('/api/image-optimizer/regenerate', {\n method: 'DELETE',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ collectionSlug }),\n })\n setCancelled(true)\n setIsRunning(false)\n setStalled(false)\n stopPolling()\n sessionStorage.removeItem(SESSION_KEY)\n fetchStats()\n } catch {\n // ignore cancel errors\n }\n }\n\n // Phase 2: Actually start regeneration (after user confirms)\n const handleConfirm = async () => {\n if (!collectionSlug) return\n setConfirming(false)\n setError(null)\n setStalled(false)\n setCancelled(false)\n setIsRunning(true)\n setQueued(null)\n setProgress(null)\n stallRef.current = { lastProcessed: 0, stallCount: 0 }\n // Capture current complete+errored as baseline before new jobs run\n baselineRef.current = stats ? stats.complete + stats.errored : 0\n\n try {\n const requestBody: Record<string, unknown> = { collectionSlug, force }\n if (hasSelection) {\n requestBody.docIds = getSelectedIds().map(String)\n }\n\n const res = await fetch('/api/image-optimizer/regenerate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(requestBody),\n })\n\n if (!res.ok) {\n const data = await res.json()\n throw new Error(data.error || 'Failed to start regeneration')\n }\n\n const data = await res.json()\n setQueued(data.queued)\n\n if (data.queued === 0) {\n setIsRunning(false)\n return\n }\n\n // Persist running state so we can resume after page navigation\n sessionStorage.setItem(SESSION_KEY, collectionSlug)\n // Start polling\n startPolling(pollProgress)\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err))\n setIsRunning(false)\n }\n }\n\n // Cleanup interval on unmount\n useEffect(() => {\n return () => stopPolling()\n }, [stopPolling])\n\n if (!collectionSlug) return null\n\n // When a batch is running, compute progress relative to the queued count\n // (not the total collection) so selective regeneration shows e.g. 1/2, not 1/167.\n const batchTotal = queued ?? progress?.total ?? 0\n const batchProcessed = progress\n ? (progress.complete + progress.errored) - (baselineRef.current ?? 0)\n : 0\n const batchComplete = progress\n ? progress.complete - Math.max((baselineRef.current ?? 0) - (progress.errored), 0)\n : 0\n const batchErrored = progress ? Math.max(batchProcessed - Math.max(batchComplete, 0), 0) : 0\n\n const progressPercent =\n batchTotal > 0\n ? Math.min(Math.round((batchProcessed / batchTotal) * 100), 100)\n : 0\n\n const showProgressBar = (isRunning && progress) || (stalled && progress)\n\n // Stats computations\n const statsPercent =\n stats && stats.total > 0\n ? Math.round((stats.complete / stats.total) * 100)\n : 0\n const allOptimized = stats && stats.total > 0 && stats.complete === stats.total\n\n return (\n <div\n style={{\n padding: '16px 24px',\n borderBottom: '1px solid #e5e7eb',\n display: 'flex',\n alignItems: 'center',\n gap: '16px',\n flexWrap: 'wrap',\n }}\n >\n {!confirming && !isRunning && (\n <button\n onClick={handlePreflight}\n style={{\n backgroundColor: '#4f46e5',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '8px 16px',\n fontSize: '14px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n {hasSelection\n ? `Regenerate ${selectionCount} Selected`\n : 'Regenerate All Images'}\n </button>\n )}\n\n {!confirming && isRunning && (\n <button\n onClick={handleStop}\n style={{\n backgroundColor: '#ef4444',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '8px 16px',\n fontSize: '14px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Stop Processing\n </button>\n )}\n\n {confirming && stats && (\n <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>\n <span style={{ fontSize: '13px', color: '#374151' }}>\n {hasSelection\n ? `Regenerate ${selectionCount} selected image${selectionCount !== 1 ? 's' : ''}?`\n : force\n ? `Re-process all ${stats.total} images across the entire collection?`\n : `Regenerate ${stats.pending} unoptimized image${stats.pending !== 1 ? 's' : ''} across the entire collection?`}\n </span>\n <button\n onClick={handleConfirm}\n style={{\n backgroundColor: '#4f46e5',\n color: '#fff',\n border: 'none',\n borderRadius: '6px',\n padding: '6px 14px',\n fontSize: '13px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Confirm\n </button>\n <button\n onClick={handleCancel}\n style={{\n backgroundColor: 'transparent',\n color: '#6b7280',\n border: '1px solid #d1d5db',\n borderRadius: '6px',\n padding: '6px 14px',\n fontSize: '13px',\n fontWeight: 500,\n cursor: 'pointer',\n }}\n >\n Cancel\n </button>\n </div>\n )}\n\n {!confirming && (\n <label\n style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '13px' }}\n >\n <input\n type=\"checkbox\"\n checked={force}\n onChange={(e) => setForce(e.target.checked)}\n disabled={isRunning}\n />\n Force re-process all\n </label>\n )}\n\n {error && (\n <span style={{ color: '#ef4444', fontSize: '13px' }}>{error}</span>\n )}\n\n {queued !== null && queued > 0 && isRunning && !confirming && (\n <span style={{ color: '#4f46e5', fontSize: '13px' }}>\n Queued {queued} image{queued !== 1 ? 's' : ''} for processing\n </span>\n )}\n\n {queued === 0 && !isRunning && !stalled && !confirming && !cancelled && (\n <span style={{ color: '#10b981', fontSize: '13px' }}>\n All images already optimized.\n </span>\n )}\n\n {cancelled && !isRunning && !confirming && (\n <span style={{ color: '#f59e0b', fontSize: '13px' }}>\n Processing cancelled.\n </span>\n )}\n\n {stalled && progress && (\n <span style={{ color: '#f59e0b', fontSize: '13px' }}>\n Processing appears slow — {progress.pending} image{progress.pending !== 1 ? 's' : ''} still pending.\n Jobs may still be running server-side.\n </span>\n )}\n\n {showProgressBar && (\n <div style={{ flex: 1, minWidth: '200px' }}>\n <div\n style={{\n display: 'flex',\n justifyContent: 'space-between',\n fontSize: '12px',\n marginBottom: '4px',\n }}\n >\n <span>\n {Math.max(batchProcessed, 0)} / {batchTotal} complete\n </span>\n {batchErrored > 0 && (\n <span style={{ color: '#ef4444' }}>{batchErrored} errors</span>\n )}\n <span>{progressPercent}%</span>\n </div>\n <div\n style={{\n height: '6px',\n backgroundColor: '#e5e7eb',\n borderRadius: '3px',\n overflow: 'hidden',\n display: 'flex',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${batchTotal > 0 ? Math.min(Math.round(((batchProcessed - batchErrored) / batchTotal) * 100), 100) : 0}%`,\n backgroundColor: '#10b981',\n transition: 'width 0.3s ease',\n }}\n />\n {batchErrored > 0 && (\n <div\n style={{\n height: '100%',\n width: `${batchTotal > 0 ? Math.round((batchErrored / batchTotal) * 100) : 0}%`,\n backgroundColor: '#ef4444',\n transition: 'width 0.3s ease',\n }}\n />\n )}\n </div>\n </div>\n )}\n\n {!isRunning && !stalled && !cancelled && progress && batchProcessed > 0 && queued !== 0 && !confirming && (\n <span style={{ fontSize: '13px' }}>\n <span style={{ color: batchErrored > 0 ? '#f59e0b' : '#10b981' }}>\n Done! {Math.max(batchProcessed - batchErrored, 0)}/{batchTotal} optimized.\n </span>\n {batchErrored > 0 && (\n <span style={{ color: '#ef4444' }}>\n {' '}{batchErrored} failed.\n </span>\n )}\n </span>\n )}\n\n {/* Persistent optimization stats — always visible when not actively regenerating */}\n {!isRunning && !stalled && stats && stats.total > 0 && (\n <div\n style={{\n marginLeft: 'auto',\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'flex-end',\n gap: '4px',\n minWidth: '180px',\n }}\n >\n <div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }}>\n {allOptimized ? (\n <span style={{ color: '#10b981' }}>\n ✓ All {stats.total} images optimized\n </span>\n ) : (\n <>\n <span style={{ color: '#6b7280' }}>\n {stats.complete}/{stats.total} optimized\n </span>\n {stats.errored > 0 && (\n <>\n <span style={{ color: '#d1d5db' }}>·</span>\n <span style={{ color: '#ef4444' }}>{stats.errored} errors</span>\n </>\n )}\n </>\n )}\n </div>\n {!allOptimized && (\n <div\n style={{\n width: '100%',\n height: '3px',\n backgroundColor: '#e5e7eb',\n borderRadius: '2px',\n overflow: 'hidden',\n }}\n >\n <div\n style={{\n height: '100%',\n width: `${statsPercent}%`,\n backgroundColor: stats.errored > 0 ? '#f59e0b' : '#10b981',\n borderRadius: '2px',\n transition: 'width 0.3s ease',\n }}\n />\n </div>\n )}\n </div>\n )}\n </div>\n )\n}\n"],"names":["React","useState","useEffect","useCallback","useRef","useSelection","POLL_INTERVAL_MS","STALL_THRESHOLD","SESSION_KEY","RegenerationButton","count","selectionCount","getSelectedIds","hasSelection","isRunning","setIsRunning","progress","setProgress","queued","setQueued","force","setForce","error","setError","stalled","setStalled","cancelled","setCancelled","collectionSlug","setCollectionSlug","stats","setStats","confirming","setConfirming","intervalRef","stallRef","lastProcessed","stallCount","baselineRef","prevIsRunningRef","slug","window","location","pathname","split","fetchStats","res","fetch","ok","data","json","stopPolling","current","clearInterval","startPolling","pollFn","setInterval","pollProgress","sessionStorage","removeItem","pending","processed","complete","errored","loadStats","wasRunning","getItem","handlePreflight","handleCancel","handleStop","method","headers","body","JSON","stringify","handleConfirm","requestBody","docIds","map","String","Error","setItem","err","message","batchTotal","total","batchProcessed","batchComplete","Math","max","batchErrored","progressPercent","min","round","showProgressBar","statsPercent","allOptimized","div","style","padding","borderBottom","display","alignItems","gap","flexWrap","button","onClick","backgroundColor","color","border","borderRadius","fontSize","fontWeight","cursor","span","label","input","type","checked","onChange","e","target","disabled","flex","minWidth","justifyContent","marginBottom","height","overflow","width","transition","marginLeft","flexDirection"],"mappings":"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"}
|
package/dist/defaults.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import { uuidFilename } from './utilities/filenameStrategies.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the generateFilename option:
|
|
4
|
+
* - Explicit `generateFilename` callback takes priority
|
|
5
|
+
* - `uniqueFileNames: true` maps to `uuidFilename` for backwards compat
|
|
6
|
+
* - Otherwise undefined (keep original filename)
|
|
7
|
+
*/ const resolveGenerateFilename = (config)=>{
|
|
8
|
+
if (config.generateFilename) return config.generateFilename;
|
|
9
|
+
if (config.uniqueFileNames) return uuidFilename;
|
|
10
|
+
return undefined;
|
|
11
|
+
};
|
|
1
12
|
export const resolveConfig = (config)=>({
|
|
2
13
|
clientOptimization: config.clientOptimization ?? true,
|
|
3
14
|
collections: config.collections,
|
|
@@ -8,14 +19,15 @@ export const resolveConfig = (config)=>({
|
|
|
8
19
|
quality: 80
|
|
9
20
|
}
|
|
10
21
|
],
|
|
22
|
+
generateFilename: resolveGenerateFilename(config),
|
|
11
23
|
generateThumbHash: config.generateThumbHash ?? true,
|
|
12
24
|
maxDimensions: config.maxDimensions ?? {
|
|
13
25
|
width: 2560,
|
|
14
26
|
height: 2560
|
|
15
27
|
},
|
|
28
|
+
regenerateButton: config.regenerateButton ?? true,
|
|
16
29
|
replaceOriginal: config.replaceOriginal ?? true,
|
|
17
|
-
stripMetadata: config.stripMetadata ?? true
|
|
18
|
-
uniqueFileNames: config.uniqueFileNames ?? false
|
|
30
|
+
stripMetadata: config.stripMetadata ?? true
|
|
19
31
|
});
|
|
20
32
|
export const resolveCollectionConfig = (resolvedConfig, collectionSlug)=>{
|
|
21
33
|
const collectionValue = resolvedConfig.collections[collectionSlug];
|
package/dist/defaults.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type { CollectionSlug } from 'payload'\n\nimport type { ImageOptimizerConfig, ResolvedCollectionOptimizerConfig, ResolvedImageOptimizerConfig } from './types.js'\n\nexport const resolveConfig = (config: ImageOptimizerConfig): ResolvedImageOptimizerConfig => ({\n clientOptimization: config.clientOptimization ?? true,\n collections: config.collections,\n disabled: config.disabled ?? false,\n formats: config.formats ?? [\n { format: 'webp', quality: 80 },\n ],\n generateThumbHash: config.generateThumbHash ?? true,\n maxDimensions: config.maxDimensions ?? { width: 2560, height: 2560 },\n
|
|
1
|
+
{"version":3,"sources":["../src/defaults.ts"],"sourcesContent":["import type { CollectionSlug } from 'payload'\n\nimport type { ImageOptimizerConfig, ResolvedCollectionOptimizerConfig, ResolvedImageOptimizerConfig } from './types.js'\nimport { uuidFilename } from './utilities/filenameStrategies.js'\n\n/**\n * Resolve the generateFilename option:\n * - Explicit `generateFilename` callback takes priority\n * - `uniqueFileNames: true` maps to `uuidFilename` for backwards compat\n * - Otherwise undefined (keep original filename)\n */\nconst resolveGenerateFilename = (config: ImageOptimizerConfig) => {\n if (config.generateFilename) return config.generateFilename\n if (config.uniqueFileNames) return uuidFilename\n return undefined\n}\n\nexport const resolveConfig = (config: ImageOptimizerConfig): ResolvedImageOptimizerConfig => ({\n clientOptimization: config.clientOptimization ?? true,\n collections: config.collections,\n disabled: config.disabled ?? false,\n formats: config.formats ?? [\n { format: 'webp', quality: 80 },\n ],\n generateFilename: resolveGenerateFilename(config),\n generateThumbHash: config.generateThumbHash ?? true,\n maxDimensions: config.maxDimensions ?? { width: 2560, height: 2560 },\n regenerateButton: config.regenerateButton ?? true,\n replaceOriginal: config.replaceOriginal ?? true,\n stripMetadata: config.stripMetadata ?? true,\n})\n\nexport const resolveCollectionConfig = (\n resolvedConfig: ResolvedImageOptimizerConfig,\n collectionSlug: string,\n): ResolvedCollectionOptimizerConfig => {\n const collectionValue = resolvedConfig.collections[collectionSlug as CollectionSlug]\n\n if (!collectionValue || collectionValue === true) {\n return {\n formats: resolvedConfig.formats,\n maxDimensions: resolvedConfig.maxDimensions,\n replaceOriginal: resolvedConfig.replaceOriginal,\n }\n }\n\n return {\n formats: collectionValue.formats ?? resolvedConfig.formats,\n maxDimensions: collectionValue.maxDimensions ?? resolvedConfig.maxDimensions,\n replaceOriginal: collectionValue.replaceOriginal ?? resolvedConfig.replaceOriginal,\n }\n}\n"],"names":["uuidFilename","resolveGenerateFilename","config","generateFilename","uniqueFileNames","undefined","resolveConfig","clientOptimization","collections","disabled","formats","format","quality","generateThumbHash","maxDimensions","width","height","regenerateButton","replaceOriginal","stripMetadata","resolveCollectionConfig","resolvedConfig","collectionSlug","collectionValue"],"mappings":"AAGA,SAASA,YAAY,QAAQ,oCAAmC;AAEhE;;;;;CAKC,GACD,MAAMC,0BAA0B,CAACC;IAC/B,IAAIA,OAAOC,gBAAgB,EAAE,OAAOD,OAAOC,gBAAgB;IAC3D,IAAID,OAAOE,eAAe,EAAE,OAAOJ;IACnC,OAAOK;AACT;AAEA,OAAO,MAAMC,gBAAgB,CAACJ,SAAgE,CAAA;QAC5FK,oBAAoBL,OAAOK,kBAAkB,IAAI;QACjDC,aAAaN,OAAOM,WAAW;QAC/BC,UAAUP,OAAOO,QAAQ,IAAI;QAC7BC,SAASR,OAAOQ,OAAO,IAAI;YACzB;gBAAEC,QAAQ;gBAAQC,SAAS;YAAG;SAC/B;QACDT,kBAAkBF,wBAAwBC;QAC1CW,mBAAmBX,OAAOW,iBAAiB,IAAI;QAC/CC,eAAeZ,OAAOY,aAAa,IAAI;YAAEC,OAAO;YAAMC,QAAQ;QAAK;QACnEC,kBAAkBf,OAAOe,gBAAgB,IAAI;QAC7CC,iBAAiBhB,OAAOgB,eAAe,IAAI;QAC3CC,eAAejB,OAAOiB,aAAa,IAAI;IACzC,CAAA,EAAE;AAEF,OAAO,MAAMC,0BAA0B,CACrCC,gBACAC;IAEA,MAAMC,kBAAkBF,eAAeb,WAAW,CAACc,eAAiC;IAEpF,IAAI,CAACC,mBAAmBA,oBAAoB,MAAM;QAChD,OAAO;YACLb,SAASW,eAAeX,OAAO;YAC/BI,eAAeO,eAAeP,aAAa;YAC3CI,iBAAiBG,eAAeH,eAAe;QACjD;IACF;IAEA,OAAO;QACLR,SAASa,gBAAgBb,OAAO,IAAIW,eAAeX,OAAO;QAC1DI,eAAeS,gBAAgBT,aAAa,IAAIO,eAAeP,aAAa;QAC5EI,iBAAiBK,gBAAgBL,eAAe,IAAIG,eAAeH,eAAe;IACpF;AACF,EAAC"}
|
|
@@ -2,3 +2,4 @@ import type { PayloadHandler } from 'payload';
|
|
|
2
2
|
import type { ResolvedImageOptimizerConfig } from '../types.js';
|
|
3
3
|
export declare const createRegenerateHandler: (resolvedConfig: ResolvedImageOptimizerConfig) => PayloadHandler;
|
|
4
4
|
export declare const createRegenerateStatusHandler: (resolvedConfig: ResolvedImageOptimizerConfig) => PayloadHandler;
|
|
5
|
+
export declare const createCancelHandler: (resolvedConfig: ResolvedImageOptimizerConfig) => PayloadHandler;
|
|
@@ -1,4 +1,36 @@
|
|
|
1
1
|
import { waitUntil } from '../utilities/waitUntil.js';
|
|
2
|
+
const GLOBAL_SLUG = 'image-optimizer-state';
|
|
3
|
+
async function getCollectionState(payload, slug) {
|
|
4
|
+
try {
|
|
5
|
+
const state = await payload.findGlobal({
|
|
6
|
+
slug: GLOBAL_SLUG
|
|
7
|
+
});
|
|
8
|
+
return state?.collections?.[slug] || {};
|
|
9
|
+
} catch {
|
|
10
|
+
return {};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function setCollectionState(payload, slug, update) {
|
|
14
|
+
let existing = {};
|
|
15
|
+
try {
|
|
16
|
+
const state = await payload.findGlobal({
|
|
17
|
+
slug: GLOBAL_SLUG
|
|
18
|
+
});
|
|
19
|
+
existing = state?.collections || {};
|
|
20
|
+
} catch {
|
|
21
|
+
// Global may not exist yet
|
|
22
|
+
}
|
|
23
|
+
existing[slug] = {
|
|
24
|
+
...existing[slug],
|
|
25
|
+
...update
|
|
26
|
+
};
|
|
27
|
+
await payload.updateGlobal({
|
|
28
|
+
slug: GLOBAL_SLUG,
|
|
29
|
+
data: {
|
|
30
|
+
collections: existing
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
2
34
|
export const createRegenerateHandler = (resolvedConfig)=>{
|
|
3
35
|
const handler = async (req)=>{
|
|
4
36
|
if (!req.user) {
|
|
@@ -91,6 +123,12 @@ export const createRegenerateHandler = (resolvedConfig)=>{
|
|
|
91
123
|
}
|
|
92
124
|
}
|
|
93
125
|
req.payload.logger.info(`Image optimizer: queued ${queued} images from '${collectionSlug}' for regeneration`);
|
|
126
|
+
// Clear any previous cancellation and record the start time + batch size
|
|
127
|
+
await setCollectionState(req.payload, collectionSlug, {
|
|
128
|
+
startedAt: Date.now(),
|
|
129
|
+
cancelledAt: undefined,
|
|
130
|
+
queued
|
|
131
|
+
});
|
|
94
132
|
// Fire the job runner — use waitUntil to keep the serverless function alive
|
|
95
133
|
// after the response is sent, so jobs actually complete on Vercel/serverless.
|
|
96
134
|
if (queued > 0) {
|
|
@@ -159,12 +197,50 @@ export const createRegenerateStatusHandler = (resolvedConfig)=>{
|
|
|
159
197
|
}
|
|
160
198
|
}
|
|
161
199
|
});
|
|
200
|
+
// Include cancellation state so the UI can react
|
|
201
|
+
const collState = await getCollectionState(req.payload, collectionSlug);
|
|
202
|
+
const cancelled = !!(collState.cancelledAt && collState.startedAt && collState.cancelledAt > collState.startedAt);
|
|
162
203
|
return Response.json({
|
|
163
204
|
collectionSlug,
|
|
164
205
|
total: total.totalDocs,
|
|
165
206
|
complete: complete.totalDocs,
|
|
166
207
|
errored: errored.totalDocs,
|
|
167
|
-
pending: total.totalDocs - complete.totalDocs - errored.totalDocs
|
|
208
|
+
pending: total.totalDocs - complete.totalDocs - errored.totalDocs,
|
|
209
|
+
cancelled
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
return handler;
|
|
213
|
+
};
|
|
214
|
+
export const createCancelHandler = (resolvedConfig)=>{
|
|
215
|
+
const handler = async (req)=>{
|
|
216
|
+
if (!req.user) {
|
|
217
|
+
return Response.json({
|
|
218
|
+
error: 'Unauthorized'
|
|
219
|
+
}, {
|
|
220
|
+
status: 401
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
let body;
|
|
224
|
+
try {
|
|
225
|
+
body = await req.json();
|
|
226
|
+
} catch {
|
|
227
|
+
body = {};
|
|
228
|
+
}
|
|
229
|
+
const collectionSlug = body.collectionSlug;
|
|
230
|
+
if (!collectionSlug || !resolvedConfig.collections[collectionSlug]) {
|
|
231
|
+
return Response.json({
|
|
232
|
+
error: 'Invalid or unconfigured collection slug'
|
|
233
|
+
}, {
|
|
234
|
+
status: 400
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
await setCollectionState(req.payload, collectionSlug, {
|
|
238
|
+
cancelledAt: Date.now()
|
|
239
|
+
});
|
|
240
|
+
req.payload.logger.info(`Image optimizer: cancellation requested for '${collectionSlug}'`);
|
|
241
|
+
return Response.json({
|
|
242
|
+
cancelled: true,
|
|
243
|
+
collectionSlug
|
|
168
244
|
});
|
|
169
245
|
};
|
|
170
246
|
return handler;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/regenerate.ts"],"sourcesContent":["import type { PayloadHandler } from 'payload'\nimport type { CollectionSlug, Where } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { waitUntil } from '../utilities/waitUntil.js'\n\nexport const createRegenerateHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n const handler: PayloadHandler = async (req) => {\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: { collectionSlug?: string; force?: boolean; docIds?: string[] }\n try {\n body = await req.json!()\n } catch {\n body = {}\n }\n\n const collectionSlug = body.collectionSlug\n if (!collectionSlug || !resolvedConfig.collections[collectionSlug as CollectionSlug]) {\n return Response.json(\n { error: 'Invalid or unconfigured collection slug' },\n { status: 400 },\n )\n }\n\n let queued = 0\n\n if (body.docIds && body.docIds.length > 0) {\n // Regenerate specific documents by ID\n for (const docId of body.docIds) {\n await req.payload.jobs.queue({\n task: 'imageOptimizer_regenerateDocument',\n input: {\n collectionSlug,\n docId: String(docId),\n },\n })\n queued++\n }\n } else {\n // Find all image documents in the collection\n // Unless force=true, skip already-processed docs\n const where: Where = body.force\n ? { mimeType: { contains: 'image/' } }\n : {\n and: [\n { mimeType: { contains: 'image/' } },\n {\n or: [\n { 'imageOptimizer.status': { not_equals: 'complete' } },\n { 'imageOptimizer.status': { exists: false } },\n ],\n },\n ],\n }\n\n let page = 1\n let hasMore = true\n\n while (hasMore) {\n const result = await req.payload.find({\n collection: collectionSlug as CollectionSlug,\n limit: 50,\n page,\n depth: 0,\n where,\n sort: 'createdAt',\n })\n\n for (const doc of result.docs) {\n await req.payload.jobs.queue({\n task: 'imageOptimizer_regenerateDocument',\n input: {\n collectionSlug,\n docId: String(doc.id),\n },\n })\n queued++\n }\n\n hasMore = result.hasNextPage\n page++\n }\n }\n\n req.payload.logger.info(`Image optimizer: queued ${queued} images from '${collectionSlug}' for regeneration`)\n\n // Fire the job runner — use waitUntil to keep the serverless function alive\n // after the response is sent, so jobs actually complete on Vercel/serverless.\n if (queued > 0) {\n const runPromise = req.payload.jobs.run({ limit: queued, sequential: true }).catch((err: unknown) => {\n req.payload.logger.error({ err }, 'Regeneration job runner failed')\n })\n waitUntil(runPromise, req)\n }\n\n return Response.json({ queued, collectionSlug })\n }\n\n return handler\n}\n\nexport const createRegenerateStatusHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n const handler: PayloadHandler = async (req) => {\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url!)\n const collectionSlug = url.searchParams.get('collection')\n\n if (!collectionSlug || !resolvedConfig.collections[collectionSlug as CollectionSlug]) {\n return Response.json({ error: 'Invalid collection slug' }, { status: 400 })\n }\n\n const total = await req.payload.count({\n collection: collectionSlug as CollectionSlug,\n where: { mimeType: { contains: 'image/' } },\n })\n\n const complete = await req.payload.count({\n collection: collectionSlug as CollectionSlug,\n where: {\n mimeType: { contains: 'image/' },\n 'imageOptimizer.status': { equals: 'complete' },\n },\n })\n\n const errored = await req.payload.count({\n collection: collectionSlug as CollectionSlug,\n where: {\n mimeType: { contains: 'image/' },\n 'imageOptimizer.status': { equals: 'error' },\n },\n })\n\n return Response.json({\n collectionSlug,\n total: total.totalDocs,\n complete: complete.totalDocs,\n errored: errored.totalDocs,\n pending: total.totalDocs - complete.totalDocs - errored.totalDocs,\n })\n }\n\n return handler\n}\n"],"names":["waitUntil","createRegenerateHandler","resolvedConfig","handler","req","user","Response","json","error","status","body","collectionSlug","collections","queued","docIds","length","docId","payload","jobs","queue","task","input","String","where","force","mimeType","contains","and","or","not_equals","exists","page","hasMore","result","find","collection","limit","depth","sort","doc","docs","id","hasNextPage","logger","info","runPromise","run","sequential","catch","err","createRegenerateStatusHandler","url","URL","searchParams","get","total","count","complete","equals","errored","totalDocs","pending"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/regenerate.ts"],"sourcesContent":["import type { PayloadHandler } from 'payload'\nimport type { CollectionSlug, Where } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { waitUntil } from '../utilities/waitUntil.js'\n\ntype CollectionState = { startedAt?: number; cancelledAt?: number; queued?: number }\ntype StateCollections = Record<string, CollectionState>\n\nconst GLOBAL_SLUG = 'image-optimizer-state'\n\nasync function getCollectionState(payload: any, slug: string): Promise<CollectionState> {\n try {\n const state = await payload.findGlobal({ slug: GLOBAL_SLUG })\n return (state?.collections as StateCollections)?.[slug] || {}\n } catch {\n return {}\n }\n}\n\nasync function setCollectionState(payload: any, slug: string, update: Partial<CollectionState>): Promise<void> {\n let existing: StateCollections = {}\n try {\n const state = await payload.findGlobal({ slug: GLOBAL_SLUG })\n existing = (state?.collections as StateCollections) || {}\n } catch {\n // Global may not exist yet\n }\n existing[slug] = { ...existing[slug], ...update }\n await payload.updateGlobal({ slug: GLOBAL_SLUG, data: { collections: existing } })\n}\n\nexport const createRegenerateHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n const handler: PayloadHandler = async (req) => {\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: { collectionSlug?: string; force?: boolean; docIds?: string[] }\n try {\n body = await req.json!()\n } catch {\n body = {}\n }\n\n const collectionSlug = body.collectionSlug\n if (!collectionSlug || !resolvedConfig.collections[collectionSlug as CollectionSlug]) {\n return Response.json(\n { error: 'Invalid or unconfigured collection slug' },\n { status: 400 },\n )\n }\n\n let queued = 0\n\n if (body.docIds && body.docIds.length > 0) {\n // Regenerate specific documents by ID\n for (const docId of body.docIds) {\n await req.payload.jobs.queue({\n task: 'imageOptimizer_regenerateDocument',\n input: {\n collectionSlug,\n docId: String(docId),\n },\n })\n queued++\n }\n } else {\n // Find all image documents in the collection\n // Unless force=true, skip already-processed docs\n const where: Where = body.force\n ? { mimeType: { contains: 'image/' } }\n : {\n and: [\n { mimeType: { contains: 'image/' } },\n {\n or: [\n { 'imageOptimizer.status': { not_equals: 'complete' } },\n { 'imageOptimizer.status': { exists: false } },\n ],\n },\n ],\n }\n\n let page = 1\n let hasMore = true\n\n while (hasMore) {\n const result = await req.payload.find({\n collection: collectionSlug as CollectionSlug,\n limit: 50,\n page,\n depth: 0,\n where,\n sort: 'createdAt',\n })\n\n for (const doc of result.docs) {\n await req.payload.jobs.queue({\n task: 'imageOptimizer_regenerateDocument',\n input: {\n collectionSlug,\n docId: String(doc.id),\n },\n })\n queued++\n }\n\n hasMore = result.hasNextPage\n page++\n }\n }\n\n req.payload.logger.info(`Image optimizer: queued ${queued} images from '${collectionSlug}' for regeneration`)\n\n // Clear any previous cancellation and record the start time + batch size\n await setCollectionState(req.payload, collectionSlug, {\n startedAt: Date.now(),\n cancelledAt: undefined,\n queued,\n })\n\n // Fire the job runner — use waitUntil to keep the serverless function alive\n // after the response is sent, so jobs actually complete on Vercel/serverless.\n if (queued > 0) {\n const runPromise = req.payload.jobs.run({ limit: queued, sequential: true }).catch((err: unknown) => {\n req.payload.logger.error({ err }, 'Regeneration job runner failed')\n })\n waitUntil(runPromise, req)\n }\n\n return Response.json({ queued, collectionSlug })\n }\n\n return handler\n}\n\nexport const createRegenerateStatusHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n const handler: PayloadHandler = async (req) => {\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const url = new URL(req.url!)\n const collectionSlug = url.searchParams.get('collection')\n\n if (!collectionSlug || !resolvedConfig.collections[collectionSlug as CollectionSlug]) {\n return Response.json({ error: 'Invalid collection slug' }, { status: 400 })\n }\n\n const total = await req.payload.count({\n collection: collectionSlug as CollectionSlug,\n where: { mimeType: { contains: 'image/' } },\n })\n\n const complete = await req.payload.count({\n collection: collectionSlug as CollectionSlug,\n where: {\n mimeType: { contains: 'image/' },\n 'imageOptimizer.status': { equals: 'complete' },\n },\n })\n\n const errored = await req.payload.count({\n collection: collectionSlug as CollectionSlug,\n where: {\n mimeType: { contains: 'image/' },\n 'imageOptimizer.status': { equals: 'error' },\n },\n })\n\n // Include cancellation state so the UI can react\n const collState = await getCollectionState(req.payload, collectionSlug)\n const cancelled = !!(collState.cancelledAt && collState.startedAt && collState.cancelledAt > collState.startedAt)\n\n return Response.json({\n collectionSlug,\n total: total.totalDocs,\n complete: complete.totalDocs,\n errored: errored.totalDocs,\n pending: total.totalDocs - complete.totalDocs - errored.totalDocs,\n cancelled,\n })\n }\n\n return handler\n}\n\nexport const createCancelHandler = (resolvedConfig: ResolvedImageOptimizerConfig) => {\n const handler: PayloadHandler = async (req) => {\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let body: { collectionSlug?: string }\n try {\n body = await req.json!()\n } catch {\n body = {}\n }\n\n const collectionSlug = body.collectionSlug\n if (!collectionSlug || !resolvedConfig.collections[collectionSlug as CollectionSlug]) {\n return Response.json({ error: 'Invalid or unconfigured collection slug' }, { status: 400 })\n }\n\n await setCollectionState(req.payload, collectionSlug, {\n cancelledAt: Date.now(),\n })\n\n req.payload.logger.info(`Image optimizer: cancellation requested for '${collectionSlug}'`)\n\n return Response.json({ cancelled: true, collectionSlug })\n }\n\n return handler\n}\n"],"names":["waitUntil","GLOBAL_SLUG","getCollectionState","payload","slug","state","findGlobal","collections","setCollectionState","update","existing","updateGlobal","data","createRegenerateHandler","resolvedConfig","handler","req","user","Response","json","error","status","body","collectionSlug","queued","docIds","length","docId","jobs","queue","task","input","String","where","force","mimeType","contains","and","or","not_equals","exists","page","hasMore","result","find","collection","limit","depth","sort","doc","docs","id","hasNextPage","logger","info","startedAt","Date","now","cancelledAt","undefined","runPromise","run","sequential","catch","err","createRegenerateStatusHandler","url","URL","searchParams","get","total","count","complete","equals","errored","collState","cancelled","totalDocs","pending","createCancelHandler"],"mappings":"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"}
|
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import crypto from 'crypto';
|
|
2
1
|
import path from 'path';
|
|
3
2
|
import { resolveCollectionConfig } from '../defaults.js';
|
|
4
3
|
import { convertFormat, generateThumbHash, stripAndResize } from '../processing/index.js';
|
|
5
4
|
import { isCloudStorage } from '../utilities/storage.js';
|
|
6
5
|
export const createBeforeChangeHook = (resolvedConfig, collectionSlug)=>{
|
|
7
|
-
return async ({ context, data, req })=>{
|
|
6
|
+
return async ({ context, data, originalDoc, req })=>{
|
|
8
7
|
if (context?.imageOptimizer_skip) return data;
|
|
9
8
|
if (!req.file || !req.file.data || !req.file.mimetype?.startsWith('image/')) return data;
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
if (resolvedConfig.
|
|
9
|
+
// Apply custom filename strategy (seoFilename, uuidFilename, or user-provided).
|
|
10
|
+
// The callback returns a stem (no extension) — we append the original extension here,
|
|
11
|
+
// and replaceOriginal may swap it to the target format extension later.
|
|
12
|
+
if (resolvedConfig.generateFilename) {
|
|
13
|
+
const existingFilename = originalDoc?.filename;
|
|
14
14
|
const ext = path.extname(req.file.name);
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
const stem = resolvedConfig.generateFilename({
|
|
16
|
+
altText: data.alt,
|
|
17
|
+
originalFilename: req.file.name,
|
|
18
|
+
mimeType: req.file.mimetype,
|
|
19
|
+
collectionSlug,
|
|
20
|
+
existingFilename
|
|
21
|
+
});
|
|
22
|
+
const newFilename = `${stem}${ext}`;
|
|
23
|
+
req.file.name = newFilename;
|
|
24
|
+
data.filename = newFilename;
|
|
18
25
|
}
|
|
19
26
|
const originalSize = req.file.data.length;
|
|
20
27
|
const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug);
|