perf(ui): download only images and optimize image selection for document edit view, prioritize best-fit size (#11844)
### What? In the same vein as #11696, this PR optimizes how images are selected for display in the document edit view. It ensures that only image files are processed and selects the most appropriate size to minimize unnecessary downloads and improve performance. #### Previously: - Non-image files were being processed unnecessarily, despite not generating thumbnails. - Images without a `thumbnailURL` defaulted to their original full size, even when smaller, optimized versions were available. #### Now: - **Only images** are processed for thumbnails, avoiding redundant requests for non-images. - **The smallest available image within a target range** (`40px - 180px`) is prioritized for display. - **If no images fit within this range**, the logic selects: - The next smallest larger image (if available). - The **original** image if it is smaller than the next available larger size. - The largest **smaller** image if no better fit exists. ### Why? Prevents unnecessary downloads of non-image files, reduces bandwidth usage by selecting more efficient image sizes and improves load times and performance in the edit view. ### How? - **Filters out non-image files** when determining which assets to display. - Uses the same algorithm as in #11696 but turns it into a reusable function to be used in various areas around the codebase. Namely the upload field hasOne and hasMany components. Before (4.5mb transfer):  After (15.9kb transfer): 
This commit is contained in:
@@ -6,16 +6,15 @@ import type {
|
|||||||
UploadFieldClient,
|
UploadFieldClient,
|
||||||
} from 'payload'
|
} from 'payload'
|
||||||
|
|
||||||
|
import { isImage } from 'payload/shared'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { Thumbnail } from '../../../../Thumbnail/index.js'
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
import { getBestFitFromSizes } from '../../../../../utilities/getBestFitFromSizes.js'
|
||||||
|
import { Thumbnail } from '../../../../Thumbnail/index.js'
|
||||||
|
|
||||||
const baseClass = 'file'
|
const baseClass = 'file'
|
||||||
|
|
||||||
const targetThumbnailSizeMin = 40
|
|
||||||
const targetThumbnailSizeMax = 180
|
|
||||||
|
|
||||||
export interface FileCellProps
|
export interface FileCellProps
|
||||||
extends DefaultCellComponentProps<TextFieldClient | UploadFieldClient> {
|
extends DefaultCellComponentProps<TextFieldClient | UploadFieldClient> {
|
||||||
readonly collectionConfig: ClientCollectionConfig
|
readonly collectionConfig: ClientCollectionConfig
|
||||||
@@ -33,46 +32,13 @@ export const FileCell: React.FC<FileCellProps> = ({
|
|||||||
if (previewAllowed) {
|
if (previewAllowed) {
|
||||||
let fileSrc: string | undefined = rowData?.thumbnailURL ?? rowData?.url
|
let fileSrc: string | undefined = rowData?.thumbnailURL ?? rowData?.url
|
||||||
|
|
||||||
if (
|
if (isImage(rowData?.mimeType)) {
|
||||||
rowData?.url &&
|
fileSrc = getBestFitFromSizes({
|
||||||
!rowData?.thumbnailURL &&
|
sizes: rowData?.sizes,
|
||||||
typeof rowData?.mimeType === 'string' &&
|
thumbnailURL: rowData?.thumbnailURL,
|
||||||
rowData?.mimeType.startsWith('image') &&
|
url: rowData?.url,
|
||||||
rowData?.sizes
|
width: rowData?.width,
|
||||||
) {
|
})
|
||||||
const sizes = Object.values<{ url?: string; width?: number }>(rowData.sizes)
|
|
||||||
|
|
||||||
const bestFit = sizes.reduce(
|
|
||||||
(closest, current) => {
|
|
||||||
if (!current.width || current.width < targetThumbnailSizeMin) {
|
|
||||||
return closest
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current.width >= targetThumbnailSizeMin && current.width <= targetThumbnailSizeMax) {
|
|
||||||
return !closest.width ||
|
|
||||||
current.width < closest.width ||
|
|
||||||
closest.width < targetThumbnailSizeMin ||
|
|
||||||
closest.width > targetThumbnailSizeMax
|
|
||||||
? current
|
|
||||||
: closest
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!closest.width ||
|
|
||||||
(!closest.original &&
|
|
||||||
closest.width < targetThumbnailSizeMin &&
|
|
||||||
current.width > closest.width) ||
|
|
||||||
(closest.width > targetThumbnailSizeMax && current.width < closest.width)
|
|
||||||
) {
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
|
|
||||||
return closest
|
|
||||||
},
|
|
||||||
{ original: true, url: rowData?.url, width: rowData?.width },
|
|
||||||
)
|
|
||||||
|
|
||||||
fileSrc = bestFit.url || fileSrc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -11,10 +11,14 @@ import { UploadCard } from '../UploadCard/index.js'
|
|||||||
|
|
||||||
const baseClass = 'upload upload--has-many'
|
const baseClass = 'upload upload--has-many'
|
||||||
|
|
||||||
import type { ReloadDoc } from '../types.js'
|
import { isImage } from 'payload/shared'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
|
import type { ReloadDoc } from '../types.js'
|
||||||
|
|
||||||
|
import { getBestFitFromSizes } from '../../../utilities/getBestFitFromSizes.js'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
readonly className?: string
|
readonly className?: string
|
||||||
readonly displayPreview?: boolean
|
readonly displayPreview?: boolean
|
||||||
@@ -96,6 +100,15 @@ export function UploadComponentHasMany(props: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isImage(value.mimeType)) {
|
||||||
|
thumbnailSrc = getBestFitFromSizes({
|
||||||
|
sizes: value.sizes,
|
||||||
|
thumbnailURL: thumbnailSrc,
|
||||||
|
url: src,
|
||||||
|
width: value.width,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DraggableSortableItem disabled={!isSortable || readonly} id={id} key={id}>
|
<DraggableSortableItem disabled={!isSortable || readonly} id={id} key={id}>
|
||||||
{(draggableSortableItemProps) => (
|
{(draggableSortableItemProps) => (
|
||||||
@@ -137,7 +150,7 @@ export function UploadComponentHasMany(props: Props) {
|
|||||||
onRemove={() => removeItem(index)}
|
onRemove={() => removeItem(index)}
|
||||||
reloadDoc={reloadDoc}
|
reloadDoc={reloadDoc}
|
||||||
src={src}
|
src={src}
|
||||||
thumbnailSrc={thumbnailSrc || src}
|
thumbnailSrc={thumbnailSrc}
|
||||||
withMeta={false}
|
withMeta={false}
|
||||||
x={value?.width as number}
|
x={value?.width as number}
|
||||||
y={value?.height as number}
|
y={value?.height as number}
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
|
|
||||||
import type { JsonObject } from 'payload'
|
import type { JsonObject } from 'payload'
|
||||||
|
|
||||||
|
import { isImage } from 'payload/shared'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { ReloadDoc } from '../types.js'
|
import type { ReloadDoc } from '../types.js'
|
||||||
|
|
||||||
|
import { getBestFitFromSizes } from '../../../utilities/getBestFitFromSizes.js'
|
||||||
|
import './index.scss'
|
||||||
import { RelationshipContent } from '../RelationshipContent/index.js'
|
import { RelationshipContent } from '../RelationshipContent/index.js'
|
||||||
import { UploadCard } from '../UploadCard/index.js'
|
import { UploadCard } from '../UploadCard/index.js'
|
||||||
import './index.scss'
|
|
||||||
|
|
||||||
const baseClass = 'upload upload--has-one'
|
const baseClass = 'upload upload--has-one'
|
||||||
|
|
||||||
@@ -49,6 +51,15 @@ export function UploadComponentHasOne(props: Props) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isImage(value.mimeType)) {
|
||||||
|
thumbnailSrc = getBestFitFromSizes({
|
||||||
|
sizes: value.sizes,
|
||||||
|
thumbnailURL: thumbnailSrc,
|
||||||
|
url: src,
|
||||||
|
width: value.width,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UploadCard className={[baseClass, className].filter(Boolean).join(' ')}>
|
<UploadCard className={[baseClass, className].filter(Boolean).join(' ')}>
|
||||||
<RelationshipContent
|
<RelationshipContent
|
||||||
@@ -64,7 +75,7 @@ export function UploadComponentHasOne(props: Props) {
|
|||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
reloadDoc={reloadDoc}
|
reloadDoc={reloadDoc}
|
||||||
src={src}
|
src={src}
|
||||||
thumbnailSrc={thumbnailSrc || src}
|
thumbnailSrc={thumbnailSrc}
|
||||||
x={value?.width as number}
|
x={value?.width as number}
|
||||||
y={value?.height as number}
|
y={value?.height as number}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { TypeWithID } from 'payload'
|
import type { TypeWithID } from 'payload'
|
||||||
|
|
||||||
import { formatFilesize } from 'payload/shared'
|
import { formatFilesize, isImage } from 'payload/shared'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { ReloadDoc } from '../types.js'
|
import type { ReloadDoc } from '../types.js'
|
||||||
@@ -99,7 +99,7 @@ export function RelationshipContent(props: Props) {
|
|||||||
alt={alt}
|
alt={alt}
|
||||||
className={`${baseClass}__thumbnail`}
|
className={`${baseClass}__thumbnail`}
|
||||||
filename={filename}
|
filename={filename}
|
||||||
fileSrc={thumbnailSrc}
|
fileSrc={isImage(mimeType) && thumbnailSrc}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
70
packages/ui/src/utilities/getBestFitFromSizes.ts
Normal file
70
packages/ui/src/utilities/getBestFitFromSizes.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Takes image sizes and a target range and returns the url of the image within that range.
|
||||||
|
* If no images fit within the range, it selects the next smallest adequate image, the original,
|
||||||
|
* or the largest smaller image if no better fit exists.
|
||||||
|
*
|
||||||
|
* @param sizes The given FileSizes.
|
||||||
|
* @param targetSizeMax The ideal image maximum width. Defaults to 180.
|
||||||
|
* @param targetSizeMin The ideal image minimum width. Defaults to 40.
|
||||||
|
* @param thumbnailURL The thumbnail url set in config. If passed a url, will return early with it.
|
||||||
|
* @param url The url of the original file.
|
||||||
|
* @param width The width of the original file.
|
||||||
|
* @returns A url of the best fit file.
|
||||||
|
*/
|
||||||
|
export const getBestFitFromSizes = ({
|
||||||
|
sizes,
|
||||||
|
targetSizeMax = 180,
|
||||||
|
targetSizeMin = 40,
|
||||||
|
thumbnailURL,
|
||||||
|
url,
|
||||||
|
width,
|
||||||
|
}: {
|
||||||
|
sizes?: Record<string, { url?: string; width?: number }>
|
||||||
|
targetSizeMax?: number
|
||||||
|
targetSizeMin?: number
|
||||||
|
thumbnailURL?: string
|
||||||
|
url: string
|
||||||
|
width?: number
|
||||||
|
}) => {
|
||||||
|
if (thumbnailURL) {
|
||||||
|
return thumbnailURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sizes) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
const bestFit = Object.values(sizes).reduce<{
|
||||||
|
original?: boolean
|
||||||
|
url?: string
|
||||||
|
width?: number
|
||||||
|
}>(
|
||||||
|
(closest, current) => {
|
||||||
|
if (!current.width || current.width < targetSizeMin) {
|
||||||
|
return closest
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.width >= targetSizeMin && current.width <= targetSizeMax) {
|
||||||
|
return !closest.width ||
|
||||||
|
current.width < closest.width ||
|
||||||
|
closest.width < targetSizeMin ||
|
||||||
|
closest.width > targetSizeMax
|
||||||
|
? current
|
||||||
|
: closest
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!closest.width ||
|
||||||
|
(!closest.original && closest.width < targetSizeMin && current.width > closest.width) ||
|
||||||
|
(closest.width > targetSizeMax && current.width < closest.width)
|
||||||
|
) {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
return closest
|
||||||
|
},
|
||||||
|
{ original: true, url, width },
|
||||||
|
)
|
||||||
|
|
||||||
|
return bestFit.url || url
|
||||||
|
}
|
||||||
@@ -735,6 +735,31 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
slug: 'best-fit',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'withAdminThumbnail',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'admin-thumbnail-function',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'withinRange',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: enlargeSlug,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'nextSmallestOutOfRange',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'focal-only',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'original',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'focal-only',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
onInit: async (payload) => {
|
onInit: async (payload) => {
|
||||||
const uploadsDir = path.resolve(dirname, './media')
|
const uploadsDir = path.resolve(dirname, './media')
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ let uploadsOne: AdminUrlUtil
|
|||||||
let uploadsTwo: AdminUrlUtil
|
let uploadsTwo: AdminUrlUtil
|
||||||
let customUploadFieldURL: AdminUrlUtil
|
let customUploadFieldURL: AdminUrlUtil
|
||||||
let hideFileInputOnCreateURL: AdminUrlUtil
|
let hideFileInputOnCreateURL: AdminUrlUtil
|
||||||
|
let bestFitURL: AdminUrlUtil
|
||||||
let consoleErrorsFromPage: string[] = []
|
let consoleErrorsFromPage: string[] = []
|
||||||
let collectErrorsFromPage: () => boolean
|
let collectErrorsFromPage: () => boolean
|
||||||
let stopCollectingErrorsFromPage: () => boolean
|
let stopCollectingErrorsFromPage: () => boolean
|
||||||
@@ -99,6 +100,7 @@ describe('Uploads', () => {
|
|||||||
uploadsTwo = new AdminUrlUtil(serverURL, 'uploads-2')
|
uploadsTwo = new AdminUrlUtil(serverURL, 'uploads-2')
|
||||||
customUploadFieldURL = new AdminUrlUtil(serverURL, customUploadFieldSlug)
|
customUploadFieldURL = new AdminUrlUtil(serverURL, customUploadFieldSlug)
|
||||||
hideFileInputOnCreateURL = new AdminUrlUtil(serverURL, hideFileInputOnCreateSlug)
|
hideFileInputOnCreateURL = new AdminUrlUtil(serverURL, hideFileInputOnCreateSlug)
|
||||||
|
bestFitURL = new AdminUrlUtil(serverURL, 'best-fit')
|
||||||
|
|
||||||
const context = await browser.newContext()
|
const context = await browser.newContext()
|
||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
@@ -1349,4 +1351,53 @@ describe('Uploads', () => {
|
|||||||
|
|
||||||
await expect(page.locator('.file-field .file-details__remove')).toBeHidden()
|
await expect(page.locator('.file-field .file-details__remove')).toBeHidden()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('imageSizes best fit', () => {
|
||||||
|
test('should select adminThumbnail if one exists', async () => {
|
||||||
|
await page.goto(bestFitURL.create)
|
||||||
|
await page.locator('#field-withAdminThumbnail button.upload__listToggler').click()
|
||||||
|
await page.locator('tr.row-1 td.cell-filename button.default-cell__first-cell').click()
|
||||||
|
const thumbnail = page.locator('#field-withAdminThumbnail div.thumbnail > img')
|
||||||
|
await expect(thumbnail).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
'https://payloadcms.com/images/universal-truth.jpg',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should select an image within target range', async () => {
|
||||||
|
await page.goto(bestFitURL.create)
|
||||||
|
await page.locator('#field-withinRange button.upload__createNewToggler').click()
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser')
|
||||||
|
await page.getByText('Select a file').click()
|
||||||
|
const fileChooser = await fileChooserPromise
|
||||||
|
await fileChooser.setFiles(path.join(dirname, 'test-image.jpg'))
|
||||||
|
await page.locator('dialog button#action-save').click()
|
||||||
|
const thumbnail = page.locator('#field-withinRange div.thumbnail > img')
|
||||||
|
await expect(thumbnail).toHaveAttribute('src', '/api/enlarge/file/test-image-180x50.jpg')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should select next smallest image outside of range but smaller than original', async () => {
|
||||||
|
await page.goto(bestFitURL.create)
|
||||||
|
await page.locator('#field-nextSmallestOutOfRange button.upload__createNewToggler').click()
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser')
|
||||||
|
await page.getByText('Select a file').click()
|
||||||
|
const fileChooser = await fileChooserPromise
|
||||||
|
await fileChooser.setFiles(path.join(dirname, 'test-image.jpg'))
|
||||||
|
await page.locator('dialog button#action-save').click()
|
||||||
|
const thumbnail = page.locator('#field-nextSmallestOutOfRange div.thumbnail > img')
|
||||||
|
await expect(thumbnail).toHaveAttribute('src', '/api/focal-only/file/test-image-400x300.jpg')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should select original if smaller than next available size', async () => {
|
||||||
|
await page.goto(bestFitURL.create)
|
||||||
|
await page.locator('#field-original button.upload__createNewToggler').click()
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser')
|
||||||
|
await page.getByText('Select a file').click()
|
||||||
|
const fileChooser = await fileChooserPromise
|
||||||
|
await fileChooser.setFiles(path.join(dirname, 'small.png'))
|
||||||
|
await page.locator('dialog button#action-save').click()
|
||||||
|
const thumbnail = page.locator('#field-original div.thumbnail > img')
|
||||||
|
await expect(thumbnail).toHaveAttribute('src', '/api/focal-only/file/small.png')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export interface Config {
|
|||||||
'media-without-relation-preview': MediaWithoutRelationPreview;
|
'media-without-relation-preview': MediaWithoutRelationPreview;
|
||||||
'relation-preview': RelationPreview;
|
'relation-preview': RelationPreview;
|
||||||
'hide-file-input-on-create': HideFileInputOnCreate;
|
'hide-file-input-on-create': HideFileInputOnCreate;
|
||||||
|
'best-fit': BestFit;
|
||||||
users: User;
|
users: User;
|
||||||
'payload-locked-documents': PayloadLockedDocument;
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
'payload-preferences': PayloadPreference;
|
'payload-preferences': PayloadPreference;
|
||||||
@@ -142,6 +143,7 @@ export interface Config {
|
|||||||
'media-without-relation-preview': MediaWithoutRelationPreviewSelect<false> | MediaWithoutRelationPreviewSelect<true>;
|
'media-without-relation-preview': MediaWithoutRelationPreviewSelect<false> | MediaWithoutRelationPreviewSelect<true>;
|
||||||
'relation-preview': RelationPreviewSelect<false> | RelationPreviewSelect<true>;
|
'relation-preview': RelationPreviewSelect<false> | RelationPreviewSelect<true>;
|
||||||
'hide-file-input-on-create': HideFileInputOnCreateSelect<false> | HideFileInputOnCreateSelect<true>;
|
'hide-file-input-on-create': HideFileInputOnCreateSelect<false> | HideFileInputOnCreateSelect<true>;
|
||||||
|
'best-fit': BestFitSelect<false> | BestFitSelect<true>;
|
||||||
users: UsersSelect<false> | UsersSelect<true>;
|
users: UsersSelect<false> | UsersSelect<true>;
|
||||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||||
@@ -1260,6 +1262,19 @@ export interface RelationPreview {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "best-fit".
|
||||||
|
*/
|
||||||
|
export interface BestFit {
|
||||||
|
id: string;
|
||||||
|
withAdminThumbnail?: (string | null) | AdminThumbnailFunction;
|
||||||
|
withinRange?: (string | null) | Enlarge;
|
||||||
|
nextSmallestOutOfRange?: (string | null) | FocalOnly;
|
||||||
|
original?: (string | null) | FocalOnly;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "users".
|
* via the `definition` "users".
|
||||||
@@ -1420,6 +1435,10 @@ export interface PayloadLockedDocument {
|
|||||||
relationTo: 'hide-file-input-on-create';
|
relationTo: 'hide-file-input-on-create';
|
||||||
value: string | HideFileInputOnCreate;
|
value: string | HideFileInputOnCreate;
|
||||||
} | null)
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'best-fit';
|
||||||
|
value: string | BestFit;
|
||||||
|
} | null)
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'users';
|
relationTo: 'users';
|
||||||
value: string | User;
|
value: string | User;
|
||||||
@@ -2632,6 +2651,18 @@ export interface HideFileInputOnCreateSelect<T extends boolean = true> {
|
|||||||
focalX?: T;
|
focalX?: T;
|
||||||
focalY?: T;
|
focalY?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "best-fit_select".
|
||||||
|
*/
|
||||||
|
export interface BestFitSelect<T extends boolean = true> {
|
||||||
|
withAdminThumbnail?: T;
|
||||||
|
withinRange?: T;
|
||||||
|
nextSmallestOutOfRange?: T;
|
||||||
|
original?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "users_select".
|
* via the `definition` "users_select".
|
||||||
|
|||||||
Reference in New Issue
Block a user