Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -373,3 +373,4 @@ tsconfig.tsbuildinfo
/dist
.notes/
package-lock.json
alt-text/dev/media-with-folders
1 change: 1 addition & 0 deletions alt-text/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ payloadAltTextPlugin({
```

- feat: scope alt text tracking, validation, and health to configurable per-collection MIME types (default `['image/*']`)
- feat: add a per-collection `validate` option to override the alt text field validator. Exports the default `validateAltText` so projects can compose around it — e.g. to skip the required-alt check when the request body does not touch `alt` (folder moves, partial API updates in localized setups with `fallback: false`, [#95](https://github.com/jhb-software/payload-plugins/issues/95))
- refactor: stop auto-injecting the alt text health widget into `admin.dashboard.defaultLayout`. The widget is still registered under `admin.dashboard.widgets`; add `{ widgetSlug: 'alt-text-health', width: 'full' }` to your `defaultLayout` to show it by default.
- fix: support both Next.js 15 and 16 `revalidateTag` type signatures in the alt text health invalidation hook

Expand Down
6 changes: 6 additions & 0 deletions alt-text/dev/src/app/(payload)/admin/importMap.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { AltTextField as AltTextField_0a0a871430f540863f89f94882312cf1 } from '@jhb.software/payload-alt-text-plugin/client'
import { BulkGenerateAltTextsButton as BulkGenerateAltTextsButton_0a0a871430f540863f89f94882312cf1 } from '@jhb.software/payload-alt-text-plugin/client'
import { FolderTableCell as FolderTableCell_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
import { FolderField as FolderField_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
import { FolderTypeField as FolderTypeField_2b8867833a34864a02ddf429b0728a40 } from '@payloadcms/next/client'
import { AltTextHealthWidget as AltTextHealthWidget_a35949d21b38efe8500764b5b0b3638b } from '@jhb.software/payload-alt-text-plugin/server'
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'

/** @type import('payload').ImportMap */
export const importMap = {
"@jhb.software/payload-alt-text-plugin/client#AltTextField": AltTextField_0a0a871430f540863f89f94882312cf1,
"@jhb.software/payload-alt-text-plugin/client#BulkGenerateAltTextsButton": BulkGenerateAltTextsButton_0a0a871430f540863f89f94882312cf1,
"@payloadcms/next/rsc#FolderTableCell": FolderTableCell_f9c02e79a4aed9a3924487c0cd4cafb1,
"@payloadcms/next/rsc#FolderField": FolderField_f9c02e79a4aed9a3924487c0cd4cafb1,
"@payloadcms/next/client#FolderTypeField": FolderTypeField_2b8867833a34864a02ddf429b0728a40,
"@jhb.software/payload-alt-text-plugin/server#AltTextHealthWidget": AltTextHealthWidget_a35949d21b38efe8500764b5b0b3638b,
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
}
33 changes: 33 additions & 0 deletions alt-text/dev/src/collections/MediaWithFolders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { CollectionConfig } from 'payload'

// Folder-enabled upload collection used to verify that moving a document
// between folders does not trigger alt-text validation when `alt` is absent
// from the request body. Combined with the dev app's `localization.fallback:
// false`, leaving alt text empty in one locale used to block folder moves;
// this collection lets a reviewer click through that scenario end-to-end.

export const MediaWithFolders: CollectionConfig = {
slug: 'media-with-folders',
labels: {
plural: { de: 'Medien mit Ordnern', en: 'Media with Folders' },
singular: { de: 'Medium mit Ordner', en: 'Media with Folder' },
},
folders: true,
upload: {
mimeTypes: ['image/*'],
},
fields: [
{
name: 'url',
type: 'text',
admin: {
hidden: true,
},
hooks: {
afterRead: [
async () => 'https://images.pexels.com/photos/16981245/pexels-photo-16981245.jpeg',
],
},
},
],
}
100 changes: 99 additions & 1 deletion alt-text/dev/src/payload-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,25 @@ export interface Config {
users: User;
media: Media;
images: Image;
'media-with-folders': MediaWithFolder;
'payload-kv': PayloadKv;
'payload-folders': FolderInterface;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsJoins: {
'payload-folders': {
documentsAndFolders: 'payload-folders' | 'media-with-folders';
};
};
collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>;
media: MediaSelect<false> | MediaSelect<true>;
images: ImagesSelect<false> | ImagesSelect<true>;
'media-with-folders': MediaWithFoldersSelect<false> | MediaWithFoldersSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
'payload-folders': PayloadFoldersSelect<false> | PayloadFoldersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
Expand Down Expand Up @@ -191,6 +199,56 @@ export interface Image {
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media-with-folders".
*/
export interface MediaWithFolder {
id: string;
alt: string;
/**
* Keywords which describe the image. Used when searching for the image.
*/
keywords?: string[] | null;
folder?: (string | null) | FolderInterface;
updatedAt: string;
createdAt: string;
url?: string | null;
thumbnailURL?: string | null;
filename?: string | null;
mimeType?: string | null;
filesize?: number | null;
width?: number | null;
height?: number | null;
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-folders".
*/
export interface FolderInterface {
id: string;
name: string;
folder?: (string | null) | FolderInterface;
documentsAndFolders?: {
docs?: (
| {
relationTo?: 'payload-folders';
value: string | FolderInterface;
}
| {
relationTo?: 'media-with-folders';
value: string | MediaWithFolder;
}
)[];
hasNextPage?: boolean;
totalDocs?: number;
};
folderType?: 'media-with-folders'[] | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv".
Expand Down Expand Up @@ -226,6 +284,14 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'images';
value: string | Image;
} | null)
| ({
relationTo: 'media-with-folders';
value: string | MediaWithFolder;
} | null)
| ({
relationTo: 'payload-folders';
value: string | FolderInterface;
} | null);
globalSlug?: string | null;
user: {
Expand Down Expand Up @@ -329,6 +395,26 @@ export interface ImagesSelect<T extends boolean = true> {
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media-with-folders_select".
*/
export interface MediaWithFoldersSelect<T extends boolean = true> {
alt?: T;
keywords?: T;
folder?: T;
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-kv_select".
Expand All @@ -337,6 +423,18 @@ export interface PayloadKvSelect<T extends boolean = true> {
key?: T;
data?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-folders_select".
*/
export interface PayloadFoldersSelect<T extends boolean = true> {
name?: T;
folder?: T;
documentsAndFolders?: T;
folderType?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
Expand Down
25 changes: 23 additions & 2 deletions alt-text/dev/src/payload.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { openAIResolver, payloadAltTextPlugin } from '@jhb.software/payload-alt-text-plugin'
import {
openAIResolver,
payloadAltTextPlugin,
validateAltText,
} from '@jhb.software/payload-alt-text-plugin'
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { de } from '@payloadcms/translations/languages/de'
import { en } from '@payloadcms/translations/languages/en'
Expand All @@ -7,6 +11,7 @@ import { buildConfig } from 'payload'
import { fileURLToPath } from 'url'
import { Media } from './collections/Media'
import { Images } from './collections/Images'
import { MediaWithFolders } from './collections/MediaWithFolders'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
Expand All @@ -26,7 +31,10 @@ export default buildConfig({
localization: {
locales: ['en', 'de'],
defaultLocale: 'en',
fallback: true,
// `fallback: false` lets us reproduce the folder-move scenario from #95:
// a doc with alt text only in `en` truly has empty alt in `de`, so the
// pre-fix validator rejected folder moves that didn't touch the alt field.
fallback: false,
},
i18n: {
supportedLanguages: { en, de },
Expand All @@ -39,6 +47,7 @@ export default buildConfig({
},
Media,
Images,
MediaWithFolders,
],
db: mongooseAdapter({
url: process.env.MONGODB_URL!,
Expand All @@ -54,6 +63,18 @@ export default buildConfig({
{ slug: 'media', mimeTypes: ['image/*'] },
// Bare slug defaults to `['image/*']`.
'images',
// Demonstrates the per-collection `validate` option: skip the required-alt
// check when the request body does not touch `alt` (e.g. folder moves,
// partial API updates). Without this, folder moves on docs with empty
// locales fail validation under `localization.fallback: false`. See #95.
{
slug: 'media-with-folders',
validate: (value, args) => {
const { req } = args
if (!req.data || !('alt' in req.data)) return true
return validateAltText(value, args)
},
},
],
resolver: openAIResolver({
apiKey: process.env.OPENAI_API_KEY!,
Expand Down
8 changes: 5 additions & 3 deletions alt-text/src/fields/altTextField.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { TextareaField } from 'payload'
import type { TextareaField, TextareaFieldValidation } from 'payload'

import { validateAltText } from '../utilities/mimeTypes.js'
import { translatedLabel } from '../utils/translatedLabel.js'
Expand All @@ -7,12 +7,15 @@ export function altTextField({
localized,
supportedMimeTypes,
trackedMimeTypes,
validate,
}: {
localized?: TextareaField['localized']
/** MIME types the resolver can generate for — used to disable the Generate button. */
supportedMimeTypes?: string[]
/** MIME types for which alt text is tracked — used to hide the field and skip validation for others. */
trackedMimeTypes?: string[]
/** Custom validator that fully replaces the default. */
validate?: TextareaFieldValidation
}): TextareaField {
return {
name: 'alt',
Expand All @@ -29,7 +32,6 @@ export function altTextField({
label: translatedLabel('alternateText'),
localized,
required: true,
validate: (value, args) =>
validateAltText(value, args as Parameters<typeof validateAltText>[1], trackedMimeTypes),
validate: validate ?? ((value, args) => validateAltText(value, args, trackedMimeTypes)),
}
}
1 change: 1 addition & 0 deletions alt-text/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export type {
AltTextCollectionConfig,
IncomingAltTextPluginConfig as AltTextPluginConfig,
} from './types/AltTextPluginConfig.js'
export { matchesMimeType, validateAltText } from './utilities/mimeTypes.js'
1 change: 1 addition & 0 deletions alt-text/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export const payloadAltTextPlugin =
localized: Boolean(config.localization),
supportedMimeTypes: pluginConfig.resolver.supportedMimeTypes,
trackedMimeTypes: altTextCollectionConfig.mimeTypes,
validate: altTextCollectionConfig.validate,
}),
keywordsField({
localized: Boolean(config.localization),
Expand Down
Loading
Loading