fix(ui): uploading from drawer & focal point positioning (#7117)
Fixes #7101 Fixes #7006 Drawers were sending duplicate query params. This new approach modeled after the fix in V2, ensures that each drawer has its own action url created per document and the query params will be created when that is generated. Also fixes the following: - incorrect focal point cropping - generated filenames for animated image names used incorrect heights
This commit is contained in:
@@ -180,7 +180,6 @@ import {
|
|||||||
useFormInitializing,
|
useFormInitializing,
|
||||||
useFormModified,
|
useFormModified,
|
||||||
useFormProcessing,
|
useFormProcessing,
|
||||||
useFormQueryParams,
|
|
||||||
useFormSubmitted,
|
useFormSubmitted,
|
||||||
useHotkey,
|
useHotkey,
|
||||||
useIntersect,
|
useIntersect,
|
||||||
@@ -221,7 +220,6 @@ import {
|
|||||||
EntityVisibilityProvider,
|
EntityVisibilityProvider,
|
||||||
FieldComponentsProvider,
|
FieldComponentsProvider,
|
||||||
FieldPropsProvider,
|
FieldPropsProvider,
|
||||||
FormQueryParamsProvider,
|
|
||||||
ListInfoProvider,
|
ListInfoProvider,
|
||||||
ListQueryProvider,
|
ListQueryProvider,
|
||||||
LocaleProvider,
|
LocaleProvider,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { AdminViewProps, ServerSideEditViewProps } from 'payload'
|
import type { AdminViewProps, ServerSideEditViewProps } from 'payload'
|
||||||
|
|
||||||
import { DocumentInfoProvider, FormQueryParamsProvider, HydrateClientUser } from '@payloadcms/ui'
|
import { DocumentInfoProvider, HydrateClientUser } from '@payloadcms/ui'
|
||||||
import { RenderCustomComponent } from '@payloadcms/ui/shared'
|
import { RenderCustomComponent } from '@payloadcms/ui/shared'
|
||||||
import { notFound } from 'next/navigation.js'
|
import { notFound } from 'next/navigation.js'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@@ -65,7 +65,6 @@ export const Account: React.FC<AdminViewProps> = async ({
|
|||||||
return (
|
return (
|
||||||
<DocumentInfoProvider
|
<DocumentInfoProvider
|
||||||
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} />}
|
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} />}
|
||||||
action={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
|
|
||||||
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
|
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
|
||||||
collectionSlug={userSlug}
|
collectionSlug={userSlug}
|
||||||
docPermissions={docPermissions}
|
docPermissions={docPermissions}
|
||||||
@@ -84,31 +83,22 @@ export const Account: React.FC<AdminViewProps> = async ({
|
|||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
/>
|
/>
|
||||||
<HydrateClientUser permissions={permissions} user={user} />
|
<HydrateClientUser permissions={permissions} user={user} />
|
||||||
<FormQueryParamsProvider
|
<RenderCustomComponent
|
||||||
initialParams={{
|
CustomComponent={
|
||||||
depth: 0,
|
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined
|
||||||
'fallback-locale': 'null',
|
}
|
||||||
locale: locale?.code,
|
DefaultComponent={EditView}
|
||||||
uploadEdits: undefined,
|
componentProps={viewComponentProps}
|
||||||
|
serverOnlyProps={{
|
||||||
|
i18n,
|
||||||
|
locale,
|
||||||
|
params,
|
||||||
|
payload,
|
||||||
|
permissions,
|
||||||
|
searchParams,
|
||||||
|
user,
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<RenderCustomComponent
|
|
||||||
CustomComponent={
|
|
||||||
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined
|
|
||||||
}
|
|
||||||
DefaultComponent={EditView}
|
|
||||||
componentProps={viewComponentProps}
|
|
||||||
serverOnlyProps={{
|
|
||||||
i18n,
|
|
||||||
locale,
|
|
||||||
params,
|
|
||||||
payload,
|
|
||||||
permissions,
|
|
||||||
searchParams,
|
|
||||||
user,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormQueryParamsProvider>
|
|
||||||
</DocumentInfoProvider>
|
</DocumentInfoProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import type {
|
import type { AdminViewComponent, AdminViewProps, EditViewComponent } from 'payload'
|
||||||
AdminViewComponent,
|
|
||||||
AdminViewProps,
|
|
||||||
EditViewComponent,
|
|
||||||
ServerSideEditViewProps,
|
|
||||||
} from 'payload'
|
|
||||||
|
|
||||||
import {
|
import { DocumentInfoProvider, EditDepthProvider, HydrateClientUser } from '@payloadcms/ui'
|
||||||
DocumentInfoProvider,
|
|
||||||
EditDepthProvider,
|
|
||||||
FormQueryParamsProvider,
|
|
||||||
HydrateClientUser,
|
|
||||||
} from '@payloadcms/ui'
|
|
||||||
import { RenderCustomComponent, isEditing as getIsEditing } from '@payloadcms/ui/shared'
|
import { RenderCustomComponent, isEditing as getIsEditing } from '@payloadcms/ui/shared'
|
||||||
import { notFound, redirect } from 'next/navigation.js'
|
import { notFound, redirect } from 'next/navigation.js'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
@@ -65,7 +55,6 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
let ErrorView: AdminViewComponent
|
let ErrorView: AdminViewComponent
|
||||||
|
|
||||||
let apiURL: string
|
let apiURL: string
|
||||||
let action: string
|
|
||||||
|
|
||||||
const { data, formState } = await getDocumentData({
|
const { data, formState } = await getDocumentData({
|
||||||
id,
|
id,
|
||||||
@@ -88,8 +77,6 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}`
|
|
||||||
|
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (collectionConfig.versions?.drafts) {
|
if (collectionConfig.versions?.drafts) {
|
||||||
params.append('draft', 'true')
|
params.append('draft', 'true')
|
||||||
@@ -128,8 +115,6 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
action = `${serverURL}${apiRoute}/globals/${globalSlug}`
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
locale: locale?.code,
|
locale: locale?.code,
|
||||||
})
|
})
|
||||||
@@ -198,7 +183,6 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DocumentInfoProvider
|
<DocumentInfoProvider
|
||||||
action={action}
|
|
||||||
apiURL={apiURL}
|
apiURL={apiURL}
|
||||||
collectionSlug={collectionConfig?.slug}
|
collectionSlug={collectionConfig?.slug}
|
||||||
disableActions={false}
|
disableActions={false}
|
||||||
@@ -225,34 +209,25 @@ export const Document: React.FC<AdminViewProps> = async ({
|
|||||||
depth={1}
|
depth={1}
|
||||||
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}
|
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}
|
||||||
>
|
>
|
||||||
<FormQueryParamsProvider
|
{ErrorView ? (
|
||||||
initialParams={{
|
<ErrorView initPageResult={initPageResult} searchParams={searchParams} />
|
||||||
depth: 0,
|
) : (
|
||||||
'fallback-locale': 'null',
|
<RenderCustomComponent
|
||||||
locale: locale?.code,
|
CustomComponent={ViewOverride || CustomView}
|
||||||
uploadEdits: undefined,
|
DefaultComponent={DefaultView}
|
||||||
}}
|
serverOnlyProps={{
|
||||||
>
|
i18n,
|
||||||
{ErrorView ? (
|
initPageResult,
|
||||||
<ErrorView initPageResult={initPageResult} searchParams={searchParams} />
|
locale,
|
||||||
) : (
|
params,
|
||||||
<RenderCustomComponent
|
payload,
|
||||||
CustomComponent={ViewOverride || CustomView}
|
permissions,
|
||||||
DefaultComponent={DefaultView}
|
routeSegments: segments,
|
||||||
serverOnlyProps={{
|
searchParams,
|
||||||
i18n,
|
user,
|
||||||
initPageResult,
|
}}
|
||||||
locale,
|
/>
|
||||||
params,
|
)}
|
||||||
payload,
|
|
||||||
permissions,
|
|
||||||
routeSegments: segments,
|
|
||||||
searchParams,
|
|
||||||
user,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</FormQueryParamsProvider>
|
|
||||||
</EditDepthProvider>
|
</EditDepthProvider>
|
||||||
</DocumentInfoProvider>
|
</DocumentInfoProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
useDocumentEvents,
|
useDocumentEvents,
|
||||||
useDocumentInfo,
|
useDocumentInfo,
|
||||||
useEditDepth,
|
useEditDepth,
|
||||||
useFormQueryParams,
|
useUploadEdits,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
import { getFormState } from '@payloadcms/ui/shared'
|
import { getFormState } from '@payloadcms/ui/shared'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation.js'
|
import { useRouter, useSearchParams } from 'next/navigation.js'
|
||||||
@@ -58,11 +58,13 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
const { refreshCookieAsync, user } = useAuth()
|
const { refreshCookieAsync, user } = useAuth()
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { dispatchFormQueryParams } = useFormQueryParams()
|
|
||||||
const { getComponentMap, getFieldMap } = useComponentMap()
|
const { getComponentMap, getFieldMap } = useComponentMap()
|
||||||
const params = useSearchParams()
|
|
||||||
const depth = useEditDepth()
|
const depth = useEditDepth()
|
||||||
|
const params = useSearchParams()
|
||||||
const { reportUpdate } = useDocumentEvents()
|
const { reportUpdate } = useDocumentEvents()
|
||||||
|
const { resetUploadEdits } = useUploadEdits()
|
||||||
|
|
||||||
|
const locale = params.get('locale')
|
||||||
|
|
||||||
const {
|
const {
|
||||||
admin: { user: userSlug },
|
admin: { user: userSlug },
|
||||||
@@ -72,8 +74,6 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
serverURL,
|
serverURL,
|
||||||
} = config
|
} = config
|
||||||
|
|
||||||
const locale = params.get('locale')
|
|
||||||
|
|
||||||
const collectionConfig =
|
const collectionConfig =
|
||||||
collectionSlug && collections.find((collection) => collection.slug === collectionSlug)
|
collectionSlug && collections.find((collection) => collection.slug === collectionSlug)
|
||||||
|
|
||||||
@@ -130,12 +130,7 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
const redirectRoute = `${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`
|
const redirectRoute = `${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`
|
||||||
router.push(redirectRoute)
|
router.push(redirectRoute)
|
||||||
} else {
|
} else {
|
||||||
dispatchFormQueryParams({
|
resetUploadEdits()
|
||||||
type: 'SET',
|
|
||||||
params: {
|
|
||||||
uploadEdits: null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -151,9 +146,9 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
isEditing,
|
isEditing,
|
||||||
refreshCookieAsync,
|
refreshCookieAsync,
|
||||||
adminRoute,
|
adminRoute,
|
||||||
locale,
|
|
||||||
router,
|
router,
|
||||||
dispatchFormQueryParams,
|
locale,
|
||||||
|
resetUploadEdits,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,31 @@
|
|||||||
import type { SharpOptions } from 'sharp'
|
import type { SharpOptions } from 'sharp'
|
||||||
|
|
||||||
|
import type { SanitizedConfig } from '../config/types.js'
|
||||||
|
import type { PayloadRequest } from '../types/index.js'
|
||||||
|
import type { UploadEdits } from './types.js'
|
||||||
|
|
||||||
export const percentToPixel = (value, dimension) => {
|
export const percentToPixel = (value, dimension) => {
|
||||||
return Math.floor((parseFloat(value) / 100) * dimension)
|
return Math.floor((parseFloat(value) / 100) * dimension)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cropImage({ cropData, dimensions, file, sharp }) {
|
type CropImageArgs = {
|
||||||
|
cropData: UploadEdits['crop']
|
||||||
|
dimensions: { height: number; width: number }
|
||||||
|
file: PayloadRequest['file']
|
||||||
|
heightInPixels: number
|
||||||
|
sharp: SanitizedConfig['sharp']
|
||||||
|
widthInPixels: number
|
||||||
|
}
|
||||||
|
export async function cropImage({
|
||||||
|
cropData,
|
||||||
|
dimensions,
|
||||||
|
file,
|
||||||
|
heightInPixels,
|
||||||
|
sharp,
|
||||||
|
widthInPixels,
|
||||||
|
}: CropImageArgs) {
|
||||||
try {
|
try {
|
||||||
const { heightPixels, widthPixels, x, y } = cropData
|
const { x, y } = cropData
|
||||||
|
|
||||||
const fileIsAnimatedType = ['image/avif', 'image/gif', 'image/webp'].includes(file.mimetype)
|
const fileIsAnimatedType = ['image/avif', 'image/gif', 'image/webp'].includes(file.mimetype)
|
||||||
|
|
||||||
@@ -15,10 +34,10 @@ export async function cropImage({ cropData, dimensions, file, sharp }) {
|
|||||||
if (fileIsAnimatedType) sharpOptions.animated = true
|
if (fileIsAnimatedType) sharpOptions.animated = true
|
||||||
|
|
||||||
const formattedCropData = {
|
const formattedCropData = {
|
||||||
height: Number(heightPixels),
|
height: Number(heightInPixels),
|
||||||
left: percentToPixel(x, dimensions.width),
|
left: percentToPixel(x, dimensions.width),
|
||||||
top: percentToPixel(y, dimensions.height),
|
top: percentToPixel(y, dimensions.height),
|
||||||
width: Number(widthPixels),
|
width: Number(widthInPixels),
|
||||||
}
|
}
|
||||||
|
|
||||||
const cropped = sharp(file.tempFilePath || file.data, sharpOptions).extract(formattedCropData)
|
const cropped = sharp(file.tempFilePath || file.data, sharpOptions).extract(formattedCropData)
|
||||||
|
|||||||
@@ -203,7 +203,14 @@ export const generateFileData = async <T>({
|
|||||||
let fileForResize = file
|
let fileForResize = file
|
||||||
|
|
||||||
if (cropData && sharp) {
|
if (cropData && sharp) {
|
||||||
const { data: croppedImage, info } = await cropImage({ cropData, dimensions, file, sharp })
|
const { data: croppedImage, info } = await cropImage({
|
||||||
|
cropData,
|
||||||
|
dimensions,
|
||||||
|
file,
|
||||||
|
heightInPixels: uploadEdits.heightInPixels,
|
||||||
|
sharp,
|
||||||
|
widthInPixels: uploadEdits.widthInPixels,
|
||||||
|
})
|
||||||
|
|
||||||
filesToSave.push({
|
filesToSave.push({
|
||||||
buffer: croppedImage,
|
buffer: croppedImage,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { OutputInfo, Sharp, SharpOptions } from 'sharp'
|
import type { Sharp, Metadata as SharpMetadata, SharpOptions } from 'sharp'
|
||||||
|
|
||||||
import { fileTypeFromBuffer } from 'file-type'
|
import { fileTypeFromBuffer } from 'file-type'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
@@ -68,11 +68,20 @@ const getSanitizedImageData = (sourceImage: string): SanitizedImageData => {
|
|||||||
* @param extension - the extension to use
|
* @param extension - the extension to use
|
||||||
* @returns the new image name that is not taken
|
* @returns the new image name that is not taken
|
||||||
*/
|
*/
|
||||||
const createImageName = (
|
type CreateImageNameArgs = {
|
||||||
outputImageName: string,
|
extension: string
|
||||||
{ height, width }: OutputInfo,
|
height: number
|
||||||
extension: string,
|
outputImageName: string
|
||||||
) => `${outputImageName}-${width}x${height}.${extension}`
|
width: number
|
||||||
|
}
|
||||||
|
const createImageName = ({
|
||||||
|
extension,
|
||||||
|
height,
|
||||||
|
outputImageName,
|
||||||
|
width,
|
||||||
|
}: CreateImageNameArgs): string => {
|
||||||
|
return `${outputImageName}-${width}x${height}.${extension}`
|
||||||
|
}
|
||||||
|
|
||||||
type CreateResultArgs = {
|
type CreateResultArgs = {
|
||||||
filename?: FileSize['filename']
|
filename?: FileSize['filename']
|
||||||
@@ -122,71 +131,61 @@ const createResult = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the image needs to be resized according to the requested dimensions
|
* Determine whether or not to resize the image.
|
||||||
* and the original image size. If the resize options withoutEnlargement or withoutReduction are provided,
|
* - resize using image config
|
||||||
* the image will be resized regardless of the requested dimensions, given that the
|
* - resize using image config with focal adjustments
|
||||||
* width or height to be resized is provided.
|
* - do not resize at all
|
||||||
*
|
*
|
||||||
* @param resizeConfig - object containing the requested dimensions and resize options
|
* `imageResizeConfig.withoutEnlargement`:
|
||||||
* @param original - the original image size
|
* - undefined [default]: uploading images with smaller width AND height than the image size will return null
|
||||||
* @returns true if resizing is not needed, false otherwise
|
* - false: always enlarge images to the image size
|
||||||
*/
|
* - true: if the image is smaller than the image size, return the original image
|
||||||
const preventResize = (
|
|
||||||
{ height: desiredHeight, width: desiredWidth, withoutEnlargement, withoutReduction }: ImageSize,
|
|
||||||
original: ProbedImageSize,
|
|
||||||
): boolean => {
|
|
||||||
// default is to allow reduction
|
|
||||||
if (withoutReduction !== undefined) {
|
|
||||||
return false // needs resize
|
|
||||||
}
|
|
||||||
|
|
||||||
// default is to prevent enlargement
|
|
||||||
if (withoutEnlargement !== undefined) {
|
|
||||||
return false // needs resize
|
|
||||||
}
|
|
||||||
|
|
||||||
const isWidthOrHeightNotDefined = !desiredHeight || !desiredWidth
|
|
||||||
if (isWidthOrHeightNotDefined) {
|
|
||||||
// If width and height are not defined, it means there is a format conversion
|
|
||||||
// and the image needs to be "resized" (transformed).
|
|
||||||
return false // needs resize
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasInsufficientWidth = desiredWidth > original.width
|
|
||||||
const hasInsufficientHeight = desiredHeight > original.height
|
|
||||||
if (hasInsufficientWidth && hasInsufficientHeight) {
|
|
||||||
// doesn't need resize - prevent enlargement. This should only happen if both width and height are insufficient.
|
|
||||||
// if only one dimension is insufficient and the other is sufficient, resizing needs to happen, as the image
|
|
||||||
// should be resized to the sufficient dimension.
|
|
||||||
return true // do not create a new size
|
|
||||||
}
|
|
||||||
|
|
||||||
return false // needs resize
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the image should be passed directly to sharp without payload adjusting properties.
|
|
||||||
*
|
*
|
||||||
* @param resizeConfig - object containing the requested dimensions and resize options
|
* `imageResizeConfig.withoutReduction`:
|
||||||
* @param original - the original image size
|
* - false [default]: always enlarge images to the image size
|
||||||
* @returns true if the image should passed directly to sharp
|
* - true: if the image is smaller than the image size, return the original image
|
||||||
|
*
|
||||||
|
* @return 'omit' | 'resize' | 'resizeWithFocalPoint'
|
||||||
*/
|
*/
|
||||||
const applyPayloadAdjustments = (
|
const getImageResizeAction = ({
|
||||||
{ fit, height, width, withoutEnlargement, withoutReduction }: ImageSize,
|
dimensions: originalImage,
|
||||||
original: ProbedImageSize,
|
hasFocalPoint,
|
||||||
) => {
|
imageResizeConfig,
|
||||||
if (fit === 'contain' || fit === 'inside') return false
|
}: {
|
||||||
if (!isNumber(height) && !isNumber(width)) return false
|
dimensions: ProbedImageSize
|
||||||
|
hasFocalPoint?: boolean
|
||||||
|
imageResizeConfig: ImageSize
|
||||||
|
}): 'omit' | 'resize' | 'resizeWithFocalPoint' => {
|
||||||
|
const {
|
||||||
|
fit,
|
||||||
|
height: targetHeight,
|
||||||
|
width: targetWidth,
|
||||||
|
withoutEnlargement,
|
||||||
|
withoutReduction,
|
||||||
|
} = imageResizeConfig
|
||||||
|
|
||||||
const targetAspectRatio = width / height
|
// prevent upscaling by default when x and y are both smaller than target image size
|
||||||
const originalAspectRatio = original.width / original.height
|
if (targetHeight && targetWidth) {
|
||||||
if (originalAspectRatio === targetAspectRatio) return false
|
const originalImageIsSmallerXAndY =
|
||||||
|
originalImage.width < targetWidth && originalImage.height < targetHeight
|
||||||
|
if (withoutEnlargement === undefined && originalImageIsSmallerXAndY) {
|
||||||
|
return 'omit' // prevent image size from being enlarged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const skipEnlargement = withoutEnlargement && (original.height < height || original.width < width)
|
const originalImageIsSmallerXOrY =
|
||||||
const skipReduction = withoutReduction && (original.height > height || original.width > width)
|
originalImage.width < targetWidth || originalImage.height < targetHeight
|
||||||
if (skipEnlargement || skipReduction) return false
|
if (fit === 'contain' || fit === 'inside') return 'resize'
|
||||||
|
if (!isNumber(targetHeight) && !isNumber(targetWidth)) return 'resize'
|
||||||
|
|
||||||
return true
|
const targetAspectRatio = targetWidth / targetHeight
|
||||||
|
const originalAspectRatio = originalImage.width / originalImage.height
|
||||||
|
if (originalAspectRatio === targetAspectRatio) return 'resize'
|
||||||
|
|
||||||
|
if (withoutEnlargement && originalImageIsSmallerXOrY) return 'resize'
|
||||||
|
if (withoutReduction && !originalImageIsSmallerXOrY) return 'resize'
|
||||||
|
|
||||||
|
return hasFocalPoint ? 'resizeWithFocalPoint' : 'resize'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -209,6 +208,19 @@ const sanitizeResizeConfig = (resizeConfig: ImageSize): ImageSize => {
|
|||||||
return resizeConfig
|
return resizeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to extract height from images, animated or not.
|
||||||
|
*
|
||||||
|
* @param sharpMetadata - the sharp metadata
|
||||||
|
* @returns the height of the image
|
||||||
|
*/
|
||||||
|
function extractHeightFromImage(sharpMetadata: SharpMetadata): number {
|
||||||
|
if (sharpMetadata?.pages) {
|
||||||
|
return sharpMetadata.height / sharpMetadata.pages
|
||||||
|
}
|
||||||
|
return sharpMetadata.height
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For the provided image sizes, handle the resizing and the transforms
|
* For the provided image sizes, handle the resizing and the transforms
|
||||||
* (format, trim, etc.) of each requested image size and return the result object.
|
* (format, trim, etc.) of each requested image size and return the result object.
|
||||||
@@ -261,24 +273,28 @@ export async function resizeAndTransformImageSizes({
|
|||||||
if (fileIsAnimatedType) sharpOptions.animated = true
|
if (fileIsAnimatedType) sharpOptions.animated = true
|
||||||
|
|
||||||
const sharpBase: Sharp | undefined = sharp(file.tempFilePath || file.data, sharpOptions).rotate() // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
|
const sharpBase: Sharp | undefined = sharp(file.tempFilePath || file.data, sharpOptions).rotate() // pass rotate() to auto-rotate based on EXIF data. https://github.com/payloadcms/payload/pull/3081
|
||||||
|
const originalImageMeta = await sharpBase.metadata()
|
||||||
|
|
||||||
|
const resizeImageMeta = {
|
||||||
|
height: extractHeightFromImage(originalImageMeta),
|
||||||
|
width: originalImageMeta.width,
|
||||||
|
}
|
||||||
|
|
||||||
const results: ImageSizesResult[] = await Promise.all(
|
const results: ImageSizesResult[] = await Promise.all(
|
||||||
imageSizes.map(async (imageResizeConfig): Promise<ImageSizesResult> => {
|
imageSizes.map(async (imageResizeConfig): Promise<ImageSizesResult> => {
|
||||||
imageResizeConfig = sanitizeResizeConfig(imageResizeConfig)
|
imageResizeConfig = sanitizeResizeConfig(imageResizeConfig)
|
||||||
|
|
||||||
// This checks if a resize should happen. If not, the resized image will be
|
const resizeAction = getImageResizeAction({
|
||||||
// skipped COMPLETELY and thus will not be included in the resulting images.
|
dimensions,
|
||||||
// All further format/trim options will thus be skipped as well.
|
hasFocalPoint: Boolean(incomingFocalPoint),
|
||||||
if (preventResize(imageResizeConfig, dimensions)) {
|
imageResizeConfig,
|
||||||
return createResult({ name: imageResizeConfig.name })
|
})
|
||||||
}
|
if (resizeAction === 'omit') return createResult({ name: imageResizeConfig.name })
|
||||||
|
|
||||||
const imageToResize = sharpBase.clone()
|
const imageToResize = sharpBase.clone()
|
||||||
let resized = imageToResize
|
let resized = imageToResize
|
||||||
|
|
||||||
const metadata = await sharpBase.metadata()
|
if (resizeAction === 'resizeWithFocalPoint') {
|
||||||
|
|
||||||
if (incomingFocalPoint && applyPayloadAdjustments(imageResizeConfig, dimensions)) {
|
|
||||||
let { height: resizeHeight, width: resizeWidth } = imageResizeConfig
|
let { height: resizeHeight, width: resizeWidth } = imageResizeConfig
|
||||||
|
|
||||||
const originalAspectRatio = dimensions.width / dimensions.height
|
const originalAspectRatio = dimensions.width / dimensions.height
|
||||||
@@ -293,44 +309,62 @@ export async function resizeAndTransformImageSizes({
|
|||||||
resizeHeight = Math.round(resizeWidth / originalAspectRatio)
|
resizeHeight = Math.round(resizeWidth / originalAspectRatio)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale the image up or down to fit the resize dimensions
|
if (!resizeHeight) resizeHeight = resizeImageMeta.height
|
||||||
const scaledImage = imageToResize.resize({
|
if (!resizeWidth) resizeWidth = resizeImageMeta.width
|
||||||
height: resizeHeight,
|
|
||||||
width: resizeWidth,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { info: scaledImageInfo } = await scaledImage.toBuffer({ resolveWithObject: true })
|
// if requested image is larger than the incoming size, then resize using sharp and then extract with focal point
|
||||||
|
if (resizeHeight > resizeImageMeta.height || resizeWidth > resizeImageMeta.width) {
|
||||||
|
const resizeAspectRatio = resizeWidth / resizeHeight
|
||||||
|
const prioritizeHeight = resizeAspectRatio < originalAspectRatio
|
||||||
|
resized = imageToResize.resize({
|
||||||
|
height: prioritizeHeight ? resizeHeight : undefined,
|
||||||
|
width: prioritizeHeight ? undefined : resizeWidth,
|
||||||
|
})
|
||||||
|
|
||||||
const safeResizeWidth = resizeWidth ?? scaledImageInfo.width
|
// must read from buffer, resize.metadata will return the original image metadata
|
||||||
const maxOffsetX = scaledImageInfo.width - safeResizeWidth
|
const { info } = await resized.toBuffer({ resolveWithObject: true })
|
||||||
const leftFocalEdge = Math.round(
|
resizeImageMeta.height = extractHeightFromImage({
|
||||||
scaledImageInfo.width * (incomingFocalPoint.x / 100) - safeResizeWidth / 2,
|
...originalImageMeta,
|
||||||
)
|
height: info.height,
|
||||||
const safeOffsetX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX)
|
})
|
||||||
|
resizeImageMeta.width = info.width
|
||||||
const isAnimated = fileIsAnimatedType && metadata.pages
|
|
||||||
|
|
||||||
let safeResizeHeight = resizeHeight ?? scaledImageInfo.height
|
|
||||||
|
|
||||||
if (isAnimated && resizeHeight === undefined) {
|
|
||||||
safeResizeHeight = scaledImageInfo.height / metadata.pages
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxOffsetY = isAnimated
|
const halfResizeX = resizeWidth / 2
|
||||||
? safeResizeHeight - (resizeHeight ?? safeResizeHeight)
|
const xFocalCenter = resizeImageMeta.width * (incomingFocalPoint.x / 100)
|
||||||
: scaledImageInfo.height - safeResizeHeight
|
const calculatedRightPixelBound = xFocalCenter + halfResizeX
|
||||||
|
let leftBound = xFocalCenter - halfResizeX
|
||||||
|
|
||||||
const topFocalEdge = Math.round(
|
// if the right bound is greater than the image width, adjust the left bound
|
||||||
scaledImageInfo.height * (incomingFocalPoint.y / 100) - safeResizeHeight / 2,
|
// keeping focus on the right
|
||||||
)
|
if (calculatedRightPixelBound > resizeImageMeta.width) {
|
||||||
const safeOffsetY = Math.min(Math.max(0, topFocalEdge), maxOffsetY)
|
leftBound = resizeImageMeta.width - resizeWidth
|
||||||
|
}
|
||||||
|
|
||||||
// extract the focal area from the scaled image
|
// if the left bound is less than 0, adjust the left bound to 0
|
||||||
resized = (fileIsAnimatedType ? imageToResize : scaledImage).extract({
|
// keeping the focus on the left
|
||||||
height: safeResizeHeight,
|
if (leftBound < 0) leftBound = 0
|
||||||
left: safeOffsetX,
|
|
||||||
top: safeOffsetY,
|
const halfResizeY = resizeHeight / 2
|
||||||
width: safeResizeWidth,
|
const yFocalCenter = resizeImageMeta.height * (incomingFocalPoint.y / 100)
|
||||||
|
const calculatedBottomPixelBound = yFocalCenter + halfResizeY
|
||||||
|
let topBound = yFocalCenter - halfResizeY
|
||||||
|
|
||||||
|
// if the bottom bound is greater than the image height, adjust the top bound
|
||||||
|
// keeping the image as far right as possible
|
||||||
|
if (calculatedBottomPixelBound > resizeImageMeta.height) {
|
||||||
|
topBound = resizeImageMeta.height - resizeHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the top bound is less than 0, adjust the top bound to 0
|
||||||
|
// keeping the image focus near the top
|
||||||
|
if (topBound < 0) topBound = 0
|
||||||
|
|
||||||
|
resized = resized.extract({
|
||||||
|
height: resizeHeight,
|
||||||
|
left: Math.floor(leftBound),
|
||||||
|
top: Math.floor(topBound),
|
||||||
|
width: resizeWidth,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
resized = imageToResize.resize(imageResizeConfig)
|
resized = imageToResize.resize(imageResizeConfig)
|
||||||
@@ -359,11 +393,15 @@ export async function resizeAndTransformImageSizes({
|
|||||||
|
|
||||||
const mimeInfo = await fileTypeFromBuffer(bufferData)
|
const mimeInfo = await fileTypeFromBuffer(bufferData)
|
||||||
|
|
||||||
const imageNameWithDimensions = createImageName(
|
const imageNameWithDimensions = createImageName({
|
||||||
sanitizedImage.name,
|
extension: mimeInfo?.ext || sanitizedImage.ext,
|
||||||
bufferInfo,
|
height: extractHeightFromImage({
|
||||||
mimeInfo?.ext || sanitizedImage.ext,
|
...originalImageMeta,
|
||||||
)
|
height: bufferInfo.height,
|
||||||
|
}),
|
||||||
|
outputImageName: sanitizedImage.name,
|
||||||
|
width: bufferInfo.width,
|
||||||
|
})
|
||||||
|
|
||||||
const imagePath = `${staticPath}/${imageNameWithDimensions}`
|
const imagePath = `${staticPath}/${imageNameWithDimensions}`
|
||||||
|
|
||||||
@@ -380,7 +418,8 @@ export async function resizeAndTransformImageSizes({
|
|||||||
name: imageResizeConfig.name,
|
name: imageResizeConfig.name,
|
||||||
filename: imageNameWithDimensions,
|
filename: imageNameWithDimensions,
|
||||||
filesize: size,
|
filesize: size,
|
||||||
height: fileIsAnimatedType && metadata.pages ? height / metadata.pages : height,
|
height:
|
||||||
|
fileIsAnimatedType && originalImageMeta.pages ? height / originalImageMeta.pages : height,
|
||||||
mimeType: mimeInfo?.mime || mimeType,
|
mimeType: mimeInfo?.mime || mimeType,
|
||||||
sizesToSave: [{ buffer: bufferData, path: imagePath }],
|
sizesToSave: [{ buffer: bufferData, path: imagePath }],
|
||||||
width,
|
width,
|
||||||
|
|||||||
@@ -189,15 +189,22 @@ export type FileToSave = {
|
|||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UploadEdits = {
|
type Crop = {
|
||||||
crop?: {
|
height: number
|
||||||
height?: number
|
unit: '%' | 'px'
|
||||||
width?: number
|
width: number
|
||||||
x?: number
|
x: number
|
||||||
y?: number
|
y: number
|
||||||
}
|
}
|
||||||
focalPoint?: {
|
|
||||||
x?: number
|
type FocalPoint = {
|
||||||
y?: number
|
x: number
|
||||||
}
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UploadEdits = {
|
||||||
|
crop?: Crop
|
||||||
|
focalPoint?: FocalPoint
|
||||||
|
heightInPixels?: number
|
||||||
|
widthInPixels?: number
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useModal } from '@faceless-ui/modal'
|
import { useModal } from '@faceless-ui/modal'
|
||||||
import * as qs from 'qs-esm'
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
@@ -12,7 +11,6 @@ import { XIcon } from '../../icons/X/index.js'
|
|||||||
import { useComponentMap } from '../../providers/ComponentMap/index.js'
|
import { useComponentMap } from '../../providers/ComponentMap/index.js'
|
||||||
import { useConfig } from '../../providers/Config/index.js'
|
import { useConfig } from '../../providers/Config/index.js'
|
||||||
import { DocumentInfoProvider, useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
import { DocumentInfoProvider, useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||||
import { useFormQueryParams } from '../../providers/FormQueryParams/index.js'
|
|
||||||
import { useLocale } from '../../providers/Locale/index.js'
|
import { useLocale } from '../../providers/Locale/index.js'
|
||||||
import { useTranslation } from '../../providers/Translation/index.js'
|
import { useTranslation } from '../../providers/Translation/index.js'
|
||||||
import { Gutter } from '../Gutter/index.js'
|
import { Gutter } from '../Gutter/index.js'
|
||||||
@@ -40,8 +38,6 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
|||||||
const [docID, setDocID] = useState(existingDocID)
|
const [docID, setDocID] = useState(existingDocID)
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [collectionConfig] = useRelatedCollections(collectionSlug)
|
const [collectionConfig] = useRelatedCollections(collectionSlug)
|
||||||
const { formQueryParams } = useFormQueryParams()
|
|
||||||
const formattedQueryParams = qs.stringify(formQueryParams)
|
|
||||||
|
|
||||||
const { componentMap } = useComponentMap()
|
const { componentMap } = useComponentMap()
|
||||||
|
|
||||||
@@ -50,9 +46,6 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
|||||||
const apiURL = docID
|
const apiURL = docID
|
||||||
? `${serverURL}${apiRoute}/${collectionSlug}/${docID}${locale?.code ? `?locale=${locale.code}` : ''}`
|
? `${serverURL}${apiRoute}/${collectionSlug}/${docID}${locale?.code ? `?locale=${locale.code}` : ''}`
|
||||||
: null
|
: null
|
||||||
const action = `${serverURL}${apiRoute}/${collectionSlug}${
|
|
||||||
isEditing ? `/${docID}` : ''
|
|
||||||
}?${formattedQueryParams}`
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsOpen(Boolean(modalState[drawerSlug]?.isOpen))
|
setIsOpen(Boolean(modalState[drawerSlug]?.isOpen))
|
||||||
@@ -104,7 +97,6 @@ export const DocumentDrawerContent: React.FC<DocumentDrawerProps> = ({
|
|||||||
<DocumentTitle />
|
<DocumentTitle />
|
||||||
</Gutter>
|
</Gutter>
|
||||||
}
|
}
|
||||||
action={action}
|
|
||||||
apiURL={apiURL}
|
apiURL={apiURL}
|
||||||
collectionSlug={collectionConfig.slug}
|
collectionSlug={collectionConfig.slug}
|
||||||
disableActions
|
disableActions
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type CropType from 'react-image-crop'
|
|
||||||
|
import type { UploadEdits } from 'payload'
|
||||||
|
|
||||||
import { useModal } from '@faceless-ui/modal'
|
import { useModal } from '@faceless-ui/modal'
|
||||||
import React, { forwardRef, useRef, useState } from 'react'
|
import React, { forwardRef, useRef, useState } from 'react'
|
||||||
@@ -46,19 +47,17 @@ export type EditUploadProps = {
|
|||||||
fileName: string
|
fileName: string
|
||||||
fileSrc: string
|
fileSrc: string
|
||||||
imageCacheTag?: string
|
imageCacheTag?: string
|
||||||
initialCrop?: CropType
|
initialCrop?: UploadEdits['crop']
|
||||||
initialFocalPoint?: FocalPosition
|
initialFocalPoint?: FocalPosition
|
||||||
onSave?: ({ crop, focalPosition }: { crop: CropType; focalPosition: FocalPosition }) => void
|
onSave?: (uploadEdits: UploadEdits) => void
|
||||||
showCrop?: boolean
|
showCrop?: boolean
|
||||||
showFocalPoint?: boolean
|
showFocalPoint?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultCrop: CropType = {
|
const defaultCrop: UploadEdits['crop'] = {
|
||||||
height: 100,
|
height: 100,
|
||||||
heightPixels: 0,
|
|
||||||
unit: '%',
|
unit: '%',
|
||||||
width: 100,
|
width: 100,
|
||||||
widthPixels: 0,
|
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
}
|
}
|
||||||
@@ -76,9 +75,9 @@ export const EditUpload: React.FC<EditUploadProps> = ({
|
|||||||
const { closeModal } = useModal()
|
const { closeModal } = useModal()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [crop, setCrop] = useState<CropType>(() => ({
|
const [crop, setCrop] = useState<UploadEdits['crop']>(() => ({
|
||||||
...defaultCrop,
|
...defaultCrop,
|
||||||
...initialCrop,
|
...(initialCrop || {}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const defaultFocalPosition: FocalPosition = {
|
const defaultFocalPosition: FocalPosition = {
|
||||||
@@ -90,31 +89,34 @@ export const EditUpload: React.FC<EditUploadProps> = ({
|
|||||||
...defaultFocalPosition,
|
...defaultFocalPosition,
|
||||||
...initialFocalPoint,
|
...initialFocalPoint,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const [checkBounds, setCheckBounds] = useState<boolean>(false)
|
const [checkBounds, setCheckBounds] = useState<boolean>(false)
|
||||||
const [originalHeight, setOriginalHeight] = useState<number>(0)
|
const [uncroppedPixelHeight, setUncroppedPixelHeight] = useState<number>(0)
|
||||||
const [originalWidth, setOriginalWidth] = useState<number>(0)
|
const [uncroppedPixelWidth, setUncroppedPixelWidth] = useState<number>(0)
|
||||||
|
|
||||||
const focalWrapRef = useRef<HTMLDivElement | undefined>(undefined)
|
const focalWrapRef = useRef<HTMLDivElement | undefined>(undefined)
|
||||||
const imageRef = useRef<HTMLImageElement | undefined>(undefined)
|
const imageRef = useRef<HTMLImageElement | undefined>(undefined)
|
||||||
const cropRef = useRef<HTMLDivElement | undefined>(undefined)
|
const cropRef = useRef<HTMLDivElement | undefined>(undefined)
|
||||||
|
|
||||||
const heightRef = useRef<HTMLInputElement | null>(null)
|
const heightInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const widthRef = useRef<HTMLInputElement | null>(null)
|
const widthInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
const [imageLoaded, setImageLoaded] = useState<boolean>(false)
|
const [imageLoaded, setImageLoaded] = useState<boolean>(false)
|
||||||
|
|
||||||
const onImageLoad = (e) => {
|
const onImageLoad = (e) => {
|
||||||
setOriginalHeight(e.currentTarget.naturalHeight)
|
// set the default image height/width on load
|
||||||
setOriginalWidth(e.currentTarget.naturalWidth)
|
setUncroppedPixelHeight(e.currentTarget.naturalHeight)
|
||||||
|
setUncroppedPixelWidth(e.currentTarget.naturalWidth)
|
||||||
setImageLoaded(true)
|
setImageLoaded(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fineTuneCrop = ({ dimension, value }: { dimension: 'height' | 'width'; value: string }) => {
|
const fineTuneCrop = ({ dimension, value }: { dimension: 'height' | 'width'; value: string }) => {
|
||||||
const intValue = parseInt(value)
|
const intValue = parseInt(value)
|
||||||
if (dimension === 'width' && intValue >= originalWidth) return null
|
if (dimension === 'width' && intValue >= uncroppedPixelWidth) return null
|
||||||
if (dimension === 'height' && intValue >= originalHeight) return null
|
if (dimension === 'height' && intValue >= uncroppedPixelHeight) return null
|
||||||
|
|
||||||
const percentage = 100 * (intValue / (dimension === 'width' ? originalWidth : originalHeight))
|
const percentage =
|
||||||
|
100 * (intValue / (dimension === 'width' ? uncroppedPixelWidth : uncroppedPixelHeight))
|
||||||
|
|
||||||
if (percentage === 100 || percentage === 0) return null
|
if (percentage === 100 || percentage === 0) return null
|
||||||
|
|
||||||
@@ -140,14 +142,10 @@ export const EditUpload: React.FC<EditUploadProps> = ({
|
|||||||
const saveEdits = () => {
|
const saveEdits = () => {
|
||||||
if (typeof onSave === 'function')
|
if (typeof onSave === 'function')
|
||||||
onSave({
|
onSave({
|
||||||
crop: crop
|
crop: crop ? crop : undefined,
|
||||||
? {
|
focalPoint: focalPosition,
|
||||||
...crop,
|
heightInPixels: Number(heightInputRef?.current?.value ?? uncroppedPixelHeight),
|
||||||
heightPixels: Number(heightRef.current?.value ?? crop.heightPixels),
|
widthInPixels: Number(widthInputRef?.current?.value ?? uncroppedPixelWidth),
|
||||||
widthPixels: Number(widthRef.current?.value ?? crop.widthPixels),
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
focalPosition,
|
|
||||||
})
|
})
|
||||||
closeModal(editDrawerSlug)
|
closeModal(editDrawerSlug)
|
||||||
}
|
}
|
||||||
@@ -203,7 +201,7 @@ export const EditUpload: React.FC<EditUploadProps> = ({
|
|||||||
className={`${baseClass}__focal-wrapper`}
|
className={`${baseClass}__focal-wrapper`}
|
||||||
ref={focalWrapRef}
|
ref={focalWrapRef}
|
||||||
style={{
|
style={{
|
||||||
aspectRatio: `${originalWidth / originalHeight}`,
|
aspectRatio: `${uncroppedPixelWidth / uncroppedPixelHeight}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showCrop ? (
|
{showCrop ? (
|
||||||
@@ -259,10 +257,8 @@ export const EditUpload: React.FC<EditUploadProps> = ({
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setCrop({
|
setCrop({
|
||||||
height: 100,
|
height: 100,
|
||||||
heightPixels: originalHeight,
|
|
||||||
unit: '%',
|
unit: '%',
|
||||||
width: 100,
|
width: 100,
|
||||||
widthPixels: originalWidth,
|
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
})
|
})
|
||||||
@@ -279,14 +275,14 @@ export const EditUpload: React.FC<EditUploadProps> = ({
|
|||||||
<Input
|
<Input
|
||||||
name={`${t('upload:width')} (px)`}
|
name={`${t('upload:width')} (px)`}
|
||||||
onChange={(value) => fineTuneCrop({ dimension: 'width', value })}
|
onChange={(value) => fineTuneCrop({ dimension: 'width', value })}
|
||||||
ref={widthRef}
|
ref={widthInputRef}
|
||||||
value={((crop.width / 100) * originalWidth).toFixed(0)}
|
value={((crop.width / 100) * uncroppedPixelWidth).toFixed(0)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
name={`${t('upload:height')} (px)`}
|
name={`${t('upload:height')} (px)`}
|
||||||
onChange={(value) => fineTuneCrop({ dimension: 'height', value })}
|
onChange={(value) => fineTuneCrop({ dimension: 'height', value })}
|
||||||
ref={heightRef}
|
ref={heightInputRef}
|
||||||
value={((crop.height / 100) * originalHeight).toFixed(0)}
|
value={((crop.height / 100) * uncroppedPixelHeight).toFixed(0)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FormState, SanitizedCollectionConfig } from 'payload'
|
import type { FormState, SanitizedCollectionConfig , UploadEdits } from 'payload'
|
||||||
|
|
||||||
|
import { useForm, useUploadEdits } from '@payloadcms/ui'
|
||||||
import { isImage, reduceFieldsToValues } from 'payload/shared'
|
import { isImage, reduceFieldsToValues } from 'payload/shared'
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
import { FieldError } from '../../fields/FieldError/index.js'
|
import { FieldError } from '../../fields/FieldError/index.js'
|
||||||
import { fieldBaseClass } from '../../fields/shared/index.js'
|
import { fieldBaseClass } from '../../fields/shared/index.js'
|
||||||
import { useForm } from '../../forms/Form/context.js'
|
|
||||||
import { useField } from '../../forms/useField/index.js'
|
import { useField } from '../../forms/useField/index.js'
|
||||||
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||||
import { useFormQueryParams } from '../../providers/FormQueryParams/index.js'
|
|
||||||
import { useTranslation } from '../../providers/Translation/index.js'
|
import { useTranslation } from '../../providers/Translation/index.js'
|
||||||
import { Button } from '../Button/index.js'
|
import { Button } from '../Button/index.js'
|
||||||
import { Drawer, DrawerToggler } from '../Drawer/index.js'
|
import { Drawer, DrawerToggler } from '../Drawer/index.js'
|
||||||
@@ -92,14 +91,13 @@ export const Upload: React.FC<UploadProps> = (props) => {
|
|||||||
const [fileSrc, setFileSrc] = useState<null | string>(null)
|
const [fileSrc, setFileSrc] = useState<null | string>(null)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { setModified } = useForm()
|
const { setModified } = useForm()
|
||||||
const { dispatchFormQueryParams, formQueryParams } = useFormQueryParams()
|
const { resetUploadEdits, updateUploadEdits, uploadEdits } = useUploadEdits()
|
||||||
const [doc, setDoc] = useState(reduceFieldsToValues(initialState || {}, true))
|
const [doc, setDoc] = useState(reduceFieldsToValues(initialState || {}, true))
|
||||||
const { docPermissions } = useDocumentInfo()
|
const { docPermissions } = useDocumentInfo()
|
||||||
const { errorMessage, setValue, showError, value } = useField<File>({
|
const { errorMessage, setValue, showError, value } = useField<File>({
|
||||||
path: 'file',
|
path: 'file',
|
||||||
validate,
|
validate,
|
||||||
})
|
})
|
||||||
const [_crop, setCrop] = useState({ x: 0, y: 0 })
|
|
||||||
|
|
||||||
const [showUrlInput, setShowUrlInput] = useState(false)
|
const [showUrlInput, setShowUrlInput] = useState(false)
|
||||||
const [fileUrl, setFileUrl] = useState<string>('')
|
const [fileUrl, setFileUrl] = useState<string>('')
|
||||||
@@ -167,31 +165,16 @@ export const Upload: React.FC<UploadProps> = (props) => {
|
|||||||
setFileSrc('')
|
setFileSrc('')
|
||||||
setFileUrl('')
|
setFileUrl('')
|
||||||
setDoc({})
|
setDoc({})
|
||||||
|
resetUploadEdits()
|
||||||
setShowUrlInput(false)
|
setShowUrlInput(false)
|
||||||
}, [handleFileChange])
|
}, [handleFileChange, resetUploadEdits])
|
||||||
|
|
||||||
const onEditsSave = useCallback(
|
const onEditsSave = useCallback(
|
||||||
({ crop, focalPosition }) => {
|
(args: UploadEdits) => {
|
||||||
setCrop({
|
|
||||||
x: crop.x || 0,
|
|
||||||
y: crop.y || 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
setModified(true)
|
setModified(true)
|
||||||
dispatchFormQueryParams({
|
updateUploadEdits(args)
|
||||||
type: 'SET',
|
|
||||||
params: {
|
|
||||||
uploadEdits:
|
|
||||||
crop || focalPosition
|
|
||||||
? {
|
|
||||||
crop: crop || null,
|
|
||||||
focalPoint: focalPosition ? focalPosition : null,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
[dispatchFormQueryParams, setModified],
|
[setModified, updateUploadEdits],
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePasteUrlClick = () => {
|
const handlePasteUrlClick = () => {
|
||||||
@@ -342,10 +325,10 @@ export const Upload: React.FC<UploadProps> = (props) => {
|
|||||||
fileName={value?.name || doc?.filename}
|
fileName={value?.name || doc?.filename}
|
||||||
fileSrc={doc?.url || fileSrc}
|
fileSrc={doc?.url || fileSrc}
|
||||||
imageCacheTag={doc.updatedAt}
|
imageCacheTag={doc.updatedAt}
|
||||||
initialCrop={formQueryParams?.uploadEdits?.crop ?? {}}
|
initialCrop={uploadEdits?.crop ?? undefined}
|
||||||
initialFocalPoint={{
|
initialFocalPoint={{
|
||||||
x: formQueryParams?.uploadEdits?.focalPoint.x || doc.focalX || 50,
|
x: uploadEdits?.focalPoint?.x || doc.focalX || 50,
|
||||||
y: formQueryParams?.uploadEdits?.focalPoint.y || doc.focalY || 50,
|
y: uploadEdits?.focalPoint?.y || doc.focalY || 50,
|
||||||
}}
|
}}
|
||||||
onSave={onEditsSave}
|
onSave={onEditsSave}
|
||||||
showCrop={showCrop}
|
showCrop={showCrop}
|
||||||
|
|||||||
@@ -202,10 +202,7 @@ export {
|
|||||||
FieldComponentsProvider,
|
FieldComponentsProvider,
|
||||||
useFieldComponents,
|
useFieldComponents,
|
||||||
} from '../../providers/FieldComponents/index.js'
|
} from '../../providers/FieldComponents/index.js'
|
||||||
export {
|
export { UploadEditsProvider, useUploadEdits } from '../../providers/UploadEdits/index.js'
|
||||||
FormQueryParamsProvider,
|
|
||||||
useFormQueryParams,
|
|
||||||
} from '../../providers/FormQueryParams/index.js'
|
|
||||||
export {
|
export {
|
||||||
type ColumnPreferences,
|
type ColumnPreferences,
|
||||||
ListInfoProvider,
|
ListInfoProvider,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
reduceFieldsToValues,
|
reduceFieldsToValues,
|
||||||
wait,
|
wait,
|
||||||
} from 'payload/shared'
|
} from 'payload/shared'
|
||||||
import * as qs from 'qs-esm'
|
|
||||||
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
@@ -26,7 +25,6 @@ import { useThrottledEffect } from '../../hooks/useThrottledEffect.js'
|
|||||||
import { useAuth } from '../../providers/Auth/index.js'
|
import { useAuth } from '../../providers/Auth/index.js'
|
||||||
import { useConfig } from '../../providers/Config/index.js'
|
import { useConfig } from '../../providers/Config/index.js'
|
||||||
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||||
import { useFormQueryParams } from '../../providers/FormQueryParams/index.js'
|
|
||||||
import { useLocale } from '../../providers/Locale/index.js'
|
import { useLocale } from '../../providers/Locale/index.js'
|
||||||
import { useOperation } from '../../providers/Operation/index.js'
|
import { useOperation } from '../../providers/Operation/index.js'
|
||||||
import { useTranslation } from '../../providers/Translation/index.js'
|
import { useTranslation } from '../../providers/Translation/index.js'
|
||||||
@@ -80,7 +78,6 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
const { i18n, t } = useTranslation()
|
const { i18n, t } = useTranslation()
|
||||||
const { refreshCookie, user } = useAuth()
|
const { refreshCookie, user } = useAuth()
|
||||||
const operation = useOperation()
|
const operation = useOperation()
|
||||||
const { formQueryParams } = useFormQueryParams()
|
|
||||||
|
|
||||||
const config = useConfig()
|
const config = useConfig()
|
||||||
const {
|
const {
|
||||||
@@ -168,7 +165,7 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
const submit = useCallback(
|
const submit = useCallback(
|
||||||
async (options: SubmitOptions = {}, e): Promise<void> => {
|
async (options: SubmitOptions = {}, e): Promise<void> => {
|
||||||
const {
|
const {
|
||||||
action: actionArg,
|
action: actionArg = action,
|
||||||
method: methodToUse = method,
|
method: methodToUse = method,
|
||||||
overrides = {},
|
overrides = {},
|
||||||
skipValidation,
|
skipValidation,
|
||||||
@@ -277,14 +274,9 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let res
|
let res
|
||||||
const actionEndpoint =
|
|
||||||
actionArg ||
|
|
||||||
(typeof action === 'string'
|
|
||||||
? `${action}${qs.stringify(formQueryParams, { addQueryPrefix: true })}`
|
|
||||||
: null)
|
|
||||||
|
|
||||||
if (actionEndpoint) {
|
if (typeof actionArg === 'string') {
|
||||||
res = await requests[methodToUse.toLowerCase()](actionEndpoint, {
|
res = await requests[methodToUse.toLowerCase()](actionArg, {
|
||||||
body: formData,
|
body: formData,
|
||||||
headers: {
|
headers: {
|
||||||
'Accept-Language': i18n.language,
|
'Accept-Language': i18n.language,
|
||||||
@@ -400,7 +392,6 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
t,
|
t,
|
||||||
i18n,
|
i18n,
|
||||||
waitForAutocomplete,
|
waitForAutocomplete,
|
||||||
formQueryParams,
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -628,14 +619,9 @@ export const Form: React.FC<FormProps> = (props) => {
|
|||||||
[contextRef.current.fields, dispatchFields, onChange, modified],
|
[contextRef.current.fields, dispatchFields, onChange, modified],
|
||||||
)
|
)
|
||||||
|
|
||||||
const actionString =
|
|
||||||
typeof action === 'string'
|
|
||||||
? `${action}${qs.stringify(formQueryParams, { addQueryPrefix: true })}`
|
|
||||||
: ''
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
action={method ? actionString : (action as string)}
|
action={action}
|
||||||
className={classes}
|
className={classes}
|
||||||
method={method}
|
method={method}
|
||||||
noValidate
|
noValidate
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { useConfig } from '../Config/index.js'
|
|||||||
import { useLocale } from '../Locale/index.js'
|
import { useLocale } from '../Locale/index.js'
|
||||||
import { usePreferences } from '../Preferences/index.js'
|
import { usePreferences } from '../Preferences/index.js'
|
||||||
import { useTranslation } from '../Translation/index.js'
|
import { useTranslation } from '../Translation/index.js'
|
||||||
|
import { UploadEditsProvider, useUploadEdits } from '../UploadEdits/index.js'
|
||||||
|
|
||||||
const Context = createContext({} as DocumentInfoContext)
|
const Context = createContext({} as DocumentInfoContext)
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ export type * from './types.js'
|
|||||||
|
|
||||||
export const useDocumentInfo = (): DocumentInfoContext => useContext(Context)
|
export const useDocumentInfo = (): DocumentInfoContext => useContext(Context)
|
||||||
|
|
||||||
export const DocumentInfoProvider: React.FC<
|
const DocumentInfo: React.FC<
|
||||||
{
|
{
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
} & DocumentInfoProps
|
} & DocumentInfoProps
|
||||||
@@ -66,6 +67,8 @@ export const DocumentInfoProvider: React.FC<
|
|||||||
|
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
|
|
||||||
|
const { uploadEdits } = useUploadEdits()
|
||||||
|
|
||||||
const [documentTitle, setDocumentTitle] = useState(() => {
|
const [documentTitle, setDocumentTitle] = useState(() => {
|
||||||
if (!initialDataFromProps) return ''
|
if (!initialDataFromProps) return ''
|
||||||
|
|
||||||
@@ -104,15 +107,18 @@ export const DocumentInfoProvider: React.FC<
|
|||||||
|
|
||||||
const baseURL = `${serverURL}${api}`
|
const baseURL = `${serverURL}${api}`
|
||||||
let slug: string
|
let slug: string
|
||||||
|
let pluralType: 'collections' | 'globals'
|
||||||
let preferencesKey: string
|
let preferencesKey: string
|
||||||
|
|
||||||
if (globalSlug) {
|
if (globalSlug) {
|
||||||
slug = globalSlug
|
slug = globalSlug
|
||||||
|
pluralType = 'globals'
|
||||||
preferencesKey = `global-${slug}`
|
preferencesKey = `global-${slug}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collectionSlug) {
|
if (collectionSlug) {
|
||||||
slug = collectionSlug
|
slug = collectionSlug
|
||||||
|
pluralType = 'collections'
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
preferencesKey = `collection-${slug}-${id}`
|
preferencesKey = `collection-${slug}-${id}`
|
||||||
@@ -510,10 +516,25 @@ export const DocumentInfoProvider: React.FC<
|
|||||||
data,
|
data,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const action: string = React.useMemo(() => {
|
||||||
|
const docURL = `${baseURL}${pluralType === 'globals' ? `/globals` : ''}/${slug}${id ? `/${id}` : ''}`
|
||||||
|
const params = {
|
||||||
|
depth: 0,
|
||||||
|
'fallback-locale': 'null',
|
||||||
|
locale,
|
||||||
|
uploadEdits: uploadEdits || undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${docURL}${qs.stringify(params, {
|
||||||
|
addQueryPrefix: true,
|
||||||
|
})}`
|
||||||
|
}, [baseURL, locale, pluralType, id, slug, uploadEdits])
|
||||||
|
|
||||||
if (isError) notFound()
|
if (isError) notFound()
|
||||||
|
|
||||||
const value: DocumentInfoContext = {
|
const value: DocumentInfoContext = {
|
||||||
...props,
|
...props,
|
||||||
|
action,
|
||||||
docConfig,
|
docConfig,
|
||||||
docPermissions,
|
docPermissions,
|
||||||
getDocPermissions,
|
getDocPermissions,
|
||||||
@@ -536,3 +557,15 @@ export const DocumentInfoProvider: React.FC<
|
|||||||
|
|
||||||
return <Context.Provider value={value}>{children}</Context.Provider>
|
return <Context.Provider value={value}>{children}</Context.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DocumentInfoProvider: React.FC<
|
||||||
|
{
|
||||||
|
children: React.ReactNode
|
||||||
|
} & DocumentInfoProps
|
||||||
|
> = (props) => {
|
||||||
|
return (
|
||||||
|
<UploadEditsProvider>
|
||||||
|
<DocumentInfo {...props} />
|
||||||
|
</UploadEditsProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { createContext, useContext } from 'react'
|
|
||||||
|
|
||||||
import type { Action, FormQueryParamsContext, State } from './types.js'
|
|
||||||
|
|
||||||
import { useLocale } from '../Locale/index.js'
|
|
||||||
|
|
||||||
export type * from './types.js'
|
|
||||||
|
|
||||||
export const FormQueryParams = createContext({} as FormQueryParamsContext)
|
|
||||||
|
|
||||||
export const FormQueryParamsProvider: React.FC<{
|
|
||||||
children: React.ReactNode
|
|
||||||
initialParams?: State
|
|
||||||
}> = ({ children, initialParams: formQueryParamsFromProps }) => {
|
|
||||||
const [formQueryParams, dispatchFormQueryParams] = React.useReducer(
|
|
||||||
(state: State, action: Action) => {
|
|
||||||
const newState = { ...state }
|
|
||||||
|
|
||||||
switch (action.type) {
|
|
||||||
case 'SET':
|
|
||||||
if (action.params?.uploadEdits === null && newState?.uploadEdits) {
|
|
||||||
delete newState.uploadEdits
|
|
||||||
}
|
|
||||||
if (action.params?.uploadEdits?.crop === null && newState?.uploadEdits?.crop) {
|
|
||||||
delete newState.uploadEdits.crop
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
action.params?.uploadEdits?.focalPoint === null &&
|
|
||||||
newState?.uploadEdits?.focalPoint
|
|
||||||
) {
|
|
||||||
delete newState.uploadEdits.focalPoint
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...newState,
|
|
||||||
...action.params,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formQueryParamsFromProps || ({} as State),
|
|
||||||
)
|
|
||||||
|
|
||||||
const locale = useLocale()
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (locale?.code) {
|
|
||||||
dispatchFormQueryParams({
|
|
||||||
type: 'SET',
|
|
||||||
params: {
|
|
||||||
locale: locale.code,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [locale.code])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormQueryParams.Provider value={{ dispatchFormQueryParams, formQueryParams }}>
|
|
||||||
{children}
|
|
||||||
</FormQueryParams.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useFormQueryParams = (): {
|
|
||||||
dispatchFormQueryParams: React.Dispatch<Action>
|
|
||||||
formQueryParams: State
|
|
||||||
} => useContext(FormQueryParams)
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { UploadEdits } from 'payload'
|
|
||||||
|
|
||||||
export type FormQueryParamsContext = {
|
|
||||||
dispatchFormQueryParams: (action: Action) => void
|
|
||||||
formQueryParams: State
|
|
||||||
}
|
|
||||||
|
|
||||||
export type State = {
|
|
||||||
depth: number
|
|
||||||
'fallback-locale': string
|
|
||||||
locale: string
|
|
||||||
uploadEdits?: UploadEdits
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Action = {
|
|
||||||
params: Partial<State>
|
|
||||||
type: 'SET'
|
|
||||||
}
|
|
||||||
38
packages/ui/src/providers/UploadEdits/index.tsx
Normal file
38
packages/ui/src/providers/UploadEdits/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import type { UploadEdits } from 'payload'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export type UploadEditsContext = {
|
||||||
|
resetUploadEdits: () => void
|
||||||
|
updateUploadEdits: (edits: UploadEdits) => void
|
||||||
|
uploadEdits: UploadEdits
|
||||||
|
}
|
||||||
|
|
||||||
|
const Context = React.createContext<UploadEditsContext>({
|
||||||
|
resetUploadEdits: undefined,
|
||||||
|
updateUploadEdits: undefined,
|
||||||
|
uploadEdits: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const UploadEditsProvider = ({ children }) => {
|
||||||
|
const [uploadEdits, setUploadEdits] = React.useState<UploadEdits>(undefined)
|
||||||
|
|
||||||
|
const resetUploadEdits = () => {
|
||||||
|
setUploadEdits({})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUploadEdits = (edits: UploadEdits) => {
|
||||||
|
setUploadEdits((prevEdits) => ({
|
||||||
|
...(prevEdits || {}),
|
||||||
|
...(edits || {}),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context.Provider value={{ resetUploadEdits, updateUploadEdits, uploadEdits }}>
|
||||||
|
{children}
|
||||||
|
</Context.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUploadEdits = (): UploadEditsContext => React.useContext(Context)
|
||||||
@@ -30,7 +30,7 @@ export const getFieldSchemaMap = (req: PayloadRequest): FieldSchemaMap => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const buildFormState = async ({ req }: { req: PayloadRequest }): Promise<FormState> => {
|
export const buildFormState = async ({ req }: { req: PayloadRequest }): Promise<FormState> => {
|
||||||
const reqData: BuildFormStateArgs = req.data as BuildFormStateArgs
|
const reqData: BuildFormStateArgs = (req.data || {}) as BuildFormStateArgs
|
||||||
const { collectionSlug, formState, globalSlug, locale, operation, schemaPath } = reqData
|
const { collectionSlug, formState, globalSlug, locale, operation, schemaPath } = reqData
|
||||||
|
|
||||||
const incomingUserSlug = req.user?.collection
|
const incomingUserSlug = req.user?.collection
|
||||||
@@ -67,7 +67,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }): Promise<
|
|||||||
const fieldSchemaMap = getFieldSchemaMap(req)
|
const fieldSchemaMap = getFieldSchemaMap(req)
|
||||||
|
|
||||||
const id = collectionSlug ? reqData.id : undefined
|
const id = collectionSlug ? reqData.id : undefined
|
||||||
const schemaPathSegments = schemaPath.split('.')
|
const schemaPathSegments = schemaPath && schemaPath.split('.')
|
||||||
|
|
||||||
let fieldSchema: Field[]
|
let fieldSchema: Field[]
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,46 @@ describe('Upload', () => {
|
|||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should upload after editing image inside a document drawer', async () => {
|
||||||
|
await uploadImage()
|
||||||
|
await wait(1000)
|
||||||
|
// Open the media drawer and create a png upload
|
||||||
|
|
||||||
|
await openDocDrawer(page, '.field-type.upload .upload__toggler.doc-drawer__toggler')
|
||||||
|
|
||||||
|
await page
|
||||||
|
.locator('[id^=doc-drawer_uploads_1_] .file-field__upload input[type="file"]')
|
||||||
|
.setInputFiles(path.resolve(dirname, './uploads/payload.png'))
|
||||||
|
await expect(
|
||||||
|
page.locator('[id^=doc-drawer_uploads_1_] .file-field__upload .file-field__filename'),
|
||||||
|
).toHaveValue('payload.png')
|
||||||
|
await page.locator('[id^=doc-drawer_uploads_1_] .file-field__edit').click()
|
||||||
|
await page
|
||||||
|
.locator('[id^=edit-upload] .edit-upload__input input[name="Width (px)"]')
|
||||||
|
.nth(1)
|
||||||
|
.fill('200')
|
||||||
|
await page
|
||||||
|
.locator('[id^=edit-upload] .edit-upload__input input[name="Height (px)"]')
|
||||||
|
.nth(1)
|
||||||
|
.fill('200')
|
||||||
|
await page.locator('[id^=edit-upload] button:has-text("Apply Changes")').nth(1).click()
|
||||||
|
await page.locator('[id^=doc-drawer_uploads_1_] #action-save').click()
|
||||||
|
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
||||||
|
|
||||||
|
// Assert that the media field has the png upload
|
||||||
|
await expect(
|
||||||
|
page.locator('.field-type.upload .file-details .file-meta__url a'),
|
||||||
|
).toHaveAttribute('href', '/api/uploads/file/payload-1.png')
|
||||||
|
await expect(page.locator('.field-type.upload .file-details .file-meta__url a')).toContainText(
|
||||||
|
'payload-1.png',
|
||||||
|
)
|
||||||
|
await expect(page.locator('.field-type.upload .file-details img')).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
'/api/uploads/file/payload-1.png',
|
||||||
|
)
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
})
|
||||||
|
|
||||||
test('should clear selected upload', async () => {
|
test('should clear selected upload', async () => {
|
||||||
await uploadImage()
|
await uploadImage()
|
||||||
await wait(1000) // TODO: Fix this. Need to wait a bit until the form in the drawer mounted, otherwise values sometimes disappear. This is an issue for all drawers
|
await wait(1000) // TODO: Fix this. Need to wait a bit until the form in the drawer mounted, otherwise values sometimes disappear. This is an issue for all drawers
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
adminThumbnailSizeSlug,
|
adminThumbnailSizeSlug,
|
||||||
animatedTypeMedia,
|
animatedTypeMedia,
|
||||||
audioSlug,
|
audioSlug,
|
||||||
|
focalOnlySlug,
|
||||||
mediaSlug,
|
mediaSlug,
|
||||||
relationSlug,
|
relationSlug,
|
||||||
} from './shared.js'
|
} from './shared.js'
|
||||||
@@ -41,6 +42,7 @@ let audioURL: AdminUrlUtil
|
|||||||
let relationURL: AdminUrlUtil
|
let relationURL: AdminUrlUtil
|
||||||
let adminThumbnailSizeURL: AdminUrlUtil
|
let adminThumbnailSizeURL: AdminUrlUtil
|
||||||
let adminThumbnailFunctionURL: AdminUrlUtil
|
let adminThumbnailFunctionURL: AdminUrlUtil
|
||||||
|
let focalOnlyURL: AdminUrlUtil
|
||||||
|
|
||||||
describe('uploads', () => {
|
describe('uploads', () => {
|
||||||
let page: Page
|
let page: Page
|
||||||
@@ -59,6 +61,7 @@ describe('uploads', () => {
|
|||||||
relationURL = new AdminUrlUtil(serverURL, relationSlug)
|
relationURL = new AdminUrlUtil(serverURL, relationSlug)
|
||||||
adminThumbnailSizeURL = new AdminUrlUtil(serverURL, adminThumbnailSizeSlug)
|
adminThumbnailSizeURL = new AdminUrlUtil(serverURL, adminThumbnailSizeSlug)
|
||||||
adminThumbnailFunctionURL = new AdminUrlUtil(serverURL, adminThumbnailFunctionSlug)
|
adminThumbnailFunctionURL = new AdminUrlUtil(serverURL, adminThumbnailFunctionSlug)
|
||||||
|
focalOnlyURL = new AdminUrlUtil(serverURL, focalOnlySlug)
|
||||||
|
|
||||||
const context = await browser.newContext()
|
const context = await browser.newContext()
|
||||||
page = await context.newPage()
|
page = await context.newPage()
|
||||||
@@ -143,6 +146,25 @@ describe('uploads', () => {
|
|||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should show proper file names for resized animated file', async () => {
|
||||||
|
await page.goto(animatedTypeMediaURL.create)
|
||||||
|
|
||||||
|
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './animated.webp'))
|
||||||
|
const animatedFilename = page.locator('.file-field__filename')
|
||||||
|
|
||||||
|
await expect(animatedFilename).toHaveValue('animated.webp')
|
||||||
|
|
||||||
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
|
await page.locator('.file-field__previewSizes').click()
|
||||||
|
|
||||||
|
const smallSquareFilename = page
|
||||||
|
.locator('.preview-sizes__list .preview-sizes__sizeOption')
|
||||||
|
.nth(1)
|
||||||
|
.locator('.file-meta__url a')
|
||||||
|
await expect(smallSquareFilename).toContainText(/480x480\.webp$/)
|
||||||
|
})
|
||||||
|
|
||||||
test('should show resized images', async () => {
|
test('should show resized images', async () => {
|
||||||
await page.goto(mediaURL.edit(pngDoc.id))
|
await page.goto(mediaURL.edit(pngDoc.id))
|
||||||
|
|
||||||
@@ -399,5 +421,43 @@ describe('uploads', () => {
|
|||||||
expect(greenDoc.filesize).toEqual(1205)
|
expect(greenDoc.filesize).toEqual(1205)
|
||||||
expect(redDoc.filesize).toEqual(1207)
|
expect(redDoc.filesize).toEqual(1207)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should update image alignment based on focal point', async () => {
|
||||||
|
const updateFocalPosition = async (page: Page) => {
|
||||||
|
await page.goto(focalOnlyURL.create)
|
||||||
|
await page.waitForURL(focalOnlyURL.create)
|
||||||
|
// select and upload file
|
||||||
|
const fileChooserPromise = page.waitForEvent('filechooser')
|
||||||
|
await page.getByText('Select a file').click()
|
||||||
|
const fileChooser = await fileChooserPromise
|
||||||
|
await wait(1000)
|
||||||
|
await fileChooser.setFiles(path.join(dirname, 'horizontal-squares.jpg'))
|
||||||
|
|
||||||
|
await page.locator('.file-field__edit').click()
|
||||||
|
|
||||||
|
// set focal point
|
||||||
|
await page.locator('.edit-upload__input input[name="X %"]').fill('12') // left focal point
|
||||||
|
await page.locator('.edit-upload__input input[name="Y %"]').fill('50') // top focal point
|
||||||
|
|
||||||
|
// apply focal point
|
||||||
|
await page.locator('button:has-text("Apply Changes")').click()
|
||||||
|
await page.waitForSelector('button#action-save')
|
||||||
|
await page.locator('button#action-save').click()
|
||||||
|
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
|
||||||
|
await wait(1000) // Wait for the save
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateFocalPosition(page) // red square
|
||||||
|
const redSquareMediaID = page.url().split('/').pop() // get the ID of the doc
|
||||||
|
|
||||||
|
const { doc: redDoc } = await client.findByID({
|
||||||
|
id: redSquareMediaID,
|
||||||
|
slug: focalOnlySlug,
|
||||||
|
auth: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// without focal point update this generated size was equal to 1736
|
||||||
|
expect(redDoc.sizes.focalTest.filesize).toEqual(1598)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
BIN
test/uploads/horizontal-squares.jpg
Normal file
BIN
test/uploads/horizontal-squares.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
Reference in New Issue
Block a user