From 00771b1f2a4dc68cff6f74d1cdf64788dad106be Mon Sep 17 00:00:00 2001 From: Patrik Date: Thu, 18 Jul 2024 13:43:53 -0400 Subject: [PATCH] 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 --- docs/migration-guide/overview.mdx | 2 - packages/next/src/views/Account/index.tsx | 42 ++- packages/next/src/views/Document/index.tsx | 67 ++--- .../next/src/views/Edit/Default/index.tsx | 21 +- packages/payload/src/uploads/cropImage.ts | 27 +- .../payload/src/uploads/generateFileData.ts | 9 +- packages/payload/src/uploads/imageResizer.ts | 263 ++++++++++-------- packages/payload/src/uploads/types.ts | 29 +- .../elements/DocumentDrawer/DrawerContent.tsx | 8 - packages/ui/src/elements/EditUpload/index.tsx | 60 ++-- packages/ui/src/elements/Upload/index.tsx | 39 +-- packages/ui/src/exports/client/index.ts | 5 +- packages/ui/src/forms/Form/index.tsx | 22 +- .../ui/src/providers/DocumentInfo/index.tsx | 35 ++- .../src/providers/FormQueryParams/index.tsx | 69 ----- .../ui/src/providers/FormQueryParams/types.ts | 18 -- .../ui/src/providers/UploadEdits/index.tsx | 38 +++ packages/ui/src/utilities/buildFormState.ts | 4 +- test/fields/collections/Upload/e2e.spec.ts | 40 +++ test/uploads/e2e.spec.ts | 60 ++++ test/uploads/horizontal-squares.jpg | Bin 0 -> 2601 bytes 21 files changed, 463 insertions(+), 395 deletions(-) delete mode 100644 packages/ui/src/providers/FormQueryParams/index.tsx delete mode 100644 packages/ui/src/providers/FormQueryParams/types.ts create mode 100644 packages/ui/src/providers/UploadEdits/index.tsx create mode 100644 test/uploads/horizontal-squares.jpg diff --git a/docs/migration-guide/overview.mdx b/docs/migration-guide/overview.mdx index d2ffa0901..ff1ee1e2b 100644 --- a/docs/migration-guide/overview.mdx +++ b/docs/migration-guide/overview.mdx @@ -180,7 +180,6 @@ import { useFormInitializing, useFormModified, useFormProcessing, - useFormQueryParams, useFormSubmitted, useHotkey, useIntersect, @@ -221,7 +220,6 @@ import { EntityVisibilityProvider, FieldComponentsProvider, FieldPropsProvider, - FormQueryParamsProvider, ListInfoProvider, ListQueryProvider, LocaleProvider, diff --git a/packages/next/src/views/Account/index.tsx b/packages/next/src/views/Account/index.tsx index 814b08f25..842d3e6ed 100644 --- a/packages/next/src/views/Account/index.tsx +++ b/packages/next/src/views/Account/index.tsx @@ -1,6 +1,6 @@ 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 { notFound } from 'next/navigation.js' import React from 'react' @@ -65,7 +65,6 @@ export const Account: React.FC = async ({ return ( } - action={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`} apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`} collectionSlug={userSlug} docPermissions={docPermissions} @@ -84,31 +83,22 @@ export const Account: React.FC = async ({ permissions={permissions} /> - - - + /> ) } diff --git a/packages/next/src/views/Document/index.tsx b/packages/next/src/views/Document/index.tsx index 01e434386..ac7b2df28 100644 --- a/packages/next/src/views/Document/index.tsx +++ b/packages/next/src/views/Document/index.tsx @@ -1,16 +1,6 @@ -import type { - AdminViewComponent, - AdminViewProps, - EditViewComponent, - ServerSideEditViewProps, -} from 'payload' +import type { AdminViewComponent, AdminViewProps, EditViewComponent } from 'payload' -import { - DocumentInfoProvider, - EditDepthProvider, - FormQueryParamsProvider, - HydrateClientUser, -} from '@payloadcms/ui' +import { DocumentInfoProvider, EditDepthProvider, HydrateClientUser } from '@payloadcms/ui' import { RenderCustomComponent, isEditing as getIsEditing } from '@payloadcms/ui/shared' import { notFound, redirect } from 'next/navigation.js' import React from 'react' @@ -65,7 +55,6 @@ export const Document: React.FC = async ({ let ErrorView: AdminViewComponent let apiURL: string - let action: string const { data, formState } = await getDocumentData({ id, @@ -88,8 +77,6 @@ export const Document: React.FC = async ({ notFound() } - action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}` - const params = new URLSearchParams() if (collectionConfig.versions?.drafts) { params.append('draft', 'true') @@ -128,8 +115,6 @@ export const Document: React.FC = async ({ notFound() } - action = `${serverURL}${apiRoute}/globals/${globalSlug}` - const params = new URLSearchParams({ locale: locale?.code, }) @@ -198,7 +183,6 @@ export const Document: React.FC = async ({ return ( = async ({ depth={1} key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`} > - - {ErrorView ? ( - - ) : ( - - )} - + {ErrorView ? ( + + ) : ( + + )} ) diff --git a/packages/next/src/views/Edit/Default/index.tsx b/packages/next/src/views/Edit/Default/index.tsx index dad04a718..554b3cec4 100644 --- a/packages/next/src/views/Edit/Default/index.tsx +++ b/packages/next/src/views/Edit/Default/index.tsx @@ -13,7 +13,7 @@ import { useDocumentEvents, useDocumentInfo, useEditDepth, - useFormQueryParams, + useUploadEdits, } from '@payloadcms/ui' import { getFormState } from '@payloadcms/ui/shared' import { useRouter, useSearchParams } from 'next/navigation.js' @@ -58,11 +58,13 @@ export const DefaultEditView: React.FC = () => { const { refreshCookieAsync, user } = useAuth() const config = useConfig() const router = useRouter() - const { dispatchFormQueryParams } = useFormQueryParams() const { getComponentMap, getFieldMap } = useComponentMap() - const params = useSearchParams() const depth = useEditDepth() + const params = useSearchParams() const { reportUpdate } = useDocumentEvents() + const { resetUploadEdits } = useUploadEdits() + + const locale = params.get('locale') const { admin: { user: userSlug }, @@ -72,8 +74,6 @@ export const DefaultEditView: React.FC = () => { serverURL, } = config - const locale = params.get('locale') - const collectionConfig = 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}` : ''}` router.push(redirectRoute) } else { - dispatchFormQueryParams({ - type: 'SET', - params: { - uploadEdits: null, - }, - }) + resetUploadEdits() } }, [ @@ -151,9 +146,9 @@ export const DefaultEditView: React.FC = () => { isEditing, refreshCookieAsync, adminRoute, - locale, router, - dispatchFormQueryParams, + locale, + resetUploadEdits, ], ) diff --git a/packages/payload/src/uploads/cropImage.ts b/packages/payload/src/uploads/cropImage.ts index c744fa56d..bbf114a4f 100644 --- a/packages/payload/src/uploads/cropImage.ts +++ b/packages/payload/src/uploads/cropImage.ts @@ -1,12 +1,31 @@ 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) => { 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 { - const { heightPixels, widthPixels, x, y } = cropData + const { x, y } = cropData 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 const formattedCropData = { - height: Number(heightPixels), + height: Number(heightInPixels), left: percentToPixel(x, dimensions.width), top: percentToPixel(y, dimensions.height), - width: Number(widthPixels), + width: Number(widthInPixels), } const cropped = sharp(file.tempFilePath || file.data, sharpOptions).extract(formattedCropData) diff --git a/packages/payload/src/uploads/generateFileData.ts b/packages/payload/src/uploads/generateFileData.ts index 5ff9b81a3..77b4f6d07 100644 --- a/packages/payload/src/uploads/generateFileData.ts +++ b/packages/payload/src/uploads/generateFileData.ts @@ -203,7 +203,14 @@ export const generateFileData = async ({ let fileForResize = file 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({ buffer: croppedImage, diff --git a/packages/payload/src/uploads/imageResizer.ts b/packages/payload/src/uploads/imageResizer.ts index 0bbc89bba..db81e1ee6 100644 --- a/packages/payload/src/uploads/imageResizer.ts +++ b/packages/payload/src/uploads/imageResizer.ts @@ -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 fs from 'fs' @@ -68,11 +68,20 @@ const getSanitizedImageData = (sourceImage: string): SanitizedImageData => { * @param extension - the extension to use * @returns the new image name that is not taken */ -const createImageName = ( - outputImageName: string, - { height, width }: OutputInfo, - extension: string, -) => `${outputImageName}-${width}x${height}.${extension}` +type CreateImageNameArgs = { + extension: string + height: number + outputImageName: string + width: number +} +const createImageName = ({ + extension, + height, + outputImageName, + width, +}: CreateImageNameArgs): string => { + return `${outputImageName}-${width}x${height}.${extension}` +} type CreateResultArgs = { filename?: FileSize['filename'] @@ -122,71 +131,61 @@ const createResult = ({ } /** - * Check if the image needs to be resized according to the requested dimensions - * and the original image size. If the resize options withoutEnlargement or withoutReduction are provided, - * the image will be resized regardless of the requested dimensions, given that the - * width or height to be resized is provided. + * Determine whether or not to resize the image. + * - resize using image config + * - resize using image config with focal adjustments + * - do not resize at all * - * @param resizeConfig - object containing the requested dimensions and resize options - * @param original - the original image size - * @returns true if resizing is not needed, false otherwise - */ -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. + * `imageResizeConfig.withoutEnlargement`: + * - undefined [default]: uploading images with smaller width AND height than the image size will return null + * - false: always enlarge images to the image size + * - true: if the image is smaller than the image size, return the original image * - * @param resizeConfig - object containing the requested dimensions and resize options - * @param original - the original image size - * @returns true if the image should passed directly to sharp + * `imageResizeConfig.withoutReduction`: + * - false [default]: always enlarge images to the image size + * - true: if the image is smaller than the image size, return the original image + * + * @return 'omit' | 'resize' | 'resizeWithFocalPoint' */ -const applyPayloadAdjustments = ( - { fit, height, width, withoutEnlargement, withoutReduction }: ImageSize, - original: ProbedImageSize, -) => { - if (fit === 'contain' || fit === 'inside') return false - if (!isNumber(height) && !isNumber(width)) return false +const getImageResizeAction = ({ + dimensions: originalImage, + hasFocalPoint, + imageResizeConfig, +}: { + dimensions: ProbedImageSize + hasFocalPoint?: boolean + imageResizeConfig: ImageSize +}): 'omit' | 'resize' | 'resizeWithFocalPoint' => { + const { + fit, + height: targetHeight, + width: targetWidth, + withoutEnlargement, + withoutReduction, + } = imageResizeConfig - const targetAspectRatio = width / height - const originalAspectRatio = original.width / original.height - if (originalAspectRatio === targetAspectRatio) return false + // prevent upscaling by default when x and y are both smaller than target image size + if (targetHeight && targetWidth) { + 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 skipReduction = withoutReduction && (original.height > height || original.width > width) - if (skipEnlargement || skipReduction) return false + const originalImageIsSmallerXOrY = + originalImage.width < targetWidth || originalImage.height < targetHeight + 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 } +/** + * 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 * (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 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( imageSizes.map(async (imageResizeConfig): Promise => { imageResizeConfig = sanitizeResizeConfig(imageResizeConfig) - // This checks if a resize should happen. If not, the resized image will be - // skipped COMPLETELY and thus will not be included in the resulting images. - // All further format/trim options will thus be skipped as well. - if (preventResize(imageResizeConfig, dimensions)) { - return createResult({ name: imageResizeConfig.name }) - } + const resizeAction = getImageResizeAction({ + dimensions, + hasFocalPoint: Boolean(incomingFocalPoint), + imageResizeConfig, + }) + if (resizeAction === 'omit') return createResult({ name: imageResizeConfig.name }) const imageToResize = sharpBase.clone() let resized = imageToResize - const metadata = await sharpBase.metadata() - - if (incomingFocalPoint && applyPayloadAdjustments(imageResizeConfig, dimensions)) { + if (resizeAction === 'resizeWithFocalPoint') { let { height: resizeHeight, width: resizeWidth } = imageResizeConfig const originalAspectRatio = dimensions.width / dimensions.height @@ -293,44 +309,62 @@ export async function resizeAndTransformImageSizes({ resizeHeight = Math.round(resizeWidth / originalAspectRatio) } - // Scale the image up or down to fit the resize dimensions - const scaledImage = imageToResize.resize({ - height: resizeHeight, - width: resizeWidth, - }) + if (!resizeHeight) resizeHeight = resizeImageMeta.height + if (!resizeWidth) resizeWidth = resizeImageMeta.width - 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 - const maxOffsetX = scaledImageInfo.width - safeResizeWidth - const leftFocalEdge = Math.round( - scaledImageInfo.width * (incomingFocalPoint.x / 100) - safeResizeWidth / 2, - ) - const safeOffsetX = Math.min(Math.max(0, leftFocalEdge), maxOffsetX) - - const isAnimated = fileIsAnimatedType && metadata.pages - - let safeResizeHeight = resizeHeight ?? scaledImageInfo.height - - if (isAnimated && resizeHeight === undefined) { - safeResizeHeight = scaledImageInfo.height / metadata.pages + // must read from buffer, resize.metadata will return the original image metadata + const { info } = await resized.toBuffer({ resolveWithObject: true }) + resizeImageMeta.height = extractHeightFromImage({ + ...originalImageMeta, + height: info.height, + }) + resizeImageMeta.width = info.width } - const maxOffsetY = isAnimated - ? safeResizeHeight - (resizeHeight ?? safeResizeHeight) - : scaledImageInfo.height - safeResizeHeight + const halfResizeX = resizeWidth / 2 + const xFocalCenter = resizeImageMeta.width * (incomingFocalPoint.x / 100) + const calculatedRightPixelBound = xFocalCenter + halfResizeX + let leftBound = xFocalCenter - halfResizeX - const topFocalEdge = Math.round( - scaledImageInfo.height * (incomingFocalPoint.y / 100) - safeResizeHeight / 2, - ) - const safeOffsetY = Math.min(Math.max(0, topFocalEdge), maxOffsetY) + // if the right bound is greater than the image width, adjust the left bound + // keeping focus on the right + if (calculatedRightPixelBound > resizeImageMeta.width) { + leftBound = resizeImageMeta.width - resizeWidth + } - // extract the focal area from the scaled image - resized = (fileIsAnimatedType ? imageToResize : scaledImage).extract({ - height: safeResizeHeight, - left: safeOffsetX, - top: safeOffsetY, - width: safeResizeWidth, + // if the left bound is less than 0, adjust the left bound to 0 + // keeping the focus on the left + if (leftBound < 0) leftBound = 0 + + const halfResizeY = resizeHeight / 2 + 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 { resized = imageToResize.resize(imageResizeConfig) @@ -359,11 +393,15 @@ export async function resizeAndTransformImageSizes({ const mimeInfo = await fileTypeFromBuffer(bufferData) - const imageNameWithDimensions = createImageName( - sanitizedImage.name, - bufferInfo, - mimeInfo?.ext || sanitizedImage.ext, - ) + const imageNameWithDimensions = createImageName({ + extension: mimeInfo?.ext || sanitizedImage.ext, + height: extractHeightFromImage({ + ...originalImageMeta, + height: bufferInfo.height, + }), + outputImageName: sanitizedImage.name, + width: bufferInfo.width, + }) const imagePath = `${staticPath}/${imageNameWithDimensions}` @@ -380,7 +418,8 @@ export async function resizeAndTransformImageSizes({ name: imageResizeConfig.name, filename: imageNameWithDimensions, filesize: size, - height: fileIsAnimatedType && metadata.pages ? height / metadata.pages : height, + height: + fileIsAnimatedType && originalImageMeta.pages ? height / originalImageMeta.pages : height, mimeType: mimeInfo?.mime || mimeType, sizesToSave: [{ buffer: bufferData, path: imagePath }], width, diff --git a/packages/payload/src/uploads/types.ts b/packages/payload/src/uploads/types.ts index e86be233b..61530054f 100644 --- a/packages/payload/src/uploads/types.ts +++ b/packages/payload/src/uploads/types.ts @@ -189,15 +189,22 @@ export type FileToSave = { path: string } -export type UploadEdits = { - crop?: { - height?: number - width?: number - x?: number - y?: number - } - focalPoint?: { - x?: number - y?: number - } +type Crop = { + height: number + unit: '%' | 'px' + width: number + x: number + y: number +} + +type FocalPoint = { + x: number + y: number +} + +export type UploadEdits = { + crop?: Crop + focalPoint?: FocalPoint + heightInPixels?: number + widthInPixels?: number } diff --git a/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx b/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx index 2a295d6a7..1133052e6 100644 --- a/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx +++ b/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx @@ -1,7 +1,6 @@ 'use client' import { useModal } from '@faceless-ui/modal' -import * as qs from 'qs-esm' import React, { useCallback, useEffect, useState } from 'react' import { toast } from 'sonner' @@ -12,7 +11,6 @@ import { XIcon } from '../../icons/X/index.js' import { useComponentMap } from '../../providers/ComponentMap/index.js' import { useConfig } from '../../providers/Config/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 { useTranslation } from '../../providers/Translation/index.js' import { Gutter } from '../Gutter/index.js' @@ -40,8 +38,6 @@ export const DocumentDrawerContent: React.FC = ({ const [docID, setDocID] = useState(existingDocID) const [isOpen, setIsOpen] = useState(false) const [collectionConfig] = useRelatedCollections(collectionSlug) - const { formQueryParams } = useFormQueryParams() - const formattedQueryParams = qs.stringify(formQueryParams) const { componentMap } = useComponentMap() @@ -50,9 +46,6 @@ export const DocumentDrawerContent: React.FC = ({ const apiURL = docID ? `${serverURL}${apiRoute}/${collectionSlug}/${docID}${locale?.code ? `?locale=${locale.code}` : ''}` : null - const action = `${serverURL}${apiRoute}/${collectionSlug}${ - isEditing ? `/${docID}` : '' - }?${formattedQueryParams}` useEffect(() => { setIsOpen(Boolean(modalState[drawerSlug]?.isOpen)) @@ -104,7 +97,6 @@ export const DocumentDrawerContent: React.FC = ({ } - action={action} apiURL={apiURL} collectionSlug={collectionConfig.slug} disableActions diff --git a/packages/ui/src/elements/EditUpload/index.tsx b/packages/ui/src/elements/EditUpload/index.tsx index 7d0c02b5d..5dff698c4 100644 --- a/packages/ui/src/elements/EditUpload/index.tsx +++ b/packages/ui/src/elements/EditUpload/index.tsx @@ -1,5 +1,6 @@ 'use client' -import type CropType from 'react-image-crop' + +import type { UploadEdits } from 'payload' import { useModal } from '@faceless-ui/modal' import React, { forwardRef, useRef, useState } from 'react' @@ -46,19 +47,17 @@ export type EditUploadProps = { fileName: string fileSrc: string imageCacheTag?: string - initialCrop?: CropType + initialCrop?: UploadEdits['crop'] initialFocalPoint?: FocalPosition - onSave?: ({ crop, focalPosition }: { crop: CropType; focalPosition: FocalPosition }) => void + onSave?: (uploadEdits: UploadEdits) => void showCrop?: boolean showFocalPoint?: boolean } -const defaultCrop: CropType = { +const defaultCrop: UploadEdits['crop'] = { height: 100, - heightPixels: 0, unit: '%', width: 100, - widthPixels: 0, x: 0, y: 0, } @@ -76,9 +75,9 @@ export const EditUpload: React.FC = ({ const { closeModal } = useModal() const { t } = useTranslation() - const [crop, setCrop] = useState(() => ({ + const [crop, setCrop] = useState(() => ({ ...defaultCrop, - ...initialCrop, + ...(initialCrop || {}), })) const defaultFocalPosition: FocalPosition = { @@ -90,31 +89,34 @@ export const EditUpload: React.FC = ({ ...defaultFocalPosition, ...initialFocalPoint, })) + const [checkBounds, setCheckBounds] = useState(false) - const [originalHeight, setOriginalHeight] = useState(0) - const [originalWidth, setOriginalWidth] = useState(0) + const [uncroppedPixelHeight, setUncroppedPixelHeight] = useState(0) + const [uncroppedPixelWidth, setUncroppedPixelWidth] = useState(0) const focalWrapRef = useRef(undefined) const imageRef = useRef(undefined) const cropRef = useRef(undefined) - const heightRef = useRef(null) - const widthRef = useRef(null) + const heightInputRef = useRef(null) + const widthInputRef = useRef(null) const [imageLoaded, setImageLoaded] = useState(false) const onImageLoad = (e) => { - setOriginalHeight(e.currentTarget.naturalHeight) - setOriginalWidth(e.currentTarget.naturalWidth) + // set the default image height/width on load + setUncroppedPixelHeight(e.currentTarget.naturalHeight) + setUncroppedPixelWidth(e.currentTarget.naturalWidth) setImageLoaded(true) } const fineTuneCrop = ({ dimension, value }: { dimension: 'height' | 'width'; value: string }) => { const intValue = parseInt(value) - if (dimension === 'width' && intValue >= originalWidth) return null - if (dimension === 'height' && intValue >= originalHeight) return null + if (dimension === 'width' && intValue >= uncroppedPixelWidth) 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 @@ -140,14 +142,10 @@ export const EditUpload: React.FC = ({ const saveEdits = () => { if (typeof onSave === 'function') onSave({ - crop: crop - ? { - ...crop, - heightPixels: Number(heightRef.current?.value ?? crop.heightPixels), - widthPixels: Number(widthRef.current?.value ?? crop.widthPixels), - } - : undefined, - focalPosition, + crop: crop ? crop : undefined, + focalPoint: focalPosition, + heightInPixels: Number(heightInputRef?.current?.value ?? uncroppedPixelHeight), + widthInPixels: Number(widthInputRef?.current?.value ?? uncroppedPixelWidth), }) closeModal(editDrawerSlug) } @@ -203,7 +201,7 @@ export const EditUpload: React.FC = ({ className={`${baseClass}__focal-wrapper`} ref={focalWrapRef} style={{ - aspectRatio: `${originalWidth / originalHeight}`, + aspectRatio: `${uncroppedPixelWidth / uncroppedPixelHeight}`, }} > {showCrop ? ( @@ -259,10 +257,8 @@ export const EditUpload: React.FC = ({ onClick={() => setCrop({ height: 100, - heightPixels: originalHeight, unit: '%', width: 100, - widthPixels: originalWidth, x: 0, y: 0, }) @@ -279,14 +275,14 @@ export const EditUpload: React.FC = ({ fineTuneCrop({ dimension: 'width', value })} - ref={widthRef} - value={((crop.width / 100) * originalWidth).toFixed(0)} + ref={widthInputRef} + value={((crop.width / 100) * uncroppedPixelWidth).toFixed(0)} /> fineTuneCrop({ dimension: 'height', value })} - ref={heightRef} - value={((crop.height / 100) * originalHeight).toFixed(0)} + ref={heightInputRef} + value={((crop.height / 100) * uncroppedPixelHeight).toFixed(0)} /> diff --git a/packages/ui/src/elements/Upload/index.tsx b/packages/ui/src/elements/Upload/index.tsx index d126bdee0..6178b1451 100644 --- a/packages/ui/src/elements/Upload/index.tsx +++ b/packages/ui/src/elements/Upload/index.tsx @@ -1,16 +1,15 @@ '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 React, { useCallback, useEffect, useRef, useState } from 'react' import { toast } from 'sonner' import { FieldError } from '../../fields/FieldError/index.js' import { fieldBaseClass } from '../../fields/shared/index.js' -import { useForm } from '../../forms/Form/context.js' import { useField } from '../../forms/useField/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' -import { useFormQueryParams } from '../../providers/FormQueryParams/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { Button } from '../Button/index.js' import { Drawer, DrawerToggler } from '../Drawer/index.js' @@ -92,14 +91,13 @@ export const Upload: React.FC = (props) => { const [fileSrc, setFileSrc] = useState(null) const { t } = useTranslation() const { setModified } = useForm() - const { dispatchFormQueryParams, formQueryParams } = useFormQueryParams() + const { resetUploadEdits, updateUploadEdits, uploadEdits } = useUploadEdits() const [doc, setDoc] = useState(reduceFieldsToValues(initialState || {}, true)) const { docPermissions } = useDocumentInfo() const { errorMessage, setValue, showError, value } = useField({ path: 'file', validate, }) - const [_crop, setCrop] = useState({ x: 0, y: 0 }) const [showUrlInput, setShowUrlInput] = useState(false) const [fileUrl, setFileUrl] = useState('') @@ -167,31 +165,16 @@ export const Upload: React.FC = (props) => { setFileSrc('') setFileUrl('') setDoc({}) + resetUploadEdits() setShowUrlInput(false) - }, [handleFileChange]) + }, [handleFileChange, resetUploadEdits]) const onEditsSave = useCallback( - ({ crop, focalPosition }) => { - setCrop({ - x: crop.x || 0, - y: crop.y || 0, - }) - + (args: UploadEdits) => { setModified(true) - dispatchFormQueryParams({ - type: 'SET', - params: { - uploadEdits: - crop || focalPosition - ? { - crop: crop || null, - focalPoint: focalPosition ? focalPosition : null, - } - : null, - }, - }) + updateUploadEdits(args) }, - [dispatchFormQueryParams, setModified], + [setModified, updateUploadEdits], ) const handlePasteUrlClick = () => { @@ -342,10 +325,10 @@ export const Upload: React.FC = (props) => { fileName={value?.name || doc?.filename} fileSrc={doc?.url || fileSrc} imageCacheTag={doc.updatedAt} - initialCrop={formQueryParams?.uploadEdits?.crop ?? {}} + initialCrop={uploadEdits?.crop ?? undefined} initialFocalPoint={{ - x: formQueryParams?.uploadEdits?.focalPoint.x || doc.focalX || 50, - y: formQueryParams?.uploadEdits?.focalPoint.y || doc.focalY || 50, + x: uploadEdits?.focalPoint?.x || doc.focalX || 50, + y: uploadEdits?.focalPoint?.y || doc.focalY || 50, }} onSave={onEditsSave} showCrop={showCrop} diff --git a/packages/ui/src/exports/client/index.ts b/packages/ui/src/exports/client/index.ts index 567d18135..d2f49c0f3 100644 --- a/packages/ui/src/exports/client/index.ts +++ b/packages/ui/src/exports/client/index.ts @@ -202,10 +202,7 @@ export { FieldComponentsProvider, useFieldComponents, } from '../../providers/FieldComponents/index.js' -export { - FormQueryParamsProvider, - useFormQueryParams, -} from '../../providers/FormQueryParams/index.js' +export { UploadEditsProvider, useUploadEdits } from '../../providers/UploadEdits/index.js' export { type ColumnPreferences, ListInfoProvider, diff --git a/packages/ui/src/forms/Form/index.tsx b/packages/ui/src/forms/Form/index.tsx index 6bc9601a9..cf17120b6 100644 --- a/packages/ui/src/forms/Form/index.tsx +++ b/packages/ui/src/forms/Form/index.tsx @@ -10,7 +10,6 @@ import { reduceFieldsToValues, wait, } from 'payload/shared' -import * as qs from 'qs-esm' import React, { useCallback, useEffect, useReducer, useRef, useState } from 'react' import { toast } from 'sonner' @@ -26,7 +25,6 @@ import { useThrottledEffect } from '../../hooks/useThrottledEffect.js' import { useAuth } from '../../providers/Auth/index.js' import { useConfig } from '../../providers/Config/index.js' import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' -import { useFormQueryParams } from '../../providers/FormQueryParams/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useOperation } from '../../providers/Operation/index.js' import { useTranslation } from '../../providers/Translation/index.js' @@ -80,7 +78,6 @@ export const Form: React.FC = (props) => { const { i18n, t } = useTranslation() const { refreshCookie, user } = useAuth() const operation = useOperation() - const { formQueryParams } = useFormQueryParams() const config = useConfig() const { @@ -168,7 +165,7 @@ export const Form: React.FC = (props) => { const submit = useCallback( async (options: SubmitOptions = {}, e): Promise => { const { - action: actionArg, + action: actionArg = action, method: methodToUse = method, overrides = {}, skipValidation, @@ -277,14 +274,9 @@ export const Form: React.FC = (props) => { try { let res - const actionEndpoint = - actionArg || - (typeof action === 'string' - ? `${action}${qs.stringify(formQueryParams, { addQueryPrefix: true })}` - : null) - if (actionEndpoint) { - res = await requests[methodToUse.toLowerCase()](actionEndpoint, { + if (typeof actionArg === 'string') { + res = await requests[methodToUse.toLowerCase()](actionArg, { body: formData, headers: { 'Accept-Language': i18n.language, @@ -400,7 +392,6 @@ export const Form: React.FC = (props) => { t, i18n, waitForAutocomplete, - formQueryParams, ], ) @@ -628,14 +619,9 @@ export const Form: React.FC = (props) => { [contextRef.current.fields, dispatchFields, onChange, modified], ) - const actionString = - typeof action === 'string' - ? `${action}${qs.stringify(formQueryParams, { addQueryPrefix: true })}` - : '' - return (
useContext(Context) -export const DocumentInfoProvider: React.FC< +const DocumentInfo: React.FC< { children: React.ReactNode } & DocumentInfoProps @@ -66,6 +67,8 @@ export const DocumentInfoProvider: React.FC< const { i18n } = useTranslation() + const { uploadEdits } = useUploadEdits() + const [documentTitle, setDocumentTitle] = useState(() => { if (!initialDataFromProps) return '' @@ -104,15 +107,18 @@ export const DocumentInfoProvider: React.FC< const baseURL = `${serverURL}${api}` let slug: string + let pluralType: 'collections' | 'globals' let preferencesKey: string if (globalSlug) { slug = globalSlug + pluralType = 'globals' preferencesKey = `global-${slug}` } if (collectionSlug) { slug = collectionSlug + pluralType = 'collections' if (id) { preferencesKey = `collection-${slug}-${id}` @@ -510,10 +516,25 @@ export const DocumentInfoProvider: React.FC< 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() const value: DocumentInfoContext = { ...props, + action, docConfig, docPermissions, getDocPermissions, @@ -536,3 +557,15 @@ export const DocumentInfoProvider: React.FC< return {children} } + +export const DocumentInfoProvider: React.FC< + { + children: React.ReactNode + } & DocumentInfoProps +> = (props) => { + return ( + + + + ) +} diff --git a/packages/ui/src/providers/FormQueryParams/index.tsx b/packages/ui/src/providers/FormQueryParams/index.tsx deleted file mode 100644 index 37e6cec61..000000000 --- a/packages/ui/src/providers/FormQueryParams/index.tsx +++ /dev/null @@ -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 ( - - {children} - - ) -} - -export const useFormQueryParams = (): { - dispatchFormQueryParams: React.Dispatch - formQueryParams: State -} => useContext(FormQueryParams) diff --git a/packages/ui/src/providers/FormQueryParams/types.ts b/packages/ui/src/providers/FormQueryParams/types.ts deleted file mode 100644 index 91bd7cb2c..000000000 --- a/packages/ui/src/providers/FormQueryParams/types.ts +++ /dev/null @@ -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 - type: 'SET' -} diff --git a/packages/ui/src/providers/UploadEdits/index.tsx b/packages/ui/src/providers/UploadEdits/index.tsx new file mode 100644 index 000000000..460834950 --- /dev/null +++ b/packages/ui/src/providers/UploadEdits/index.tsx @@ -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({ + resetUploadEdits: undefined, + updateUploadEdits: undefined, + uploadEdits: undefined, +}) + +export const UploadEditsProvider = ({ children }) => { + const [uploadEdits, setUploadEdits] = React.useState(undefined) + + const resetUploadEdits = () => { + setUploadEdits({}) + } + + const updateUploadEdits = (edits: UploadEdits) => { + setUploadEdits((prevEdits) => ({ + ...(prevEdits || {}), + ...(edits || {}), + })) + } + + return ( + + {children} + + ) +} + +export const useUploadEdits = (): UploadEditsContext => React.useContext(Context) diff --git a/packages/ui/src/utilities/buildFormState.ts b/packages/ui/src/utilities/buildFormState.ts index a028a3941..f56b09e4b 100644 --- a/packages/ui/src/utilities/buildFormState.ts +++ b/packages/ui/src/utilities/buildFormState.ts @@ -30,7 +30,7 @@ export const getFieldSchemaMap = (req: PayloadRequest): FieldSchemaMap => { } export const buildFormState = async ({ req }: { req: PayloadRequest }): Promise => { - const reqData: BuildFormStateArgs = req.data as BuildFormStateArgs + const reqData: BuildFormStateArgs = (req.data || {}) as BuildFormStateArgs const { collectionSlug, formState, globalSlug, locale, operation, schemaPath } = reqData const incomingUserSlug = req.user?.collection @@ -67,7 +67,7 @@ export const buildFormState = async ({ req }: { req: PayloadRequest }): Promise< const fieldSchemaMap = getFieldSchemaMap(req) const id = collectionSlug ? reqData.id : undefined - const schemaPathSegments = schemaPath.split('.') + const schemaPathSegments = schemaPath && schemaPath.split('.') let fieldSchema: Field[] diff --git a/test/fields/collections/Upload/e2e.spec.ts b/test/fields/collections/Upload/e2e.spec.ts index 784413110..ab206bd99 100644 --- a/test/fields/collections/Upload/e2e.spec.ts +++ b/test/fields/collections/Upload/e2e.spec.ts @@ -152,6 +152,46 @@ describe('Upload', () => { 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 () => { 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 diff --git a/test/uploads/e2e.spec.ts b/test/uploads/e2e.spec.ts index c0c69529a..d83d1e95d 100644 --- a/test/uploads/e2e.spec.ts +++ b/test/uploads/e2e.spec.ts @@ -24,6 +24,7 @@ import { adminThumbnailSizeSlug, animatedTypeMedia, audioSlug, + focalOnlySlug, mediaSlug, relationSlug, } from './shared.js' @@ -41,6 +42,7 @@ let audioURL: AdminUrlUtil let relationURL: AdminUrlUtil let adminThumbnailSizeURL: AdminUrlUtil let adminThumbnailFunctionURL: AdminUrlUtil +let focalOnlyURL: AdminUrlUtil describe('uploads', () => { let page: Page @@ -59,6 +61,7 @@ describe('uploads', () => { relationURL = new AdminUrlUtil(serverURL, relationSlug) adminThumbnailSizeURL = new AdminUrlUtil(serverURL, adminThumbnailSizeSlug) adminThumbnailFunctionURL = new AdminUrlUtil(serverURL, adminThumbnailFunctionSlug) + focalOnlyURL = new AdminUrlUtil(serverURL, focalOnlySlug) const context = await browser.newContext() page = await context.newPage() @@ -143,6 +146,25 @@ describe('uploads', () => { 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 () => { await page.goto(mediaURL.edit(pngDoc.id)) @@ -399,5 +421,43 @@ describe('uploads', () => { expect(greenDoc.filesize).toEqual(1205) 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) + }) }) }) diff --git a/test/uploads/horizontal-squares.jpg b/test/uploads/horizontal-squares.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e543b942a02701057a6a66fe3fa0e8fb59814c43 GIT binary patch literal 2601 zcmex=PKf)jmb{qpT;N;>4N{9$BA`61pKv4;ZGmvGtxH&jM>IE^?-(uilW(3;BEXZKb z(EVQX`Uhl<5cXx~_8a~U7~+<*S3Jzc5M|&p*vBr7sanffr)&nMYF-nI+Lj*};>