@inoo-ch/payload-image-optimizer 1.3.6 → 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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/afterChange.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\nimport type { CollectionAfterChangeHook } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { resolveStaticDir } from '../utilities/resolveStaticDir.js'\nimport { isCloudStorage } from '../utilities/storage.js'\n\nexport const createAfterChangeHook = (\n resolvedConfig: ResolvedImageOptimizerConfig,\n collectionSlug: string,\n): CollectionAfterChangeHook => {\n return async ({ context, doc, req }) => {\n if (context?.imageOptimizer_skip) return doc\n\n // Use context flag from beforeChange instead of checking req.file.data directly.\n // Cloud storage adapters may consume req.file.data in their own afterChange hook\n // before ours runs, which would cause this guard to bail out and leave status as 'pending'.\n if (!context?.imageOptimizer_hasUpload) return doc\n\n const collectionConfig = req.payload.collections[collectionSlug as keyof typeof req.payload.collections].config\n const cloudStorage = isCloudStorage(collectionConfig)\n\n // When using local storage, overwrite the file on disk with the processed buffer.\n // Payload's uploadFiles step writes the original buffer; we replace it here.\n // When using cloud storage, skip — the cloud adapter's afterChange hook already\n // uploads the correct buffer from req.file.data (set in our beforeChange hook).\n if (!cloudStorage) {\n const staticDir = resolveStaticDir(collectionConfig)\n const processedBuffer = context.imageOptimizer_processedBuffer as Buffer | undefined\n if (processedBuffer && doc.filename && staticDir) {\n const safeFilename = path.basename(doc.filename as string)\n const filePath = path.join(staticDir, safeFilename)\n await fs.writeFile(filePath, processedBuffer)\n\n // If replaceOriginal changed the filename, clean up the old file Payload wrote\n const originalFilename = context.imageOptimizer_originalFilename as string | undefined\n if (originalFilename && originalFilename !== safeFilename) {\n const oldFilePath = path.join(staticDir, path.basename(originalFilename))\n await fs.unlink(oldFilePath).catch(() => {\n // Old file may not exist if Payload used the new filename\n })\n }\n }\n }\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug)\n\n // When replaceOriginal is on and only one format is configured, the main file\n // is already converted — skip the async job and mark complete immediately.\n if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length <= 1) {\n await req.payload.update({\n collection: collectionSlug,\n id: doc.id,\n data: {\n imageOptimizer: {\n ...doc.imageOptimizer,\n status: 'complete',\n variants: [],\n error: null,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n return doc\n }\n\n // With cloud storage, variant files cannot be written — skip the async job\n // and mark complete. CDN-level image optimization (e.g. Next.js Image) can\n // serve alternative formats on the fly.\n if (cloudStorage) {\n await req.payload.update({\n collection: collectionSlug,\n id: doc.id,\n data: {\n imageOptimizer: {\n ...doc.imageOptimizer,\n status: 'complete',\n variants: [],\n error: null,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n return doc\n }\n\n // Queue async format conversion job for remaining variants (local storage only)\n await req.payload.jobs.queue({\n task: 'imageOptimizer_convertFormats',\n input: {\n collectionSlug,\n docId: String(doc.id),\n },\n })\n\n req.payload.jobs.run().catch((err: unknown) => {\n req.payload.logger.error({ err }, 'Image optimizer job runner failed')\n })\n\n return doc\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","resolveStaticDir","isCloudStorage","createAfterChangeHook","resolvedConfig","collectionSlug","context","doc","req","imageOptimizer_skip","imageOptimizer_hasUpload","collectionConfig","payload","collections","config","cloudStorage","staticDir","processedBuffer","imageOptimizer_processedBuffer","filename","safeFilename","basename","filePath","join","writeFile","originalFilename","imageOptimizer_originalFilename","oldFilePath","unlink","catch","perCollectionConfig","replaceOriginal","formats","length","update","collection","id","data","imageOptimizer","status","variants","error","jobs","queue","task","input","docId","String","run","err","logger"],"mappings":"AAAA,OAAOA,QAAQ,cAAa;AAC5B,OAAOC,UAAU,OAAM;AAIvB,SAASC,uBAAuB,QAAQ,iBAAgB;AACxD,SAASC,gBAAgB,QAAQ,mCAAkC;AACnE,SAASC,cAAc,QAAQ,0BAAyB;AAExD,OAAO,MAAMC,wBAAwB,CACnCC,gBACAC;IAEA,OAAO,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,GAAG,EAAE;QACjC,IAAIF,SAASG,qBAAqB,OAAOF;QAEzC,iFAAiF;QACjF,iFAAiF;QACjF,4FAA4F;QAC5F,IAAI,CAACD,SAASI,0BAA0B,OAAOH;QAE/C,MAAMI,mBAAmBH,IAAII,OAAO,CAACC,WAAW,CAACR,eAAuD,CAACS,MAAM;QAC/G,MAAMC,eAAeb,eAAeS;QAEpC,kFAAkF;QAClF,6EAA6E;QAC7E,gFAAgF;QAChF,gFAAgF;QAChF,IAAI,CAACI,cAAc;YACjB,MAAMC,YAAYf,iBAAiBU;YACnC,MAAMM,kBAAkBX,QAAQY,8BAA8B;YAC9D,IAAID,mBAAmBV,IAAIY,QAAQ,IAAIH,WAAW;gBAChD,MAAMI,eAAerB,KAAKsB,QAAQ,CAACd,IAAIY,QAAQ;gBAC/C,MAAMG,WAAWvB,KAAKwB,IAAI,CAACP,WAAWI;gBACtC,MAAMtB,GAAG0B,SAAS,CAACF,UAAUL;gBAE7B,+EAA+E;gBAC/E,MAAMQ,mBAAmBnB,QAAQoB,+BAA+B;gBAChE,IAAID,oBAAoBA,qBAAqBL,cAAc;oBACzD,MAAMO,cAAc5B,KAAKwB,IAAI,CAACP,WAAWjB,KAAKsB,QAAQ,CAACI;oBACvD,MAAM3B,GAAG8B,MAAM,CAACD,aAAaE,KAAK,CAAC;oBACjC,0DAA0D;oBAC5D;gBACF;YACF;QACF;QAEA,MAAMC,sBAAsB9B,wBAAwBI,gBAAgBC;QAEpE,8EAA8E;QAC9E,2EAA2E;QAC3E,IAAIyB,oBAAoBC,eAAe,IAAID,oBAAoBE,OAAO,CAACC,MAAM,IAAI,GAAG;YAClF,MAAMzB,IAAII,OAAO,CAACsB,MAAM,CAAC;gBACvBC,YAAY9B;gBACZ+B,IAAI7B,IAAI6B,EAAE;gBACVC,MAAM;oBACJC,gBAAgB;wBACd,GAAG/B,IAAI+B,cAAc;wBACrBC,QAAQ;wBACRC,UAAU,EAAE;wBACZC,OAAO;oBACT;gBACF;gBACAnC,SAAS;oBAAEG,qBAAqB;gBAAK;YACvC;YACA,OAAOF;QACT;QAEA,2EAA2E;QAC3E,2EAA2E;QAC3E,wCAAwC;QACxC,IAAIQ,cAAc;YAChB,MAAMP,IAAII,OAAO,CAACsB,MAAM,CAAC;gBACvBC,YAAY9B;gBACZ+B,IAAI7B,IAAI6B,EAAE;gBACVC,MAAM;oBACJC,gBAAgB;wBACd,GAAG/B,IAAI+B,cAAc;wBACrBC,QAAQ;wBACRC,UAAU,EAAE;wBACZC,OAAO;oBACT;gBACF;gBACAnC,SAAS;oBAAEG,qBAAqB;gBAAK;YACvC;YACA,OAAOF;QACT;QAEA,gFAAgF;QAChF,MAAMC,IAAII,OAAO,CAAC8B,IAAI,CAACC,KAAK,CAAC;YAC3BC,MAAM;YACNC,OAAO;gBACLxC;gBACAyC,OAAOC,OAAOxC,IAAI6B,EAAE;YACtB;QACF;QAEA5B,IAAII,OAAO,CAAC8B,IAAI,CAACM,GAAG,GAAGnB,KAAK,CAAC,CAACoB;YAC5BzC,IAAII,OAAO,CAACsC,MAAM,CAACT,KAAK,CAAC;gBAAEQ;YAAI,GAAG;QACpC;QAEA,OAAO1C;IACT;AACF,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/afterChange.ts"],"sourcesContent":["import fs from 'fs/promises'\nimport path from 'path'\nimport type { CollectionAfterChangeHook, CollectionSlug } from 'payload'\n\nimport type { ResolvedImageOptimizerConfig } from '../types.js'\nimport { resolveCollectionConfig } from '../defaults.js'\nimport { resolveStaticDir } from '../utilities/resolveStaticDir.js'\nimport { isCloudStorage } from '../utilities/storage.js'\n\nexport const createAfterChangeHook = (\n resolvedConfig: ResolvedImageOptimizerConfig,\n collectionSlug: string,\n): CollectionAfterChangeHook => {\n return async ({ context, doc, req }) => {\n if (context?.imageOptimizer_skip) return doc\n\n // Use context flag from beforeChange instead of checking req.file.data directly.\n // Cloud storage adapters may consume req.file.data in their own afterChange hook\n // before ours runs, which would cause this guard to bail out and leave status as 'pending'.\n if (!context?.imageOptimizer_hasUpload) return doc\n\n const collectionConfig = req.payload.collections[collectionSlug as keyof typeof req.payload.collections].config\n const cloudStorage = isCloudStorage(collectionConfig)\n\n // When using local storage, overwrite the file on disk with the processed buffer.\n // Payload's uploadFiles step writes the original buffer; we replace it here.\n // When using cloud storage, skip — the cloud adapter's afterChange hook already\n // uploads the correct buffer from req.file.data (set in our beforeChange hook).\n if (!cloudStorage) {\n const staticDir = resolveStaticDir(collectionConfig)\n const processedBuffer = context.imageOptimizer_processedBuffer as Buffer | undefined\n if (processedBuffer && doc.filename && staticDir) {\n const safeFilename = path.basename(doc.filename as string)\n const filePath = path.join(staticDir, safeFilename)\n await fs.writeFile(filePath, processedBuffer)\n\n // If replaceOriginal changed the filename, clean up the old file Payload wrote\n const originalFilename = context.imageOptimizer_originalFilename as string | undefined\n if (originalFilename && originalFilename !== safeFilename) {\n const oldFilePath = path.join(staticDir, path.basename(originalFilename))\n await fs.unlink(oldFilePath).catch(() => {\n // Old file may not exist if Payload used the new filename\n })\n }\n }\n }\n\n const perCollectionConfig = resolveCollectionConfig(resolvedConfig, collectionSlug)\n\n // When replaceOriginal is on and only one format is configured, the main file\n // is already converted — skip the async job and mark complete immediately.\n if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length <= 1) {\n await req.payload.update({\n collection: collectionSlug as CollectionSlug,\n id: doc.id,\n data: {\n imageOptimizer: {\n ...doc.imageOptimizer,\n status: 'complete',\n variants: [],\n error: null,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n return doc\n }\n\n // With cloud storage, variant files cannot be written — skip the async job\n // and mark complete. CDN-level image optimization (e.g. Next.js Image) can\n // serve alternative formats on the fly.\n if (cloudStorage) {\n await req.payload.update({\n collection: collectionSlug as CollectionSlug,\n id: doc.id,\n data: {\n imageOptimizer: {\n ...doc.imageOptimizer,\n status: 'complete',\n variants: [],\n error: null,\n },\n },\n context: { imageOptimizer_skip: true },\n })\n return doc\n }\n\n // Queue async format conversion job for remaining variants (local storage only)\n await req.payload.jobs.queue({\n task: 'imageOptimizer_convertFormats',\n input: {\n collectionSlug,\n docId: String(doc.id),\n },\n })\n\n req.payload.jobs.run().catch((err: unknown) => {\n req.payload.logger.error({ err }, 'Image optimizer job runner failed')\n })\n\n return doc\n }\n}\n"],"names":["fs","path","resolveCollectionConfig","resolveStaticDir","isCloudStorage","createAfterChangeHook","resolvedConfig","collectionSlug","context","doc","req","imageOptimizer_skip","imageOptimizer_hasUpload","collectionConfig","payload","collections","config","cloudStorage","staticDir","processedBuffer","imageOptimizer_processedBuffer","filename","safeFilename","basename","filePath","join","writeFile","originalFilename","imageOptimizer_originalFilename","oldFilePath","unlink","catch","perCollectionConfig","replaceOriginal","formats","length","update","collection","id","data","imageOptimizer","status","variants","error","jobs","queue","task","input","docId","String","run","err","logger"],"mappings":"AAAA,OAAOA,QAAQ,cAAa;AAC5B,OAAOC,UAAU,OAAM;AAIvB,SAASC,uBAAuB,QAAQ,iBAAgB;AACxD,SAASC,gBAAgB,QAAQ,mCAAkC;AACnE,SAASC,cAAc,QAAQ,0BAAyB;AAExD,OAAO,MAAMC,wBAAwB,CACnCC,gBACAC;IAEA,OAAO,OAAO,EAAEC,OAAO,EAAEC,GAAG,EAAEC,GAAG,EAAE;QACjC,IAAIF,SAASG,qBAAqB,OAAOF;QAEzC,iFAAiF;QACjF,iFAAiF;QACjF,4FAA4F;QAC5F,IAAI,CAACD,SAASI,0BAA0B,OAAOH;QAE/C,MAAMI,mBAAmBH,IAAII,OAAO,CAACC,WAAW,CAACR,eAAuD,CAACS,MAAM;QAC/G,MAAMC,eAAeb,eAAeS;QAEpC,kFAAkF;QAClF,6EAA6E;QAC7E,gFAAgF;QAChF,gFAAgF;QAChF,IAAI,CAACI,cAAc;YACjB,MAAMC,YAAYf,iBAAiBU;YACnC,MAAMM,kBAAkBX,QAAQY,8BAA8B;YAC9D,IAAID,mBAAmBV,IAAIY,QAAQ,IAAIH,WAAW;gBAChD,MAAMI,eAAerB,KAAKsB,QAAQ,CAACd,IAAIY,QAAQ;gBAC/C,MAAMG,WAAWvB,KAAKwB,IAAI,CAACP,WAAWI;gBACtC,MAAMtB,GAAG0B,SAAS,CAACF,UAAUL;gBAE7B,+EAA+E;gBAC/E,MAAMQ,mBAAmBnB,QAAQoB,+BAA+B;gBAChE,IAAID,oBAAoBA,qBAAqBL,cAAc;oBACzD,MAAMO,cAAc5B,KAAKwB,IAAI,CAACP,WAAWjB,KAAKsB,QAAQ,CAACI;oBACvD,MAAM3B,GAAG8B,MAAM,CAACD,aAAaE,KAAK,CAAC;oBACjC,0DAA0D;oBAC5D;gBACF;YACF;QACF;QAEA,MAAMC,sBAAsB9B,wBAAwBI,gBAAgBC;QAEpE,8EAA8E;QAC9E,2EAA2E;QAC3E,IAAIyB,oBAAoBC,eAAe,IAAID,oBAAoBE,OAAO,CAACC,MAAM,IAAI,GAAG;YAClF,MAAMzB,IAAII,OAAO,CAACsB,MAAM,CAAC;gBACvBC,YAAY9B;gBACZ+B,IAAI7B,IAAI6B,EAAE;gBACVC,MAAM;oBACJC,gBAAgB;wBACd,GAAG/B,IAAI+B,cAAc;wBACrBC,QAAQ;wBACRC,UAAU,EAAE;wBACZC,OAAO;oBACT;gBACF;gBACAnC,SAAS;oBAAEG,qBAAqB;gBAAK;YACvC;YACA,OAAOF;QACT;QAEA,2EAA2E;QAC3E,2EAA2E;QAC3E,wCAAwC;QACxC,IAAIQ,cAAc;YAChB,MAAMP,IAAII,OAAO,CAACsB,MAAM,CAAC;gBACvBC,YAAY9B;gBACZ+B,IAAI7B,IAAI6B,EAAE;gBACVC,MAAM;oBACJC,gBAAgB;wBACd,GAAG/B,IAAI+B,cAAc;wBACrBC,QAAQ;wBACRC,UAAU,EAAE;wBACZC,OAAO;oBACT;gBACF;gBACAnC,SAAS;oBAAEG,qBAAqB;gBAAK;YACvC;YACA,OAAOF;QACT;QAEA,gFAAgF;QAChF,MAAMC,IAAII,OAAO,CAAC8B,IAAI,CAACC,KAAK,CAAC;YAC3BC,MAAM;YACNC,OAAO;gBACLxC;gBACAyC,OAAOC,OAAOxC,IAAI6B,EAAE;YACtB;QACF;QAEA5B,IAAII,OAAO,CAAC8B,IAAI,CAACM,GAAG,GAAGnB,KAAK,CAAC,CAACoB;YAC5BzC,IAAII,OAAO,CAACsC,MAAM,CAACT,KAAK,CAAC;gBAAEQ;YAAI,GAAG;QACpC;QAEA,OAAO1C;IACT;AACF,EAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inoo-ch/payload-image-optimizer",
|
|
3
|
-
"version": "1.3.
|
|
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": [
|
|
@@ -24,19 +24,19 @@
|
|
|
24
24
|
"type": "module",
|
|
25
25
|
"exports": {
|
|
26
26
|
".": {
|
|
27
|
-
"import": "./
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
28
|
"types": "./src/index.ts",
|
|
29
|
-
"default": "./
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
30
|
},
|
|
31
31
|
"./client": {
|
|
32
|
-
"import": "./
|
|
32
|
+
"import": "./dist/exports/client.js",
|
|
33
33
|
"types": "./src/exports/client.ts",
|
|
34
|
-
"default": "./
|
|
34
|
+
"default": "./dist/exports/client.js"
|
|
35
35
|
},
|
|
36
36
|
"./rsc": {
|
|
37
|
-
"import": "./
|
|
37
|
+
"import": "./dist/exports/rsc.js",
|
|
38
38
|
"types": "./src/exports/rsc.ts",
|
|
39
|
-
"default": "./
|
|
39
|
+
"default": "./dist/exports/rsc.js"
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"main": "./src/index.ts",
|
|
@@ -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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
}
|
package/src/hooks/afterChange.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs/promises'
|
|
2
2
|
import path from 'path'
|
|
3
|
-
import type { CollectionAfterChangeHook } from 'payload'
|
|
3
|
+
import type { CollectionAfterChangeHook, CollectionSlug } from 'payload'
|
|
4
4
|
|
|
5
5
|
import type { ResolvedImageOptimizerConfig } from '../types.js'
|
|
6
6
|
import { resolveCollectionConfig } from '../defaults.js'
|
|
@@ -51,7 +51,7 @@ export const createAfterChangeHook = (
|
|
|
51
51
|
// is already converted — skip the async job and mark complete immediately.
|
|
52
52
|
if (perCollectionConfig.replaceOriginal && perCollectionConfig.formats.length <= 1) {
|
|
53
53
|
await req.payload.update({
|
|
54
|
-
collection: collectionSlug,
|
|
54
|
+
collection: collectionSlug as CollectionSlug,
|
|
55
55
|
id: doc.id,
|
|
56
56
|
data: {
|
|
57
57
|
imageOptimizer: {
|
|
@@ -71,7 +71,7 @@ export const createAfterChangeHook = (
|
|
|
71
71
|
// serve alternative formats on the fly.
|
|
72
72
|
if (cloudStorage) {
|
|
73
73
|
await req.payload.update({
|
|
74
|
-
collection: collectionSlug,
|
|
74
|
+
collection: collectionSlug as CollectionSlug,
|
|
75
75
|
id: doc.id,
|
|
76
76
|
data: {
|
|
77
77
|
imageOptimizer: {
|