@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.
Files changed (48) hide show
  1. package/AGENT_DOCS.md +8 -9
  2. package/README.md +10 -9
  3. package/dist/components/RegenerationButton.js +85 -21
  4. package/dist/components/RegenerationButton.js.map +1 -1
  5. package/dist/defaults.js +14 -2
  6. package/dist/defaults.js.map +1 -1
  7. package/dist/endpoints/regenerate.d.ts +1 -0
  8. package/dist/endpoints/regenerate.js +77 -1
  9. package/dist/endpoints/regenerate.js.map +1 -1
  10. package/dist/hooks/afterChange.js +3 -0
  11. package/dist/hooks/afterChange.js.map +1 -1
  12. package/dist/hooks/beforeChange.js +48 -29
  13. package/dist/hooks/beforeChange.js.map +1 -1
  14. package/dist/index.d.ts +2 -1
  15. package/dist/index.js +32 -5
  16. package/dist/index.js.map +1 -1
  17. package/dist/processing/index.d.ts +21 -0
  18. package/dist/processing/index.js +29 -0
  19. package/dist/processing/index.js.map +1 -1
  20. package/dist/tasks/convertFormats.js +11 -4
  21. package/dist/tasks/convertFormats.js.map +1 -1
  22. package/dist/tasks/regenerateDocument.js +27 -4
  23. package/dist/tasks/regenerateDocument.js.map +1 -1
  24. package/dist/types.d.ts +49 -2
  25. package/dist/types.js.map +1 -1
  26. package/dist/utilities/filenameStrategies.d.ts +25 -0
  27. package/dist/utilities/filenameStrategies.js +46 -0
  28. package/dist/utilities/filenameStrategies.js.map +1 -0
  29. package/dist/utilities/stripDiacritics.d.ts +9 -0
  30. package/dist/utilities/stripDiacritics.js +10 -0
  31. package/dist/utilities/stripDiacritics.js.map +1 -0
  32. package/dist/utilities/toKebabCase.d.ts +10 -0
  33. package/dist/utilities/toKebabCase.js +11 -0
  34. package/dist/utilities/toKebabCase.js.map +1 -0
  35. package/package.json +1 -1
  36. package/src/components/RegenerationButton.tsx +92 -24
  37. package/src/defaults.ts +15 -1
  38. package/src/endpoints/regenerate.ts +68 -0
  39. package/src/hooks/afterChange.ts +4 -0
  40. package/src/hooks/beforeChange.ts +53 -35
  41. package/src/index.ts +27 -6
  42. package/src/processing/index.ts +39 -0
  43. package/src/tasks/convertFormats.ts +24 -16
  44. package/src/tasks/regenerateDocument.ts +24 -4
  45. package/src/types.ts +51 -2
  46. package/src/utilities/filenameStrategies.ts +61 -0
  47. package/src/utilities/stripDiacritics.ts +10 -0
  48. 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: true`: renames file to UUID (e.g., `photo.jpg` → `a1b2c3d4.jpg`)
103
- - Auto-rotates based on EXIF orientation
104
- - Resizes to fit within `maxDimensions`
105
- - Strips metadata (if enabled)
106
- - If `replaceOriginal: true`: converts to primary format (first in `formats` array), updates filename/mimeType
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
- - Updates document: `imageOptimizer.status = 'complete'`, populates `variants` array
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** — The `beforeChange` hook strips metadata, resizes the image, and generates a ThumbHash
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 WebP/AVIF variants asynchronously
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
- All format conversion runs as async background jobs, so uploads return immediately.
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, ThumbHash generation, 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:
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 **does not double-process your images** — it intercepts the raw upload in a `beforeChange` hook *before* Payload's own sharp pipeline runs, and writes the optimized buffer back to `req.file.data`. When Payload's built-in `uploadFiles` step kicks in to generate your configured sizes, it works from the already-optimized file, not the raw original.
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
- const progressPercent = progress && progress.total > 0 ? Math.round((progress.complete + progress.errored) / progress.total * 100) : 0;
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: isRunning ? '#9ca3af' : '#4f46e5',
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: isRunning ? 'not-allowed' : 'pointer'
298
+ cursor: 'pointer'
242
299
  },
243
- children: isRunning ? 'Processing images...' : hasSelection ? `Regenerate ${selectionCount} Selected` : 'Regenerate All Images'
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
- progress.complete,
426
+ Math.max(batchProcessed, 0),
363
427
  " / ",
364
- progress.total,
428
+ batchTotal,
365
429
  " complete"
366
430
  ]
367
431
  }),
368
- progress.errored > 0 && /*#__PURE__*/ _jsxs("span", {
432
+ batchErrored > 0 && /*#__PURE__*/ _jsxs("span", {
369
433
  style: {
370
434
  color: '#ef4444'
371
435
  },
372
436
  children: [
373
- progress.errored,
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: `${progress.total > 0 ? Math.round(progress.complete / progress.total * 100) : 0}%`,
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
- progress.errored > 0 && /*#__PURE__*/ _jsx("div", {
466
+ batchErrored > 0 && /*#__PURE__*/ _jsx("div", {
403
467
  style: {
404
468
  height: '100%',
405
- width: `${progress.total > 0 ? Math.round(progress.errored / progress.total * 100) : 0}%`,
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 && progress && progress.complete > 0 && queued !== 0 && !confirming && /*#__PURE__*/ _jsxs("span", {
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: progress.errored > 0 ? '#f59e0b' : '#10b981'
485
+ color: batchErrored > 0 ? '#f59e0b' : '#10b981'
422
486
  },
423
487
  children: [
424
488
  "Done! ",
425
- progress.complete,
489
+ Math.max(batchProcessed - batchErrored, 0),
426
490
  "/",
427
- progress.total,
428
- " optimized (across entire collection)."
491
+ batchTotal,
492
+ " optimized."
429
493
  ]
430
494
  }),
431
- progress.errored > 0 && /*#__PURE__*/ _jsxs("span", {
495
+ batchErrored > 0 && /*#__PURE__*/ _jsxs("span", {
432
496
  style: {
433
497
  color: '#ef4444'
434
498
  },
435
499
  children: [
436
500
  ' ',
437
- progress.errored,
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 &#10003; 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' }}>&middot;</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 &#10003; 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' }}>&middot;</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];
@@ -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 replaceOriginal: config.replaceOriginal ?? true,\n stripMetadata: config.stripMetadata ?? true,\n uniqueFileNames: config.uniqueFileNames ?? false,\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":["resolveConfig","config","clientOptimization","collections","disabled","formats","format","quality","generateThumbHash","maxDimensions","width","height","replaceOriginal","stripMetadata","uniqueFileNames","resolveCollectionConfig","resolvedConfig","collectionSlug","collectionValue"],"mappings":"AAIA,OAAO,MAAMA,gBAAgB,CAACC,SAAgE,CAAA;QAC5FC,oBAAoBD,OAAOC,kBAAkB,IAAI;QACjDC,aAAaF,OAAOE,WAAW;QAC/BC,UAAUH,OAAOG,QAAQ,IAAI;QAC7BC,SAASJ,OAAOI,OAAO,IAAI;YACzB;gBAAEC,QAAQ;gBAAQC,SAAS;YAAG;SAC/B;QACDC,mBAAmBP,OAAOO,iBAAiB,IAAI;QAC/CC,eAAeR,OAAOQ,aAAa,IAAI;YAAEC,OAAO;YAAMC,QAAQ;QAAK;QACnEC,iBAAiBX,OAAOW,eAAe,IAAI;QAC3CC,eAAeZ,OAAOY,aAAa,IAAI;QACvCC,iBAAiBb,OAAOa,eAAe,IAAI;IAC7C,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;YAC3CG,iBAAiBI,eAAeJ,eAAe;QACjD;IACF;IAEA,OAAO;QACLP,SAASa,gBAAgBb,OAAO,IAAIW,eAAeX,OAAO;QAC1DI,eAAeS,gBAAgBT,aAAa,IAAIO,eAAeP,aAAa;QAC5EG,iBAAiBM,gBAAgBN,eAAe,IAAII,eAAeJ,eAAe;IACpF;AACF,EAAC"}
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;