@inoo-ch/payload-image-optimizer 1.3.7 → 1.3.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inoo-ch/payload-image-optimizer",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
4
4
  "description": "Payload CMS plugin for automatic image optimization — WebP/AVIF conversion, resize, EXIF strip, ThumbHash placeholders, and bulk regeneration",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -20,6 +20,7 @@ export const RegenerationButton: React.FC = () => {
20
20
  const [stalled, setStalled] = useState(false)
21
21
  const [collectionSlug, setCollectionSlug] = useState<string | null>(null)
22
22
  const [stats, setStats] = useState<RegenerationProgress | null>(null)
23
+ const [confirming, setConfirming] = useState(false)
23
24
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
24
25
  const stallRef = useRef({ lastProcessed: 0, stallCount: 0 })
25
26
  const prevIsRunningRef = useRef(false)
@@ -129,9 +130,24 @@ export const RegenerationButton: React.FC = () => {
129
130
  prevIsRunningRef.current = isRunning
130
131
  }, [isRunning, fetchStats])
131
132
 
132
- const handleRegenerate = async () => {
133
+ // Phase 1: Show confirmation with counts
134
+ const handlePreflight = async () => {
133
135
  if (!collectionSlug) return
134
136
  setError(null)
137
+ // Refresh stats to get the latest counts before confirming
138
+ await fetchStats()
139
+ setConfirming(true)
140
+ }
141
+
142
+ const handleCancel = () => {
143
+ setConfirming(false)
144
+ }
145
+
146
+ // Phase 2: Actually start regeneration (after user confirms)
147
+ const handleConfirm = async () => {
148
+ if (!collectionSlug) return
149
+ setConfirming(false)
150
+ setError(null)
135
151
  setStalled(false)
136
152
  setIsRunning(true)
137
153
  setQueued(null)
@@ -200,40 +216,90 @@ export const RegenerationButton: React.FC = () => {
200
216
  flexWrap: 'wrap',
201
217
  }}
202
218
  >
203
- <button
204
- onClick={handleRegenerate}
205
- disabled={isRunning}
206
- style={{
207
- backgroundColor: isRunning ? '#9ca3af' : '#4f46e5',
208
- color: '#fff',
209
- border: 'none',
210
- borderRadius: '6px',
211
- padding: '8px 16px',
212
- fontSize: '14px',
213
- fontWeight: 500,
214
- cursor: isRunning ? 'not-allowed' : 'pointer',
215
- }}
216
- >
217
- {isRunning ? 'Regenerating...' : 'Regenerate Images'}
218
- </button>
219
-
220
- <label
221
- style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '13px' }}
222
- >
223
- <input
224
- type="checkbox"
225
- checked={force}
226
- onChange={(e) => setForce(e.target.checked)}
219
+ {!confirming && (
220
+ <button
221
+ onClick={handlePreflight}
227
222
  disabled={isRunning}
228
- />
229
- Force re-process all
230
- </label>
223
+ style={{
224
+ backgroundColor: isRunning ? '#9ca3af' : '#4f46e5',
225
+ color: '#fff',
226
+ border: 'none',
227
+ borderRadius: '6px',
228
+ padding: '8px 16px',
229
+ fontSize: '14px',
230
+ fontWeight: 500,
231
+ cursor: isRunning ? 'not-allowed' : 'pointer',
232
+ }}
233
+ >
234
+ {isRunning ? 'Processing all images...' : 'Regenerate All Images'}
235
+ </button>
236
+ )}
237
+
238
+ {confirming && stats && (
239
+ <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
240
+ <span style={{ fontSize: '13px', color: '#374151' }}>
241
+ {force
242
+ ? `Re-process all ${stats.total} images across the entire collection?`
243
+ : `Regenerate ${stats.pending} unoptimized image${stats.pending !== 1 ? 's' : ''} across the entire collection?`}
244
+ </span>
245
+ <button
246
+ onClick={handleConfirm}
247
+ style={{
248
+ backgroundColor: '#4f46e5',
249
+ color: '#fff',
250
+ border: 'none',
251
+ borderRadius: '6px',
252
+ padding: '6px 14px',
253
+ fontSize: '13px',
254
+ fontWeight: 500,
255
+ cursor: 'pointer',
256
+ }}
257
+ >
258
+ Confirm
259
+ </button>
260
+ <button
261
+ onClick={handleCancel}
262
+ style={{
263
+ backgroundColor: 'transparent',
264
+ color: '#6b7280',
265
+ border: '1px solid #d1d5db',
266
+ borderRadius: '6px',
267
+ padding: '6px 14px',
268
+ fontSize: '13px',
269
+ fontWeight: 500,
270
+ cursor: 'pointer',
271
+ }}
272
+ >
273
+ Cancel
274
+ </button>
275
+ </div>
276
+ )}
277
+
278
+ {!confirming && (
279
+ <label
280
+ style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '13px' }}
281
+ >
282
+ <input
283
+ type="checkbox"
284
+ checked={force}
285
+ onChange={(e) => setForce(e.target.checked)}
286
+ disabled={isRunning}
287
+ />
288
+ Force re-process all
289
+ </label>
290
+ )}
231
291
 
