@jhb.software/payload-alt-text-plugin 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -14
- package/dist/components/AltTextField.js +3 -2
- package/dist/components/AltTextField.js.map +1 -1
- package/dist/components/AltTextHealthWidget.d.ts +1 -1
- package/dist/components/AltTextHealthWidget.js +5 -1
- package/dist/components/AltTextHealthWidget.js.map +1 -1
- package/dist/components/BulkGenerateAltTextsButton.js +6 -3
- package/dist/components/BulkGenerateAltTextsButton.js.map +1 -1
- package/dist/components/GenerateAltTextButton.d.ts +1 -1
- package/dist/components/GenerateAltTextButton.js +8 -2
- package/dist/components/GenerateAltTextButton.js.map +1 -1
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +8 -0
- package/dist/constants.js.map +1 -0
- package/dist/endpoints/altTextHealth.js +2 -1
- package/dist/endpoints/altTextHealth.js.map +1 -1
- package/dist/endpoints/bulkGenerateAltTexts.js +57 -6
- package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
- package/dist/endpoints/generateAltText.js +51 -12
- package/dist/endpoints/generateAltText.js.map +1 -1
- package/dist/plugin.js +12 -5
- package/dist/plugin.js.map +1 -1
- package/dist/resolvers/openAI.js +10 -6
- package/dist/resolvers/openAI.js.map +1 -1
- package/dist/types/AltTextPluginConfig.d.ts +29 -3
- package/dist/types/AltTextPluginConfig.js.map +1 -1
- package/dist/utilities/altTextHealth.d.ts +14 -0
- package/dist/utilities/altTextHealth.js +54 -4
- package/dist/utilities/altTextHealth.js.map +1 -1
- package/package.json +19 -16
package/README.md
CHANGED
|
@@ -77,16 +77,17 @@ This is also the recommended escape hatch if you hit Payload's Postgres SQL-buil
|
|
|
77
77
|
|
|
78
78
|
### Plugin Options
|
|
79
79
|
|
|
80
|
-
| Option | Type | Required | Description
|
|
81
|
-
| ---------------------------- | ------------------------------------- | -------- |
|
|
82
|
-
| `collections` | `(CollectionSlug \| CollectionObj)[]` | Yes | Collections to enable alt text generation for (see [Per-collection options](#per-collection-options))
|
|
83
|
-
| `resolver` | `AltTextResolver` | Yes | Alt text resolver to use (e.g., `openAIResolver`)
|
|
84
|
-
| `getImageThumbnail` | `Function` | Yes | Function to get the thumbnail URL from an image document
|
|
85
|
-
| `enabled` | `boolean` | No | Whether to enable the plugin
|
|
86
|
-
| `locale` | `string` | No | Locale for alt text generation (required when localization is disabled)
|
|
87
|
-
| `maxBulkGenerateConcurrency` | `number` | No | Maximum concurrent API requests for bulk operations (default: 16)
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
80
|
+
| Option | Type | Required | Description |
|
|
81
|
+
| ---------------------------- | ------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
82
|
+
| `collections` | `(CollectionSlug \| CollectionObj)[]` | Yes | Collections to enable alt text generation for (see [Per-collection options](#per-collection-options)) |
|
|
83
|
+
| `resolver` | `AltTextResolver` | Yes | Alt text resolver to use (e.g., `openAIResolver`) |
|
|
84
|
+
| `getImageThumbnail` | `Function` | Yes | Function to get the thumbnail URL from an image document |
|
|
85
|
+
| `enabled` | `boolean` | No | Whether to enable the plugin |
|
|
86
|
+
| `locale` | `string` | No | Locale for alt text generation (required when localization is disabled) |
|
|
87
|
+
| `maxBulkGenerateConcurrency` | `number` | No | Maximum concurrent API requests for bulk operations (default: 16) |
|
|
88
|
+
| `maxBulkGenerateIds` | `number` | No | Maximum number of image IDs accepted per bulk generate request; larger requests are rejected with `400`. Duplicate IDs are collapsed before the limit is applied (default: 100) |
|
|
89
|
+
| `fieldsOverride` | `Function` | No | Override the default fields inserted by the plugin |
|
|
90
|
+
| `healthCheck` | `boolean \| Function` | No | Alt text health tracking (REST endpoint, cache revalidation hooks, dashboard widget). `false` disables it; `true` enables it gated by `access`; a `({ req }) => boolean` function enables it and gates both the endpoint and the widget — use it to restrict the collection-wide report, e.g. to admins (default: `true`) |
|
|
90
91
|
|
|
91
92
|
### Per-collection options
|
|
92
93
|
|
|
@@ -204,9 +205,9 @@ export const customResolver = (): AltTextResolver => ({
|
|
|
204
205
|
|
|
205
206
|
## REST API Endpoints
|
|
206
207
|
|
|
207
|
-
The plugin registers the following REST API endpoints under `/api/alt-text
|
|
208
|
+
The plugin registers the following REST API endpoints under `/api/alt-text/`. All endpoints require authentication by default (configurable via the `access` option). Beyond that gate, the generate endpoints enforce each collection's own access control on the documents they read and write, and the health endpoint reports only the collections the requesting user can read (and can be gated separately via the `healthCheck` function).
|
|
208
209
|
|
|
209
|
-
### `POST /api/alt-text
|
|
210
|
+
### `POST /api/alt-text/generate`
|
|
210
211
|
|
|
211
212
|
Generates alt text for a single image. By default, returns the result without saving it (preview mode). Pass `update: true` to also persist the generated alt text and keywords to the document.
|
|
212
213
|
|
|
@@ -230,7 +231,7 @@ Generates alt text for a single image. By default, returns the result without sa
|
|
|
230
231
|
}
|
|
231
232
|
```
|
|
232
233
|
|
|
233
|
-
### `POST /api/alt-text
|
|
234
|
+
### `POST /api/alt-text/generate/bulk`
|
|
234
235
|
|
|
235
236
|
Generates and persists alt text for multiple images across all configured locales.
|
|
236
237
|
|
|
@@ -251,7 +252,7 @@ Generates and persists alt text for multiple images across all configured locale
|
|
|
251
252
|
}
|
|
252
253
|
```
|
|
253
254
|
|
|
254
|
-
### `GET /api/alt-text
|
|
255
|
+
### `GET /api/alt-text/health`
|
|
255
256
|
|
|
256
257
|
Returns alt text coverage statistics across all configured collections. Only available when `healthCheck` is enabled.
|
|
257
258
|
|
|
@@ -4,7 +4,7 @@ import { FieldLabel, TextareaInput, useDocumentInfo, useField } from '@payloadcm
|
|
|
4
4
|
import { matchesMimeType } from '../utilities/mimeTypes.js';
|
|
5
5
|
import { GenerateAltTextButton } from './GenerateAltTextButton.js';
|
|
6
6
|
export const AltTextField = (clientProps)=>{
|
|
7
|
-
const { field, path } = clientProps;
|
|
7
|
+
const { field, path, readOnly } = clientProps;
|
|
8
8
|
const supportedMimeTypes = field.admin?.custom?.supportedMimeTypes;
|
|
9
9
|
const trackedMimeTypes = field.admin?.custom?.trackedMimeTypes;
|
|
10
10
|
const { setValue, value } = useField({
|
|
@@ -36,11 +36,12 @@ export const AltTextField = (clientProps)=>{
|
|
|
36
36
|
/*#__PURE__*/ _jsx("div", {
|
|
37
37
|
className: "field-type__wrap",
|
|
38
38
|
children: /*#__PURE__*/ _jsx(TextareaInput, {
|
|
39
|
-
AfterInput: /*#__PURE__*/ _jsx(GenerateAltTextButton, {
|
|
39
|
+
AfterInput: readOnly ? undefined : /*#__PURE__*/ _jsx(GenerateAltTextButton, {
|
|
40
40
|
supportedMimeTypes: supportedMimeTypes
|
|
41
41
|
}),
|
|
42
42
|
onChange: (e)=>setValue(e.target.value),
|
|
43
43
|
path: path,
|
|
44
|
+
readOnly: readOnly,
|
|
44
45
|
required: required,
|
|
45
46
|
value: value
|
|
46
47
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/AltTextField.tsx"],"sourcesContent":["'use client'\n\nimport type { TextareaFieldClientProps } from 'payload'\n\nimport { FieldLabel, TextareaInput, useDocumentInfo, useField } from '@payloadcms/ui'\n\nimport { matchesMimeType } from '../utilities/mimeTypes.js'\nimport { GenerateAltTextButton } from './GenerateAltTextButton.js'\n\nexport const AltTextField = (clientProps: TextareaFieldClientProps) => {\n const { field, path } = clientProps\n\n const supportedMimeTypes = field.admin?.custom?.supportedMimeTypes as string[] | undefined\n const trackedMimeTypes = field.admin?.custom?.trackedMimeTypes as string[] | undefined\n\n const { setValue, value } = useField<string>({ path })\n const { id } = useDocumentInfo()\n const { value: mimeType } = useField<string>({ path: 'mimeType' })\n\n const isTrackedMimeType =\n !trackedMimeTypes ||\n trackedMimeTypes.length === 0 ||\n (!!mimeType && matchesMimeType(mimeType, trackedMimeTypes))\n\n if (!isTrackedMimeType) {\n return null\n }\n\n // the field should be optional when the document is created\n // (since the alt text generation can only be used once the document is created and the image uploaded)\n const required = id ? field.required : false\n\n return (\n <div className=\"field-type textarea\" style={{ flex: '1 1 auto' }}>\n <FieldLabel\n htmlFor={`field-${path}`}\n label={field.label}\n localized={field.localized}\n required={required}\n />\n\n <div className=\"field-type__wrap\">\n <TextareaInput\n AfterInput={<GenerateAltTextButton supportedMimeTypes={supportedMimeTypes}
|
|
1
|
+
{"version":3,"sources":["../../src/components/AltTextField.tsx"],"sourcesContent":["'use client'\n\nimport type { TextareaFieldClientProps } from 'payload'\n\nimport { FieldLabel, TextareaInput, useDocumentInfo, useField } from '@payloadcms/ui'\n\nimport { matchesMimeType } from '../utilities/mimeTypes.js'\nimport { GenerateAltTextButton } from './GenerateAltTextButton.js'\n\nexport const AltTextField = (clientProps: TextareaFieldClientProps) => {\n const { field, path, readOnly } = clientProps\n\n const supportedMimeTypes = field.admin?.custom?.supportedMimeTypes as string[] | undefined\n const trackedMimeTypes = field.admin?.custom?.trackedMimeTypes as string[] | undefined\n\n const { setValue, value } = useField<string>({ path })\n const { id } = useDocumentInfo()\n const { value: mimeType } = useField<string>({ path: 'mimeType' })\n\n const isTrackedMimeType =\n !trackedMimeTypes ||\n trackedMimeTypes.length === 0 ||\n (!!mimeType && matchesMimeType(mimeType, trackedMimeTypes))\n\n if (!isTrackedMimeType) {\n return null\n }\n\n // the field should be optional when the document is created\n // (since the alt text generation can only be used once the document is created and the image uploaded)\n const required = id ? field.required : false\n\n return (\n <div className=\"field-type textarea\" style={{ flex: '1 1 auto' }}>\n <FieldLabel\n htmlFor={`field-${path}`}\n label={field.label}\n localized={field.localized}\n required={required}\n />\n\n <div className=\"field-type__wrap\">\n <TextareaInput\n AfterInput={\n readOnly ? undefined : <GenerateAltTextButton supportedMimeTypes={supportedMimeTypes} />\n }\n onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setValue(e.target.value)}\n path={path}\n readOnly={readOnly}\n required={required}\n value={value}\n />\n </div>\n </div>\n )\n}\n"],"names":["FieldLabel","TextareaInput","useDocumentInfo","useField","matchesMimeType","GenerateAltTextButton","AltTextField","clientProps","field","path","readOnly","supportedMimeTypes","admin","custom","trackedMimeTypes","setValue","value","id","mimeType","isTrackedMimeType","length","required","div","className","style","flex","htmlFor","label","localized","AfterInput","undefined","onChange","e","target"],"mappings":"AAAA;;AAIA,SAASA,UAAU,EAAEC,aAAa,EAAEC,eAAe,EAAEC,QAAQ,QAAQ,iBAAgB;AAErF,SAASC,eAAe,QAAQ,4BAA2B;AAC3D,SAASC,qBAAqB,QAAQ,6BAA4B;AAElE,OAAO,MAAMC,eAAe,CAACC;IAC3B,MAAM,EAAEC,KAAK,EAAEC,IAAI,EAAEC,QAAQ,EAAE,GAAGH;IAElC,MAAMI,qBAAqBH,MAAMI,KAAK,EAAEC,QAAQF;IAChD,MAAMG,mBAAmBN,MAAMI,KAAK,EAAEC,QAAQC;IAE9C,MAAM,EAAEC,QAAQ,EAAEC,KAAK,EAAE,GAAGb,SAAiB;QAAEM;IAAK;IACpD,MAAM,EAAEQ,EAAE,EAAE,GAAGf;IACf,MAAM,EAAEc,OAAOE,QAAQ,EAAE,GAAGf,SAAiB;QAAEM,MAAM;IAAW;IAEhE,MAAMU,oBACJ,CAACL,oBACDA,iBAAiBM,MAAM,KAAK,KAC3B,CAAC,CAACF,YAAYd,gBAAgBc,UAAUJ;IAE3C,IAAI,CAACK,mBAAmB;QACtB,OAAO;IACT;IAEA,4DAA4D;IAC5D,uGAAuG;IACvG,MAAME,WAAWJ,KAAKT,MAAMa,QAAQ,GAAG;IAEvC,qBACE,MAACC;QAAIC,WAAU;QAAsBC,OAAO;YAAEC,MAAM;QAAW;;0BAC7D,KAACzB;gBACC0B,SAAS,CAAC,MAAM,EAAEjB,MAAM;gBACxBkB,OAAOnB,MAAMmB,KAAK;gBAClBC,WAAWpB,MAAMoB,SAAS;gBAC1BP,UAAUA;;0BAGZ,KAACC;gBAAIC,WAAU;0BACb,cAAA,KAACtB;oBACC4B,YACEnB,WAAWoB,0BAAY,KAACzB;wBAAsBM,oBAAoBA;;oBAEpEoB,UAAU,CAACC,IAA8CjB,SAASiB,EAAEC,MAAM,CAACjB,KAAK;oBAChFP,MAAMA;oBACNC,UAAUA;oBACVW,UAAUA;oBACVL,OAAOA;;;;;AAKjB,EAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { WidgetServerProps } from 'payload';
|
|
2
|
-
export declare function AltTextHealthWidget({ req }: WidgetServerProps): Promise<import("react").JSX.Element>;
|
|
2
|
+
export declare function AltTextHealthWidget({ req }: WidgetServerProps): Promise<import("react").JSX.Element | null>;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Pill } from '@payloadcms/ui/elements/Pill';
|
|
3
3
|
import { formatAdminURL } from 'payload/shared';
|
|
4
|
-
import { getAltTextHealthWidgetData } from '../utilities/altTextHealth.js';
|
|
4
|
+
import { canViewHealthReport, getAltTextHealthWidgetData } from '../utilities/altTextHealth.js';
|
|
5
5
|
import { getAltTextHealthWidgetDisplayState } from '../utilities/altTextHealthWidgetDisplay.js';
|
|
6
6
|
import { getCollectionLabel } from '../utilities/getCollectionLabel.js';
|
|
7
7
|
import { ArrowRightIcon } from './icons/ArrowRightIcon.js';
|
|
8
8
|
import { CheckIcon } from './icons/CheckIcon.js';
|
|
9
9
|
import { ImageIcon } from './icons/ImageIcon.js';
|
|
10
10
|
export async function AltTextHealthWidget({ req }) {
|
|
11
|
+
// Hide the widget from users the health gate denies, matching the endpoint.
|
|
12
|
+
if (!await canViewHealthReport(req)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
11
15
|
const t = req.t;
|
|
12
16
|
const { collections, errors, isLocalized, localeCount, totalDocs } = await getAltTextHealthWidgetData(req);
|
|
13
17
|
const adminRoute = req.payload.config.routes.admin;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/AltTextHealthWidget.tsx"],"sourcesContent":["import type { TFunction } from '@payloadcms/translations'\nimport type { WidgetServerProps } from 'payload'\n\nimport { Pill } from '@payloadcms/ui/elements/Pill'\nimport { formatAdminURL } from 'payload/shared'\n\nimport type { PluginAltTextTranslationKeys } from '../translations/index.js'\n\nimport { getAltTextHealthWidgetData } from '../utilities/altTextHealth.js'\nimport { getAltTextHealthWidgetDisplayState } from '../utilities/altTextHealthWidgetDisplay.js'\nimport { getCollectionLabel } from '../utilities/getCollectionLabel.js'\nimport { ArrowRightIcon } from './icons/ArrowRightIcon.js'\nimport { CheckIcon } from './icons/CheckIcon.js'\nimport { ImageIcon } from './icons/ImageIcon.js'\n\nexport async function AltTextHealthWidget({ req }: WidgetServerProps) {\n const t = req.t as TFunction<PluginAltTextTranslationKeys>\n const { collections, errors, isLocalized, localeCount, totalDocs } =\n await getAltTextHealthWidgetData(req)\n const adminRoute = req.payload.config.routes.admin\n\n return (\n <div\n className=\"card\"\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: '16px',\n height: '100%',\n }}\n >\n <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>\n <div style={{ alignItems: 'center', display: 'flex', gap: '0.5rem' }}>\n <div style={{ color: 'var(--theme-elevation-500)' }}>\n <ImageIcon />\n </div>\n <h3 style={{ margin: 0 }}>\n {t('@jhb.software/payload-alt-text-plugin:altTextHealthWidget')}\n </h3>\n </div>\n <p style={{ color: 'var(--theme-text)', fontSize: '14px', margin: 0, opacity: 0.75 }}>\n {t('@jhb.software/payload-alt-text-plugin:altTextHealthDescription')}\n </p>\n </div>\n\n {totalDocs === 0 && errors.length === 0 && (\n <p style={{ color: 'var(--theme-text)', margin: 0, opacity: 0.75 }}>\n {t('@jhb.software/payload-alt-text-plugin:noImagesFound')}\n </p>\n )}\n\n {errors.length > 0 && (\n <p style={{ color: '#92400e', fontSize: '13px', margin: 0 }}>\n {t('@jhb.software/payload-alt-text-plugin:healthCheckPartialWarning')}\n </p>\n )}\n\n <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>\n {collections.map((collection) => {\n const displayState = getAltTextHealthWidgetDisplayState(collection)\n\n return (\n <div\n key={collection.collection}\n style={{\n alignItems: 'center',\n background: 'var(--theme-elevation-50)',\n border: '1px solid var(--theme-border-color)',\n borderRadius: 'var(--style-radius-m)',\n display: 'flex',\n gap: '12px',\n justifyContent: 'space-between',\n padding: '12px 16px',\n }}\n >\n <div style={{ display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0 }}>\n <a\n href={formatAdminURL({\n adminRoute,\n path: `/collections/${collection.collection}`,\n })}\n style={{\n color: 'var(--theme-text)',\n fontSize: '14px',\n fontWeight: 500,\n textDecoration: 'none',\n }}\n >\n {getCollectionLabel(\n collection.collection,\n req.payload.config.collections,\n req.locale,\n )}\n </a>\n\n {displayState === 'unavailable' ? (\n <span style={{ color: '#92400e', fontSize: '13px' }}>\n {t('@jhb.software/payload-alt-text-plugin:collectionCheckFailed')}\n </span>\n ) : (\n <span style={{ fontSize: '13px', opacity: 0.7 }}>\n <span style={{ whiteSpace: 'nowrap' }}>\n {t('@jhb.software/payload-alt-text-plugin:totalImageCount', {\n count: collection.totalDocs,\n })}\n </span>\n {isLocalized && (\n <>\n {' · '}\n <span style={{ whiteSpace: 'nowrap' }}>\n {t('@jhb.software/payload-alt-text-plugin:localeCount', {\n count: localeCount,\n })}\n </span>\n </>\n )}\n </span>\n )}\n </div>\n\n {displayState === 'unhealthy' &&\n collection.invalidDocIds &&\n collection.invalidDocIds.length > 0 ? (\n <Pill\n pillStyle=\"error\"\n size=\"small\"\n to={`${formatAdminURL({\n adminRoute,\n path: `/collections/${collection.collection}`,\n })}?where[id][in]=${collection.invalidDocIds.join(',')}`}\n >\n <div style={{ alignItems: 'center', display: 'flex', gap: '0.25rem' }}>\n {t('@jhb.software/payload-alt-text-plugin:statusUnhealthy', {\n count: collection.missingDocs + collection.partialDocs,\n })}\n <ArrowRightIcon height=\"12\" width=\"12\" />\n </div>\n </Pill>\n ) : displayState === 'healthy' ? (\n <Pill pillStyle=\"success\" size=\"small\">\n <div style={{ alignItems: 'center', display: 'flex', gap: '0.25rem' }}>\n {t('@jhb.software/payload-alt-text-plugin:statusHealthy')}\n <CheckIcon height=\"12\" width=\"12\" />\n </div>\n </Pill>\n ) : displayState === 'unhealthy' ? (\n <Pill pillStyle=\"error\" size=\"small\">\n {t('@jhb.software/payload-alt-text-plugin:statusUnhealthy', {\n count: collection.missingDocs + collection.partialDocs,\n })}\n </Pill>\n ) : (\n <Pill pillStyle=\"warning\" size=\"small\">\n {t('@jhb.software/payload-alt-text-plugin:collectionCheckFailed')}\n </Pill>\n )}\n </div>\n )\n })}\n </div>\n </div>\n )\n}\n"],"names":["Pill","formatAdminURL","getAltTextHealthWidgetData","getAltTextHealthWidgetDisplayState","getCollectionLabel","ArrowRightIcon","CheckIcon","ImageIcon","AltTextHealthWidget","req","t","collections","errors","isLocalized","localeCount","totalDocs","adminRoute","payload","config","routes","admin","div","className","style","display","flexDirection","gap","height","alignItems","color","h3","margin","p","fontSize","opacity","length","map","collection","displayState","background","border","borderRadius","justifyContent","padding","minWidth","a","href","path","fontWeight","textDecoration","locale","span","whiteSpace","count","invalidDocIds","pillStyle","size","to","join","missingDocs","partialDocs","width"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/components/AltTextHealthWidget.tsx"],"sourcesContent":["import type { TFunction } from '@payloadcms/translations'\nimport type { WidgetServerProps } from 'payload'\n\nimport { Pill } from '@payloadcms/ui/elements/Pill'\nimport { formatAdminURL } from 'payload/shared'\n\nimport type { PluginAltTextTranslationKeys } from '../translations/index.js'\n\nimport { canViewHealthReport, getAltTextHealthWidgetData } from '../utilities/altTextHealth.js'\nimport { getAltTextHealthWidgetDisplayState } from '../utilities/altTextHealthWidgetDisplay.js'\nimport { getCollectionLabel } from '../utilities/getCollectionLabel.js'\nimport { ArrowRightIcon } from './icons/ArrowRightIcon.js'\nimport { CheckIcon } from './icons/CheckIcon.js'\nimport { ImageIcon } from './icons/ImageIcon.js'\n\nexport async function AltTextHealthWidget({ req }: WidgetServerProps) {\n // Hide the widget from users the health gate denies, matching the endpoint.\n if (!(await canViewHealthReport(req))) {\n return null\n }\n\n const t = req.t as TFunction<PluginAltTextTranslationKeys>\n const { collections, errors, isLocalized, localeCount, totalDocs } =\n await getAltTextHealthWidgetData(req)\n const adminRoute = req.payload.config.routes.admin\n\n return (\n <div\n className=\"card\"\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: '16px',\n height: '100%',\n }}\n >\n <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>\n <div style={{ alignItems: 'center', display: 'flex', gap: '0.5rem' }}>\n <div style={{ color: 'var(--theme-elevation-500)' }}>\n <ImageIcon />\n </div>\n <h3 style={{ margin: 0 }}>\n {t('@jhb.software/payload-alt-text-plugin:altTextHealthWidget')}\n </h3>\n </div>\n <p style={{ color: 'var(--theme-text)', fontSize: '14px', margin: 0, opacity: 0.75 }}>\n {t('@jhb.software/payload-alt-text-plugin:altTextHealthDescription')}\n </p>\n </div>\n\n {totalDocs === 0 && errors.length === 0 && (\n <p style={{ color: 'var(--theme-text)', margin: 0, opacity: 0.75 }}>\n {t('@jhb.software/payload-alt-text-plugin:noImagesFound')}\n </p>\n )}\n\n {errors.length > 0 && (\n <p style={{ color: '#92400e', fontSize: '13px', margin: 0 }}>\n {t('@jhb.software/payload-alt-text-plugin:healthCheckPartialWarning')}\n </p>\n )}\n\n <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>\n {collections.map((collection) => {\n const displayState = getAltTextHealthWidgetDisplayState(collection)\n\n return (\n <div\n key={collection.collection}\n style={{\n alignItems: 'center',\n background: 'var(--theme-elevation-50)',\n border: '1px solid var(--theme-border-color)',\n borderRadius: 'var(--style-radius-m)',\n display: 'flex',\n gap: '12px',\n justifyContent: 'space-between',\n padding: '12px 16px',\n }}\n >\n <div style={{ display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0 }}>\n <a\n href={formatAdminURL({\n adminRoute,\n path: `/collections/${collection.collection}`,\n })}\n style={{\n color: 'var(--theme-text)',\n fontSize: '14px',\n fontWeight: 500,\n textDecoration: 'none',\n }}\n >\n {getCollectionLabel(\n collection.collection,\n req.payload.config.collections,\n req.locale,\n )}\n </a>\n\n {displayState === 'unavailable' ? (\n <span style={{ color: '#92400e', fontSize: '13px' }}>\n {t('@jhb.software/payload-alt-text-plugin:collectionCheckFailed')}\n </span>\n ) : (\n <span style={{ fontSize: '13px', opacity: 0.7 }}>\n <span style={{ whiteSpace: 'nowrap' }}>\n {t('@jhb.software/payload-alt-text-plugin:totalImageCount', {\n count: collection.totalDocs,\n })}\n </span>\n {isLocalized && (\n <>\n {' · '}\n <span style={{ whiteSpace: 'nowrap' }}>\n {t('@jhb.software/payload-alt-text-plugin:localeCount', {\n count: localeCount,\n })}\n </span>\n </>\n )}\n </span>\n )}\n </div>\n\n {displayState === 'unhealthy' &&\n collection.invalidDocIds &&\n collection.invalidDocIds.length > 0 ? (\n <Pill\n pillStyle=\"error\"\n size=\"small\"\n to={`${formatAdminURL({\n adminRoute,\n path: `/collections/${collection.collection}`,\n })}?where[id][in]=${collection.invalidDocIds.join(',')}`}\n >\n <div style={{ alignItems: 'center', display: 'flex', gap: '0.25rem' }}>\n {t('@jhb.software/payload-alt-text-plugin:statusUnhealthy', {\n count: collection.missingDocs + collection.partialDocs,\n })}\n <ArrowRightIcon height=\"12\" width=\"12\" />\n </div>\n </Pill>\n ) : displayState === 'healthy' ? (\n <Pill pillStyle=\"success\" size=\"small\">\n <div style={{ alignItems: 'center', display: 'flex', gap: '0.25rem' }}>\n {t('@jhb.software/payload-alt-text-plugin:statusHealthy')}\n <CheckIcon height=\"12\" width=\"12\" />\n </div>\n </Pill>\n ) : displayState === 'unhealthy' ? (\n <Pill pillStyle=\"error\" size=\"small\">\n {t('@jhb.software/payload-alt-text-plugin:statusUnhealthy', {\n count: collection.missingDocs + collection.partialDocs,\n })}\n </Pill>\n ) : (\n <Pill pillStyle=\"warning\" size=\"small\">\n {t('@jhb.software/payload-alt-text-plugin:collectionCheckFailed')}\n </Pill>\n )}\n </div>\n )\n })}\n </div>\n </div>\n )\n}\n"],"names":["Pill","formatAdminURL","canViewHealthReport","getAltTextHealthWidgetData","getAltTextHealthWidgetDisplayState","getCollectionLabel","ArrowRightIcon","CheckIcon","ImageIcon","AltTextHealthWidget","req","t","collections","errors","isLocalized","localeCount","totalDocs","adminRoute","payload","config","routes","admin","div","className","style","display","flexDirection","gap","height","alignItems","color","h3","margin","p","fontSize","opacity","length","map","collection","displayState","background","border","borderRadius","justifyContent","padding","minWidth","a","href","path","fontWeight","textDecoration","locale","span","whiteSpace","count","invalidDocIds","pillStyle","size","to","join","missingDocs","partialDocs","width"],"mappings":"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"}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { Button, toast, useConfig, useSelection, useTranslation } from '@payloadcms/ui';
|
|
3
|
+
import { Button, toast, useAuth, useConfig, useSelection, useTranslation } from '@payloadcms/ui';
|
|
4
4
|
import { useRouter } from 'next/navigation.js';
|
|
5
5
|
import { useTransition } from 'react';
|
|
6
|
+
import { PLUGIN_SLUG } from '../constants.js';
|
|
6
7
|
import { Lightning } from './icons/Lightning.js';
|
|
7
8
|
import { Spinner } from './icons/Spinner.js';
|
|
8
9
|
export function BulkGenerateAltTextsButton({ collectionSlug }) {
|
|
9
10
|
const { t } = useTranslation();
|
|
10
11
|
const [isPending, startTransition] = useTransition();
|
|
12
|
+
const { permissions } = useAuth();
|
|
11
13
|
const { selected, setSelection } = useSelection();
|
|
14
|
+
const canUpdateCollection = Boolean(permissions?.collections?.[collectionSlug]?.update);
|
|
12
15
|
const { config: { routes: { api: apiRoute }, serverURL } } = useConfig();
|
|
13
16
|
const selectedIds = Array.from(selected.entries()).filter(([, isSelected])=>isSelected).map(([id])=>id);
|
|
14
17
|
const router = useRouter();
|
|
@@ -18,7 +21,7 @@ export function BulkGenerateAltTextsButton({ collectionSlug }) {
|
|
|
18
21
|
throw new Error('Collection slug is required');
|
|
19
22
|
}
|
|
20
23
|
try {
|
|
21
|
-
const response = await fetch(`${serverURL ?? ''}${apiRoute}/
|
|
24
|
+
const response = await fetch(`${serverURL ?? ''}${apiRoute}/${PLUGIN_SLUG}/generate/bulk`, {
|
|
22
25
|
body: JSON.stringify({
|
|
23
26
|
collection: collectionSlug,
|
|
24
27
|
ids: selectedIds
|
|
@@ -58,7 +61,7 @@ export function BulkGenerateAltTextsButton({ collectionSlug }) {
|
|
|
58
61
|
}
|
|
59
62
|
});
|
|
60
63
|
};
|
|
61
|
-
return selectedIds.length > 0 && /*#__PURE__*/ _jsx("div", {
|
|
64
|
+
return canUpdateCollection && selectedIds.length > 0 && /*#__PURE__*/ _jsx("div", {
|
|
62
65
|
className: "m-0",
|
|
63
66
|
style: {
|
|
64
67
|
display: 'flex',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/BulkGenerateAltTextsButton.tsx"],"sourcesContent":["'use client'\n\nimport { Button, toast, useConfig, useSelection, useTranslation } from '@payloadcms/ui'\nimport { useRouter } from 'next/navigation.js'\nimport { useTransition } from 'react'\n\nimport type {\n PluginAltTextTranslationKeys,\n PluginAltTextTranslations,\n} from '../translations/index.js'\n\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\n\nexport function BulkGenerateAltTextsButton({ collectionSlug }: { collectionSlug: string }) {\n const { t } = useTranslation<PluginAltTextTranslations, PluginAltTextTranslationKeys>()\n const [isPending, startTransition] = useTransition()\n const { selected, setSelection } = useSelection()\n const {\n config: {\n routes: { api: apiRoute },\n serverURL,\n },\n } = useConfig()\n\n const selectedIds = Array.from(selected.entries())\n .filter(([, isSelected]) => isSelected)\n .map(([id]) => id) as string[]\n\n const router = useRouter()\n\n const handleGenerateAltTexts = () => {\n startTransition(async () => {\n if (!collectionSlug) {\n throw new Error('Collection slug is required')\n }\n\n try {\n const response = await fetch(
|
|
1
|
+
{"version":3,"sources":["../../src/components/BulkGenerateAltTextsButton.tsx"],"sourcesContent":["'use client'\n\nimport { Button, toast, useAuth, useConfig, useSelection, useTranslation } from '@payloadcms/ui'\nimport { useRouter } from 'next/navigation.js'\nimport { useTransition } from 'react'\n\nimport type {\n PluginAltTextTranslationKeys,\n PluginAltTextTranslations,\n} from '../translations/index.js'\n\nimport { PLUGIN_SLUG } from '../constants.js'\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\n\nexport function BulkGenerateAltTextsButton({ collectionSlug }: { collectionSlug: string }) {\n const { t } = useTranslation<PluginAltTextTranslations, PluginAltTextTranslationKeys>()\n const [isPending, startTransition] = useTransition()\n const { permissions } = useAuth()\n const { selected, setSelection } = useSelection()\n\n const canUpdateCollection = Boolean(permissions?.collections?.[collectionSlug]?.update)\n const {\n config: {\n routes: { api: apiRoute },\n serverURL,\n },\n } = useConfig()\n\n const selectedIds = Array.from(selected.entries())\n .filter(([, isSelected]) => isSelected)\n .map(([id]) => id) as string[]\n\n const router = useRouter()\n\n const handleGenerateAltTexts = () => {\n startTransition(async () => {\n if (!collectionSlug) {\n throw new Error('Collection slug is required')\n }\n\n try {\n const response = await fetch(`${serverURL ?? ''}${apiRoute}/${PLUGIN_SLUG}/generate/bulk`, {\n body: JSON.stringify({\n collection: collectionSlug,\n ids: selectedIds,\n }),\n method: 'POST',\n })\n\n if (!response.ok) {\n toast.error(t('@jhb.software/payload-alt-text-plugin:failedToGenerate'))\n return\n }\n\n const data = (await response.json()) as {\n erroredDocs: string[]\n totalDocs: number\n updatedDocs: number\n }\n\n if (data.erroredDocs.length > 0) {\n toast.error(\n t('@jhb.software/payload-alt-text-plugin:failedToGenerateForXImages', {\n count: data.erroredDocs.length,\n }),\n )\n }\n\n // in case not all images were updated, show a warning instead of a success message:\n if (data.updatedDocs === data.totalDocs) {\n toast.success(\n t('@jhb.software/payload-alt-text-plugin:xOfYImagesUpdated', {\n total: data.totalDocs,\n updated: data.updatedDocs,\n }),\n )\n } else {\n toast.warning(\n t('@jhb.software/payload-alt-text-plugin:xOfYImagesUpdated', {\n total: data.totalDocs,\n updated: data.updatedDocs,\n }),\n )\n }\n\n // deselect all previously selected images\n for (const id of selectedIds) {\n setSelection(id)\n }\n\n router.refresh()\n } catch (error) {\n console.error('Error generating alt text:', error)\n toast.error(t('@jhb.software/payload-alt-text-plugin:errorGeneratingAltText'))\n }\n })\n }\n\n return (\n canUpdateCollection &&\n selectedIds.length > 0 && (\n <div className=\"m-0\" style={{ display: 'flex', justifyContent: 'right' }}>\n <Button\n className=\"m-0\"\n disabled={isPending || selectedIds.length === 0}\n icon={isPending ? <Spinner /> : <Lightning />}\n onClick={handleGenerateAltTexts}\n >\n {t('@jhb.software/payload-alt-text-plugin:generateAltTextFor', {\n count: selectedIds.length,\n })}\n </Button>\n </div>\n )\n )\n}\n"],"names":["Button","toast","useAuth","useConfig","useSelection","useTranslation","useRouter","useTransition","PLUGIN_SLUG","Lightning","Spinner","BulkGenerateAltTextsButton","collectionSlug","t","isPending","startTransition","permissions","selected","setSelection","canUpdateCollection","Boolean","collections","update","config","routes","api","apiRoute","serverURL","selectedIds","Array","from","entries","filter","isSelected","map","id","router","handleGenerateAltTexts","Error","response","fetch","body","JSON","stringify","collection","ids","method","ok","error","data","json","erroredDocs","length","count","updatedDocs","totalDocs","success","total","updated","warning","refresh","console","div","className","style","display","justifyContent","disabled","icon","onClick"],"mappings":"AAAA;;AAEA,SAASA,MAAM,EAAEC,KAAK,EAAEC,OAAO,EAAEC,SAAS,EAAEC,YAAY,EAAEC,cAAc,QAAQ,iBAAgB;AAChG,SAASC,SAAS,QAAQ,qBAAoB;AAC9C,SAASC,aAAa,QAAQ,QAAO;AAOrC,SAASC,WAAW,QAAQ,kBAAiB;AAC7C,SAASC,SAAS,QAAQ,uBAAsB;AAChD,SAASC,OAAO,QAAQ,qBAAoB;AAE5C,OAAO,SAASC,2BAA2B,EAAEC,cAAc,EAA8B;IACvF,MAAM,EAAEC,CAAC,EAAE,GAAGR;IACd,MAAM,CAACS,WAAWC,gBAAgB,GAAGR;IACrC,MAAM,EAAES,WAAW,EAAE,GAAGd;IACxB,MAAM,EAAEe,QAAQ,EAAEC,YAAY,EAAE,GAAGd;IAEnC,MAAMe,sBAAsBC,QAAQJ,aAAaK,aAAa,CAACT,eAAe,EAAEU;IAChF,MAAM,EACJC,QAAQ,EACNC,QAAQ,EAAEC,KAAKC,QAAQ,EAAE,EACzBC,SAAS,EACV,EACF,GAAGxB;IAEJ,MAAMyB,cAAcC,MAAMC,IAAI,CAACb,SAASc,OAAO,IAC5CC,MAAM,CAAC,CAAC,GAAGC,WAAW,GAAKA,YAC3BC,GAAG,CAAC,CAAC,CAACC,GAAG,GAAKA;IAEjB,MAAMC,SAAS9B;IAEf,MAAM+B,yBAAyB;QAC7BtB,gBAAgB;YACd,IAAI,CAACH,gBAAgB;gBACnB,MAAM,IAAI0B,MAAM;YAClB;YAEA,IAAI;gBACF,MAAMC,WAAW,MAAMC,MAAM,GAAGb,aAAa,KAAKD,SAAS,CAAC,EAAElB,YAAY,cAAc,CAAC,EAAE;oBACzFiC,MAAMC,KAAKC,SAAS,CAAC;wBACnBC,YAAYhC;wBACZiC,KAAKjB;oBACP;oBACAkB,QAAQ;gBACV;gBAEA,IAAI,CAACP,SAASQ,EAAE,EAAE;oBAChB9C,MAAM+C,KAAK,CAACnC,EAAE;oBACd;gBACF;gBAEA,MAAMoC,OAAQ,MAAMV,SAASW,IAAI;gBAMjC,IAAID,KAAKE,WAAW,CAACC,MAAM,GAAG,GAAG;oBAC/BnD,MAAM+C,KAAK,CACTnC,EAAE,oEAAoE;wBACpEwC,OAAOJ,KAAKE,WAAW,CAACC,MAAM;oBAChC;gBAEJ;gBAEA,oFAAoF;gBACpF,IAAIH,KAAKK,WAAW,KAAKL,KAAKM,SAAS,EAAE;oBACvCtD,MAAMuD,OAAO,CACX3C,EAAE,2DAA2D;wBAC3D4C,OAAOR,KAAKM,SAAS;wBACrBG,SAAST,KAAKK,WAAW;oBAC3B;gBAEJ,OAAO;oBACLrD,MAAM0D,OAAO,CACX9C,EAAE,2DAA2D;wBAC3D4C,OAAOR,KAAKM,SAAS;wBACrBG,SAAST,KAAKK,WAAW;oBAC3B;gBAEJ;gBAEA,0CAA0C;gBAC1C,KAAK,MAAMnB,MAAMP,YAAa;oBAC5BV,aAAaiB;gBACf;gBAEAC,OAAOwB,OAAO;YAChB,EAAE,OAAOZ,OAAO;gBACda,QAAQb,KAAK,CAAC,8BAA8BA;gBAC5C/C,MAAM+C,KAAK,CAACnC,EAAE;YAChB;QACF;IACF;IAEA,OACEM,uBACAS,YAAYwB,MAAM,GAAG,mBACnB,KAACU;QAAIC,WAAU;QAAMC,OAAO;YAAEC,SAAS;YAAQC,gBAAgB;QAAQ;kBACrE,cAAA,KAAClE;YACC+D,WAAU;YACVI,UAAUrD,aAAac,YAAYwB,MAAM,KAAK;YAC9CgB,MAAMtD,0BAAY,KAACJ,6BAAa,KAACD;YACjC4D,SAAShC;sBAERxB,EAAE,4DAA4D;gBAC7DwC,OAAOzB,YAAYwB,MAAM;YAC3B;;;AAKV"}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Button, toast, useConfig, useDocumentInfo, useField, useLocale, useTranslation } from '@payloadcms/ui';
|
|
4
4
|
import { useTransition } from 'react';
|
|
5
|
+
import { PLUGIN_SLUG } from '../constants.js';
|
|
5
6
|
import { Lightning } from './icons/Lightning.js';
|
|
6
7
|
import { Spinner } from './icons/Spinner.js';
|
|
7
8
|
export function GenerateAltTextButton({ supportedMimeTypes }) {
|
|
8
9
|
const { t } = useTranslation();
|
|
9
|
-
const { id, collectionSlug } = useDocumentInfo();
|
|
10
|
+
const { id, collectionSlug, docPermissions } = useDocumentInfo();
|
|
10
11
|
const locale = useLocale();
|
|
11
12
|
const [isPending, startTransition] = useTransition();
|
|
12
13
|
const { config: { routes: { api: apiRoute }, serverURL } } = useConfig();
|
|
@@ -20,6 +21,11 @@ export function GenerateAltTextButton({ supportedMimeTypes }) {
|
|
|
20
21
|
path: 'mimeType'
|
|
21
22
|
});
|
|
22
23
|
const isUnsupportedMimeType = !!mimeType && !!supportedMimeTypes && !supportedMimeTypes.includes(mimeType);
|
|
24
|
+
// Hide the generate button from users who cannot update the document — the
|
|
25
|
+
// generated alt text would not be persistable by them anyway.
|
|
26
|
+
if (!docPermissions?.update) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
23
29
|
const handleGenerateAltText = ()=>{
|
|
24
30
|
if (!collectionSlug || !id) {
|
|
25
31
|
toast.error(t('@jhb.software/payload-alt-text-plugin:cannotGenerateMissingFields'));
|
|
@@ -27,7 +33,7 @@ export function GenerateAltTextButton({ supportedMimeTypes }) {
|
|
|
27
33
|
}
|
|
28
34
|
startTransition(async ()=>{
|
|
29
35
|
try {
|
|
30
|
-
const response = await fetch(`${serverURL ?? ''}${apiRoute}/
|
|
36
|
+
const response = await fetch(`${serverURL ?? ''}${apiRoute}/${PLUGIN_SLUG}/generate`, {
|
|
31
37
|
body: JSON.stringify({
|
|
32
38
|
id: id,
|
|
33
39
|
collection: collectionSlug,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/GenerateAltTextButton.tsx"],"sourcesContent":["'use client'\n\nimport {\n Button,\n toast,\n useConfig,\n useDocumentInfo,\n useField,\n useLocale,\n useTranslation,\n} from '@payloadcms/ui'\nimport { useTransition } from 'react'\n\nimport type {\n PluginAltTextTranslationKeys,\n PluginAltTextTranslations,\n} from '../translations/index.js'\n\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\n\nexport function GenerateAltTextButton({ supportedMimeTypes }: { supportedMimeTypes?: string[] }) {\n const { t } = useTranslation<PluginAltTextTranslations, PluginAltTextTranslationKeys>()\n const { id, collectionSlug } = useDocumentInfo()\n const locale = useLocale()\n const [isPending, startTransition] = useTransition()\n const {\n config: {\n routes: { api: apiRoute },\n serverURL,\n },\n } = useConfig()\n\n const { setValue: setKeywords } = useField<string>({ path: 'keywords' })\n const { setValue: setAltText } = useField<string>({ path: 'alt' })\n const { value: mimeType } = useField<string>({ path: 'mimeType' })\n\n const isUnsupportedMimeType =\n !!mimeType && !!supportedMimeTypes && !supportedMimeTypes.includes(mimeType)\n\n const handleGenerateAltText = () => {\n if (!collectionSlug || !id) {\n toast.error(t('@jhb.software/payload-alt-text-plugin:cannotGenerateMissingFields'))\n throw new Error('Missing required fields')\n }\n\n startTransition(async () => {\n try {\n const response = await fetch(`${serverURL ?? ''}${apiRoute}/
|
|
1
|
+
{"version":3,"sources":["../../src/components/GenerateAltTextButton.tsx"],"sourcesContent":["'use client'\n\nimport {\n Button,\n toast,\n useConfig,\n useDocumentInfo,\n useField,\n useLocale,\n useTranslation,\n} from '@payloadcms/ui'\nimport { useTransition } from 'react'\n\nimport type {\n PluginAltTextTranslationKeys,\n PluginAltTextTranslations,\n} from '../translations/index.js'\n\nimport { PLUGIN_SLUG } from '../constants.js'\nimport { Lightning } from './icons/Lightning.js'\nimport { Spinner } from './icons/Spinner.js'\n\nexport function GenerateAltTextButton({ supportedMimeTypes }: { supportedMimeTypes?: string[] }) {\n const { t } = useTranslation<PluginAltTextTranslations, PluginAltTextTranslationKeys>()\n const { id, collectionSlug, docPermissions } = useDocumentInfo()\n const locale = useLocale()\n const [isPending, startTransition] = useTransition()\n const {\n config: {\n routes: { api: apiRoute },\n serverURL,\n },\n } = useConfig()\n\n const { setValue: setKeywords } = useField<string>({ path: 'keywords' })\n const { setValue: setAltText } = useField<string>({ path: 'alt' })\n const { value: mimeType } = useField<string>({ path: 'mimeType' })\n\n const isUnsupportedMimeType =\n !!mimeType && !!supportedMimeTypes && !supportedMimeTypes.includes(mimeType)\n\n // Hide the generate button from users who cannot update the document — the\n // generated alt text would not be persistable by them anyway.\n if (!docPermissions?.update) {\n return null\n }\n\n const handleGenerateAltText = () => {\n if (!collectionSlug || !id) {\n toast.error(t('@jhb.software/payload-alt-text-plugin:cannotGenerateMissingFields'))\n throw new Error('Missing required fields')\n }\n\n startTransition(async () => {\n try {\n const response = await fetch(`${serverURL ?? ''}${apiRoute}/${PLUGIN_SLUG}/generate`, {\n body: JSON.stringify({\n id: id as string,\n collection: collectionSlug,\n locale: locale?.code ?? null, // sent null when localization is disabled\n }),\n method: 'POST',\n })\n\n if (!response.ok) {\n let errorMessage = t('@jhb.software/payload-alt-text-plugin:failedToGenerate')\n try {\n const errorData = (await response.json()) as { error: string }\n errorMessage = errorData.error\n } catch (error) {\n console.error('Error generating alt text:', error)\n }\n\n toast.error(errorMessage)\n return\n }\n\n const data = (await response.json()) as {\n altText: string\n keywords: string[]\n }\n\n if (data.altText && data.keywords) {\n setAltText(data.altText)\n setKeywords(data.keywords)\n toast.success(t('@jhb.software/payload-alt-text-plugin:altTextGeneratedSuccess'))\n } else {\n toast.error(t('@jhb.software/payload-alt-text-plugin:noAltTextGenerated'))\n }\n } catch (error) {\n console.error('Error generating alt text:', error)\n toast.error(t('@jhb.software/payload-alt-text-plugin:errorGeneratingAltText'))\n }\n })\n }\n\n return (\n <div style={{ display: 'flex', gap: '20px', marginTop: '10px' }}>\n <div style={{ color: 'var(--theme-elevation-400)', flex: '1' }}>\n <p>{t('@jhb.software/payload-alt-text-plugin:altTextDescription')}</p>\n <ol style={{ margin: '10px 0', paddingLeft: '20px' }}>\n <li>{t('@jhb.software/payload-alt-text-plugin:altTextRequirement1')}</li>\n <li>{t('@jhb.software/payload-alt-text-plugin:altTextRequirement2')}</li>\n <li>{t('@jhb.software/payload-alt-text-plugin:altTextRequirement3')}</li>\n </ol>\n </div>\n <div style={{ alignItems: 'center', display: 'flex' }}>\n <Button\n disabled={isPending || !id || isUnsupportedMimeType}\n icon={isPending ? <Spinner /> : <Lightning />}\n onClick={handleGenerateAltText}\n tooltip={\n isUnsupportedMimeType\n ? t('@jhb.software/payload-alt-text-plugin:unsupportedMimeType', { mimeType })\n : !id\n ? t('@jhb.software/payload-alt-text-plugin:pleaseSaveDocumentFirst')\n : undefined\n }\n >\n {t('@jhb.software/payload-alt-text-plugin:generateAltText')}\n </Button>\n </div>\n </div>\n )\n}\n"],"names":["Button","toast","useConfig","useDocumentInfo","useField","useLocale","useTranslation","useTransition","PLUGIN_SLUG","Lightning","Spinner","GenerateAltTextButton","supportedMimeTypes","t","id","collectionSlug","docPermissions","locale","isPending","startTransition","config","routes","api","apiRoute","serverURL","setValue","setKeywords","path","setAltText","value","mimeType","isUnsupportedMimeType","includes","update","handleGenerateAltText","error","Error","response","fetch","body","JSON","stringify","collection","code","method","ok","errorMessage","errorData","json","console","data","altText","keywords","success","div","style","display","gap","marginTop","color","flex","p","ol","margin","paddingLeft","li","alignItems","disabled","icon","onClick","tooltip","undefined"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Short plugin slug used as the prefix for the plugin's REST endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Endpoints are served under `<routes.api>/<PLUGIN_SLUG>/...`, e.g.
|
|
5
|
+
* `/api/alt-text/generate` with the default API route.
|
|
6
|
+
*/ export const PLUGIN_SLUG = 'alt-text';
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts"],"sourcesContent":["/**\n * Short plugin slug used as the prefix for the plugin's REST endpoints.\n *\n * Endpoints are served under `<routes.api>/<PLUGIN_SLUG>/...`, e.g.\n * `/api/alt-text/generate` with the default API route.\n */\nexport const PLUGIN_SLUG = 'alt-text'\n"],"names":["PLUGIN_SLUG"],"mappings":"AAAA;;;;;CAKC,GACD,OAAO,MAAMA,cAAc,WAAU"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PLUGIN_SLUG } from '../constants.js';
|
|
1
2
|
import { ALT_TEXT_HEALTH_PLUGIN_SLUG, getAltTextHealth } from '../utilities/altTextHealth.js';
|
|
2
3
|
export const altTextHealthEndpoint = (access)=>async (req)=>{
|
|
3
4
|
if (!await access({
|
|
@@ -16,7 +17,7 @@ export const altTextHealthEndpoint = (access)=>async (req)=>{
|
|
|
16
17
|
req.payload.logger.error({
|
|
17
18
|
err: error,
|
|
18
19
|
msg: 'Failed to build alt text health response.',
|
|
19
|
-
path:
|
|
20
|
+
path: `/${PLUGIN_SLUG}/health`,
|
|
20
21
|
plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG
|
|
21
22
|
});
|
|
22
23
|
return Response.json({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/altTextHealth.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { ALT_TEXT_HEALTH_PLUGIN_SLUG, getAltTextHealth } from '../utilities/altTextHealth.js'\n\nexport const altTextHealthEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const health = await getAltTextHealth(req)\n\n return Response.json(health)\n } catch (error) {\n req.payload.logger.error({\n err: error,\n msg: 'Failed to build alt text health response.',\n path:
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/altTextHealth.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { PLUGIN_SLUG } from '../constants.js'\nimport { ALT_TEXT_HEALTH_PLUGIN_SLUG, getAltTextHealth } from '../utilities/altTextHealth.js'\n\nexport const altTextHealthEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const health = await getAltTextHealth(req)\n\n return Response.json(health)\n } catch (error) {\n req.payload.logger.error({\n err: error,\n msg: 'Failed to build alt text health response.',\n path: `/${PLUGIN_SLUG}/health`,\n plugin: ALT_TEXT_HEALTH_PLUGIN_SLUG,\n })\n\n return Response.json({ error: 'Failed to compute alt text health' }, { status: 500 })\n }\n }\n"],"names":["PLUGIN_SLUG","ALT_TEXT_HEALTH_PLUGIN_SLUG","getAltTextHealth","altTextHealthEndpoint","access","req","Response","json","error","status","health","payload","logger","err","msg","path","plugin"],"mappings":"AAIA,SAASA,WAAW,QAAQ,kBAAiB;AAC7C,SAASC,2BAA2B,EAAEC,gBAAgB,QAAQ,gCAA+B;AAE7F,OAAO,MAAMC,wBACX,CAACC,SACD,OAAOC;QACL,IAAI,CAAE,MAAMD,OAAO;YAAEC;QAAI,IAAK;YAC5B,OAAOC,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAe,GAAG;gBAAEC,QAAQ;YAAI;QAChE;QAEA,IAAI;YACF,MAAMC,SAAS,MAAMR,iBAAiBG;YAEtC,OAAOC,SAASC,IAAI,CAACG;QACvB,EAAE,OAAOF,OAAO;YACdH,IAAIM,OAAO,CAACC,MAAM,CAACJ,KAAK,CAAC;gBACvBK,KAAKL;gBACLM,KAAK;gBACLC,MAAM,CAAC,CAAC,EAAEf,YAAY,OAAO,CAAC;gBAC9BgB,QAAQf;YACV;YAEA,OAAOK,SAASC,IAAI,CAAC;gBAAEC,OAAO;YAAoC,GAAG;gBAAEC,QAAQ;YAAI;QACrF;IACF,EAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pMap from 'p-map';
|
|
2
|
+
import { APIError, Forbidden } from 'payload';
|
|
2
3
|
import { ZodError } from 'zod';
|
|
3
4
|
import { localesFromConfig } from '../utilities/localesFromConfig.js';
|
|
4
5
|
import { matchesMimeType } from '../utilities/mimeTypes.js';
|
|
@@ -29,6 +30,17 @@ import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'
|
|
|
29
30
|
status: 500
|
|
30
31
|
});
|
|
31
32
|
}
|
|
33
|
+
// Treat the configured collections as an allowlist. Reject any other
|
|
34
|
+
// collection before touching the Local API, so the endpoint can only ever
|
|
35
|
+
// operate on the upload collections the plugin manages.
|
|
36
|
+
const collectionConfig = pluginConfig.collections.find((entry)=>entry.slug === collection);
|
|
37
|
+
if (!collectionConfig) {
|
|
38
|
+
return Response.json({
|
|
39
|
+
error: `Collection "${collection}" is not managed by the alt text plugin.`
|
|
40
|
+
}, {
|
|
41
|
+
status: 403
|
|
42
|
+
});
|
|
43
|
+
}
|
|
32
44
|
if (!pluginConfig.resolver) {
|
|
33
45
|
return Response.json({
|
|
34
46
|
error: 'No alt text resolver configured'
|
|
@@ -37,6 +49,19 @@ import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'
|
|
|
37
49
|
});
|
|
38
50
|
}
|
|
39
51
|
const concurrency = pluginConfig.maxBulkGenerateConcurrency;
|
|
52
|
+
// De-duplicate so the same image is never generated (and billed) twice,
|
|
53
|
+
// then bound the batch so a single request cannot fan out into an
|
|
54
|
+
// unbounded number of paid resolver calls.
|
|
55
|
+
const uniqueIds = [
|
|
56
|
+
...new Set(ids)
|
|
57
|
+
];
|
|
58
|
+
if (uniqueIds.length > pluginConfig.maxBulkGenerateIds) {
|
|
59
|
+
return Response.json({
|
|
60
|
+
error: `Too many ids: ${uniqueIds.length} exceeds the maximum of ${pluginConfig.maxBulkGenerateIds} per request.`
|
|
61
|
+
}, {
|
|
62
|
+
status: 400
|
|
63
|
+
});
|
|
64
|
+
}
|
|
40
65
|
// determine target locales based on config
|
|
41
66
|
const locales = localesFromConfig(req.payload.config);
|
|
42
67
|
const targetLocales = locales ?? [
|
|
@@ -49,7 +74,7 @@ import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'
|
|
|
49
74
|
status: 500
|
|
50
75
|
});
|
|
51
76
|
}
|
|
52
|
-
await pMap(
|
|
77
|
+
await pMap(uniqueIds, async (id)=>{
|
|
53
78
|
try {
|
|
54
79
|
await generateAndUpdateAltText({
|
|
55
80
|
id,
|
|
@@ -60,8 +85,15 @@ import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'
|
|
|
60
85
|
req
|
|
61
86
|
});
|
|
62
87
|
updatedDocs++;
|
|
63
|
-
console.log(`${updatedDocs}/${
|
|
88
|
+
console.log(`${updatedDocs}/${uniqueIds.length} updated (${Math.round(updatedDocs / uniqueIds.length * 100)}%)`);
|
|
64
89
|
} catch (error) {
|
|
90
|
+
// A Forbidden means the user has no read/update access to the
|
|
91
|
+
// collection at all — it applies to every id, so fail the whole
|
|
92
|
+
// request with a real 403 instead of silently listing all ids as
|
|
93
|
+
// errored. Row-level NotFound stays a per-doc error (partial success).
|
|
94
|
+
if (error instanceof Forbidden) {
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
65
97
|
console.error(`Error generating alt text for ${id}:`, error);
|
|
66
98
|
erroredDocs.push(id);
|
|
67
99
|
}
|
|
@@ -73,7 +105,7 @@ import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'
|
|
|
73
105
|
}
|
|
74
106
|
return Response.json({
|
|
75
107
|
erroredDocs,
|
|
76
|
-
totalDocs:
|
|
108
|
+
totalDocs: uniqueIds.length,
|
|
77
109
|
updatedDocs
|
|
78
110
|
});
|
|
79
111
|
} catch (error) {
|
|
@@ -82,6 +114,15 @@ import { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'
|
|
|
82
114
|
status: 400
|
|
83
115
|
});
|
|
84
116
|
}
|
|
117
|
+
// Surface Payload access errors (Forbidden 403) with their real status so
|
|
118
|
+
// an agent gets an accurate, non-retryable signal instead of a 500.
|
|
119
|
+
if (error instanceof APIError) {
|
|
120
|
+
return Response.json({
|
|
121
|
+
error: error.message
|
|
122
|
+
}, {
|
|
123
|
+
status: error.status
|
|
124
|
+
});
|
|
125
|
+
}
|
|
85
126
|
console.error('Error in bulk generation:', error);
|
|
86
127
|
return Response.json({
|
|
87
128
|
error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
@@ -94,14 +135,20 @@ async function generateAndUpdateAltText({ id, collection, locales, payload, plug
|
|
|
94
135
|
const imageDoc = await payload.findByID({
|
|
95
136
|
id,
|
|
96
137
|
collection,
|
|
97
|
-
depth: 0
|
|
138
|
+
depth: 0,
|
|
139
|
+
// Run under the requesting user's access, not Payload's default
|
|
140
|
+
// `overrideAccess: true`, so collection-level access control applies.
|
|
141
|
+
overrideAccess: false,
|
|
142
|
+
user: req.user
|
|
98
143
|
});
|
|
99
144
|
if (!imageDoc) {
|
|
100
145
|
throw new Error('Image not found');
|
|
101
146
|
}
|
|
102
147
|
const mimeType = 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined;
|
|
148
|
+
// The handler validates `collection` against the configured collections before
|
|
149
|
+
// reaching this helper, so a matching entry is guaranteed.
|
|
103
150
|
const collectionConfig = pluginConfig.collections.find((entry)=>entry.slug === collection);
|
|
104
|
-
if (mimeType &&
|
|
151
|
+
if (mimeType && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {
|
|
105
152
|
throw new Error(`Alt text is not tracked for files of type "${mimeType}" in the "${collection}" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`);
|
|
106
153
|
}
|
|
107
154
|
if (mimeType && pluginConfig.resolver.supportedMimeTypes && !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)) {
|
|
@@ -127,7 +174,11 @@ async function generateAndUpdateAltText({ id, collection, locales, payload, plug
|
|
|
127
174
|
alt: localeResult.altText,
|
|
128
175
|
keywords: localeResult.keywords
|
|
129
176
|
},
|
|
130
|
-
locale
|
|
177
|
+
locale,
|
|
178
|
+
// Run under the requesting user's access, not Payload's default
|
|
179
|
+
// `overrideAccess: true`, so collection-level access control applies.
|
|
180
|
+
overrideAccess: false,
|
|
181
|
+
user: req.user
|
|
131
182
|
});
|
|
132
183
|
}
|
|
133
184
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/endpoints/bulkGenerateAltTexts.ts"],"sourcesContent":["import type { BasePayload, CollectionSlug, PayloadHandler, PayloadRequest } from 'payload'\n\nimport pMap from 'p-map'\nimport { ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { localesFromConfig } from '../utilities/localesFromConfig.js'\nimport { matchesMimeType } from '../utilities/mimeTypes.js'\nimport { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'\n\n/**\n * Generates and updates alt text for multiple images in all locales.\n */\nexport const bulkGenerateAltTextsEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const { collection, ids } = bulkGenerateAltTextsRequestSchema.parse(data)\n\n let updatedDocs = 0\n const erroredDocs: (number | string)[] = []\n\n // Get plugin config from payload config\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n | AltTextPluginConfig\n | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const concurrency = pluginConfig.maxBulkGenerateConcurrency\n\n // determine target locales based on config\n const locales = localesFromConfig(req.payload.config)\n const targetLocales = locales ?? [pluginConfig.locale!]\n if (!targetLocales) {\n return Response.json(\n {\n error:\n 'Could not determine target locales for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n await pMap(\n ids,\n async (id) => {\n try {\n await generateAndUpdateAltText({\n id,\n collection,\n locales: targetLocales,\n payload: req.payload,\n pluginConfig,\n req,\n })\n updatedDocs++\n console.log(\n `${updatedDocs}/${ids.length} updated (${Math.round((updatedDocs / ids.length) * 100)}%)`,\n )\n } catch (error) {\n console.error(`Error generating alt text for ${id}:`, error)\n erroredDocs.push(id)\n }\n },\n { concurrency },\n )\n\n if (erroredDocs.length > 0) {\n console.error(`Failed for: ${erroredDocs.join(', ')}`)\n }\n\n return Response.json({\n erroredDocs,\n totalDocs: ids.length,\n updatedDocs,\n })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(formatZodError(error), { status: 400 })\n }\n console.error('Error in bulk generation:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n\nasync function generateAndUpdateAltText({\n id,\n collection,\n locales,\n payload,\n pluginConfig,\n req,\n}: {\n collection: CollectionSlug\n id: number | string\n locales: string[]\n payload: BasePayload\n pluginConfig: AltTextPluginConfig\n req: PayloadRequest\n}) {\n const imageDoc = await payload.findByID({\n id,\n collection,\n depth: 0,\n })\n\n if (!imageDoc) {\n throw new Error('Image not found')\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined\n\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (mimeType && collectionConfig && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n throw new Error(\n `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n )\n }\n\n if (\n mimeType &&\n pluginConfig.resolver.supportedMimeTypes &&\n !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)\n ) {\n throw new Error(\n `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`,\n )\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolveBulk({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locales,\n req,\n })\n\n if (!result.success) {\n throw new Error(result.error || 'Failed to generate alt text')\n }\n\n for (const locale of locales) {\n const localeResult = result.results[locale]\n if (localeResult) {\n await payload.update({\n id,\n collection,\n data: {\n alt: localeResult.altText,\n keywords: localeResult.keywords,\n },\n locale,\n })\n }\n }\n}\n"],"names":["pMap","ZodError","localesFromConfig","matchesMimeType","bulkGenerateAltTextsRequestSchema","formatZodError","bulkGenerateAltTextsEndpoint","access","req","Response","json","error","status","data","collection","ids","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","resolver","concurrency","maxBulkGenerateConcurrency","locales","targetLocales","locale","id","generateAndUpdateAltText","console","log","length","Math","round","push","join","totalDocs","Error","message","imageDoc","findByID","depth","mimeType","undefined","collectionConfig","collections","find","entry","slug","mimeTypes","supportedMimeTypes","includes","imageThumbnailUrl","getImageThumbnail","result","resolveBulk","filename","success","localeResult","results","update","alt","altText","keywords"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/bulkGenerateAltTexts.ts"],"sourcesContent":["import type { BasePayload, CollectionSlug, PayloadHandler, PayloadRequest } from 'payload'\n\nimport pMap from 'p-map'\nimport { APIError, Forbidden } from 'payload'\nimport { ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { localesFromConfig } from '../utilities/localesFromConfig.js'\nimport { matchesMimeType } from '../utilities/mimeTypes.js'\nimport { bulkGenerateAltTextsRequestSchema, formatZodError } from './schemas.js'\n\n/**\n * Generates and updates alt text for multiple images in all locales.\n */\nexport const bulkGenerateAltTextsEndpoint =\n (access: AltTextPluginConfig['access']): PayloadHandler =>\n async (req: PayloadRequest) => {\n try {\n if (!(await access({ req }))) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const data = 'json' in req && typeof req.json === 'function' ? await req.json() : null\n\n const { collection, ids } = bulkGenerateAltTextsRequestSchema.parse(data)\n\n let updatedDocs = 0\n const erroredDocs: (number | string)[] = []\n\n // Get plugin config from payload config\n const pluginConfig = req.payload.config.custom?.altTextPluginConfig as\n | AltTextPluginConfig\n | undefined\n\n if (!pluginConfig) {\n return Response.json({ error: 'Plugin config not found' }, { status: 500 })\n }\n\n // Treat the configured collections as an allowlist. Reject any other\n // collection before touching the Local API, so the endpoint can only ever\n // operate on the upload collections the plugin manages.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (!collectionConfig) {\n return Response.json(\n { error: `Collection \"${collection}\" is not managed by the alt text plugin.` },\n { status: 403 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const concurrency = pluginConfig.maxBulkGenerateConcurrency\n\n // De-duplicate so the same image is never generated (and billed) twice,\n // then bound the batch so a single request cannot fan out into an\n // unbounded number of paid resolver calls.\n const uniqueIds = [...new Set(ids)]\n\n if (uniqueIds.length > pluginConfig.maxBulkGenerateIds) {\n return Response.json(\n {\n error: `Too many ids: ${uniqueIds.length} exceeds the maximum of ${pluginConfig.maxBulkGenerateIds} per request.`,\n },\n { status: 400 },\n )\n }\n\n // determine target locales based on config\n const locales = localesFromConfig(req.payload.config)\n const targetLocales = locales ?? [pluginConfig.locale!]\n if (!targetLocales) {\n return Response.json(\n {\n error:\n 'Could not determine target locales for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n await pMap(\n uniqueIds,\n async (id) => {\n try {\n await generateAndUpdateAltText({\n id,\n collection,\n locales: targetLocales,\n payload: req.payload,\n pluginConfig,\n req,\n })\n updatedDocs++\n console.log(\n `${updatedDocs}/${uniqueIds.length} updated (${Math.round((updatedDocs / uniqueIds.length) * 100)}%)`,\n )\n } catch (error) {\n // A Forbidden means the user has no read/update access to the\n // collection at all — it applies to every id, so fail the whole\n // request with a real 403 instead of silently listing all ids as\n // errored. Row-level NotFound stays a per-doc error (partial success).\n if (error instanceof Forbidden) {\n throw error\n }\n console.error(`Error generating alt text for ${id}:`, error)\n erroredDocs.push(id)\n }\n },\n { concurrency },\n )\n\n if (erroredDocs.length > 0) {\n console.error(`Failed for: ${erroredDocs.join(', ')}`)\n }\n\n return Response.json({\n erroredDocs,\n totalDocs: uniqueIds.length,\n updatedDocs,\n })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(formatZodError(error), { status: 400 })\n }\n // Surface Payload access errors (Forbidden 403) with their real status so\n // an agent gets an accurate, non-retryable signal instead of a 500.\n if (error instanceof APIError) {\n return Response.json({ error: error.message }, { status: error.status })\n }\n console.error('Error in bulk generation:', error)\n return Response.json(\n {\n error: `Error generating alt text: ${error instanceof Error ? error.message : 'Unknown error'}`,\n },\n { status: 500 },\n )\n }\n }\n\nasync function generateAndUpdateAltText({\n id,\n collection,\n locales,\n payload,\n pluginConfig,\n req,\n}: {\n collection: CollectionSlug\n id: number | string\n locales: string[]\n payload: BasePayload\n pluginConfig: AltTextPluginConfig\n req: PayloadRequest\n}) {\n const imageDoc = await payload.findByID({\n id,\n collection,\n depth: 0,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n\n if (!imageDoc) {\n throw new Error('Image not found')\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined\n\n // The handler validates `collection` against the configured collections before\n // reaching this helper, so a matching entry is guaranteed.\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)!\n\n if (mimeType && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n throw new Error(\n `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n )\n }\n\n if (\n mimeType &&\n pluginConfig.resolver.supportedMimeTypes &&\n !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)\n ) {\n throw new Error(\n `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`,\n )\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolveBulk({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locales,\n req,\n })\n\n if (!result.success) {\n throw new Error(result.error || 'Failed to generate alt text')\n }\n\n for (const locale of locales) {\n const localeResult = result.results[locale]\n if (localeResult) {\n await payload.update({\n id,\n collection,\n data: {\n alt: localeResult.altText,\n keywords: localeResult.keywords,\n },\n locale,\n // Run under the requesting user's access, not Payload's default\n // `overrideAccess: true`, so collection-level access control applies.\n overrideAccess: false,\n user: req.user,\n })\n }\n }\n}\n"],"names":["pMap","APIError","Forbidden","ZodError","localesFromConfig","matchesMimeType","bulkGenerateAltTextsRequestSchema","formatZodError","bulkGenerateAltTextsEndpoint","access","req","Response","json","error","status","data","collection","ids","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","collectionConfig","collections","find","entry","slug","resolver","concurrency","maxBulkGenerateConcurrency","uniqueIds","Set","length","maxBulkGenerateIds","locales","targetLocales","locale","id","generateAndUpdateAltText","console","log","Math","round","push","join","totalDocs","message","Error","imageDoc","findByID","depth","overrideAccess","user","mimeType","undefined","mimeTypes","supportedMimeTypes","includes","imageThumbnailUrl","getImageThumbnail","result","resolveBulk","filename","success","localeResult","results","update","alt","altText","keywords"],"mappings":"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"}
|