@jhb.software/payload-alt-text-plugin 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +62 -33
  2. package/dist/components/AltTextField.d.ts +1 -1
  3. package/dist/components/AltTextField.js +9 -0
  4. package/dist/components/AltTextField.js.map +1 -1
  5. package/dist/components/BulkGenerateAltTextsButton.js +5 -9
  6. package/dist/components/BulkGenerateAltTextsButton.js.map +1 -1
  7. package/dist/components/icons/ArrowRightIcon.js +6 -7
  8. package/dist/components/icons/ArrowRightIcon.js.map +1 -1
  9. package/dist/components/icons/CheckIcon.js +6 -7
  10. package/dist/components/icons/CheckIcon.js.map +1 -1
  11. package/dist/components/icons/ImageIcon.js +9 -26
  12. package/dist/components/icons/ImageIcon.js.map +1 -1
  13. package/dist/components/icons/Lightning.js +8 -4
  14. package/dist/components/icons/Lightning.js.map +1 -1
  15. package/dist/endpoints/bulkGenerateAltTexts.js +5 -0
  16. package/dist/endpoints/bulkGenerateAltTexts.js.map +1 -1
  17. package/dist/endpoints/generateAltText.js +9 -0
  18. package/dist/endpoints/generateAltText.js.map +1 -1
  19. package/dist/fields/altTextField.d.ts +7 -2
  20. package/dist/fields/altTextField.js +5 -19
  21. package/dist/fields/altTextField.js.map +1 -1
  22. package/dist/hooks/revalidateAltTextHealth.js +5 -0
  23. package/dist/hooks/revalidateAltTextHealth.js.map +1 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/plugin.js +23 -47
  28. package/dist/plugin.js.map +1 -1
  29. package/dist/translations/de.js +4 -5
  30. package/dist/translations/de.js.map +1 -1
  31. package/dist/translations/en.js +3 -4
  32. package/dist/translations/en.js.map +1 -1
  33. package/dist/translations/translation-schema.json +4 -6
  34. package/dist/types/AltTextPluginConfig.d.ts +21 -5
  35. package/dist/types/AltTextPluginConfig.js.map +1 -1
  36. package/dist/utilities/altTextHealth.js +18 -10
  37. package/dist/utilities/altTextHealth.js.map +1 -1
  38. package/dist/utilities/mimeTypes.d.ts +70 -0
  39. package/dist/utilities/mimeTypes.js +115 -0
  40. package/dist/utilities/mimeTypes.js.map +1 -0
  41. package/package.json +14 -12
