Compare commits
4 Commits
v3.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3131122db | ||
|
|
6d0dfeafc8 | ||
|
|
00771b1f2a | ||
|
|
448186f374 |
@@ -180,7 +180,6 @@ import {
|
||||
useFormInitializing,
|
||||
useFormModified,
|
||||
useFormProcessing,
|
||||
useFormQueryParams,
|
||||
useFormSubmitted,
|
||||
useHotkey,
|
||||
useIntersect,
|
||||
@@ -221,7 +220,6 @@ import {
|
||||
EntityVisibilityProvider,
|
||||
FieldComponentsProvider,
|
||||
FieldPropsProvider,
|
||||
FormQueryParamsProvider,
|
||||
ListInfoProvider,
|
||||
ListQueryProvider,
|
||||
LocaleProvider,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload-monorepo",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-payload-app",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-mongodb",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "The officially supported MongoDB database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/db-postgres",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "The officially supported Postgres database adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-nodemailer",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "Payload Nodemailer Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/email-resend",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "Payload Resend Email Adapter",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/graphql",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview-react",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "The official live preview React SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/live-preview",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "The official live preview JavaScript SDK for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -14,12 +14,12 @@ async function build() {
|
||||
plugins: [sassPlugin({ css: 'external' })],
|
||||
})
|
||||
|
||||
await fs.rename('dist/prod/esbuildEntry.css', 'dist/prod/styles.css', (err) => {
|
||||
if (err) {
|
||||
console.error(`Error while renaming index.css: ${err}`)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
try {
|
||||
fs.renameSync('dist/prod/esbuildEntry.css', 'dist/prod/styles.css')
|
||||
} catch (err) {
|
||||
console.error(`Error while renaming index.css: ${err}`)
|
||||
throw err
|
||||
}
|
||||
|
||||
console.log('styles.css bundled successfully')
|
||||
|
||||
@@ -32,12 +32,12 @@ async function build() {
|
||||
]
|
||||
|
||||
for (const file of filesToDelete) {
|
||||
await fs.unlink(file, (err) => {
|
||||
if (err) {
|
||||
console.error(`Error while deleting ${file}: ${err}`)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
try {
|
||||
fs.unlinkSync(file)
|
||||
} catch (err) {
|
||||
console.error(`Error while deleting ${file}: ${err}`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Files renamed and deleted successfully')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/next",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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<AdminViewProps> = async ({
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} />}
|
||||
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<AdminViewProps> = async ({
|
||||
permissions={permissions}
|
||||
/>
|
||||
<HydrateClientUser permissions={permissions} user={user} />
|
||||
<FormQueryParamsProvider
|
||||
initialParams={{
|
||||
depth: 0,
|
||||
'fallback-locale': 'null',
|
||||
locale: locale?.code,
|
||||
uploadEdits: undefined,
|
||||
<RenderCustomComponent
|
||||
CustomComponent={
|
||||
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined
|
||||
}
|
||||
DefaultComponent={EditView}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<AdminViewProps> = 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<AdminViewProps> = 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<AdminViewProps> = async ({
|
||||
notFound()
|
||||
}
|
||||
|
||||
action = `${serverURL}${apiRoute}/globals/${globalSlug}`
|
||||
|
||||
const params = new URLSearchParams({
|
||||
locale: locale?.code,
|
||||
})
|
||||
@@ -198,7 +183,6 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
|
||||
return (
|
||||
<DocumentInfoProvider
|
||||
action={action}
|
||||
apiURL={apiURL}
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
disableActions={false}
|
||||
@@ -225,34 +209,25 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
depth={1}
|
||||
key={`${collectionSlug || globalSlug}${locale?.code ? `-${locale?.code}` : ''}`}
|
||||
>
|
||||
<FormQueryParamsProvider
|
||||
initialParams={{
|
||||
depth: 0,
|
||||
'fallback-locale': 'null',
|
||||
locale: locale?.code,
|
||||
uploadEdits: undefined,
|
||||
}}
|
||||
>
|
||||
{ErrorView ? (
|
||||
<ErrorView initPageResult={initPageResult} searchParams={searchParams} />
|
||||
) : (
|
||||
<RenderCustomComponent
|
||||
CustomComponent={ViewOverride || CustomView}
|
||||
DefaultComponent={DefaultView}
|
||||
serverOnlyProps={{
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: segments,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</FormQueryParamsProvider>
|
||||
{ErrorView ? (
|
||||
<ErrorView initPageResult={initPageResult} searchParams={searchParams} />
|
||||
) : (
|
||||
<RenderCustomComponent
|
||||
CustomComponent={ViewOverride || CustomView}
|
||||
DefaultComponent={DefaultView}
|
||||
serverOnlyProps={{
|
||||
i18n,
|
||||
initPageResult,
|
||||
locale,
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
routeSegments: segments,
|
||||
searchParams,
|
||||
user,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EditDepthProvider>
|
||||
</DocumentInfoProvider>
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "payload",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "Node, React, Headless CMS and Application Framework built on Next.js",
|
||||
"keywords": [
|
||||
"admin panel",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -203,7 +203,14 @@ export const generateFileData = async <T>({
|
||||
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,
|
||||
|
||||
@@ -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<ImageSizesResult> => {
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud-storage",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "The official cloud storage plugin for Payload CMS",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-cloud",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "The official Payload Cloud plugin",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-form-builder",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "Form builder plugin for Payload CMS",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-nested-docs",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "The official Nested Docs plugin for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-redirects",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "Redirects plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-relationship-object-ids",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "A Payload plugin to store all relationship IDs as ObjectIDs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-search",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "Search plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-seo",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "SEO plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/plugin-stripe",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "Stripe plugin for Payload",
|
||||
"keywords": [
|
||||
"payload",
|
||||
|
||||
@@ -30,9 +30,13 @@ async function build() {
|
||||
//external: ['*.svg'],
|
||||
plugins: [sassPlugin({ css: 'external' })],
|
||||
})
|
||||
await fs.rename('dist/field/index.css', 'dist/exports/client/bundled.css', (err) => {
|
||||
if (err) console.error(`Error while renaming index.css: ${err}`)
|
||||
})
|
||||
|
||||
try {
|
||||
fs.renameSync('dist/field/index.css', 'dist/exports/client/bundled.css')
|
||||
} catch (err) {
|
||||
console.error(`Error while renaming index.css: ${err}`)
|
||||
throw err
|
||||
}
|
||||
|
||||
console.log('dist/field/bundled.css bundled successfully')
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-lexical",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "The officially supported Lexical richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/richtext-slate",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "The officially supported Slate richtext adapter for Payload",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-azure",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "Payload storage adapter for Azure Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-gcs",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "Payload storage adapter for Google Cloud Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-s3",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "Payload storage adapter for Amazon S3",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-uploadthing",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "Payload storage adapter for uploadthing",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/storage-vercel-blob",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"description": "Payload storage adapter for Vercel Blob Storage",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/translations",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -68,19 +68,20 @@ async function build() {
|
||||
packages: 'external',
|
||||
plugins: [sassPlugin({ css: 'external' })],
|
||||
})
|
||||
await fs.rename('dist/index.css', 'dist/styles.css', (err) => {
|
||||
if (err) {
|
||||
console.error(`Error while renaming index.css: ${err}`)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
|
||||
await fs.unlink('dist/index.js', (err) => {
|
||||
if (err) {
|
||||
console.error(`Error while deleting index.js: ${err}`)
|
||||
throw err
|
||||
}
|
||||
})
|
||||
try {
|
||||
fs.renameSync('dist/index.css', 'dist/styles.css')
|
||||
} catch (err) {
|
||||
console.error(`Error while renaming index.css: ${err}`)
|
||||
throw err
|
||||
}
|
||||
|
||||
try {
|
||||
fs.unlinkSync('dist/index.js')
|
||||
} catch (err) {
|
||||
console.error(`Error while deleting index.js: ${err}`)
|
||||
throw err
|
||||
}
|
||||
|
||||
console.log('styles.css bundled successfully')
|
||||
// Bundle `client.ts`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@payloadcms/ui",
|
||||
"version": "3.0.0-beta.66",
|
||||
"version": "3.0.0-beta.67",
|
||||
"homepage": "https://payloadcms.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -43,7 +43,7 @@ export const Autosave: React.FC<Props> = ({
|
||||
} = useConfig()
|
||||
const { docConfig, getVersions, versions } = useDocumentInfo()
|
||||
const { reportUpdate } = useDocumentEvents()
|
||||
const { dispatchFields, setSubmitted } = useForm()
|
||||
const { dispatchFields, setModified, setSubmitted } = useForm()
|
||||
const submitted = useFormSubmitted()
|
||||
const versionsConfig = docConfig?.versions
|
||||
|
||||
@@ -80,7 +80,7 @@ export const Autosave: React.FC<Props> = ({
|
||||
// Store locale in ref so the autosave func
|
||||
// can always retrieve the most to date locale
|
||||
localeRef.current = locale
|
||||
|
||||
console.log(modifiedRef.current, modified)
|
||||
// When debounced fields change, autosave
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController()
|
||||
@@ -131,6 +131,7 @@ export const Autosave: React.FC<Props> = ({
|
||||
if (res.status === 200) {
|
||||
const newDate = new Date()
|
||||
setLastSaved(newDate.getTime())
|
||||
setModified(false)
|
||||
reportUpdate({
|
||||
id,
|
||||
entitySlug,
|
||||
@@ -216,6 +217,7 @@ export const Autosave: React.FC<Props> = ({
|
||||
reportUpdate,
|
||||
serverURL,
|
||||
setSubmitted,
|
||||
setModified,
|
||||
versionsConfig?.drafts,
|
||||
debouncedFields,
|
||||
submitted,
|
||||
|
||||
@@ -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<DocumentDrawerProps> = ({
|
||||
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<DocumentDrawerProps> = ({
|
||||
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<DocumentDrawerProps> = ({
|
||||
<DocumentTitle />
|
||||
</Gutter>
|
||||
}
|
||||
action={action}
|
||||
apiURL={apiURL}
|
||||
collectionSlug={collectionConfig.slug}
|
||||
disableActions
|
||||
|
||||
@@ -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<EditUploadProps> = ({
|
||||
const { closeModal } = useModal()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [crop, setCrop] = useState<CropType>(() => ({
|
||||
const [crop, setCrop] = useState<UploadEdits['crop']>(() => ({
|
||||
...defaultCrop,
|
||||
...initialCrop,
|
||||
...(initialCrop || {}),
|
||||
}))
|
||||
|
||||
const defaultFocalPosition: FocalPosition = {
|
||||
@@ -90,31 +89,34 @@ export const EditUpload: React.FC<EditUploadProps> = ({
|
||||
...defaultFocalPosition,
|
||||
...initialFocalPoint,
|
||||
}))
|
||||
|
||||
const [checkBounds, setCheckBounds] = useState<boolean>(false)
|
||||
const [originalHeight, setOriginalHeight] = useState<number>(0)
|
||||
const [originalWidth, setOriginalWidth] = useState<number>(0)
|
||||
const [uncroppedPixelHeight, setUncroppedPixelHeight] = useState<number>(0)
|
||||
const [uncroppedPixelWidth, setUncroppedPixelWidth] = useState<number>(0)
|
||||
|
||||
const focalWrapRef = useRef<HTMLDivElement | undefined>(undefined)
|
||||
const imageRef = useRef<HTMLImageElement | undefined>(undefined)
|
||||
const cropRef = useRef<HTMLDivElement | undefined>(undefined)
|
||||
|
||||
const heightRef = useRef<HTMLInputElement | null>(null)
|
||||
const widthRef = useRef<HTMLInputElement | null>(null)
|
||||
const heightInputRef = useRef<HTMLInputElement | null>(null)
|
||||
const widthInputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
const [imageLoaded, setImageLoaded] = useState<boolean>(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<EditUploadProps> = ({
|
||||
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<EditUploadProps> = ({
|
||||
className={`${baseClass}__focal-wrapper`}
|
||||
ref={focalWrapRef}
|
||||
style={{
|
||||
aspectRatio: `${originalWidth / originalHeight}`,
|
||||
aspectRatio: `${uncroppedPixelWidth / uncroppedPixelHeight}`,
|
||||
}}
|
||||
>
|
||||
{showCrop ? (
|
||||
@@ -259,10 +257,8 @@ export const EditUpload: React.FC<EditUploadProps> = ({
|
||||
onClick={() =>
|
||||
setCrop({
|
||||
height: 100,
|
||||
heightPixels: originalHeight,
|
||||
unit: '%',
|
||||
width: 100,
|
||||
widthPixels: originalWidth,
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
@@ -279,14 +275,14 @@ export const EditUpload: React.FC<EditUploadProps> = ({
|
||||
<Input
|
||||
name={`${t('upload:width')} (px)`}
|
||||
onChange={(value) => fineTuneCrop({ dimension: 'width', value })}
|
||||
ref={widthRef}
|
||||
value={((crop.width / 100) * originalWidth).toFixed(0)}
|
||||
ref={widthInputRef}
|
||||
value={((crop.width / 100) * uncroppedPixelWidth).toFixed(0)}
|
||||
/>
|
||||
<Input
|
||||
name={`${t('upload:height')} (px)`}
|
||||
onChange={(value) => fineTuneCrop({ dimension: 'height', value })}
|
||||
ref={heightRef}
|
||||
value={((crop.height / 100) * originalHeight).toFixed(0)}
|
||||
ref={heightInputRef}
|
||||
value={((crop.height / 100) * uncroppedPixelHeight).toFixed(0)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
'use client'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useLocale } from '../../providers/Locale/index.js'
|
||||
import { useRouteCache } from '../../providers/RouteCache/index.js'
|
||||
import { useSearchParams } from '../../providers/SearchParams/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { Popup, PopupList } from '../Popup/index.js'
|
||||
@@ -24,8 +22,6 @@ export const Localizer: React.FC<{
|
||||
const { i18n } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const { stringifyParams } = useSearchParams()
|
||||
const router = useRouter()
|
||||
const { clearRouteCache } = useRouteCache()
|
||||
|
||||
if (localization) {
|
||||
const { locales } = localization
|
||||
@@ -43,18 +39,13 @@ export const Localizer: React.FC<{
|
||||
return (
|
||||
<PopupList.Button
|
||||
active={locale.code === localeOption.code}
|
||||
href={stringifyParams({
|
||||
params: {
|
||||
locale: localeOption.code,
|
||||
},
|
||||
})}
|
||||
key={localeOption.code}
|
||||
onClick={() => {
|
||||
router.replace(
|
||||
stringifyParams({
|
||||
params: {
|
||||
locale: localeOption.code,
|
||||
},
|
||||
}),
|
||||
)
|
||||
clearRouteCache()
|
||||
close()
|
||||
}}
|
||||
onClick={close}
|
||||
>
|
||||
{localeOptionLabel}
|
||||
{localeOptionLabel !== localeOption.code && ` (${localeOption.code})`}
|
||||
|
||||
@@ -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<UploadProps> = (props) => {
|
||||
const [fileSrc, setFileSrc] = useState<null | string>(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<File>({
|
||||
path: 'file',
|
||||
validate,
|
||||
})
|
||||
const [_crop, setCrop] = useState({ x: 0, y: 0 })
|
||||
|
||||
const [showUrlInput, setShowUrlInput] = useState(false)
|
||||
const [fileUrl, setFileUrl] = useState<string>('')
|
||||
@@ -167,31 +165,16 @@ export const Upload: React.FC<UploadProps> = (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<UploadProps> = (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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<FormProps> = (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<FormProps> = (props) => {
|
||||
const submit = useCallback(
|
||||
async (options: SubmitOptions = {}, e): Promise<void> => {
|
||||
const {
|
||||
action: actionArg,
|
||||
action: actionArg = action,
|
||||
method: methodToUse = method,
|
||||
overrides = {},
|
||||
skipValidation,
|
||||
@@ -277,14 +274,9 @@ export const Form: React.FC<FormProps> = (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<FormProps> = (props) => {
|
||||
t,
|
||||
i18n,
|
||||
waitForAutocomplete,
|
||||
formQueryParams,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -628,14 +619,9 @@ export const Form: React.FC<FormProps> = (props) => {
|
||||
[contextRef.current.fields, dispatchFields, onChange, modified],
|
||||
)
|
||||
|
||||
const actionString =
|
||||
typeof action === 'string'
|
||||
? `${action}${qs.stringify(formQueryParams, { addQueryPrefix: true })}`
|
||||
: ''
|
||||
|
||||
return (
|
||||
<form
|
||||
action={method ? actionString : (action as string)}
|
||||
action={action}
|
||||
className={classes}
|
||||
method={method}
|
||||
noValidate
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useConfig } from '../Config/index.js'
|
||||
import { useLocale } from '../Locale/index.js'
|
||||
import { usePreferences } from '../Preferences/index.js'
|
||||
import { useTranslation } from '../Translation/index.js'
|
||||
import { UploadEditsProvider, useUploadEdits } from '../UploadEdits/index.js'
|
||||
|
||||
const Context = createContext({} as DocumentInfoContext)
|
||||
|
||||
@@ -34,7 +35,7 @@ export type * from './types.js'
|
||||
|
||||
export const useDocumentInfo = (): DocumentInfoContext => 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 <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> => {
|
||||
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[]
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { promises as fs, existsSync } from 'fs'
|
||||
import path, { join } from 'path'
|
||||
import { join } from 'path'
|
||||
import globby from 'globby'
|
||||
import process from 'node:process'
|
||||
import chalk from 'chalk'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,20 +30,20 @@ type LoginArgs = {
|
||||
const random = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min
|
||||
|
||||
const networkConditions = {
|
||||
'Slow 3G': {
|
||||
download: ((500 * 1000) / 8) * 0.8,
|
||||
upload: ((500 * 1000) / 8) * 0.8,
|
||||
latency: 400 * 5,
|
||||
},
|
||||
'Fast 3G': {
|
||||
download: ((1.6 * 1000 * 1000) / 8) * 0.9,
|
||||
upload: ((750 * 1000) / 8) * 0.9,
|
||||
latency: 1000,
|
||||
upload: ((750 * 1000) / 8) * 0.9,
|
||||
},
|
||||
'Slow 3G': {
|
||||
download: ((500 * 1000) / 8) * 0.8,
|
||||
latency: 400 * 5,
|
||||
upload: ((500 * 1000) / 8) * 0.8,
|
||||
},
|
||||
'Slow 4G': {
|
||||
download: ((4 * 1000 * 1000) / 8) * 0.8,
|
||||
upload: ((3 * 1000 * 1000) / 8) * 0.8,
|
||||
latency: 1000,
|
||||
upload: ((3 * 1000 * 1000) / 8) * 0.8,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -53,10 +53,10 @@ const networkConditions = {
|
||||
* @param serverURL
|
||||
*/
|
||||
export async function ensureAutoLoginAndCompilationIsDone({
|
||||
page,
|
||||
serverURL,
|
||||
customAdminRoutes,
|
||||
customRoutes,
|
||||
page,
|
||||
serverURL,
|
||||
}: {
|
||||
customAdminRoutes?: Config['admin']['routes']
|
||||
customRoutes?: Config['routes']
|
||||
@@ -65,7 +65,7 @@ export async function ensureAutoLoginAndCompilationIsDone({
|
||||
}): Promise<void> {
|
||||
const {
|
||||
admin: {
|
||||
routes: { login: loginRoute, createFirstUser: createFirstUserRoute },
|
||||
routes: { createFirstUser: createFirstUserRoute, login: loginRoute },
|
||||
},
|
||||
routes: { admin: adminRoute },
|
||||
} = getAdminRoutes({ customAdminRoutes, customRoutes })
|
||||
@@ -97,8 +97,8 @@ export async function ensureAutoLoginAndCompilationIsDone({
|
||||
*/
|
||||
export async function throttleTest({
|
||||
context,
|
||||
page,
|
||||
delay,
|
||||
page,
|
||||
}: {
|
||||
context: BrowserContext
|
||||
delay: 'Fast 3G' | 'Slow 3G' | 'Slow 4G'
|
||||
@@ -108,9 +108,9 @@ export async function throttleTest({
|
||||
|
||||
await cdpSession.send('Network.emulateNetworkConditions', {
|
||||
downloadThroughput: networkConditions[delay].download,
|
||||
uploadThroughput: networkConditions[delay].upload,
|
||||
latency: networkConditions[delay].latency,
|
||||
offline: false,
|
||||
uploadThroughput: networkConditions[delay].upload,
|
||||
})
|
||||
|
||||
await page.route('**/*', async (route) => {
|
||||
@@ -123,7 +123,7 @@ export async function throttleTest({
|
||||
}
|
||||
|
||||
export async function firstRegister(args: FirstRegisterArgs): Promise<void> {
|
||||
const { page, serverURL, customAdminRoutes, customRoutes } = args
|
||||
const { customAdminRoutes, customRoutes, page, serverURL } = args
|
||||
|
||||
const {
|
||||
routes: { admin: adminRoute },
|
||||
@@ -139,11 +139,11 @@ export async function firstRegister(args: FirstRegisterArgs): Promise<void> {
|
||||
}
|
||||
|
||||
export async function login(args: LoginArgs): Promise<void> {
|
||||
const { page, serverURL, data = devUser, customAdminRoutes, customRoutes } = args
|
||||
const { customAdminRoutes, customRoutes, data = devUser, page, serverURL } = args
|
||||
|
||||
const {
|
||||
admin: {
|
||||
routes: { login: loginRoute, createFirstUser: createFirstUserRoute },
|
||||
routes: { createFirstUser: createFirstUserRoute, login: loginRoute },
|
||||
},
|
||||
routes: { admin: adminRoute },
|
||||
} = getAdminRoutes({ customAdminRoutes, customRoutes })
|
||||
@@ -236,7 +236,7 @@ export async function openDocControls(page: Page): Promise<void> {
|
||||
export async function changeLocale(page: Page, newLocale: string) {
|
||||
await page.locator('.localizer >> button').first().click()
|
||||
await page
|
||||
.locator(`.localizer .popup.popup--active .popup-button-list button`, {
|
||||
.locator(`.localizer .popup.popup--active .popup-button-list__button`, {
|
||||
hasText: newLocale,
|
||||
})
|
||||
.first()
|
||||
@@ -349,8 +349,8 @@ export function describeIfInCIOrHasLocalstack(): jest.Describe {
|
||||
type AdminRoutes = Config['admin']['routes']
|
||||
|
||||
export function getAdminRoutes({
|
||||
customRoutes,
|
||||
customAdminRoutes,
|
||||
customRoutes,
|
||||
}: {
|
||||
customAdminRoutes?: AdminRoutes
|
||||
customRoutes?: Config['routes']
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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