232
292
  {error && (
233
293
  <span style={{ color: '#ef4444', fontSize: '13px' }}>{error}</span>
234
294
  )}
235
295
 
236
- {queued === 0 && !isRunning && !stalled && (
296
+ {queued !== null && queued > 0 && isRunning && !confirming && (
297
+ <span style={{ color: '#4f46e5', fontSize: '13px' }}>
298
+ Queued {queued} image{queued !== 1 ? 's' : ''} for processing across the entire collection
299
+ </span>
300
+ )}
301
+
302
+ {queued === 0 && !isRunning && !stalled && !confirming && (
237
303
  <span style={{ color: '#10b981', fontSize: '13px' }}>
238
304
  All images already optimized.
239
305
  </span>
@@ -297,10 +363,10 @@ export const RegenerationButton: React.FC = () => {
297
363
  </div>
298
364
  )}
299
365
 
300
- {!isRunning && progress && progress.complete > 0 && queued !== 0 && (
366
+ {!isRunning && progress && progress.complete > 0 && queued !== 0 && !confirming && (
301
367
  <span style={{ fontSize: '13px' }}>
302
368
  <span style={{ color: progress.errored > 0 || stalled ? '#f59e0b' : '#10b981' }}>
303
- Done! {progress.complete}/{progress.total} optimized.
369
+ Done! {progress.complete}/{progress.total} optimized (across entire collection).
304
370
  </span>
305
371
  {(progress.errored > 0 || (stalled && progress.pending > 0)) && (
306
372
  <span style={{ color: '#ef4444' }}>
@@ -1,5 +1,5 @@
1
1
  import type { PayloadHandler } from 'payload'
2
- import type { CollectionSlug } from 'payload'
2
+ import type { CollectionSlug, Where } from 'payload'
3
3
 
4
4
  import type { ResolvedImageOptimizerConfig } from '../types.js'
5
5
 
@@ -25,16 +25,20 @@ export const createRegenerateHandler = (resolvedConfig: ResolvedImageOptimizerCo
25
25
  }
26
26
 
27
27
  // Find all image documents in the collection
28
- const where: any = {
29
- mimeType: { contains: 'image/' },
30
- }
31
28
  // Unless force=true, skip already-processed docs
32
- if (!body.force) {
33
- where.or = [
34
- { 'imageOptimizer.status': { not_equals: 'complete' } },
35
- { 'imageOptimizer.status': { exists: false } },
36
- ]
37
- }
29
+ const where: Where = body.force
30
+ ? { mimeType: { contains: 'image/' } }
31
+ : {
32
+ and: [
33
+ { mimeType: { contains: 'image/' } },
34
+ {
35
+ or: [
36
+ { 'imageOptimizer.status': { not_equals: 'complete' } },
37
+ { 'imageOptimizer.status': { exists: false } },
38
+ ],
39
+ },
40
+ ],
41
+ }
38
42
 
39
43
  let queued = 0
40
44
  let page = 1
@@ -65,9 +69,11 @@ export const createRegenerateHandler = (resolvedConfig: ResolvedImageOptimizerCo
65
69
  page++
66
70
  }
67
71
 
72
+ req.payload.logger.info(`Image optimizer: queued ${queued} images from '${collectionSlug}' for regeneration`)
73
+
68
74
  // Fire the job runner (non-blocking)
69
75
  if (queued > 0) {
70
- req.payload.jobs.run().catch((err: unknown) => {
76
+ req.payload.jobs.run({ limit: queued }).catch((err: unknown) => {
71
77
  req.payload.logger.error({ err }, 'Regeneration job runner failed')
72
78
  })
73
79
  }