package/README.md CHANGED
@@ -12,20 +12,17 @@ A [Payload CMS](https://payloadcms.com/) plugin that adds AI-powered alt text ge
12
12
  - Full localization support
13
13
  - Dashboard health widget with cached coverage insights across all configured upload collections
14
14
 
15
-
16
15
  When the plugin is enabled for an upload collection, it will:
17
16
 
18
17
  1. Add an alt text field to the collection
19
18
  - A button to AI-generate the alt text
20
19
  - This field will include a description of what the alt text should be
21
20
  2. Add a keywords fields to the collection
22
- - This field will be automatically filled when generating the alt text
21
+ - This field will be automatically filled when generating the alt text
23
22
  - It will be used for improving the search of images in the admin panel
24
- 2. Add a bulk generate button to the collection list view
23
+ 3. Add a bulk generate button to the collection list view
25
24
  - This button will allow you to generate alt text for multiple images at once
26
- 3. Register an `Alt text health` dashboard widget
27
- - The widget is available in Payload's dashboard editor
28
- - It is added to the default dashboard layout for first-time and reset layouts
25
+ 4. Register an `Alt text health` dashboard widget
29
26
  - Results are cached and revalidated when documents in the configured upload collections change
30
27
 
31
28
  ## Installation
@@ -39,10 +36,7 @@ pnpm add @jhb.software/payload-alt-text-plugin
39
36
  Install the plugin and add it to your Payload config:
40
37
 
41
38
  ```ts
42
- import {
43
- payloadAltTextPlugin,
44
- openAIResolver,
45
- } from '@jhb.software/payload-alt-text-plugin'
39
+ import { payloadAltTextPlugin, openAIResolver } from '@jhb.software/payload-alt-text-plugin'
46
40
 
47
41
  export default buildConfig({
48
42
  plugins: [
@@ -60,25 +54,60 @@ export default buildConfig({
60
54
 
61
55
  Note: When localization is disabled in your Payload config (default), you need to specify the locale to generate the alt texts in via the `locale` plugin option.
62
56
 
57
+ ### Admin list search
58
+
59
+ By default, the plugin sets `admin.listSearchableFields` on the configured upload collections to `['filename', 'keywords', 'alt']` so the admin list-view search matches against these fields. To opt out, set `admin.listSearchableFields` on the collection yourself — any explicit value is preserved as-is:
60
+
61
+ ```ts
62
+ {
63
+ slug: 'media',
64
+ upload: true,
65
+ admin: {
66
+ listSearchableFields: ['filename', 'alt'],
67
+ },
68
+ // ...
69
+ }
70
+ ```
71
+
72
+ This is also the recommended escape hatch if you hit Payload's Postgres SQL-builder bug for `hasMany` localized text fields in `listSearchableFields` (see [#92](https://github.com/jhb-software/payload-plugins/issues/92)).
73
+
63
74
  ## Configuration
64
75
 
65
76
  ### Plugin Options
66
77
 
67
- | Option | Type | Required | Description |
68
- | ---------------------------- | ------------------ | -------- | ------------------------------------------------------------------------ |
69
- | `collections` | `CollectionSlug[]` | Yes | Collections to enable alt text generation for |
70
- | `resolver` | `AltTextResolver` | Yes | Alt text resolver to use (e.g., `openAIResolver`) |
71
- | `getImageThumbnail` | `Function` | Yes | Function to get the thumbnail URL from an image document |
72
- | `enabled` | `boolean` | No | Whether to enable the plugin |
73
- | `locale` | `string` | No | Locale for alt text generation (required when localization is disabled) |
74
- | `maxBulkGenerateConcurrency` | `number` | No | Maximum concurrent API requests for bulk operations (default: 16) |
75
- | `fieldsOverride` | `Function` | No | Override the default fields inserted by the plugin |
78
+ | Option | Type | Required | Description |
79
+ | ---------------------------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------- |
80
+ | `collections` | `CollectionSlug[]` | Yes | Collections to enable alt text generation for |
81
+ | `resolver` | `AltTextResolver` | Yes | Alt text resolver to use (e.g., `openAIResolver`) |
82
+ | `getImageThumbnail` | `Function` | Yes | Function to get the thumbnail URL from an image document |
83
+ | `enabled` | `boolean` | No | Whether to enable the plugin |
84
+ | `locale` | `string` | No | Locale for alt text generation (required when localization is disabled) |
85
+ | `maxBulkGenerateConcurrency` | `number` | No | Maximum concurrent API requests for bulk operations (default: 16) |
86
+ | `fieldsOverride` | `Function` | No | Override the default fields inserted by the plugin |
76
87
  | `healthCheck` | `boolean` | No | Enable alt text health tracking: REST endpoint, cache revalidation hooks, and dashboard widget (default: `true`) |
77
88
 
78
89
  ## Dashboard Widget
79
90
 
80
91
  The plugin registers an `Alt text health` dashboard widget that shows alt text coverage across all configured upload collections, with cached queries that revalidate on document changes. Collections with missing alt text show a clickable badge linking to the affected images.
81
92
 
93
+ <img width="696" height="246" alt="image" src="https://github.com/user-attachments/assets/75df7349-0307-4047-b1ac-6b2ee0814464" />
94
+
95
+ The widget is registered under `admin.dashboard.widgets` with the slug `alt-text-health`. To show it by default on the dashboard, add it to your `admin.dashboard.defaultLayout`:
96
+
97
+ ```ts
98
+ buildConfig({
99
+ admin: {
100
+ dashboard: {
101
+ defaultLayout: [
102
+ // ...other default widgets
103
+ { widgetSlug: 'alt-text-health', width: 'full' },
104
+ ],
105
+ },
106
+ },
107
+ // ...
108
+ })
109
+ ```
110
+
82
111
  Set `healthCheck: false` in the plugin config to disable the REST endpoint, cache revalidation hooks, and dashboard widget. If your project replaces the default dashboard via `admin.components.views.dashboard`, you need to integrate the widget into your custom dashboard yourself.
83
112
 
84
113
  ### Resolvers
@@ -117,12 +146,12 @@ export const customResolver = (): AltTextResolver => ({
117
146
  }
118
147
  },
119
148
  resolveBulk: async ({ imageThumbnailUrl, filename, locales, req }) => {
120
- // Your custom alt text generation logic here
149
+ // Your custom alt text generation logic here
121
150
  const altTexts = await generateAltTextBulk(imageThumbnailUrl, filename, locales, req)
122
151
 
123
- return {
124
- success: true,
125
- results: altTexts
152
+ return {
153
+ success: true,
154
+ results: altTexts,
126
155
  }
127
156
  },
128
157
  })
@@ -138,12 +167,12 @@ Generates alt text for a single image. By default, returns the result without sa
138
167
 
139
168
  **Request body:**
140
169
 
141
- | Field | Type | Required | Description |
142
- | ------------ | ------------------ | -------- | ------------------------------------------------------------------------ |
143
- | `id` | `string \| number` | Yes | The document ID |
144
- | `collection` | `string` | Yes | The collection slug |
145
- | `locale` | `string \| null` | Yes | Target locale (use `null` for non-localized setups) |
146
- | `update` | `boolean` | No | When `true`, persists the result to the document (default: `false`) |
170
+ | Field | Type | Required | Description |
171
+ | ------------ | ------------------ | -------- | ------------------------------------------------------------------- |
172
+ | `id` | `string \| number` | Yes | The document ID |
173
+ | `collection` | `string` | Yes | The collection slug |
174
+ | `locale` | `string \| null` | Yes | Target locale (use `null` for non-localized setups) |
175
+ | `update` | `boolean` | No | When `true`, persists the result to the document (default: `false`) |
147
176
 
148
177
  **Response:**
149
178
 
@@ -162,10 +191,10 @@ Generates and persists alt text for multiple images across all configured locale
162
191
 
163
192
  **Request body:**
164
193
 
165
- | Field | Type | Required | Description |
166
- | ------------ | -------------------- | -------- | ------------------------------------------------------------------- |
167
- | `collection` | `string` | Yes | The collection slug |
168
- | `ids` | `(string \| number)[]` | Yes | Array of document IDs to process |
194
+ | Field | Type | Required | Description |
195
+ | ------------ | ---------------------- | -------- | -------------------------------- |
196
+ | `collection` | `string` | Yes | The collection slug |
197
+ | `ids` | `(string \| number)[]` | Yes | Array of document IDs to process |
169
198
 
170
199
  **Response:**
171
200
 
@@ -1,2 +1,2 @@
1
1
  import type { TextareaFieldClientProps } from 'payload';
2
- export declare const AltTextField: (clientProps: TextareaFieldClientProps) => import("react").JSX.Element;
2
+ export declare const AltTextField: (clientProps: TextareaFieldClientProps) => import("react").JSX.Element | null;
@@ -1,14 +1,23 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { FieldLabel, TextareaInput, useDocumentInfo, useField } from '@payloadcms/ui';
4
+ import { matchesMimeType } from '../utilities/mimeTypes.js';
4
5
  import { GenerateAltTextButton } from './GenerateAltTextButton.js';
5
6
  export const AltTextField = (clientProps)=>{
6
7
  const { field, path } = clientProps;
7
8
  const supportedMimeTypes = field.admin?.custom?.supportedMimeTypes;
9
+ const trackedMimeTypes = field.admin?.custom?.trackedMimeTypes;
8
10
  const { setValue, value } = useField({
9
11
  path
10
12
  });
11
13
  const { id } = useDocumentInfo();
14
+ const { value: mimeType } = useField({
15
+ path: 'mimeType'
16
+ });
17
+ const isTrackedMimeType = !trackedMimeTypes || trackedMimeTypes.length === 0 || !!mimeType && matchesMimeType(mimeType, trackedMimeTypes);
18
+ if (!isTrackedMimeType) {
19
+ return null;
20
+ }
12
21
  // the field should be optional when the document is created
13
22
  // (since the alt text generation can only be used once the document is created and the image uploaded)
14
23
  const required = id ? field.required : false;
@@ -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 { 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\n const { setValue, value } = useField<string>({ path })\n const { id } = useDocumentInfo()\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} />}\n onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setValue(e.target.value)}\n path={path}\n required={required}\n value={value}\n />\n </div>\n </div>\n )\n}\n"],"names":["FieldLabel","TextareaInput","useDocumentInfo","useField","GenerateAltTextButton","AltTextField","clientProps","field","path","supportedMimeTypes","admin","custom","setValue","value","id","required","div","className","style","flex","htmlFor","label","localized","AfterInput","onChange","e","target"],"mappings":"AAAA;;AAIA,SAASA,UAAU,EAAEC,aAAa,EAAEC,eAAe,EAAEC,QAAQ,QAAQ,iBAAgB;AAErF,SAASC,qBAAqB,QAAQ,6BAA4B;AAElE,OAAO,MAAMC,eAAe,CAACC;IAC3B,MAAM,EAAEC,KAAK,EAAEC,IAAI,EAAE,GAAGF;IAExB,MAAMG,qBAAqBF,MAAMG,KAAK,EAAEC,QAAQF;IAEhD,MAAM,EAAEG,QAAQ,EAAEC,KAAK,EAAE,GAAGV,SAAiB;QAAEK;IAAK;IACpD,MAAM,EAAEM,EAAE,EAAE,GAAGZ;IAEf,4DAA4D;IAC5D,uGAAuG;IACvG,MAAMa,WAAWD,KAAKP,MAAMQ,QAAQ,GAAG;IAEvC,qBACE,MAACC;QAAIC,WAAU;QAAsBC,OAAO;YAAEC,MAAM;QAAW;;0BAC7D,KAACnB;gBACCoB,SAAS,CAAC,MAAM,EAAEZ,MAAM;gBACxBa,OAAOd,MAAMc,KAAK;gBAClBC,WAAWf,MAAMe,SAAS;gBAC1BP,UAAUA;;0BAGZ,KAACC;gBAAIC,WAAU;0BACb,cAAA,KAAChB;oBACCsB,0BAAY,KAACnB;wBAAsBK,oBAAoBA;;oBACvDe,UAAU,CAACC,IAA8Cb,SAASa,EAAEC,MAAM,CAACb,KAAK;oBAChFL,MAAMA;oBACNO,UAAUA;oBACVF,OAAOA;;;;;AAKjB,EAAC"}
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} />}\n onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setValue(e.target.value)}\n path={path}\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","supportedMimeTypes","admin","custom","trackedMimeTypes","setValue","value","id","mimeType","isTrackedMimeType","length","required","div","className","style","flex","htmlFor","label","localized","AfterInput","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,EAAE,GAAGF;IAExB,MAAMG,qBAAqBF,MAAMG,KAAK,EAAEC,QAAQF;IAChD,MAAMG,mBAAmBL,MAAMG,KAAK,EAAEC,QAAQC;IAE9C,MAAM,EAAEC,QAAQ,EAAEC,KAAK,EAAE,GAAGZ,SAAiB;QAAEM;IAAK;IACpD,MAAM,EAAEO,EAAE,EAAE,GAAGd;IACf,MAAM,EAAEa,OAAOE,QAAQ,EAAE,GAAGd,SAAiB;QAAEM,MAAM;IAAW;IAEhE,MAAMS,oBACJ,CAACL,oBACDA,iBAAiBM,MAAM,KAAK,KAC3B,CAAC,CAACF,YAAYb,gBAAgBa,UAAUJ;IAE3C,IAAI,CAACK,mBAAmB;QACtB,OAAO;IACT;IAEA,4DAA4D;IAC5D,uGAAuG;IACvG,MAAME,WAAWJ,KAAKR,MAAMY,QAAQ,GAAG;IAEvC,qBACE,MAACC;QAAIC,WAAU;QAAsBC,OAAO;YAAEC,MAAM;QAAW;;0BAC7D,KAACxB;gBACCyB,SAAS,CAAC,MAAM,EAAEhB,MAAM;gBACxBiB,OAAOlB,MAAMkB,KAAK;gBAClBC,WAAWnB,MAAMmB,SAAS;gBAC1BP,UAAUA;;0BAGZ,KAACC;gBAAIC,WAAU;0BACb,cAAA,KAACrB;oBACC2B,0BAAY,KAACvB;wBAAsBK,oBAAoBA;;oBACvDmB,UAAU,CAACC,IAA8ChB,SAASgB,EAAEC,MAAM,CAAChB,KAAK;oBAChFN,MAAMA;oBACNW,UAAUA;oBACVL,OAAOA;;;;;AAKjB,EAAC"}
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { Button, toast, useSelection, useTranslation } from '@payloadcms/ui';
4
4
  import { useRouter } from 'next/navigation.js';
5
5
  import { useTransition } from 'react';
@@ -63,18 +63,14 @@ export function BulkGenerateAltTextsButton({ collectionSlug }) {
63
63
  display: 'flex',
64
64
  justifyContent: 'right'
65
65
  },
66
- children: /*#__PURE__*/ _jsxs(Button, {
66
+ children: /*#__PURE__*/ _jsx(Button, {
67
67
  className: "m-0",
68
68
  disabled: isPending || selectedIds.length === 0,
69
69
  icon: isPending ? /*#__PURE__*/ _jsx(Spinner, {}) : /*#__PURE__*/ _jsx(Lightning, {}),
70
70
  onClick: handleGenerateAltTexts,
71
- children: [
72
- t('@jhb.software/payload-alt-text-plugin:generateAltTextFor'),
73
- " ",
74
- selectedIds.length,
75
- ' ',
76
- selectedIds.length === 1 ? t('@jhb.software/payload-alt-text-plugin:image') : t('@jhb.software/payload-alt-text-plugin:images')
77
- ]
71
+ children: t('@jhb.software/payload-alt-text-plugin:generateAltTextFor', {
72
+ count: selectedIds.length
73
+ })
78
74
  })
79
75
  });
80
76
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/BulkGenerateAltTextsButton.tsx"],"sourcesContent":["'use client'\n\nimport { Button, toast, 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\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('/api/alt-text-plugin/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 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')} {selectedIds.length}{' '}\n {selectedIds.length === 1\n ? t('@jhb.software/payload-alt-text-plugin:image')\n : t('@jhb.software/payload-alt-text-plugin:images')}\n </Button>\n </div>\n )\n )\n}\n"],"names":["Button","toast","useSelection","useTranslation","useRouter","useTransition","Lightning","Spinner","BulkGenerateAltTextsButton","collectionSlug","t","isPending","startTransition","selected","setSelection","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,YAAY,EAAEC,cAAc,QAAQ,iBAAgB;AAC5E,SAASC,SAAS,QAAQ,qBAAoB;AAC9C,SAASC,aAAa,QAAQ,QAAO;AAOrC,SAASC,SAAS,QAAQ,uBAAsB;AAChD,SAASC,OAAO,QAAQ,qBAAoB;AAE5C,OAAO,SAASC,2BAA2B,EAAEC,cAAc,EAA8B;IACvF,MAAM,EAAEC,CAAC,EAAE,GAAGP;IACd,MAAM,CAACQ,WAAWC,gBAAgB,GAAGP;IACrC,MAAM,EAAEQ,QAAQ,EAAEC,YAAY,EAAE,GAAGZ;IAEnC,MAAMa,cAAcC,MAAMC,IAAI,CAACJ,SAASK,OAAO,IAC5CC,MAAM,CAAC,CAAC,GAAGC,WAAW,GAAKA,YAC3BC,GAAG,CAAC,CAAC,CAACC,GAAG,GAAKA;IAEjB,MAAMC,SAASnB;IAEf,MAAMoB,yBAAyB;QAC7BZ,gBAAgB;YACd,IAAI,CAACH,gBAAgB;gBACnB,MAAM,IAAIgB,MAAM;YAClB;YAEA,IAAI;gBACF,MAAMC,WAAW,MAAMC,MAAM,sCAAsC;oBACjEC,MAAMC,KAAKC,SAAS,CAAC;wBACnBC,YAAYtB;wBACZuB,KAAKjB;oBACP;oBACAkB,QAAQ;gBACV;gBAEA,IAAI,CAACP,SAASQ,EAAE,EAAE;oBAChBjC,MAAMkC,KAAK,CAACzB,EAAE;oBACd;gBACF;gBAEA,MAAM0B,OAAQ,MAAMV,SAASW,IAAI;gBAMjC,IAAID,KAAKE,WAAW,CAACC,MAAM,GAAG,GAAG;oBAC/BtC,MAAMkC,KAAK,CACTzB,EAAE,oEAAoE;wBACpE8B,OAAOJ,KAAKE,WAAW,CAACC,MAAM;oBAChC;gBAEJ;gBAEA,oFAAoF;gBACpF,IAAIH,KAAKK,WAAW,KAAKL,KAAKM,SAAS,EAAE;oBACvCzC,MAAM0C,OAAO,CACXjC,EAAE,2DAA2D;wBAC3DkC,OAAOR,KAAKM,SAAS;wBACrBG,SAAST,KAAKK,WAAW;oBAC3B;gBAEJ,OAAO;oBACLxC,MAAM6C,OAAO,CACXpC,EAAE,2DAA2D;wBAC3DkC,OAAOR,KAAKM,SAAS;wBACrBG,SAAST,KAAKK,WAAW;oBAC3B;gBAEJ;gBAEA,0CAA0C;gBAC1C,KAAK,MAAMnB,MAAMP,YAAa;oBAC5BD,aAAaQ;gBACf;gBAEAC,OAAOwB,OAAO;YAChB,EAAE,OAAOZ,OAAO;gBACda,QAAQb,KAAK,CAAC,8BAA8BA;gBAC5ClC,MAAMkC,KAAK,CAACzB,EAAE;YAChB;QACF;IACF;IAEA,OACEK,YAAYwB,MAAM,GAAG,mBACnB,KAACU;QAAIC,WAAU;QAAMC,OAAO;YAAEC,SAAS;YAAQC,gBAAgB;QAAQ;kBACrE,cAAA,MAACrD;YACCkD,WAAU;YACVI,UAAU3C,aAAaI,YAAYwB,MAAM,KAAK;YAC9CgB,MAAM5C,0BAAY,KAACJ,6BAAa,KAACD;YACjCkD,SAAShC;;gBAERd,EAAE;gBAA4D;gBAAEK,YAAYwB,MAAM;gBAAE;gBACpFxB,YAAYwB,MAAM,KAAK,IACpB7B,EAAE,iDACFA,EAAE;;;;AAKhB"}
1
+ {"version":3,"sources":["../../src/components/BulkGenerateAltTextsButton.tsx"],"sourcesContent":["'use client'\n\nimport { Button, toast, 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\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('/api/alt-text-plugin/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 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","useSelection","useTranslation","useRouter","useTransition","Lightning","Spinner","BulkGenerateAltTextsButton","collectionSlug","t","isPending","startTransition","selected","setSelection","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,YAAY,EAAEC,cAAc,QAAQ,iBAAgB;AAC5E,SAASC,SAAS,QAAQ,qBAAoB;AAC9C,SAASC,aAAa,QAAQ,QAAO;AAOrC,SAASC,SAAS,QAAQ,uBAAsB;AAChD,SAASC,OAAO,QAAQ,qBAAoB;AAE5C,OAAO,SAASC,2BAA2B,EAAEC,cAAc,EAA8B;IACvF,MAAM,EAAEC,CAAC,EAAE,GAAGP;IACd,MAAM,CAACQ,WAAWC,gBAAgB,GAAGP;IACrC,MAAM,EAAEQ,QAAQ,EAAEC,YAAY,EAAE,GAAGZ;IAEnC,MAAMa,cAAcC,MAAMC,IAAI,CAACJ,SAASK,OAAO,IAC5CC,MAAM,CAAC,CAAC,GAAGC,WAAW,GAAKA,YAC3BC,GAAG,CAAC,CAAC,CAACC,GAAG,GAAKA;IAEjB,MAAMC,SAASnB;IAEf,MAAMoB,yBAAyB;QAC7BZ,gBAAgB;YACd,IAAI,CAACH,gBAAgB;gBACnB,MAAM,IAAIgB,MAAM;YAClB;YAEA,IAAI;gBACF,MAAMC,WAAW,MAAMC,MAAM,sCAAsC;oBACjEC,MAAMC,KAAKC,SAAS,CAAC;wBACnBC,YAAYtB;wBACZuB,KAAKjB;oBACP;oBACAkB,QAAQ;gBACV;gBAEA,IAAI,CAACP,SAASQ,EAAE,EAAE;oBAChBjC,MAAMkC,KAAK,CAACzB,EAAE;oBACd;gBACF;gBAEA,MAAM0B,OAAQ,MAAMV,SAASW,IAAI;gBAMjC,IAAID,KAAKE,WAAW,CAACC,MAAM,GAAG,GAAG;oBAC/BtC,MAAMkC,KAAK,CACTzB,EAAE,oEAAoE;wBACpE8B,OAAOJ,KAAKE,WAAW,CAACC,MAAM;oBAChC;gBAEJ;gBAEA,oFAAoF;gBACpF,IAAIH,KAAKK,WAAW,KAAKL,KAAKM,SAAS,EAAE;oBACvCzC,MAAM0C,OAAO,CACXjC,EAAE,2DAA2D;wBAC3DkC,OAAOR,KAAKM,SAAS;wBACrBG,SAAST,KAAKK,WAAW;oBAC3B;gBAEJ,OAAO;oBACLxC,MAAM6C,OAAO,CACXpC,EAAE,2DAA2D;wBAC3DkC,OAAOR,KAAKM,SAAS;wBACrBG,SAAST,KAAKK,WAAW;oBAC3B;gBAEJ;gBAEA,0CAA0C;gBAC1C,KAAK,MAAMnB,MAAMP,YAAa;oBAC5BD,aAAaQ;gBACf;gBAEAC,OAAOwB,OAAO;YAChB,EAAE,OAAOZ,OAAO;gBACda,QAAQb,KAAK,CAAC,8BAA8BA;gBAC5ClC,MAAMkC,KAAK,CAACzB,EAAE;YAChB;QACF;IACF;IAEA,OACEK,YAAYwB,MAAM,GAAG,mBACnB,KAACU;QAAIC,WAAU;QAAMC,OAAO;YAAEC,SAAS;YAAQC,gBAAgB;QAAQ;kBACrE,cAAA,KAACrD;YACCkD,WAAU;YACVI,UAAU3C,aAAaI,YAAYwB,MAAM,KAAK;YAC9CgB,MAAM5C,0BAAY,KAACJ,6BAAa,KAACD;YACjCkD,SAAShC;sBAERd,EAAE,4DAA4D;gBAC7D8B,OAAOzB,YAAYwB,MAAM;YAC3B;;;AAKV"}
@@ -1,21 +1,20 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  export const ArrowRightIcon = (props)=>/*#__PURE__*/ _jsx("svg", {
4
- fill: "none",
4
+ "data-testid": "geist-icon",
5
5
  height: "16",
6
- shapeRendering: "geometricPrecision",
7
- stroke: "currentColor",
8
- strokeLinecap: "round",
9
6
  strokeLinejoin: "round",
10
- strokeWidth: "1.5",
11
7
  style: {
12
8
  color: 'currentcolor'
13
9
  },
14
- viewBox: "0 0 24 24",
10
+ viewBox: "0 0 16 16",
15
11
  width: "16",
16
12
  ...props,
17
13
  children: /*#__PURE__*/ _jsx("path", {
18
- d: "M5 12h14M12 5l7 7-7 7"
14
+ clipRule: "evenodd",
15
+ d: "M9.53 2.22L9 1.69 7.94 2.75l.53.53 3.97 3.97H1v1.5h11.44l-3.97 3.97-.53.53L9 14.31l.53-.53 5.074-5.073a1 1 0 000-1.414L9.53 2.22z",
16
+ fill: "currentColor",
17
+ fillRule: "evenodd"
19
18
  })
20
19
  });
21
20
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/icons/ArrowRightIcon.tsx"],"sourcesContent":["import React from 'react'\n\nexport const ArrowRightIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (\n <svg\n fill=\"none\"\n height=\"16\"\n shapeRendering=\"geometricPrecision\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth=\"1.5\"\n style={{ color: 'currentcolor' }}\n viewBox=\"0 0 24 24\"\n width=\"16\"\n {...props}\n >\n <path d=\"M5 12h14M12 5l7 7-7 7\" />\n </svg>\n)\n"],"names":["React","ArrowRightIcon","props","svg","fill","height","shapeRendering","stroke","strokeLinecap","strokeLinejoin","strokeWidth","style","color","viewBox","width","path","d"],"mappings":";AAAA,OAAOA,WAAW,QAAO;AAEzB,OAAO,MAAMC,iBAA0D,CAACC,sBACtE,KAACC;QACCC,MAAK;QACLC,QAAO;QACPC,gBAAe;QACfC,QAAO;QACPC,eAAc;QACdC,gBAAe;QACfC,aAAY;QACZC,OAAO;YAAEC,OAAO;QAAe;QAC/BC,SAAQ;QACRC,OAAM;QACL,GAAGZ,KAAK;kBAET,cAAA,KAACa;YAAKC,GAAE;;OAEX"}
1
+ {"version":3,"sources":["../../../src/components/icons/ArrowRightIcon.tsx"],"sourcesContent":["import React from 'react'\n\nexport const ArrowRightIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (\n <svg\n data-testid=\"geist-icon\"\n height=\"16\"\n strokeLinejoin=\"round\"\n style={{ color: 'currentcolor' }}\n viewBox=\"0 0 16 16\"\n width=\"16\"\n {...props}\n >\n <path\n clipRule=\"evenodd\"\n d=\"M9.53 2.22L9 1.69 7.94 2.75l.53.53 3.97 3.97H1v1.5h11.44l-3.97 3.97-.53.53L9 14.31l.53-.53 5.074-5.073a1 1 0 000-1.414L9.53 2.22z\"\n fill=\"currentColor\"\n fillRule=\"evenodd\"\n />\n </svg>\n)\n"],"names":["React","ArrowRightIcon","props","svg","data-testid","height","strokeLinejoin","style","color","viewBox","width","path","clipRule","d","fill","fillRule"],"mappings":";AAAA,OAAOA,WAAW,QAAO;AAEzB,OAAO,MAAMC,iBAA0D,CAACC,sBACtE,KAACC;QACCC,eAAY;QACZC,QAAO;QACPC,gBAAe;QACfC,OAAO;YAAEC,OAAO;QAAe;QAC/BC,SAAQ;QACRC,OAAM;QACL,GAAGR,KAAK;kBAET,cAAA,KAACS;YACCC,UAAS;YACTC,GAAE;YACFC,MAAK;YACLC,UAAS;;OAGd"}
@@ -1,21 +1,20 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  export const CheckIcon = (props)=>/*#__PURE__*/ _jsx("svg", {
4
- fill: "none",
4
+ "data-testid": "geist-icon",
5
5
  height: "16",
6
- shapeRendering: "geometricPrecision",
7
- stroke: "currentColor",
8
- strokeLinecap: "round",
9
6
  strokeLinejoin: "round",
10
- strokeWidth: "1.5",
11
7
  style: {
12
8
  color: 'currentcolor'
13
9
  },
14
- viewBox: "0 0 24 24",
10
+ viewBox: "0 0 16 16",
15
11
  width: "16",
16
12
  ...props,
17
13
  children: /*#__PURE__*/ _jsx("path", {
18
- d: "M20 6L9 17l-5-5"
14
+ clipRule: "evenodd",
15
+ d: "M15.56 4l-.53.53-8.793 8.793a1.75 1.75 0 01-2.474 0l.53-.53-.53.53L.97 10.53.44 10 1.5 8.94l.53.53 2.793 2.793a.25.25 0 00.354 0L13.97 3.47l.53-.53L15.56 4z",
16
+ fill: "currentColor",
17
+ fillRule: "evenodd"
19
18
  })
20
19
  });
21
20
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/icons/CheckIcon.tsx"],"sourcesContent":["import React from 'react'\n\nexport const CheckIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (\n <svg\n fill=\"none\"\n height=\"16\"\n shapeRendering=\"geometricPrecision\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth=\"1.5\"\n style={{ color: 'currentcolor' }}\n viewBox=\"0 0 24 24\"\n width=\"16\"\n {...props}\n >\n <path d=\"M20 6L9 17l-5-5\" />\n </svg>\n)\n"],"names":["React","CheckIcon","props","svg","fill","height","shapeRendering","stroke","strokeLinecap","strokeLinejoin","strokeWidth","style","color","viewBox","width","path","d"],"mappings":";AAAA,OAAOA,WAAW,QAAO;AAEzB,OAAO,MAAMC,YAAqD,CAACC,sBACjE,KAACC;QACCC,MAAK;QACLC,QAAO;QACPC,gBAAe;QACfC,QAAO;QACPC,eAAc;QACdC,gBAAe;QACfC,aAAY;QACZC,OAAO;YAAEC,OAAO;QAAe;QAC/BC,SAAQ;QACRC,OAAM;QACL,GAAGZ,KAAK;kBAET,cAAA,KAACa;YAAKC,GAAE;;OAEX"}
1
+ {"version":3,"sources":["../../../src/components/icons/CheckIcon.tsx"],"sourcesContent":["import React from 'react'\n\nexport const CheckIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (\n <svg\n data-testid=\"geist-icon\"\n height=\"16\"\n strokeLinejoin=\"round\"\n style={{ color: 'currentcolor' }}\n viewBox=\"0 0 16 16\"\n width=\"16\"\n {...props}\n >\n <path\n clipRule=\"evenodd\"\n d=\"M15.56 4l-.53.53-8.793 8.793a1.75 1.75 0 01-2.474 0l.53-.53-.53.53L.97 10.53.44 10 1.5 8.94l.53.53 2.793 2.793a.25.25 0 00.354 0L13.97 3.47l.53-.53L15.56 4z\"\n fill=\"currentColor\"\n fillRule=\"evenodd\"\n />\n </svg>\n)\n"],"names":["React","CheckIcon","props","svg","data-testid","height","strokeLinejoin","style","color","viewBox","width","path","clipRule","d","fill","fillRule"],"mappings":";AAAA,OAAOA,WAAW,QAAO;AAEzB,OAAO,MAAMC,YAAqD,CAACC,sBACjE,KAACC;QACCC,eAAY;QACZC,QAAO;QACPC,gBAAe;QACfC,OAAO;YAAEC,OAAO;QAAe;QAC/BC,SAAQ;QACRC,OAAM;QACL,GAAGR,KAAK;kBAET,cAAA,KAACS;YACCC,UAAS;YACTC,GAAE;YACFC,MAAK;YACLC,UAAS;;OAGd"}
@@ -1,38 +1,21 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
- export const ImageIcon = (props)=>/*#__PURE__*/ _jsxs("svg", {
3
+ export const ImageIcon = (props)=>/*#__PURE__*/ _jsx("svg", {
4
4
  "data-testid": "geist-icon",
5
- fill: "none",
6
5
  height: "16",
7
- shapeRendering: "geometricPrecision",
8
- stroke: "currentColor",
9
- strokeLinecap: "round",
10
6
  strokeLinejoin: "round",
11
- strokeWidth: "1.5",
12
7
  style: {
13
8
  color: 'currentcolor'
14
9
  },
15
- viewBox: "0 0 24 24",
10
+ viewBox: "0 0 16 16",
16
11
  width: "16",
17
12
  ...props,
18
- children: [
19
- /*#__PURE__*/ _jsx("rect", {
20
- height: "18",
21
- rx: "2",
22
- ry: "2",
23
- width: "18",
24
- x: "3",
25
- y: "3"
26
- }),
27
- /*#__PURE__*/ _jsx("circle", {
28
- cx: "8.5",
29
- cy: "8.5",
30
- r: "1.5"
31
- }),
32
- /*#__PURE__*/ _jsx("path", {
33
- d: "M21 15l-5-5L5 21"
34
- })
35
- ]
13
+ children: /*#__PURE__*/ _jsx("path", {
14
+ clipRule: "evenodd",
15
+ d: "M14.5 2.5h-13v6.69l1.47-1.47.22-.22h3.75l.03-.03 3.5-3.5h1.06l2.97 2.97V2.5zM8 8.56l1.53 1.53.53.53L9 11.68l-.53-.53L6.32 9H3.81l-2.28 2.28-.03.03v1.19a1 1 0 001 1h11a1 1 0 001-1V9.06L11 5.56 8.03 8.53 8 8.56zm-8 2.25v1.69A2.5 2.5 0 002.5 15h11a2.5 2.5 0 002.5-2.5V9.56l.56-.56-.53-.53-.03-.03V1H0v9.69l-.06.06.06.06z",
16
+ fill: "currentColor",
17
+ fillRule: "evenodd"
18
+ })
36
19
  });
37
20
 
38
21
  //# sourceMappingURL=ImageIcon.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/icons/ImageIcon.tsx"],"sourcesContent":["import React from 'react'\n\nexport const ImageIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (\n <svg\n data-testid=\"geist-icon\"\n fill=\"none\"\n height=\"16\"\n shapeRendering=\"geometricPrecision\"\n stroke=\"currentColor\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth=\"1.5\"\n style={{ color: 'currentcolor' }}\n viewBox=\"0 0 24 24\"\n width=\"16\"\n {...props}\n >\n <rect height=\"18\" rx=\"2\" ry=\"2\" width=\"18\" x=\"3\" y=\"3\" />\n <circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\" />\n <path d=\"M21 15l-5-5L5 21\" />\n </svg>\n)\n"],"names":["React","ImageIcon","props","svg","data-testid","fill","height","shapeRendering","stroke","strokeLinecap","strokeLinejoin","strokeWidth","style","color","viewBox","width","rect","rx","ry","x","y","circle","cx","cy","r","path","d"],"mappings":";AAAA,OAAOA,WAAW,QAAO;AAEzB,OAAO,MAAMC,YAAqD,CAACC,sBACjE,MAACC;QACCC,eAAY;QACZC,MAAK;QACLC,QAAO;QACPC,gBAAe;QACfC,QAAO;QACPC,eAAc;QACdC,gBAAe;QACfC,aAAY;QACZC,OAAO;YAAEC,OAAO;QAAe;QAC/BC,SAAQ;QACRC,OAAM;QACL,GAAGb,KAAK;;0BAET,KAACc;gBAAKV,QAAO;gBAAKW,IAAG;gBAAIC,IAAG;gBAAIH,OAAM;gBAAKI,GAAE;gBAAIC,GAAE;;0BACnD,KAACC;gBAAOC,IAAG;gBAAMC,IAAG;gBAAMC,GAAE;;0BAC5B,KAACC;gBAAKC,GAAE;;;OAEX"}
1
+ {"version":3,"sources":["../../../src/components/icons/ImageIcon.tsx"],"sourcesContent":["import React from 'react'\n\nexport const ImageIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (\n <svg\n data-testid=\"geist-icon\"\n height=\"16\"\n strokeLinejoin=\"round\"\n style={{ color: 'currentcolor' }}\n viewBox=\"0 0 16 16\"\n width=\"16\"\n {...props}\n >\n <path\n clipRule=\"evenodd\"\n d=\"M14.5 2.5h-13v6.69l1.47-1.47.22-.22h3.75l.03-.03 3.5-3.5h1.06l2.97 2.97V2.5zM8 8.56l1.53 1.53.53.53L9 11.68l-.53-.53L6.32 9H3.81l-2.28 2.28-.03.03v1.19a1 1 0 001 1h11a1 1 0 001-1V9.06L11 5.56 8.03 8.53 8 8.56zm-8 2.25v1.69A2.5 2.5 0 002.5 15h11a2.5 2.5 0 002.5-2.5V9.56l.56-.56-.53-.53-.03-.03V1H0v9.69l-.06.06.06.06z\"\n fill=\"currentColor\"\n fillRule=\"evenodd\"\n />\n </svg>\n)\n"],"names":["React","ImageIcon","props","svg","data-testid","height","strokeLinejoin","style","color","viewBox","width","path","clipRule","d","fill","fillRule"],"mappings":";AAAA,OAAOA,WAAW,QAAO;AAEzB,OAAO,MAAMC,YAAqD,CAACC,sBACjE,KAACC;QACCC,eAAY;QACZC,QAAO;QACPC,gBAAe;QACfC,OAAO;YAAEC,OAAO;QAAe;QAC/BC,SAAQ;QACRC,OAAM;QACL,GAAGR,KAAK;kBAET,cAAA,KAACS;YACCC,UAAS;YACTC,GAAE;YACFC,MAAK;YACLC,UAAS;;OAGd"}
@@ -1,16 +1,20 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  export function Lightning() {
3
3
  return /*#__PURE__*/ _jsx("svg", {
4
- fill: "currentColor",
4
+ "data-testid": "geist-icon",
5
+ height: "16",
6
+ strokeLinejoin: "round",
5
7
  style: {
8
+ color: 'currentcolor',
6
9
  height: '1rem',
7
10
  width: '1rem'
8
11
  },
9
- viewBox: "0 0 20 20",
10
- xmlns: "http://www.w3.org/2000/svg",
12
+ viewBox: "0 0 16 16",
13
+ width: "16",
11
14
  children: /*#__PURE__*/ _jsx("path", {
12
15
  clipRule: "evenodd",
13
- d: "M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z",
16
+ d: "M7.157 0L2.333 9.408l-.56 1.092H7a.25.25 0 01.25.25V16h1.593l4.824-9.408.56-1.092H9a.25.25 0 01-.25-.25V0H7.157zM7 9H4.227L7.25 3.106V5.25C7.25 6.216 8.034 7 9 7h2.773L8.75 12.894V10.75A1.75 1.75 0 007 9z",
17
+ fill: "currentColor",
14
18
  fillRule: "evenodd"
15
19
  })
16
20
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/components/icons/Lightning.tsx"],"sourcesContent":["export function Lightning() {\n return (\n <svg\n fill=\"currentColor\"\n style={{\n height: '1rem',\n width: '1rem',\n }}\n viewBox=\"0 0 20 20\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n clipRule=\"evenodd\"\n d=\"M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z\"\n fillRule=\"evenodd\"\n />\n </svg>\n )\n}\n"],"names":["Lightning","svg","fill","style","height","width","viewBox","xmlns","path","clipRule","d","fillRule"],"mappings":";AAAA,OAAO,SAASA;IACd,qBACE,KAACC;QACCC,MAAK;QACLC,OAAO;YACLC,QAAQ;YACRC,OAAO;QACT;QACAC,SAAQ;QACRC,OAAM;kBAEN,cAAA,KAACC;YACCC,UAAS;YACTC,GAAE;YACFC,UAAS;;;AAIjB"}
1
+ {"version":3,"sources":["../../../src/components/icons/Lightning.tsx"],"sourcesContent":["export function Lightning() {\n return (\n <svg\n data-testid=\"geist-icon\"\n height=\"16\"\n strokeLinejoin=\"round\"\n style={{ color: 'currentcolor', height: '1rem', width: '1rem' }}\n viewBox=\"0 0 16 16\"\n width=\"16\"\n >\n <path\n clipRule=\"evenodd\"\n d=\"M7.157 0L2.333 9.408l-.56 1.092H7a.25.25 0 01.25.25V16h1.593l4.824-9.408.56-1.092H9a.25.25 0 01-.25-.25V0H7.157zM7 9H4.227L7.25 3.106V5.25C7.25 6.216 8.034 7 9 7h2.773L8.75 12.894V10.75A1.75 1.75 0 007 9z\"\n fill=\"currentColor\"\n fillRule=\"evenodd\"\n />\n </svg>\n )\n}\n"],"names":["Lightning","svg","data-testid","height","strokeLinejoin","style","color","width","viewBox","path","clipRule","d","fill","fillRule"],"mappings":";AAAA,OAAO,SAASA;IACd,qBACE,KAACC;QACCC,eAAY;QACZC,QAAO;QACPC,gBAAe;QACfC,OAAO;YAAEC,OAAO;YAAgBH,QAAQ;YAAQI,OAAO;QAAO;QAC9DC,SAAQ;QACRD,OAAM;kBAEN,cAAA,KAACE;YACCC,UAAS;YACTC,GAAE;YACFC,MAAK;YACLC,UAAS;;;AAIjB"}
@@ -1,6 +1,7 @@
1
1
  import pMap from 'p-map';
2
2
  import { z, ZodError } from 'zod';
3
3
  import { localesFromConfig } from '../utilities/localesFromConfig.js';
4
+ import { matchesMimeType } from '../utilities/mimeTypes.js';
4
5
  /**
5
6
  * Generates and updates alt text for multiple images in all locales.
6
7
  */ export const bulkGenerateAltTextsEndpoint = (access)=>async (req)=>{
@@ -111,6 +112,10 @@ async function generateAndUpdateAltText({ id, collection, locales, payload, plug
111
112
  throw new Error('Image not found');
112
113
  }
113
114
  const mimeType = 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined;
115
+ const collectionConfig = pluginConfig.collections.find((entry)=>entry.slug === collection);
116
+ if (mimeType && collectionConfig && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {
117
+ throw new Error(`Alt text is not tracked for files of type "${mimeType}" in the "${collection}" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`);
118
+ }
114
119
  if (mimeType && pluginConfig.resolver.supportedMimeTypes && !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)) {
115
120
  throw new Error(`Alt text generation is not supported for files of type "${mimeType}". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`);
116
121
  }
@@ -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 { z, ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { localesFromConfig } from '../utilities/localesFromConfig.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 schema = z.object({\n collection: z.string(),\n ids: z.array(z.union([z.string(), z.number()])),\n })\n\n const { collection, ids } = schema.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(\n {\n details: error.issues.map((e) => ({\n message: e.message,\n path: e.path.join('.'),\n })),\n error: 'Validation failed',\n },\n { status: 400 },\n )\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 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","z","ZodError","localesFromConfig","bulkGenerateAltTextsEndpoint","access","req","Response","json","error","status","data","schema","object","collection","string","ids","array","union","number","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","resolver","concurrency","maxBulkGenerateConcurrency","locales","targetLocales","locale","id","generateAndUpdateAltText","console","log","length","Math","round","push","join","totalDocs","details","issues","map","e","message","path","Error","imageDoc","findByID","depth","mimeType","undefined","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 { z, ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { localesFromConfig } from '../utilities/localesFromConfig.js'\nimport { matchesMimeType } from '../utilities/mimeTypes.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 schema = z.object({\n collection: z.string(),\n ids: z.array(z.union([z.string(), z.number()])),\n })\n\n const { collection, ids } = schema.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(\n {\n details: error.issues.map((e) => ({\n message: e.message,\n path: e.path.join('.'),\n })),\n error: 'Validation failed',\n },\n { status: 400 },\n )\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","z","ZodError","localesFromConfig","matchesMimeType","bulkGenerateAltTextsEndpoint","access","req","Response","json","error","status","data","schema","object","collection","string","ids","array","union","number","parse","updatedDocs","erroredDocs","pluginConfig","payload","config","custom","altTextPluginConfig","resolver","concurrency","maxBulkGenerateConcurrency","locales","targetLocales","locale","id","generateAndUpdateAltText","console","log","length","Math","round","push","join","totalDocs","details","issues","map","e","message","path","Error","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,4 +1,5 @@
1
1
  import { z, ZodError } from 'zod';
2
+ import { matchesMimeType } from '../utilities/mimeTypes.js';
2
3
  /**
3
4
  * Generates alt text for a single image using the configured resolver.
4
5
  *
@@ -64,6 +65,14 @@ import { z, ZodError } from 'zod';
64
65
  });
65
66
  }
66
67
  const mimeType = 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string' ? imageDoc.mimeType : undefined;
68
+ const collectionConfig = pluginConfig.collections.find((entry)=>entry.slug === collection);
69
+ if (mimeType && collectionConfig && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {
70
+ return Response.json({
71
+ error: `Alt text is not tracked for files of type "${mimeType}" in the "${collection}" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`
72
+ }, {
73
+ status: 400
74
+ });
75
+ }
67
76
  if (mimeType && pluginConfig.resolver.supportedMimeTypes && !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)) {
68
77
  return Response.json({
69
78
  error: `Alt text generation is not supported for files of type "${mimeType}". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoints/generateAltText.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport { z, ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\n/**\n * Generates alt text for a single image using the configured resolver.\n *\n * By default, returns the result without updating the document (preview mode).\n * Pass `update: true` in the request body to also persist the generated alt text\n * and keywords to the document — useful for programmatic/agent workflows.\n *\n * The response always includes the `id` and `collection` for easy correlation.\n */\nexport const generateAltTextEndpoint =\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 requestSchema = z.object({\n id: z.union([z.string(), z.number()]),\n collection: z.string(),\n locale: z.string().nullable(),\n update: z.boolean().optional().default(false),\n })\n\n const { id, collection, locale, update } = requestSchema.parse(data)\n\n const imageDoc = await req.payload.findByID({\n id,\n collection,\n depth: 0,\n })\n\n if (!imageDoc) {\n return Response.json({ error: 'Image not found' }, { status: 404 })\n }\n\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.getImageThumbnail) {\n return Response.json(\n { error: 'getImageThumbnail function not configured' },\n { status: 500 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string'\n ? imageDoc.mimeType\n : undefined\n\n if (\n mimeType &&\n pluginConfig.resolver.supportedMimeTypes &&\n !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)\n ) {\n return Response.json(\n {\n error: `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n // determine target locale\n const targetLocale = locale ?? pluginConfig.locale\n if (!targetLocale) {\n return Response.json(\n {\n error:\n 'Could not determine target locale for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolve({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locale: targetLocale,\n req,\n })\n\n if (!result.success) {\n return Response.json(\n { error: result.error || 'Failed to generate alt text' },\n { status: 500 },\n )\n }\n\n if (update) {\n await req.payload.update({\n id,\n collection,\n data: {\n alt: result.result.altText,\n keywords: result.result.keywords,\n },\n locale: targetLocale,\n })\n }\n\n return Response.json({ id, collection, ...result.result })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(\n {\n details: error.issues.map((e) => ({\n message: e.message,\n path: e.path.join('.'),\n })),\n error: 'Validation failed',\n },\n { status: 400 },\n )\n }\n console.error('Error generating alt text:', 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"],"names":["z","ZodError","generateAltTextEndpoint","access","req","Response","json","error","status","data","requestSchema","object","id","union","string","number","collection","locale","nullable","update","boolean","optional","default","parse","imageDoc","payload","findByID","depth","pluginConfig","config","custom","altTextPluginConfig","getImageThumbnail","resolver","mimeType","undefined","supportedMimeTypes","includes","join","targetLocale","imageThumbnailUrl","result","resolve","filename","success","alt","altText","keywords","details","issues","map","e","message","path","console","Error"],"mappings":"AAEA,SAASA,CAAC,EAAEC,QAAQ,QAAQ,MAAK;AAIjC;;;;;;;;CAQC,GACD,OAAO,MAAMC,0BACX,CAACC,SACD,OAAOC;QACL,IAAI;YACF,IAAI,CAAE,MAAMD,OAAO;gBAAEC;YAAI,IAAK;gBAC5B,OAAOC,SAASC,IAAI,CAAC;oBAAEC,OAAO;gBAAe,GAAG;oBAAEC,QAAQ;gBAAI;YAChE;YAEA,MAAMC,OAAO,UAAUL,OAAO,OAAOA,IAAIE,IAAI,KAAK,aAAa,MAAMF,IAAIE,IAAI,KAAK;YAElF,MAAMI,gBAAgBV,EAAEW,MAAM,CAAC;gBAC7BC,IAAIZ,EAAEa,KAAK,CAAC;oBAACb,EAAEc,MAAM;oBAAId,EAAEe,MAAM;iBAAG;gBACpCC,YAAYhB,EAAEc,MAAM;gBACpBG,QAAQjB,EAAEc,MAAM,GAAGI,QAAQ;gBAC3BC,QAAQnB,EAAEoB,OAAO,GAAGC,QAAQ,GAAGC,OAAO,CAAC;YACzC;YAEA,MAAM,EAAEV,EAAE,EAAEI,UAAU,EAAEC,MAAM,EAAEE,MAAM,EAAE,GAAGT,cAAca,KAAK,CAACd;YAE/D,MAAMe,WAAW,MAAMpB,IAAIqB,OAAO,CAACC,QAAQ,CAAC;gBAC1Cd;gBACAI;gBACAW,OAAO;YACT;YAEA,IAAI,CAACH,UAAU;gBACb,OAAOnB,SAASC,IAAI,CAAC;oBAAEC,OAAO;gBAAkB,GAAG;oBAAEC,QAAQ;gBAAI;YACnE;YAEA,MAAMoB,eAAexB,IAAIqB,OAAO,CAACI,MAAM,CAACC,MAAM,EAAEC;YAIhD,IAAI,CAACH,cAAc;gBACjB,OAAOvB,SAASC,IAAI,CAAC;oBAAEC,OAAO;gBAA0B,GAAG;oBAAEC,QAAQ;gBAAI;YAC3E;YAEA,IAAI,CAACoB,aAAaI,iBAAiB,EAAE;gBACnC,OAAO3B,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAA4C,GACrD;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,IAAI,CAACoB,aAAaK,QAAQ,EAAE;gBAC1B,OAAO5B,SAASC,IAAI,CAAC;oBAAEC,OAAO;gBAAkC,GAAG;oBAAEC,QAAQ;gBAAI;YACnF;YAEA,MAAM0B,WACJ,cAAcV,YAAY,OAAOA,SAASU,QAAQ,KAAK,WACnDV,SAASU,QAAQ,GACjBC;YAEN,IACED,YACAN,aAAaK,QAAQ,CAACG,kBAAkB,IACxC,CAACR,aAAaK,QAAQ,CAACG,kBAAkB,CAACC,QAAQ,CAACH,WACnD;gBACA,OAAO7B,SAASC,IAAI,CAClB;oBACEC,OAAO,CAAC,wDAAwD,EAAE2B,SAAS,oBAAoB,EAAEN,aAAaK,QAAQ,CAACG,kBAAkB,CAACE,IAAI,CAAC,MAAM,CAAC,CAAC;gBACzJ,GACA;oBAAE9B,QAAQ;gBAAI;YAElB;YAEA,0BAA0B;YAC1B,MAAM+B,eAAetB,UAAUW,aAAaX,MAAM;YAClD,IAAI,CAACsB,cAAc;gBACjB,OAAOlC,SAASC,IAAI,CAClB;oBACEC,OACE;gBACJ,GACA;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMgC,oBAAoBZ,aAAaI,iBAAiB,CAACR;YAEzD,MAAMiB,SAAS,MAAMb,aAAaK,QAAQ,CAACS,OAAO,CAAC;gBACjDC,UACE,cAAcnB,YAAY,OAAOA,SAASmB,QAAQ,KAAK,WACnDnB,SAASmB,QAAQ,GACjBR;gBACNK;gBACAvB,QAAQsB;gBACRnC;YACF;YAEA,IAAI,CAACqC,OAAOG,OAAO,EAAE;gBACnB,OAAOvC,SAASC,IAAI,CAClB;oBAAEC,OAAOkC,OAAOlC,KAAK,IAAI;gBAA8B,GACvD;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,IAAIW,QAAQ;gBACV,MAAMf,IAAIqB,OAAO,CAACN,MAAM,CAAC;oBACvBP;oBACAI;oBACAP,MAAM;wBACJoC,KAAKJ,OAAOA,MAAM,CAACK,OAAO;wBAC1BC,UAAUN,OAAOA,MAAM,CAACM,QAAQ;oBAClC;oBACA9B,QAAQsB;gBACV;YACF;YAEA,OAAOlC,SAASC,IAAI,CAAC;gBAAEM;gBAAII;gBAAY,GAAGyB,OAAOA,MAAM;YAAC;QAC1D,EAAE,OAAOlC,OAAO;YACd,IAAIA,iBAAiBN,UAAU;gBAC7B,OAAOI,SAASC,IAAI,CAClB;oBACE0C,SAASzC,MAAM0C,MAAM,CAACC,GAAG,CAAC,CAACC,IAAO,CAAA;4BAChCC,SAASD,EAAEC,OAAO;4BAClBC,MAAMF,EAAEE,IAAI,CAACf,IAAI,CAAC;wBACpB,CAAA;oBACA/B,OAAO;gBACT,GACA;oBAAEC,QAAQ;gBAAI;YAElB;YACA8C,QAAQ/C,KAAK,CAAC,8BAA8BA;YAC5C,OAAOF,SAASC,IAAI,CAClB;gBACEC,OAAO,CAAC,2BAA2B,EAAEA,iBAAiBgD,QAAQhD,MAAM6C,OAAO,GAAG,iBAAiB;YACjG,GACA;gBAAE5C,QAAQ;YAAI;QAElB;IACF,EAAC"}
1
+ {"version":3,"sources":["../../src/endpoints/generateAltText.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport { z, ZodError } from 'zod'\n\nimport type { AltTextPluginConfig } from '../types/AltTextPluginConfig.js'\n\nimport { matchesMimeType } from '../utilities/mimeTypes.js'\n\n/**\n * Generates alt text for a single image using the configured resolver.\n *\n * By default, returns the result without updating the document (preview mode).\n * Pass `update: true` in the request body to also persist the generated alt text\n * and keywords to the document — useful for programmatic/agent workflows.\n *\n * The response always includes the `id` and `collection` for easy correlation.\n */\nexport const generateAltTextEndpoint =\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 requestSchema = z.object({\n id: z.union([z.string(), z.number()]),\n collection: z.string(),\n locale: z.string().nullable(),\n update: z.boolean().optional().default(false),\n })\n\n const { id, collection, locale, update } = requestSchema.parse(data)\n\n const imageDoc = await req.payload.findByID({\n id,\n collection,\n depth: 0,\n })\n\n if (!imageDoc) {\n return Response.json({ error: 'Image not found' }, { status: 404 })\n }\n\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.getImageThumbnail) {\n return Response.json(\n { error: 'getImageThumbnail function not configured' },\n { status: 500 },\n )\n }\n\n if (!pluginConfig.resolver) {\n return Response.json({ error: 'No alt text resolver configured' }, { status: 500 })\n }\n\n const mimeType =\n 'mimeType' in imageDoc && typeof imageDoc.mimeType === 'string'\n ? imageDoc.mimeType\n : undefined\n\n const collectionConfig = pluginConfig.collections.find((entry) => entry.slug === collection)\n\n if (mimeType && collectionConfig && !matchesMimeType(mimeType, collectionConfig.mimeTypes)) {\n return Response.json(\n {\n error: `Alt text is not tracked for files of type \"${mimeType}\" in the \"${collection}\" collection. Tracked types: ${collectionConfig.mimeTypes.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n if (\n mimeType &&\n pluginConfig.resolver.supportedMimeTypes &&\n !pluginConfig.resolver.supportedMimeTypes.includes(mimeType)\n ) {\n return Response.json(\n {\n error: `Alt text generation is not supported for files of type \"${mimeType}\". Supported types: ${pluginConfig.resolver.supportedMimeTypes.join(', ')}.`,\n },\n { status: 400 },\n )\n }\n\n // determine target locale\n const targetLocale = locale ?? pluginConfig.locale\n if (!targetLocale) {\n return Response.json(\n {\n error:\n 'Could not determine target locale for alt text generation. Please check your plugin configuration.',\n },\n { status: 500 },\n )\n }\n\n const imageThumbnailUrl = pluginConfig.getImageThumbnail(imageDoc)\n\n const result = await pluginConfig.resolver.resolve({\n filename:\n 'filename' in imageDoc && typeof imageDoc.filename === 'string'\n ? imageDoc.filename\n : undefined,\n imageThumbnailUrl,\n locale: targetLocale,\n req,\n })\n\n if (!result.success) {\n return Response.json(\n { error: result.error || 'Failed to generate alt text' },\n { status: 500 },\n )\n }\n\n if (update) {\n await req.payload.update({\n id,\n collection,\n data: {\n alt: result.result.altText,\n keywords: result.result.keywords,\n },\n locale: targetLocale,\n })\n }\n\n return Response.json({ id, collection, ...result.result })\n } catch (error) {\n if (error instanceof ZodError) {\n return Response.json(\n {\n details: error.issues.map((e) => ({\n message: e.message,\n path: e.path.join('.'),\n })),\n error: 'Validation failed',\n },\n { status: 400 },\n )\n }\n console.error('Error generating alt text:', 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"],"names":["z","ZodError","matchesMimeType","generateAltTextEndpoint","access","req","Response","json","error","status","data","requestSchema","object","id","union","string","number","collection","locale","nullable","update","boolean","optional","default","parse","imageDoc","payload","findByID","depth","pluginConfig","config","custom","altTextPluginConfig","getImageThumbnail","resolver","mimeType","undefined","collectionConfig","collections","find","entry","slug","mimeTypes","join","supportedMimeTypes","includes","targetLocale","imageThumbnailUrl","result","resolve","filename","success","alt","altText","keywords","details","issues","map","e","message","path","console","Error"],"mappings":"AAEA,SAASA,CAAC,EAAEC,QAAQ,QAAQ,MAAK;AAIjC,SAASC,eAAe,QAAQ,4BAA2B;AAE3D;;;;;;;;CAQC,GACD,OAAO,MAAMC,0BACX,CAACC,SACD,OAAOC;QACL,IAAI;YACF,IAAI,CAAE,MAAMD,OAAO;gBAAEC;YAAI,IAAK;gBAC5B,OAAOC,SAASC,IAAI,CAAC;oBAAEC,OAAO;gBAAe,GAAG;oBAAEC,QAAQ;gBAAI;YAChE;YAEA,MAAMC,OAAO,UAAUL,OAAO,OAAOA,IAAIE,IAAI,KAAK,aAAa,MAAMF,IAAIE,IAAI,KAAK;YAElF,MAAMI,gBAAgBX,EAAEY,MAAM,CAAC;gBAC7BC,IAAIb,EAAEc,KAAK,CAAC;oBAACd,EAAEe,MAAM;oBAAIf,EAAEgB,MAAM;iBAAG;gBACpCC,YAAYjB,EAAEe,MAAM;gBACpBG,QAAQlB,EAAEe,MAAM,GAAGI,QAAQ;gBAC3BC,QAAQpB,EAAEqB,OAAO,GAAGC,QAAQ,GAAGC,OAAO,CAAC;YACzC;YAEA,MAAM,EAAEV,EAAE,EAAEI,UAAU,EAAEC,MAAM,EAAEE,MAAM,EAAE,GAAGT,cAAca,KAAK,CAACd;YAE/D,MAAMe,WAAW,MAAMpB,IAAIqB,OAAO,CAACC,QAAQ,CAAC;gBAC1Cd;gBACAI;gBACAW,OAAO;YACT;YAEA,IAAI,CAACH,UAAU;gBACb,OAAOnB,SAASC,IAAI,CAAC;oBAAEC,OAAO;gBAAkB,GAAG;oBAAEC,QAAQ;gBAAI;YACnE;YAEA,MAAMoB,eAAexB,IAAIqB,OAAO,CAACI,MAAM,CAACC,MAAM,EAAEC;YAIhD,IAAI,CAACH,cAAc;gBACjB,OAAOvB,SAASC,IAAI,CAAC;oBAAEC,OAAO;gBAA0B,GAAG;oBAAEC,QAAQ;gBAAI;YAC3E;YAEA,IAAI,CAACoB,aAAaI,iBAAiB,EAAE;gBACnC,OAAO3B,SAASC,IAAI,CAClB;oBAAEC,OAAO;gBAA4C,GACrD;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,IAAI,CAACoB,aAAaK,QAAQ,EAAE;gBAC1B,OAAO5B,SAASC,IAAI,CAAC;oBAAEC,OAAO;gBAAkC,GAAG;oBAAEC,QAAQ;gBAAI;YACnF;YAEA,MAAM0B,WACJ,cAAcV,YAAY,OAAOA,SAASU,QAAQ,KAAK,WACnDV,SAASU,QAAQ,GACjBC;YAEN,MAAMC,mBAAmBR,aAAaS,WAAW,CAACC,IAAI,CAAC,CAACC,QAAUA,MAAMC,IAAI,KAAKxB;YAEjF,IAAIkB,YAAYE,oBAAoB,CAACnC,gBAAgBiC,UAAUE,iBAAiBK,SAAS,GAAG;gBAC1F,OAAOpC,SAASC,IAAI,CAClB;oBACEC,OAAO,CAAC,2CAA2C,EAAE2B,SAAS,UAAU,EAAElB,WAAW,6BAA6B,EAAEoB,iBAAiBK,SAAS,CAACC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAC9J,GACA;oBAAElC,QAAQ;gBAAI;YAElB;YAEA,IACE0B,YACAN,aAAaK,QAAQ,CAACU,kBAAkB,IACxC,CAACf,aAAaK,QAAQ,CAACU,kBAAkB,CAACC,QAAQ,CAACV,WACnD;gBACA,OAAO7B,SAASC,IAAI,CAClB;oBACEC,OAAO,CAAC,wDAAwD,EAAE2B,SAAS,oBAAoB,EAAEN,aAAaK,QAAQ,CAACU,kBAAkB,CAACD,IAAI,CAAC,MAAM,CAAC,CAAC;gBACzJ,GACA;oBAAElC,QAAQ;gBAAI;YAElB;YAEA,0BAA0B;YAC1B,MAAMqC,eAAe5B,UAAUW,aAAaX,MAAM;YAClD,IAAI,CAAC4B,cAAc;gBACjB,OAAOxC,SAASC,IAAI,CAClB;oBACEC,OACE;gBACJ,GACA;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,MAAMsC,oBAAoBlB,aAAaI,iBAAiB,CAACR;YAEzD,MAAMuB,SAAS,MAAMnB,aAAaK,QAAQ,CAACe,OAAO,CAAC;gBACjDC,UACE,cAAczB,YAAY,OAAOA,SAASyB,QAAQ,KAAK,WACnDzB,SAASyB,QAAQ,GACjBd;gBACNW;gBACA7B,QAAQ4B;gBACRzC;YACF;YAEA,IAAI,CAAC2C,OAAOG,OAAO,EAAE;gBACnB,OAAO7C,SAASC,IAAI,CAClB;oBAAEC,OAAOwC,OAAOxC,KAAK,IAAI;gBAA8B,GACvD;oBAAEC,QAAQ;gBAAI;YAElB;YAEA,IAAIW,QAAQ;gBACV,MAAMf,IAAIqB,OAAO,CAACN,MAAM,CAAC;oBACvBP;oBACAI;oBACAP,MAAM;wBACJ0C,KAAKJ,OAAOA,MAAM,CAACK,OAAO;wBAC1BC,UAAUN,OAAOA,MAAM,CAACM,QAAQ;oBAClC;oBACApC,QAAQ4B;gBACV;YACF;YAEA,OAAOxC,SAASC,IAAI,CAAC;gBAAEM;gBAAII;gBAAY,GAAG+B,OAAOA,MAAM;YAAC;QAC1D,EAAE,OAAOxC,OAAO;YACd,IAAIA,iBAAiBP,UAAU;gBAC7B,OAAOK,SAASC,IAAI,CAClB;oBACEgD,SAAS/C,MAAMgD,MAAM,CAACC,GAAG,CAAC,CAACC,IAAO,CAAA;4BAChCC,SAASD,EAAEC,OAAO;4BAClBC,MAAMF,EAAEE,IAAI,CAACjB,IAAI,CAAC;wBACpB,CAAA;oBACAnC,OAAO;gBACT,GACA;oBAAEC,QAAQ;gBAAI;YAElB;YACAoD,QAAQrD,KAAK,CAAC,8BAA8BA;YAC5C,OAAOF,SAASC,IAAI,CAClB;gBACEC,OAAO,CAAC,2BAA2B,EAAEA,iBAAiBsD,QAAQtD,MAAMmD,OAAO,GAAG,iBAAiB;YACjG,GACA;gBAAElD,QAAQ;YAAI;QAElB;IACF,EAAC"}
@@ -1,5 +1,10 @@
1
- import type { TextareaField } from 'payload';
2
- export declare function altTextField({ localized, supportedMimeTypes, }: {
1
+ import type { TextareaField, TextareaFieldValidation } from 'payload';
2
+ export declare function altTextField({ localized, supportedMimeTypes, trackedMimeTypes, validate, }: {
3
3
  localized?: TextareaField['localized'];
4
+ /** MIME types the resolver can generate for — used to disable the Generate button. */
4
5
  supportedMimeTypes?: string[];
6
+ /** MIME types for which alt text is tracked — used to hide the field and skip validation for others. */
7
+ trackedMimeTypes?: string[];
8
+ /** Custom validator that fully replaces the default. */
9
+ validate?: TextareaFieldValidation;
5
10
  }): TextareaField;