fix(ui): field bulk upload showing stale data (#13006)

This commit is contained in:
Jarrod Flesch
2025-07-02 10:11:51 -04:00
committed by GitHub
parent 50029532aa
commit 9ba740e472
10 changed files with 291 additions and 395 deletions

View File

@@ -146,7 +146,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = ({
return (
<EntityVisibilityProvider visibleEntities={visibleEntities}>
<BulkUploadProvider>
<BulkUploadProvider drawerSlugPrefix={collectionSlug}>
<ActionsProvider Actions={Actions}>
{RenderServerComponent({
clientProps,

View File

@@ -1,7 +1,5 @@
'use client'
import { useRouter, useSearchParams } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { useCallback, useEffect } from 'react'
import type { EditFormProps } from './types.js'
@@ -12,9 +10,7 @@ import { WatchChildErrors } from '../../../forms/WatchChildErrors/index.js'
import { useConfig } from '../../../providers/Config/index.js'
import { useDocumentEvents } from '../../../providers/DocumentEvents/index.js'
import { useDocumentInfo } from '../../../providers/DocumentInfo/index.js'
import { useEditDepth } from '../../../providers/EditDepth/index.js'
import { OperationProvider } from '../../../providers/Operation/index.js'
import { useRouteTransition } from '../../../providers/RouteTransition/index.js'
import { useServerFunctions } from '../../../providers/ServerFunctions/index.js'
import { abortAndIgnore, handleAbortRef } from '../../../utilities/abortAndIgnore.js'
import { useDocumentDrawerContext } from '../../DocumentDrawer/Provider.js'
@@ -23,7 +19,6 @@ import { MoveDocToFolder } from '../../FolderView/MoveDocToFolder/index.js'
import { Upload_v4 } from '../../Upload/index.js'
import { useFormsManager } from '../FormsManager/index.js'
import './index.scss'
import { BulkUploadProvider } from '../index.js'
const baseClass = 'collection-edit'
@@ -44,7 +39,6 @@ export function EditForm({
getDocPreferences,
hasSavePermission,
initialState,
isEditing,
isInitializing,
Upload: CustomUpload,
} = useDocumentInfo()
@@ -54,23 +48,14 @@ export function EditForm({
const { getFormState } = useServerFunctions()
const {
config: {
folders,
routes: { admin: adminRoute },
},
config: { folders },
getEntityConfig,
} = useConfig()
const abortOnChangeRef = React.useRef<AbortController>(null)
const collectionConfig = getEntityConfig({ collectionSlug: docSlug })
const router = useRouter()
const depth = useEditDepth()
const params = useSearchParams()
const { reportUpdate } = useDocumentEvents()
const { startRouteTransition } = useRouteTransition()
const locale = params.get('locale')
const collectionSlug = collectionConfig.slug
@@ -89,31 +74,9 @@ export function EditForm({
operation: 'create',
})
}
if (!isEditing && depth < 2) {
// Redirect to the same locale if it's been set
const redirectRoute = formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`,
})
startRouteTransition(() => router.push(redirectRoute))
} else {
resetUploadEdits()
}
resetUploadEdits()
},
[
adminRoute,
collectionSlug,
depth,
isEditing,
locale,
onSaveFromContext,
reportUpdate,
resetUploadEdits,
router,
startRouteTransition,
],
[collectionSlug, onSaveFromContext, reportUpdate, resetUploadEdits],
)
const onChange: NonNullable<FormProps['onChange']>[0] = useCallback(
@@ -150,54 +113,52 @@ export function EditForm({
return (
<OperationProvider operation="create">
<BulkUploadProvider>
<Form
action={action}
className={`${baseClass}__form`}
disabled={isInitializing || !hasSavePermission}
initialState={isInitializing ? undefined : initialState}
isInitializing={isInitializing}
method="POST"
onChange={[onChange]}
onSuccess={onSave}
submitted={submitted}
>
<DocumentFields
BeforeFields={
<React.Fragment>
{CustomUpload || (
<Upload_v4
collectionSlug={collectionConfig.slug}
customActions={[
folders && collectionConfig.folders && (
<MoveDocToFolder
buttonProps={{
buttonStyle: 'pill',
size: 'small',
}}
folderCollectionSlug={folders.slug}
folderFieldName={folders.fieldName}
key="move-doc-to-folder"
/>
),
].filter(Boolean)}
initialState={initialState}
resetUploadEdits={resetUploadEdits}
updateUploadEdits={updateUploadEdits}
uploadConfig={collectionConfig.upload}
uploadEdits={uploadEdits}
/>
)}
</React.Fragment>
}
docPermissions={docPermissions}
fields={collectionConfig.fields}
schemaPathSegments={[collectionConfig.slug]}
/>
<ReportAllErrors />
<GetFieldProxy />
</Form>
</BulkUploadProvider>
<Form
action={action}
className={`${baseClass}__form`}
disabled={isInitializing || !hasSavePermission}
initialState={isInitializing ? undefined : initialState}
isInitializing={isInitializing}
method="POST"
onChange={[onChange]}
onSuccess={onSave}
submitted={submitted}
>
<DocumentFields
BeforeFields={
<React.Fragment>
{CustomUpload || (
<Upload_v4
collectionSlug={collectionConfig.slug}
customActions={[
folders && collectionConfig.folders && (
<MoveDocToFolder
buttonProps={{
buttonStyle: 'pill',
size: 'small',
}}
folderCollectionSlug={folders.slug}
folderFieldName={folders.fieldName}
key="move-doc-to-folder"
/>
),
].filter(Boolean)}
initialState={initialState}
resetUploadEdits={resetUploadEdits}
updateUploadEdits={updateUploadEdits}
uploadConfig={collectionConfig.upload}
uploadEdits={uploadEdits}
/>
)}
</React.Fragment>
}
docPermissions={docPermissions}
fields={collectionConfig.fields}
schemaPathSegments={[collectionConfig.slug]}
/>
<ReportAllErrors />
<GetFieldProxy />
</Form>
</OperationProvider>
)
}

View File

@@ -11,9 +11,11 @@ import type { FormProps } from '../../../forms/Form/index.js'
import type { OnFieldSelect } from '../../FieldSelect/index.js'
import type { FieldOption } from '../../FieldSelect/reduceFieldOptions.js'
import type { State } from '../FormsManager/reducer.js'
import type { EditManyBulkUploadsProps } from './index.js'
import { Button } from '../../../elements/Button/index.js'
import { Form } from '../../../forms/Form/index.js'
import { FieldPathContext } from '../../../forms/RenderFields/context.js'
import { RenderField } from '../../../forms/RenderFields/RenderField.js'
import { XIcon } from '../../../icons/X/index.js'
import { useAuth } from '../../../providers/Auth/index.js'
@@ -22,7 +24,7 @@ import { useTranslation } from '../../../providers/Translation/index.js'
import { abortAndIgnore, handleAbortRef } from '../../../utilities/abortAndIgnore.js'
import { FieldSelect } from '../../FieldSelect/index.js'
import { useFormsManager } from '../FormsManager/index.js'
import { baseClass, type EditManyBulkUploadsProps } from './index.js'
import { baseClass } from './index.js'
import './index.scss'
import '../../../forms/RenderFields/index.scss'
@@ -169,23 +171,25 @@ export const EditManyBulkUploadsDrawerContent: React.FC<
/>
{selectedFields.length === 0 ? null : (
<div className="render-fields">
{selectedFields.map((option, i) => {
const {
value: { field, fieldPermissions, path },
} = option
<FieldPathContext value={undefined}>
{selectedFields.map((option, i) => {
const {
value: { field, fieldPermissions, path },
} = option
return (
<RenderField
clientFieldConfig={field}
indexPath=""
key={`${path}-${i}`}
parentPath=""
parentSchemaPath=""
path={path}
permissions={fieldPermissions}
/>
)
})}
return (
<RenderField
clientFieldConfig={field}
indexPath=""
key={`${path}-${i}`}
parentPath=""
parentSchemaPath=""
path={path}
permissions={fieldPermissions}
/>
)
})}
</FieldPathContext>
</div>
)}
<div className={`${baseClass}__sidebar-wrap`}>

View File

@@ -118,7 +118,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
const { toggleLoadingOverlay } = useLoadingOverlay()
const { closeModal } = useModal()
const { collectionSlug, drawerSlug, initialFiles, onSuccess } = useBulkUpload()
const { collectionSlug, drawerSlug, initialFiles, onSuccess, setInitialFiles } = useBulkUpload()
const [isUploading, setIsUploading] = React.useState(false)
const [loadingText, setLoadingText] = React.useState('')
@@ -366,13 +366,10 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
setIsUploading(false)
const remainingForms = []
const thumbnailIndexesToRemove = []
currentForms.forEach(({ errorCount }, i) => {
if (errorCount) {
remainingForms.push(currentForms[i])
} else {
thumbnailIndexesToRemove.push(i)
}
})
@@ -401,8 +398,13 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
totalErrorCount: remainingForms.reduce((acc, { errorCount }) => acc + errorCount, 0),
},
})
if (remainingForms.length === 0) {
setInitialFiles(undefined)
}
},
[
setInitialFiles,
actionURL,
collectionSlug,
getUploadHandler,

View File

@@ -89,61 +89,57 @@ export function BulkUploadDrawer() {
type BulkUploadContext = {
collectionSlug: string
currentActivePath: string
drawerSlug: string
initialFiles: FileList
maxFiles: number
onCancel: () => void
onSuccess: (newDocs: JsonObject[], errorCount: number) => void
setCollectionSlug: (slug: string) => void
setCurrentActivePath: (path: string) => void
setInitialFiles: (files: FileList) => void
setMaxFiles: (maxFiles: number) => void
setOnCancel: (onCancel: BulkUploadContext['onCancel']) => void
setOnSuccess: (path: string, onSuccess: BulkUploadContext['onSuccess']) => void
setOnSuccess: (onSuccess: BulkUploadContext['onSuccess']) => void
}
const Context = React.createContext<BulkUploadContext>({
collectionSlug: '',
currentActivePath: undefined,
drawerSlug: '',
initialFiles: undefined,
maxFiles: undefined,
onCancel: () => null,
onSuccess: () => null,
setCollectionSlug: () => null,
setCurrentActivePath: () => null,
setInitialFiles: () => null,
setMaxFiles: () => null,
setOnCancel: () => null,
setOnSuccess: () => null,
})
export function BulkUploadProvider({ children }: { readonly children: React.ReactNode }) {
export function BulkUploadProvider({
children,
drawerSlugPrefix,
}: {
readonly children: React.ReactNode
readonly drawerSlugPrefix?: string
}) {
const [collection, setCollection] = React.useState<string>()
const [onSuccessFunctionMap, setOnSuccessFunctionMap] =
React.useState<Record<string, BulkUploadContext['onSuccess']>>()
const [onSuccessFunction, setOnSuccessFunction] = React.useState<BulkUploadContext['onSuccess']>()
const [onCancelFunction, setOnCancelFunction] = React.useState<BulkUploadContext['onCancel']>()
const [initialFiles, setInitialFiles] = React.useState<FileList>(undefined)
const [maxFiles, setMaxFiles] = React.useState<number>(undefined)
const [currentActivePath, setCurrentActivePath] = React.useState<string>(undefined)
const drawerSlug = useBulkUploadDrawerSlug()
const drawerSlug = `${drawerSlugPrefix ? `${drawerSlugPrefix}-` : ''}${useBulkUploadDrawerSlug()}`
const setCollectionSlug: BulkUploadContext['setCollectionSlug'] = (slug) => {
setCollection(slug)
}
const setOnSuccess: BulkUploadContext['setOnSuccess'] = React.useCallback((path, onSuccess) => {
setOnSuccessFunctionMap((prev) => ({
...prev,
[path]: onSuccess,
}))
}, [])
const setOnSuccess: BulkUploadContext['setOnSuccess'] = (onSuccess) => {
setOnSuccessFunction(() => onSuccess)
}
return (
<Context
value={{
collectionSlug: collection,
currentActivePath,
drawerSlug,
initialFiles,
maxFiles,
@@ -153,13 +149,11 @@ export function BulkUploadProvider({ children }: { readonly children: React.Reac
}
},
onSuccess: (docIDs, errorCount) => {
if (onSuccessFunctionMap && Object.hasOwn(onSuccessFunctionMap, currentActivePath)) {
const onSuccessFunction = onSuccessFunctionMap[currentActivePath]
if (typeof onSuccessFunction === 'function') {
onSuccessFunction(docIDs, errorCount)
}
},
setCollectionSlug,
setCurrentActivePath,
setInitialFiles,
setMaxFiles,
setOnCancel: setOnCancelFunction,

View File

@@ -27,12 +27,7 @@ export function ListBulkUploadButton({
*/
openBulkUpload?: () => void
}) {
const {
drawerSlug: bulkUploadDrawerSlug,
setCollectionSlug,
setCurrentActivePath,
setOnSuccess,
} = useBulkUpload()
const { drawerSlug: bulkUploadDrawerSlug, setCollectionSlug, setOnSuccess } = useBulkUpload()
const { t } = useTranslation()
const { openModal } = useModal()
const router = useRouter()
@@ -42,9 +37,8 @@ export function ListBulkUploadButton({
openBulkUploadFromProps()
} else {
setCollectionSlug(collectionSlug)
setCurrentActivePath(collectionSlug)
openModal(bulkUploadDrawerSlug)
setOnSuccess(collectionSlug, () => {
setOnSuccess(() => {
if (typeof onBulkUploadSuccess === 'function') {
onBulkUploadSuccess()
} else {
@@ -58,7 +52,6 @@ export function ListBulkUploadButton({
bulkUploadDrawerSlug,
openModal,
setCollectionSlug,
setCurrentActivePath,
setOnSuccess,
onBulkUploadSuccess,
openBulkUploadFromProps,

View File

@@ -118,14 +118,8 @@ export function UploadInput(props: UploadInputProps) {
)
const { openModal } = useModal()
const {
drawerSlug,
setCollectionSlug,
setCurrentActivePath,
setInitialFiles,
setMaxFiles,
setOnSuccess,
} = useBulkUpload()
const { drawerSlug, setCollectionSlug, setInitialFiles, setMaxFiles, setOnSuccess } =
useBulkUpload()
const { permissions } = useAuth()
const { code } = useLocale()
const { i18n, t } = useTranslation()
@@ -294,7 +288,6 @@ export function UploadInput(props: UploadInputProps) {
if (typeof maxRows === 'number') {
setMaxFiles(maxRows)
}
setCurrentActivePath(path)
openModal(drawerSlug)
},
[
@@ -306,8 +299,6 @@ export function UploadInput(props: UploadInputProps) {
setInitialFiles,
maxRows,
setMaxFiles,
path,
setCurrentActivePath,
],
)
@@ -461,7 +452,7 @@ export function UploadInput(props: UploadInputProps) {
}, [populateDocs, activeRelationTo, value])
useEffect(() => {
setOnSuccess(path, onUploadSuccess)
setOnSuccess(onUploadSuccess)
}, [value, path, onUploadSuccess, setOnSuccess])
const showDropzone =

View File

@@ -4,12 +4,13 @@ import type { UploadFieldClientProps } from 'payload'
import React, { useMemo } from 'react'
import { BulkUploadProvider } from '../../elements/BulkUpload/index.js'
import { useField } from '../../forms/useField/index.js'
import { withCondition } from '../../forms/withCondition/index.js'
import { useConfig } from '../../providers/Config/index.js'
import './index.scss'
import { mergeFieldStyles } from '../mergeFieldStyles.js'
import { UploadInput } from './Input.js'
import './index.scss'
export { UploadInput } from './Input.js'
export type { UploadInputProps } from './Input.js'
@@ -62,33 +63,35 @@ export function UploadComponent(props: UploadFieldClientProps) {
const styles = useMemo(() => mergeFieldStyles(field), [field])
return (
<UploadInput
AfterInput={AfterInput}
allowCreate={allowCreate !== false}
api={config.routes.api}
BeforeInput={BeforeInput}
className={className}
Description={Description}
description={description}
displayPreview={displayPreview}
Error={Error}
filterOptions={filterOptions}
hasMany={hasMany}
isSortable={isSortable}
label={label}
Label={Label}
localized={localized}
maxRows={maxRows}
onChange={setValue}
path={path}
readOnly={readOnly || disabled}
relationTo={relationTo}
required={required}
serverURL={config.serverURL}
showError={showError}
style={styles}
value={value}
/>
<BulkUploadProvider drawerSlugPrefix={pathFromProps}>
<UploadInput
AfterInput={AfterInput}
allowCreate={allowCreate !== false}
api={config.routes.api}
BeforeInput={BeforeInput}
className={className}
Description={Description}
description={description}
displayPreview={displayPreview}
Error={Error}
filterOptions={filterOptions}
hasMany={hasMany}
isSortable={isSortable}
label={label}
Label={Label}
localized={localized}
maxRows={maxRows}
onChange={setValue}
path={path}
readOnly={readOnly || disabled}
relationTo={relationTo}
required={required}
serverURL={config.serverURL}
showError={showError}
style={styles}
value={value}
/>
</BulkUploadProvider>
)
}

View File

@@ -86,12 +86,7 @@ export function DefaultListView(props: ListViewClientProps) {
} = useListQuery()
const { openModal } = useModal()
const {
drawerSlug: bulkUploadDrawerSlug,
setCollectionSlug,
setCurrentActivePath,
setOnSuccess,
} = useBulkUpload()
const { drawerSlug: bulkUploadDrawerSlug, setCollectionSlug, setOnSuccess } = useBulkUpload()
const collectionConfig = getEntityConfig({ collectionSlug })
@@ -124,18 +119,9 @@ export function DefaultListView(props: ListViewClientProps) {
const openBulkUpload = React.useCallback(() => {
setCollectionSlug(collectionSlug)
setCurrentActivePath(collectionSlug)
openModal(bulkUploadDrawerSlug)
setOnSuccess(collectionSlug, () => router.refresh())
}, [
router,
collectionSlug,
bulkUploadDrawerSlug,
openModal,
setCollectionSlug,
setCurrentActivePath,
setOnSuccess,
])
setOnSuccess(() => router.refresh())
}, [router, collectionSlug, bulkUploadDrawerSlug, openModal, setCollectionSlug, setOnSuccess])
useEffect(() => {
if (!isInDrawer) {

View File

@@ -199,15 +199,8 @@ describe('Uploads', () => {
await page.locator('.upload-relationship-details__edit').nth(0).click()
await page.locator('.file-details__remove').click()
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, 'test-image.jpg'))
await page.locator('button#action-save').nth(1).click()
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
await wait(1000)
await page.setInputFiles('input[type="file"]', path.join(dirname, 'test-image.jpg'))
await saveDocAndAssert(page, '.doc-drawer button#action-save')
await page.locator('.doc-drawer__header-close').click()
@@ -695,16 +688,8 @@ describe('Uploads', () => {
test('should upload image with metadata', async () => {
await page.goto(withMetadataURL.create)
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, 'test-image.jpg'))
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)
await page.setInputFiles('input[type="file"]', path.join(dirname, 'test-image.jpg'))
await saveDocAndAssert(page)
const mediaID = page.url().split('/').pop()
@@ -716,22 +701,16 @@ describe('Uploads', () => {
const acceptableFileSizes = [9431, 9435]
expect(acceptableFileSizes).toContain(mediaDoc.sizes.sizeOne.filesize)
await expect
.poll(() => acceptableFileSizes.includes(mediaDoc.sizes.sizeOne.filesize))
.toBe(true)
})
test('should upload image without metadata', async () => {
await page.goto(withoutMetadataURL.create)
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, 'test-image.jpg'))
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)
await page.setInputFiles('input[type="file"]', path.join(dirname, 'test-image.jpg'))
await saveDocAndAssert(page)
const mediaID = page.url().split('/').pop()
@@ -743,22 +722,16 @@ describe('Uploads', () => {
const acceptableFileSizes = [2424, 2445]
expect(acceptableFileSizes).toContain(mediaDoc.sizes.sizeTwo.filesize)
await expect
.poll(() => acceptableFileSizes.includes(mediaDoc.sizes.sizeTwo.filesize))
.toBe(true)
})
test('should only upload image with metadata if jpeg mimetype', async () => {
await page.goto(withOnlyJPEGMetadataURL.create)
const fileChooserPromiseForJPEG = page.waitForEvent('filechooser')
await page.getByText('Select a file').click()
const fileChooserForJPEG = await fileChooserPromiseForJPEG
await wait(1000)
await fileChooserForJPEG.setFiles(path.join(dirname, 'test-image.jpg'))
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)
await page.setInputFiles('input[type="file"]', path.join(dirname, 'test-image.jpg'))
await saveDocAndAssert(page)
const jpegMediaID = page.url().split('/').pop()
@@ -771,20 +744,14 @@ describe('Uploads', () => {
const acceptableFileSizesForJPEG = [9554, 9575]
// without metadata appended, the jpeg image filesize would be 2424
expect(acceptableFileSizesForJPEG).toContain(jpegMediaDoc.sizes.sizeThree.filesize)
await expect
.poll(() => acceptableFileSizesForJPEG.includes(jpegMediaDoc.sizes.sizeThree.filesize))
.toBe(true)
await page.goto(withOnlyJPEGMetadataURL.create)
const fileChooserPromiseForWEBP = page.waitForEvent('filechooser')
await page.getByText('Select a file').click()
const fileChooserForWEBP = await fileChooserPromiseForWEBP
await wait(1000)
await fileChooserForWEBP.setFiles(path.join(dirname, 'animated.webp'))
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)
await page.setInputFiles('input[type="file"]', path.join(dirname, 'animated.webp'))
await saveDocAndAssert(page)
const webpMediaID = page.url().split('/').pop()
@@ -795,7 +762,7 @@ describe('Uploads', () => {
})
// With metadata, the animated image filesize would be 218762
expect(webpMediaDoc.sizes.sizeThree.filesize).toEqual(211638)
await expect.poll(() => webpMediaDoc.sizes.sizeThree.filesize).toBe(211638)
})
test('should show custom upload component', async () => {
@@ -831,7 +798,8 @@ describe('Uploads', () => {
const filename = page.locator('[id^="doc-drawer_admin-thumbnail-size"] .file-field__filename')
await expect(filename).toHaveValue('test-image.png')
await page.waitForSelector('[id^="doc-drawer_admin-thumbnail-size"] #action-save')
await expect(page.locator('[id^="doc-drawer_admin-thumbnail-size"] #action-save')).toBeVisible()
await page.locator('[id^="doc-drawer_admin-thumbnail-size"] #action-save').click()
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
@@ -839,10 +807,12 @@ describe('Uploads', () => {
const href = await page.locator('#field-singleThumbnailUpload a').getAttribute('href')
// Ensure the URL starts correctly
expect(href).toMatch(/^\/api\/admin-thumbnail-size\/file\/test-image(-\d+)?\.png$/i)
await expect
.poll(() => href)
.toMatch(/^\/api\/admin-thumbnail-size\/file\/test-image(-\d+)?\.png$/i)
// Ensure no "-100x100" or any similar suffix
expect(href).not.toMatch(/-\d+x\d+\.png$/)
await expect.poll(() => !/-\d+x\d+\.png$/.test(href!)).toBe(true)
})
test('should show original image url on a hasMany upload card for an upload with adminThumbnail defined', async () => {
@@ -853,23 +823,29 @@ describe('Uploads', () => {
})
await hasManyThumbnailButton.click()
const hasManyThumbnailModal = page.locator('#bulk-upload-drawer-slug-1')
const hasManyThumbnailModal = page.locator('#hasManyThumbnailUpload-bulk-upload-drawer-slug-1')
await expect(hasManyThumbnailModal).toBeVisible()
await page.setInputFiles('#bulk-upload-drawer-slug-1 .dropzone input[type="file"]', [
path.resolve(dirname, './test-image.png'),
])
await hasManyThumbnailModal
.locator('.dropzone input[type="file"]')
.setInputFiles([path.resolve(dirname, './test-image.png')])
const saveButton = page.locator('.bulk-upload--actions-bar__saveButtons button')
const saveButton = hasManyThumbnailModal.locator(
'.bulk-upload--actions-bar__saveButtons button',
)
await saveButton.click()
await page.waitForSelector('#field-hasManyThumbnailUpload .upload--has-many__dragItem')
await expect(
page.locator('#field-hasManyThumbnailUpload .upload--has-many__dragItem'),
).toBeVisible()
const itemCount = await page
.locator('#field-hasManyThumbnailUpload .upload--has-many__dragItem')
.count()
expect(itemCount).toEqual(1)
await expect.poll(() => itemCount).toBe(1)
await page.waitForSelector('#field-hasManyThumbnailUpload .upload--has-many__dragItem a')
await expect(
page.locator('#field-hasManyThumbnailUpload .upload--has-many__dragItem a'),
).toBeVisible()
const href = await page
.locator('#field-hasManyThumbnailUpload .upload--has-many__dragItem a')
.getAttribute('href')
@@ -913,36 +889,37 @@ describe('Uploads', () => {
})
await bulkUploadButton.click()
const bulkUploadModal = page.locator('#bulk-upload-drawer-slug-1')
const bulkUploadModal = page.locator('#hasManyUpload-bulk-upload-drawer-slug-1')
await expect(bulkUploadModal).toBeVisible()
// Bulk upload multiple files at once
await page.setInputFiles('#bulk-upload-drawer-slug-1 .dropzone input[type="file"]', [
path.resolve(dirname, './image.png'),
path.resolve(dirname, './test-image.png'),
])
await bulkUploadModal
.locator('.dropzone input[type="file"]')
.setInputFiles([
path.resolve(dirname, './image.png'),
path.resolve(dirname, './test-image.png'),
])
await page
await bulkUploadModal
.locator('.bulk-upload--file-manager .render-fields #field-prefix')
.fill('prefix-one')
const nextImageChevronButton = page.locator(
const nextImageChevronButton = bulkUploadModal.locator(
'.bulk-upload--actions-bar__controls button:nth-of-type(2)',
)
await nextImageChevronButton.click()
await page
await bulkUploadModal
.locator('.bulk-upload--file-manager .render-fields #field-prefix')
.fill('prefix-two')
const saveButton = page.locator('.bulk-upload--actions-bar__saveButtons button')
const saveButton = bulkUploadModal.locator('.bulk-upload--actions-bar__saveButtons button')
await saveButton.click()
await page.waitForSelector('#field-hasManyUpload .upload--has-many__dragItem')
const itemCount = await page
.locator('#field-hasManyUpload .upload--has-many__dragItem')
.count()
expect(itemCount).toEqual(2)
const items = page.locator('#field-hasManyUpload .upload--has-many__dragItem')
await expect(items).toHaveCount(2)
await expect(items.nth(0)).toBeVisible()
await expect(items.nth(1)).toBeVisible()
await saveDocAndAssert(page)
})
@@ -967,29 +944,27 @@ describe('Uploads', () => {
})
await bulkUploadButton.click()
const bulkUploadModal = page.locator('#bulk-upload-drawer-slug-1')
const bulkUploadModal = page.locator('#hasManyUpload-bulk-upload-drawer-slug-1')
await expect(bulkUploadModal).toBeVisible()
await page.setInputFiles('#bulk-upload-drawer-slug-1 .dropzone input[type="file"]', [
path.resolve(dirname, './test-pdf.pdf'),
])
await bulkUploadModal
.locator('.dropzone input[type="file"]')
.setInputFiles([path.resolve(dirname, './test-pdf.pdf')])
await page
await bulkUploadModal
.locator('.bulk-upload--file-manager .render-fields #field-prefix')
.fill('prefix-one')
const saveButton = page.locator('.bulk-upload--actions-bar__saveButtons button')
const saveButton = bulkUploadModal.locator('.bulk-upload--actions-bar__saveButtons button')
await saveButton.click()
await page.waitForSelector('#field-hasManyUpload .upload--has-many__dragItem')
const itemCount = await page
.locator('#field-hasManyUpload .upload--has-many__dragItem')
.count()
expect(itemCount).toEqual(1)
const items = page.locator('#field-hasManyUpload .upload--has-many__dragItem')
await expect(items).toHaveCount(1)
await expect(items.nth(0)).toBeVisible()
await saveDocAndAssert(page)
// Assert no console errors occurred for this test only
expect(consoleErrorsFromPage).toEqual([])
await expect.poll(() => consoleErrorsFromPage).toEqual([])
// Reset global behavior for other tests
stopCollectingErrorsFromPage()
@@ -1013,34 +988,37 @@ describe('Uploads', () => {
await bulkUploadButton.click()
const bulkUploadModal = page.locator('#bulk-upload-drawer-slug-1')
const bulkUploadModal = page.locator('#hasManyUpload-bulk-upload-drawer-slug-1')
await expect(bulkUploadModal).toBeVisible()
// Bulk upload multiple files at once
await page.setInputFiles('#bulk-upload-drawer-slug-1 .dropzone input[type="file"]', [
path.resolve(dirname, './image.png'),
path.resolve(dirname, './test-image.png'),
])
await bulkUploadModal
.locator('.dropzone input[type="file"]')
.setInputFiles([
path.resolve(dirname, './image.png'),
path.resolve(dirname, './test-image.png'),
])
await page.locator('#bulk-upload-drawer-slug-1 .edit-many-bulk-uploads__toggle').click()
await bulkUploadModal.locator('.edit-many-bulk-uploads__toggle').click()
const editManyBulkUploadModal = page.locator('#edit-uploads-2-bulk-uploads')
await expect(editManyBulkUploadModal).toBeVisible()
await page.locator('.edit-many-bulk-uploads__form .react-select').click({ delay: 100 })
const options = page.locator('.rs__option')
await editManyBulkUploadModal
.locator('.edit-many-bulk-uploads__form .react-select')
.click({ delay: 100 })
const options = editManyBulkUploadModal.locator('.rs__option')
await options.locator('text=Prefix').click()
await page.locator('#edit-uploads-2-bulk-uploads #field-prefix').fill('some prefix')
await editManyBulkUploadModal.locator('#field-prefix').fill('some prefix')
await page.locator('.edit-many-bulk-uploads__sidebar-wrap button').click()
await page.locator('.bulk-upload--actions-bar__saveButtons button').click()
await page.waitForSelector('#field-hasManyUpload .upload--has-many__dragItem')
await editManyBulkUploadModal.locator('.edit-many-bulk-uploads__sidebar-wrap button').click()
await bulkUploadModal.locator('.bulk-upload--actions-bar__saveButtons button').click()
const itemCount = await page
.locator('#field-hasManyUpload .upload--has-many__dragItem')
.count()
expect(itemCount).toEqual(2)
const items = page.locator('#field-hasManyUpload .upload--has-many__dragItem')
await expect(items).toHaveCount(2)
await expect(items.nth(0)).toBeVisible()
await expect(items.nth(1)).toBeVisible()
await saveDocAndAssert(page)
})
@@ -1062,37 +1040,39 @@ describe('Uploads', () => {
})
await bulkUploadButton.click()
const bulkUploadModal = page.locator('#bulk-upload-drawer-slug-1')
const bulkUploadModal = page.locator('#hasManyUpload-bulk-upload-drawer-slug-1')
await expect(bulkUploadModal).toBeVisible()
// Bulk upload multiple files at once
await page.setInputFiles('#bulk-upload-drawer-slug-1 .dropzone input[type="file"]', [
path.resolve(dirname, './image.png'),
path.resolve(dirname, './test-image.png'),
])
await bulkUploadModal
.locator('.dropzone input[type="file"]')
.setInputFiles([
path.resolve(dirname, './image.png'),
path.resolve(dirname, './test-image.png'),
])
const saveButton = page.locator('.bulk-upload--actions-bar__saveButtons button')
const saveButton = bulkUploadModal.locator('.bulk-upload--actions-bar__saveButtons button')
await saveButton.click()
await expect(page.locator('.payload-toast-container')).toContainText('Failed to save 2 files')
const errorCount = page
.locator('#bulk-upload-drawer-slug-1 .file-selections .error-pill__count')
.first()
const errorCount = bulkUploadModal.locator('.file-selections .error-pill__count').first()
await expect(errorCount).toHaveText('3')
await page.locator('#bulk-upload-drawer-slug-1 .edit-many-bulk-uploads__toggle').click()
await bulkUploadModal.locator('.edit-many-bulk-uploads__toggle').click()
const editManyBulkUploadModal = page.locator('#edit-uploads-2-bulk-uploads')
await expect(editManyBulkUploadModal).toBeVisible()
const fieldSelector = page.locator('.edit-many-bulk-uploads__form .react-select')
const fieldSelector = editManyBulkUploadModal.locator(
'.edit-many-bulk-uploads__form .react-select',
)
await fieldSelector.click({ delay: 100 })
const options = page.locator('.rs__option')
const options = editManyBulkUploadModal.locator('.rs__option')
// Select an option
await options.locator('text=Prefix').click()
await page.locator('#edit-uploads-2-bulk-uploads #field-prefix').fill('some prefix')
await editManyBulkUploadModal.locator('#field-prefix').fill('some prefix')
await page.locator('.edit-many-bulk-uploads__sidebar-wrap button').click()
await editManyBulkUploadModal.locator('.edit-many-bulk-uploads__sidebar-wrap button').click()
await saveButton.click()
await expect(page.locator('.payload-toast-container')).toContainText(
@@ -1119,53 +1099,51 @@ describe('Uploads', () => {
})
await bulkUploadButton.click()
const bulkUploadModal = page.locator('#bulk-upload-drawer-slug-1')
const bulkUploadModal = page.locator('#hasManyUpload-bulk-upload-drawer-slug-1')
await expect(bulkUploadModal).toBeVisible()
// Bulk upload multiple files at once
await page.setInputFiles('#bulk-upload-drawer-slug-1 .dropzone input[type="file"]', [
path.resolve(dirname, './image.png'),
path.resolve(dirname, './test-image.png'),
])
await bulkUploadModal
.locator('.dropzone input[type="file"]')
.setInputFiles([
path.resolve(dirname, './image.png'),
path.resolve(dirname, './test-image.png'),
])
await page.locator('#bulk-upload-drawer-slug-1 .edit-many-bulk-uploads__toggle').click()
await bulkUploadModal.locator('.edit-many-bulk-uploads__toggle').click()
const editManyBulkUploadModal = page.locator('#edit-uploads-2-bulk-uploads')
await expect(editManyBulkUploadModal).toBeVisible()
const fieldSelector = page.locator('.edit-many-bulk-uploads__form .react-select')
const fieldSelector = editManyBulkUploadModal.locator(
'.edit-many-bulk-uploads__form .react-select',
)
await fieldSelector.click({ delay: 100 })
const options = page.locator('.rs__option')
const options = editManyBulkUploadModal.locator('.rs__option')
// Select an option
await options.locator('text=Prefix').click()
await page.locator('#edit-uploads-2-bulk-uploads #field-prefix').fill('some prefix')
await editManyBulkUploadModal.locator('#field-prefix').fill('some prefix')
await page.locator('.edit-many-bulk-uploads__sidebar-wrap button').click()
await editManyBulkUploadModal.locator('.edit-many-bulk-uploads__sidebar-wrap button').click()
await page
.locator('#bulk-upload-drawer-slug-1 .file-field__upload .file-field__remove')
.click()
await bulkUploadModal.locator('.file-field__upload .file-field__remove').click()
const chevronRight = page.locator(
'#bulk-upload-drawer-slug-1 .bulk-upload--actions-bar__controls button:nth-of-type(2)',
const chevronRight = bulkUploadModal.locator(
'.bulk-upload--actions-bar__controls button:nth-of-type(2)',
)
await chevronRight.click()
await expect(
page
.locator(
'#bulk-upload-drawer-slug-1 .file-selections .file-selections__fileRow .file-selections__fileName',
)
bulkUploadModal
.locator('.file-selections .file-selections__fileRow .file-selections__fileName')
.first(),
).toContainText('No file')
const saveButton = page.locator('.bulk-upload--actions-bar__saveButtons button')
const saveButton = bulkUploadModal.locator('.bulk-upload--actions-bar__saveButtons button')
await saveButton.click()
const errorCount = page
.locator('#bulk-upload-drawer-slug-1 .file-selections .error-pill__count')
.first()
const errorCount = bulkUploadModal.locator('.file-selections .error-pill__count').first()
await expect(errorCount).toHaveText('1')
})
@@ -1216,6 +1194,32 @@ describe('Uploads', () => {
'Related Document Title',
)
})
test('should reset state once all files are saved successfully from field bulk upload', async () => {
await page.goto(uploadsOne.create)
const fieldBulkUploadButton = page.locator('#field-hasManyThumbnailUpload button', {
hasText: exactText('Create New'),
})
await fieldBulkUploadButton.click()
const fieldBulkUploadDrawer = page.locator(
'#hasManyThumbnailUpload-bulk-upload-drawer-slug-1',
)
await expect(fieldBulkUploadDrawer).toBeVisible()
await fieldBulkUploadDrawer
.locator('.dropzone input[type="file"]')
.setInputFiles([
path.resolve(dirname, './image.png'),
path.resolve(dirname, './test-image.png'),
])
await fieldBulkUploadDrawer
.locator('.bulk-upload--actions-bar button', { hasText: 'Save' })
.click()
await expect(fieldBulkUploadDrawer).toBeHidden()
await fieldBulkUploadButton.click()
// should show add files dropzone view
await expect(fieldBulkUploadDrawer.locator('.bulk-upload--add-files')).toBeVisible()
})
})
describe('remote url fetching', () => {
@@ -1314,12 +1318,8 @@ describe('Uploads', () => {
const createFocalCrop = async (page: Page, position: 'bottom-right' | 'top-left') => {
const { dragX, dragY, focalX, focalY } = positions[position]
await page.goto(mediaURL.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, 'test-image.jpg'))
await page.setInputFiles('input[type="file"]', path.join(dirname, 'test-image.jpg'))
await page.locator('.file-field__edit').click()
@@ -1342,10 +1342,7 @@ describe('Uploads', () => {
// apply crop
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 saveDocAndAssert(page)
}
await createFocalCrop(page, 'bottom-right') // green square
@@ -1373,12 +1370,7 @@ describe('Uploads', () => {
test('should update image alignment based on focal point', async () => {
const updateFocalPosition = async (page: Page) => {
await page.goto(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.setInputFiles('input[type="file"]', path.join(dirname, 'horizontal-squares.jpg'))
await page.locator('.file-field__edit').click()
@@ -1388,10 +1380,7 @@ describe('Uploads', () => {
// 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 saveDocAndAssert(page)
}
await updateFocalPosition(page) // red square
@@ -1404,17 +1393,13 @@ describe('Uploads', () => {
})
// without focal point update this generated size was equal to 1736
expect(redDoc.sizes.focalTest.filesize).toEqual(1586)
await expect.poll(() => redDoc.sizes.focalTest.filesize).toBe(1586)
})
test('should resize image after crop if resizeOptions defined', async () => {
await page.goto(animatedTypeMediaURL.create)
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, 'test-image.jpg'))
await page.setInputFiles('input[type="file"]', path.join(dirname, 'test-image.jpg'))
await page.locator('.file-field__edit').click()
@@ -1426,10 +1411,7 @@ describe('Uploads', () => {
await page.locator('.edit-upload__input input[name="Y %"]').fill('50') // init top 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 saveDocAndAssert(page)
const resizeOptionMedia = page.locator('.file-meta .file-meta__size-type')
await expect(resizeOptionMedia).toContainText('200x200')
@@ -1505,25 +1487,14 @@ describe('Uploads', () => {
test('should skip applying resizeOptions after updating an image if resizeOptions.withoutEnlargement is true and the original image size is smaller than the dimensions defined in resizeOptions', async () => {
await page.goto(withoutEnlargementResizeOptionsURL.create)
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, 'test-image.jpg'))
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)
await page.setInputFiles('input[type="file"]', path.join(dirname, 'test-image.jpg'))
await saveDocAndAssert(page)
await page.locator('.file-field__edit').click()
// no need to make any changes to the image if resizeOptions.withoutEnlargement is actually being respected now
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)
await saveDocAndAssert(page)
const resizeOptionMedia = page.locator('.file-meta .file-meta__size-type')
@@ -1546,10 +1517,7 @@ describe('Uploads', () => {
test('should select an image within target range', async () => {
await page.goto(bestFitURL.create)
await page.locator('#field-withinRange button.upload__createNewToggler').click()
const fileChooserPromise = page.waitForEvent('filechooser')
await page.getByText('Select a file').click()
const fileChooser = await fileChooserPromise
await fileChooser.setFiles(path.join(dirname, 'test-image.jpg'))
await page.setInputFiles('input[type="file"]', path.join(dirname, 'test-image.jpg'))
await page.locator('dialog button#action-save').click()
const thumbnail = page.locator('#field-withinRange div.thumbnail > img')
await expect(thumbnail).toHaveAttribute('src', '/api/enlarge/file/test-image-180x50.jpg')
@@ -1558,10 +1526,7 @@ describe('Uploads', () => {
test('should select next smallest image outside of range but smaller than original', async () => {
await page.goto(bestFitURL.create)
await page.locator('#field-nextSmallestOutOfRange button.upload__createNewToggler').click()
const fileChooserPromise = page.waitForEvent('filechooser')
await page.getByText('Select a file').click()
const fileChooser = await fileChooserPromise
await fileChooser.setFiles(path.join(dirname, 'test-image.jpg'))
await page.setInputFiles('input[type="file"]', path.join(dirname, 'test-image.jpg'))
await page.locator('dialog button#action-save').click()
const thumbnail = page.locator('#field-nextSmallestOutOfRange div.thumbnail > img')
await expect(thumbnail).toHaveAttribute('src', '/api/focal-only/file/test-image-400x300.jpg')
@@ -1570,10 +1535,7 @@ describe('Uploads', () => {
test('should select original if smaller than next available size', async () => {
await page.goto(bestFitURL.create)
await page.locator('#field-original button.upload__createNewToggler').click()
const fileChooserPromise = page.waitForEvent('filechooser')
await page.getByText('Select a file').click()
const fileChooser = await fileChooserPromise
await fileChooser.setFiles(path.join(dirname, 'small.png'))
await page.setInputFiles('input[type="file"]', path.join(dirname, 'small.png'))
await page.locator('dialog button#action-save').click()
const thumbnail = page.locator('#field-original div.thumbnail > img')
await expect(thumbnail).toHaveAttribute('src', '/api/focal-only/file/small.png')