feat: adds disableListColumn and disableListFilter to imageSize admin props (#13699)

### What?

Added support for `disableListColumn` and `disableListFilter` admin
properties on imageSize configurations that automatically apply to all
fields within the corresponding size group.

### Why?

Upload collections with multiple image sizes can clutter the admin list
view with many size-specific columns and filters. This feature allows
developers to selectively hide size fields from list views while keeping
them accessible in the document edit view.

### How?

Modified `getBaseFields.ts` to inherit admin properties from imageSize
configuration and apply them to all nested fields (url, width, height,
mimeType, filesize, filename) within each size group. The implementation
uses conditional spread operators to only apply these properties when
explicitly set to `true`, maintaining backward compatibility.
This commit is contained in:
Patrik
2025-09-09 11:56:57 -04:00
committed by GitHub
parent 6a0637ecb2
commit e1ea07441e
7 changed files with 354 additions and 4 deletions

View File

@@ -192,6 +192,29 @@ Behind the scenes, Payload relies on [`sharp`](https://sharp.pixelplumbing.com/a
Note that for image resizing to work, `sharp` must be specified in your [Payload Config](../configuration/overview). This is configured by default if you created your Payload project with `create-payload-app`. See `sharp` in [Config Options](../configuration/overview#config-options).
#### Admin List View Options
Each image size also supports `admin` options to control whether it appears in the [Admin Panel](../admin/overview) list view.
```ts
{
name: 'thumbnail',
width: 400,
height: 300,
admin: {
disableListColumn: true, // hide from list view columns
disableListFilter: true, // hide from list view filters
},
}
```
| Option | Description |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **`disableListColumn`** | If set to `true`, this image size will not be available as a selectable column in the collection list view. Defaults to `false`. |
| **`disableListFilter`** | If set to `true`, this image size will not be available as a filter option in the collection list view. Defaults to `false`. |
This is useful for hiding large or rarely used image sizes from the list view UI while still keeping them available in the API.
#### Accessing the resized images in hooks
All auto-resized images are exposed to be re-used in hooks and similar via an object that is bound to `req.payloadUploadSizes`.

View File

@@ -183,6 +183,9 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
mimeType.validate = mimeTypeValidator(uploadOptions.mimeTypes)
}
// In Payload v4, image size subfields (`url`, `width`, `height`, etc.) should
// default to `disableListColumn: true` and `disableListFilter: true`
// to avoid cluttering the collection list view and filters by default.
if (uploadOptions.imageSizes) {
uploadFields = uploadFields.concat([
{
@@ -196,10 +199,17 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
type: 'group',
admin: {
hidden: true,
...(size.admin?.disableListColumn && { disableListColumn: true }),
...(size.admin?.disableListFilter && { disableListFilter: true }),
},
fields: [
{
...url,
admin: {
...url.admin,
...(size.admin?.disableListColumn && { disableListColumn: true }),
...(size.admin?.disableListFilter && { disableListFilter: true }),
},
hooks: {
afterRead: [
({ data, value }) => {
@@ -218,12 +228,45 @@ export const getBaseUploadFields = ({ collection, config }: Options): Field[] =>
],
},
},
width,
height,
mimeType,
filesize,
{
...width,
admin: {
...width.admin,
...(size.admin?.disableListColumn && { disableListColumn: true }),
...(size.admin?.disableListFilter && { disableListFilter: true }),
},
},
{
...height,
admin: {
...height.admin,
...(size.admin?.disableListColumn && { disableListColumn: true }),
...(size.admin?.disableListFilter && { disableListFilter: true }),
},
},
{
...mimeType,
admin: {
...mimeType.admin,
...(size.admin?.disableListColumn && { disableListColumn: true }),
...(size.admin?.disableListFilter && { disableListFilter: true }),
},
},
{
...filesize,
admin: {
...filesize.admin,
...(size.admin?.disableListColumn && { disableListColumn: true }),
...(size.admin?.disableListFilter && { disableListFilter: true }),
},
},
{
...filename,
admin: {
...filename.admin,
...(size.admin?.disableListColumn && { disableListColumn: true }),
...(size.admin?.disableListFilter && { disableListFilter: true }),
},
unique: false,
},
],

View File

@@ -69,6 +69,27 @@ export type GenerateImageName = (args: {
}) => string
export type ImageSize = {
/**
* Admin UI options that control how this image size appears in list views.
*
* NOTE: In Payload v4, these options (`disableListColumn`, `disableListFilter`)
* should default to `true` so image size subfields are hidden from list columns
* and filters by default, reducing noise in the admin UI.
*/
admin?: {
/**
* If set to true, this image size will not be available
* as a selectable column in the collection list view.
* @default false
*/
disableListColumn?: boolean
/**
* If set to true, this image size will not be available
* as a filter option in the collection list view.
* @default false
*/
disableListFilter?: boolean
}
/**
* @deprecated prefer position
*/

View File

@@ -30,6 +30,7 @@ import {
imageSizesOnlySlug,
listViewPreviewSlug,
mediaSlug,
mediaWithImageSizeAdminPropsSlug,
mediaWithoutCacheTagsSlug,
mediaWithoutDeleteAccessSlug,
mediaWithoutRelationPreviewSlug,
@@ -944,6 +945,45 @@ export default buildConfigWithDefaults({
upload: true,
access: { delete: () => false },
},
{
slug: mediaWithImageSizeAdminPropsSlug,
fields: [],
upload: {
imageSizes: [
{
name: 'one',
height: 200,
width: 200,
admin: {
disableListFilter: true,
disableListColumn: true,
},
},
{
name: 'two',
height: 300,
width: 300,
admin: {
disableListColumn: true,
},
},
{
name: 'three',
height: 400,
width: 400,
admin: {
disableListColumn: false,
disableListFilter: true,
},
},
{
name: 'four',
height: 400,
width: 300,
},
],
},
},
],
onInit: async (payload) => {
const uploadsDir = path.resolve(dirname, './media')

View File

@@ -2,6 +2,8 @@ import type { Page } from '@playwright/test'
import { expect, test } from '@playwright/test'
import { statSync } from 'fs'
import { openListColumns } from 'helpers/e2e/openListColumns.js'
import { openListFilters } from 'helpers/e2e/openListFilters.js'
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
import { openDocDrawer } from 'helpers/e2e/toggleDocDrawer.js'
import path from 'path'
@@ -40,6 +42,7 @@ import {
imageSizesOnlySlug,
listViewPreviewSlug,
mediaSlug,
mediaWithImageSizeAdminPropsSlug,
mediaWithoutCacheTagsSlug,
mediaWithoutDeleteAccessSlug,
relationPreviewSlug,
@@ -92,6 +95,7 @@ let bulkUploadsURL: AdminUrlUtil
let fileMimeTypeURL: AdminUrlUtil
let svgOnlyURL: AdminUrlUtil
let mediaWithoutDeleteAccessURL: AdminUrlUtil
let mediaWithImageSizeAdminPropsURL: AdminUrlUtil
describe('Uploads', () => {
let page: Page
@@ -133,6 +137,7 @@ describe('Uploads', () => {
fileMimeTypeURL = new AdminUrlUtil(serverURL, fileMimeTypeSlug)
svgOnlyURL = new AdminUrlUtil(serverURL, svgOnlySlug)
mediaWithoutDeleteAccessURL = new AdminUrlUtil(serverURL, mediaWithoutDeleteAccessSlug)
mediaWithImageSizeAdminPropsURL = new AdminUrlUtil(serverURL, mediaWithImageSizeAdminPropsSlug)
const context = await browser.newContext()
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
@@ -1656,4 +1661,102 @@ describe('Uploads', () => {
).docs[0]?.filename
expect(filenameFromAPI).toBe('test-image.jpg')
})
test('should not show image sizes in column selector in list view if imageSize has admin.disableListColumn true', async () => {
await page.goto(mediaWithImageSizeAdminPropsURL.list)
await openListColumns(page, {})
await expect(page.locator('button:has-text("Sizes > one > URL")')).toBeHidden()
await expect(page.locator('button:has-text("Sizes > one > Width")')).toBeHidden()
await expect(page.locator('button:has-text("Sizes > one > Height")')).toBeHidden()
await expect(page.locator('button:has-text("Sizes > one > MIME Type")')).toBeHidden()
await expect(page.locator('button:has-text("Sizes > one > File Size")')).toBeHidden()
await expect(page.locator('button:has-text("Sizes > one > File Name")')).toBeHidden()
await expect(page.locator('button:has-text("Sizes > two > URL")')).toBeHidden()
await expect(page.locator('button:has-text("Sizes > two > Width")')).toBeHidden()
await expect(page.locator('button:has-text("Sizes > two > Height")')).toBeHidden()
await expect(page.locator('button:has-text("Sizes > two > MIME Type")')).toBeHidden()
await expect(page.locator('button:has-text("Sizes > two > File Size")')).toBeHidden()
await expect(page.locator('button:has-text("Sizes > two > File Name")')).toBeHidden()
})
test('should show image size in column selector in list view if imageSize has admin.disableListColumn false', async () => {
await page.goto(mediaWithImageSizeAdminPropsURL.list)
await openListColumns(page, {})
await expect(page.locator('button:has-text("Sizes > three > URL")')).toBeVisible()
await expect(page.locator('button:has-text("Sizes > three > Width")')).toBeVisible()
await expect(page.locator('button:has-text("Sizes > three > Height")')).toBeVisible()
await expect(page.locator('button:has-text("Sizes > three > MIME Type")')).toBeVisible()
await expect(page.locator('button:has-text("Sizes > three > File Size")')).toBeVisible()
await expect(page.locator('button:has-text("Sizes > three > File Name")')).toBeVisible()
await expect(page.locator('button:has-text("Sizes > four > URL")')).toBeVisible()
await expect(page.locator('button:has-text("Sizes > four > Width")')).toBeVisible()
await expect(page.locator('button:has-text("Sizes > four > Height")')).toBeVisible()
await expect(page.locator('button:has-text("Sizes > four > MIME Type")')).toBeVisible()
await expect(page.locator('button:has-text("Sizes > four > File Size")')).toBeVisible()
await expect(page.locator('button:has-text("Sizes > four > File Name")')).toBeVisible()
})
test('should not show image size in where filter drodown in list view if imageSize has admin.disableListFilter true', async () => {
await page.goto(mediaWithImageSizeAdminPropsURL.list)
await openListFilters(page, {})
const whereBuilder = page.locator('.where-builder')
await whereBuilder.locator('.where-builder__add-first-filter').click()
const conditionField = whereBuilder.locator('.condition__field')
await conditionField.click()
const menuList = conditionField.locator('.rs__menu-list')
// ensure the image size is not present
await expect(menuList.getByText('Sizes > one > URL', { exact: true })).toHaveCount(0)
await expect(menuList.getByText('Sizes > one > Width', { exact: true })).toHaveCount(0)
await expect(menuList.getByText('Sizes > one > Height', { exact: true })).toHaveCount(0)
await expect(menuList.getByText('Sizes > one > MIME Type', { exact: true })).toHaveCount(0)
await expect(menuList.getByText('Sizes > one > File Size', { exact: true })).toHaveCount(0)
await expect(menuList.getByText('Sizes > one > File Name', { exact: true })).toHaveCount(0)
await expect(menuList.getByText('Sizes > three > URL', { exact: true })).toHaveCount(0)
await expect(menuList.getByText('Sizes > three > Width', { exact: true })).toHaveCount(0)
await expect(menuList.getByText('Sizes > three > Height', { exact: true })).toHaveCount(0)
await expect(menuList.getByText('Sizes > three > MIME Type', { exact: true })).toHaveCount(0)
await expect(menuList.getByText('Sizes > three > File Size', { exact: true })).toHaveCount(0)
await expect(menuList.getByText('Sizes > three > File Name', { exact: true })).toHaveCount(0)
})
test('should show image size in where filter drodown in list view if imageSize has admin.disableListFilter false', async () => {
await page.goto(mediaWithImageSizeAdminPropsURL.list)
await openListFilters(page, {})
const whereBuilder = page.locator('.where-builder')
await whereBuilder.locator('.where-builder__add-first-filter').click()
const conditionField = whereBuilder.locator('.condition__field')
await conditionField.click()
const menuList = conditionField.locator('.rs__menu-list')
// ensure the image size is present
await expect(menuList.getByText('Sizes > two > URL', { exact: true })).toHaveCount(1)
await expect(menuList.getByText('Sizes > two > Width', { exact: true })).toHaveCount(1)
await expect(menuList.getByText('Sizes > two > Height', { exact: true })).toHaveCount(1)
await expect(menuList.getByText('Sizes > two > MIME Type', { exact: true })).toHaveCount(1)
await expect(menuList.getByText('Sizes > two > File Size', { exact: true })).toHaveCount(1)
await expect(menuList.getByText('Sizes > two > File Name', { exact: true })).toHaveCount(1)
await expect(menuList.getByText('Sizes > four > URL', { exact: true })).toHaveCount(1)
await expect(menuList.getByText('Sizes > four > Width', { exact: true })).toHaveCount(1)
await expect(menuList.getByText('Sizes > four > Height', { exact: true })).toHaveCount(1)
await expect(menuList.getByText('Sizes > four > MIME Type', { exact: true })).toHaveCount(1)
await expect(menuList.getByText('Sizes > four > File Size', { exact: true })).toHaveCount(1)
await expect(menuList.getByText('Sizes > four > File Name', { exact: true })).toHaveCount(1)
})
})

View File

@@ -121,6 +121,7 @@ export interface Config {
'file-mime-type': FileMimeType;
'svg-only': SvgOnly;
'media-without-delete-access': MediaWithoutDeleteAccess;
'media-with-image-size-admin-props': MediaWithImageSizeAdminProp;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@@ -182,6 +183,7 @@ export interface Config {
'file-mime-type': FileMimeTypeSelect<false> | FileMimeTypeSelect<true>;
'svg-only': SvgOnlySelect<false> | SvgOnlySelect<true>;
'media-without-delete-access': MediaWithoutDeleteAccessSelect<false> | MediaWithoutDeleteAccessSelect<true>;
'media-with-image-size-admin-props': MediaWithImageSizeAdminPropsSelect<false> | MediaWithImageSizeAdminPropsSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -1664,6 +1666,58 @@ export interface MediaWithoutDeleteAccess {
focalX?: number | null;
focalY?: number | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media-with-image-size-admin-props".
*/
export interface MediaWithImageSizeAdminProp {
id: string;
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;
sizes?: {
one?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
two?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
three?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
four?: {
url?: string | null;
width?: number | null;
height?: number | null;
mimeType?: string | null;
filesize?: number | null;
filename?: string | null;
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
@@ -1911,6 +1965,10 @@ export interface PayloadLockedDocument {
relationTo: 'media-without-delete-access';
value: string | MediaWithoutDeleteAccess;
} | null)
| ({
relationTo: 'media-with-image-size-admin-props';
value: string | MediaWithImageSizeAdminProp;
} | null)
| ({
relationTo: 'users';
value: string | User;
@@ -3470,6 +3528,67 @@ export interface MediaWithoutDeleteAccessSelect<T extends boolean = true> {
focalX?: T;
focalY?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media-with-image-size-admin-props_select".
*/
export interface MediaWithImageSizeAdminPropsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
url?: T;
thumbnailURL?: T;
filename?: T;
mimeType?: T;
filesize?: T;
width?: T;
height?: T;
focalX?: T;
focalY?: T;
sizes?:
| T
| {
one?:
| T
| {
url?: T;
width?: T;
height?: T;
mimeType?: T;
filesize?: T;
filename?: T;
};
two?:
| T
| {
url?: T;
width?: T;
height?: T;
mimeType?: T;
filesize?: T;
filename?: T;
};
three?:
| T
| {
url?: T;
width?: T;
height?: T;
mimeType?: T;
filesize?: T;
filename?: T;
};
four?:
| T
| {
url?: T;
width?: T;
height?: T;
mimeType?: T;
filesize?: T;
filename?: T;
};
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".

View File

@@ -41,3 +41,4 @@ export const fileMimeTypeSlug = 'file-mime-type'
export const svgOnlySlug = 'svg-only'
export const anyImagesSlug = 'any-images'
export const mediaWithoutDeleteAccessSlug = 'media-without-delete-access'
export const mediaWithImageSizeAdminPropsSlug = 'media-with-image-size-admin-props'