@inoo-ch/payload-image-optimizer 1.8.1 → 1.10.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/AGENT_DOCS.md +8 -9
- package/README.md +10 -9
- 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/afterChange.js +3 -0
- package/dist/hooks/afterChange.js.map +1 -1
- package/dist/hooks/beforeChange.js +48 -29
- 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/processing/index.d.ts +21 -0
- package/dist/processing/index.js +29 -0
- package/dist/processing/index.js.map +1 -1
- package/dist/tasks/convertFormats.js +11 -4
- package/dist/tasks/convertFormats.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/afterChange.ts +4 -0
- package/src/hooks/beforeChange.ts +53 -35
- package/src/index.ts +27 -6
- package/src/processing/index.ts +39 -0
- package/src/tasks/convertFormats.ts +24 -16
- 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
package/AGENT_DOCS.md
CHANGED
|
@@ -98,14 +98,12 @@ collections: {
|
|
|
98
98
|
|
|
99
99
|
When an image is uploaded to an optimized collection:
|
|
100
100
|
|
|
101
|
-
1. **`beforeChange` hook** (in-memory processing):
|
|
102
|
-
- If `uniqueFileNames
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
- Generates ThumbHash (if enabled)
|
|
108
|
-
- Sets `imageOptimizer.status = 'pending'`
|
|
101
|
+
1. **`beforeChange` hook** (single-pass in-memory processing):
|
|
102
|
+
- If `generateFilename` / `uniqueFileNames`: renames file (e.g., `photo.jpg` → `a1b2c3d4.jpg`)
|
|
103
|
+
- Single sharp pipeline: resizes to `maxDimensions`, strips metadata, and optionally converts to primary format — all in one decode/encode cycle
|
|
104
|
+
- Skips redundant `.rotate()` — Payload's `generateFileData()` already auto-rotated before hooks run
|
|
105
|
+
- If no async job is needed: generates ThumbHash synchronously (included in initial DB write)
|
|
106
|
+
- Sets `imageOptimizer.status` to `'pending'` (async job) or `'complete'` (no job needed)
|
|
109
107
|
|
|
110
108
|
2. **`afterChange` hook** (disk + async):
|
|
111
109
|
- Writes processed buffer to disk (overwriting Payload's original)
|
|
@@ -115,7 +113,8 @@ When an image is uploaded to an optimized collection:
|
|
|
115
113
|
3. **Background job** (`imageOptimizer_convertFormats`):
|
|
116
114
|
- Generates variant files for any additional formats (e.g., AVIF)
|
|
117
115
|
- Writes variants to disk with `-optimized` suffix
|
|
118
|
-
-
|
|
116
|
+
- Generates ThumbHash (deferred from the sync save path to avoid blocking uploads)
|
|
117
|
+
- Updates document: `imageOptimizer.status = 'complete'`, populates `variants` array and `thumbHash`
|
|
119
118
|
|
|
120
119
|
### File Naming
|
|
121
120
|
|
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ imageOptimizer({
|
|
|
83
83
|
// Global defaults (overridden by per-collection config)
|
|
84
84
|
formats: [
|
|
85
85
|
{ format: 'webp', quality: 80 },
|
|
86
|
-
{ format: 'avif', quality: 65 },
|
|
86
|
+
// { format: 'avif', quality: 65 }, // opt-in — AVIF is ~5-10x slower to encode than WebP
|
|
87
87
|
],
|
|
88
88
|
maxDimensions: { width: 2560, height: 2560 },
|
|
89
89
|
generateThumbHash: true,
|
|
@@ -152,16 +152,16 @@ imageOptimizer({
|
|
|
152
152
|
## How It Works
|
|
153
153
|
|
|
154
154
|
1. **Upload** — An image is uploaded to a configured collection
|
|
155
|
-
2. **Pre-process** —
|
|
155
|
+
2. **Pre-process** — A single-pass sharp pipeline strips metadata, resizes, and optionally converts format — all in one operation
|
|
156
156
|
3. **Save** — Payload writes the optimized image to disk
|
|
157
|
-
4. **Convert** — A background job converts the image to
|
|
158
|
-
5. **Done** — The document is updated with variant URLs, file sizes, and optimization status
|
|
157
|
+
4. **Convert** — A background job converts the image to additional format variants (e.g. AVIF) and generates the ThumbHash asynchronously
|
|
158
|
+
5. **Done** — The document is updated with variant URLs, file sizes, ThumbHash, and optimization status
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
Format conversion and ThumbHash generation run as async background jobs, so uploads return immediately.
|
|
161
161
|
|
|
162
162
|
### Vercel / Serverless Deployment
|
|
163
163
|
|
|
164
|
-
Image processing (especially AVIF encoding
|
|
164
|
+
Image processing (especially AVIF encoding and metadata stripping) can exceed the default serverless function timeout. The plugin exports a recommended `maxDuration` that you can re-export from your Payload API route:
|
|
165
165
|
|
|
166
166
|
```ts
|
|
167
167
|
// src/app/(payload)/api/[...slug]/route.ts
|
|
@@ -212,7 +212,7 @@ vercelBlobStorage({
|
|
|
212
212
|
|
|
213
213
|
## How It Differs from Payload's Default Image Handling
|
|
214
214
|
|
|
215
|
-
Payload CMS ships with [sharp](https://sharp.pixelplumbing.com/) built-in and can resize images and generate sizes on upload. This plugin
|
|
215
|
+
Payload CMS ships with [sharp](https://sharp.pixelplumbing.com/) built-in and can resize images and generate sizes on upload. This plugin optimizes the uploaded image in a `beforeChange` hook and writes the result back to `req.file.data`. Payload's `generateFileData` runs before hooks and handles `imageSizes` generation using `Promise.all`, so the plugin focuses on what Payload doesn't do natively: format conversion (WebP/AVIF), metadata stripping, and ThumbHash generation. Using `clientOptimization: true` (the default) is the most effective way to speed up uploads with many `imageSizes`, since it reduces the source image before Payload processes it.
|
|
216
216
|
|
|
217
217
|
### Comparison
|
|
218
218
|
|
|
@@ -229,9 +229,10 @@ Payload CMS ships with [sharp](https://sharp.pixelplumbing.com/) built-in and ca
|
|
|
229
229
|
|
|
230
230
|
### CPU & Resource Impact
|
|
231
231
|
|
|
232
|
+
- **Single-pass pipeline** — Metadata stripping, resizing, and format conversion run in a single sharp pipeline (one decode/encode cycle), minimizing processing overhead.
|
|
233
|
+
- **Deferred ThumbHash** — ThumbHash generation runs in the background (via the format conversion job or `waitUntil`) rather than blocking the upload response.
|
|
232
234
|
- **Single-format mode** (e.g. WebP only with `replaceOriginal: true`) adds virtually zero overhead compared to Payload's default sharp processing — the plugin replaces the sharp pass rather than adding a second one.
|
|
233
|
-
- **Additional format variants** (e.g. both WebP and AVIF) run as background jobs after upload — this is the one area where you'll see extra CPU usage vs vanilla Payload.
|
|
234
|
-
- **ThumbHash generation** processes a 100×100px thumbnail — negligible impact.
|
|
235
|
+
- **Additional format variants** (e.g. both WebP and AVIF) run as background jobs after upload — this is the one area where you'll see extra CPU usage vs vanilla Payload. Note that AVIF encoding is ~5-10x slower than WebP.
|
|
235
236
|
- **Bulk regeneration** processes images sequentially, not all at once, so it won't spike your server.
|
|
236
237
|
|
|
237
238
|
If you're on a resource-constrained server, use single-format mode and you'll be at roughly the same CPU cost as stock Payload.
|
|
@@ -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":"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"}
|
|
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;
|