From 9ba740e472e0fa8aef0ded70a0b68c23f51397c1 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:11:51 -0400 Subject: [PATCH] fix(ui): field bulk upload showing stale data (#13006) --- packages/next/src/templates/Default/index.tsx | 2 +- .../elements/BulkUpload/EditForm/index.tsx | 137 +++---- .../BulkUpload/EditMany/DrawerContent.tsx | 38 +- .../BulkUpload/FormsManager/index.tsx | 10 +- packages/ui/src/elements/BulkUpload/index.tsx | 34 +- .../TitleActions/ListBulkUploadButton.tsx | 11 +- packages/ui/src/fields/Upload/Input.tsx | 15 +- packages/ui/src/fields/Upload/index.tsx | 59 +-- packages/ui/src/views/List/index.tsx | 20 +- test/uploads/e2e.spec.ts | 360 ++++++++---------- 10 files changed, 291 insertions(+), 395 deletions(-) diff --git a/packages/next/src/templates/Default/index.tsx b/packages/next/src/templates/Default/index.tsx index 431d5f32d..5bedabe49 100644 --- a/packages/next/src/templates/Default/index.tsx +++ b/packages/next/src/templates/Default/index.tsx @@ -146,7 +146,7 @@ export const DefaultTemplate: React.FC = ({ return ( - + {RenderServerComponent({ clientProps, diff --git a/packages/ui/src/elements/BulkUpload/EditForm/index.tsx b/packages/ui/src/elements/BulkUpload/EditForm/index.tsx index 3a462bc32..526ce08d2 100644 --- a/packages/ui/src/elements/BulkUpload/EditForm/index.tsx +++ b/packages/ui/src/elements/BulkUpload/EditForm/index.tsx @@ -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(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[0] = useCallback( @@ -150,54 +113,52 @@ export function EditForm({ return ( - -
- - {CustomUpload || ( - - ), - ].filter(Boolean)} - initialState={initialState} - resetUploadEdits={resetUploadEdits} - updateUploadEdits={updateUploadEdits} - uploadConfig={collectionConfig.upload} - uploadEdits={uploadEdits} - /> - )} - - } - docPermissions={docPermissions} - fields={collectionConfig.fields} - schemaPathSegments={[collectionConfig.slug]} - /> - - - -
+
+ + {CustomUpload || ( + + ), + ].filter(Boolean)} + initialState={initialState} + resetUploadEdits={resetUploadEdits} + updateUploadEdits={updateUploadEdits} + uploadConfig={collectionConfig.upload} + uploadEdits={uploadEdits} + /> + )} + + } + docPermissions={docPermissions} + fields={collectionConfig.fields} + schemaPathSegments={[collectionConfig.slug]} + /> + + +
) } diff --git a/packages/ui/src/elements/BulkUpload/EditMany/DrawerContent.tsx b/packages/ui/src/elements/BulkUpload/EditMany/DrawerContent.tsx index 6464b518c..4277a9a83 100644 --- a/packages/ui/src/elements/BulkUpload/EditMany/DrawerContent.tsx +++ b/packages/ui/src/elements/BulkUpload/EditMany/DrawerContent.tsx @@ -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 : (
- {selectedFields.map((option, i) => { - const { - value: { field, fieldPermissions, path }, - } = option + + {selectedFields.map((option, i) => { + const { + value: { field, fieldPermissions, path }, + } = option - return ( - - ) - })} + return ( + + ) + })} +
)}
diff --git a/packages/ui/src/elements/BulkUpload/FormsManager/index.tsx b/packages/ui/src/elements/BulkUpload/FormsManager/index.tsx index fdf4394c7..c4f707494 100644 --- a/packages/ui/src/elements/BulkUpload/FormsManager/index.tsx +++ b/packages/ui/src/elements/BulkUpload/FormsManager/index.tsx @@ -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, diff --git a/packages/ui/src/elements/BulkUpload/index.tsx b/packages/ui/src/elements/BulkUpload/index.tsx index 1fba50a5c..cbd3c0348 100644 --- a/packages/ui/src/elements/BulkUpload/index.tsx +++ b/packages/ui/src/elements/BulkUpload/index.tsx @@ -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({ 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() - const [onSuccessFunctionMap, setOnSuccessFunctionMap] = - React.useState>() + const [onSuccessFunction, setOnSuccessFunction] = React.useState() const [onCancelFunction, setOnCancelFunction] = React.useState() const [initialFiles, setInitialFiles] = React.useState(undefined) const [maxFiles, setMaxFiles] = React.useState(undefined) - const [currentActivePath, setCurrentActivePath] = React.useState(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 ( { - if (onSuccessFunctionMap && Object.hasOwn(onSuccessFunctionMap, currentActivePath)) { - const onSuccessFunction = onSuccessFunctionMap[currentActivePath] + if (typeof onSuccessFunction === 'function') { onSuccessFunction(docIDs, errorCount) } }, setCollectionSlug, - setCurrentActivePath, setInitialFiles, setMaxFiles, setOnCancel: setOnCancelFunction, diff --git a/packages/ui/src/elements/ListHeader/TitleActions/ListBulkUploadButton.tsx b/packages/ui/src/elements/ListHeader/TitleActions/ListBulkUploadButton.tsx index a01940b77..dcdfa35e0 100644 --- a/packages/ui/src/elements/ListHeader/TitleActions/ListBulkUploadButton.tsx +++ b/packages/ui/src/elements/ListHeader/TitleActions/ListBulkUploadButton.tsx @@ -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, diff --git a/packages/ui/src/fields/Upload/Input.tsx b/packages/ui/src/fields/Upload/Input.tsx index dc72b240a..708aca76d 100644 --- a/packages/ui/src/fields/Upload/Input.tsx +++ b/packages/ui/src/fields/Upload/Input.tsx @@ -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 = diff --git a/packages/ui/src/fields/Upload/index.tsx b/packages/ui/src/fields/Upload/index.tsx index 9fc43b203..a1b4380a3 100644 --- a/packages/ui/src/fields/Upload/index.tsx +++ b/packages/ui/src/fields/Upload/index.tsx @@ -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 ( - + + + ) } diff --git a/packages/ui/src/views/List/index.tsx b/packages/ui/src/views/List/index.tsx index 0494f802b..69e841380 100644 --- a/packages/ui/src/views/List/index.tsx +++ b/packages/ui/src/views/List/index.tsx @@ -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) { diff --git a/test/uploads/e2e.spec.ts b/test/uploads/e2e.spec.ts index cb0bc1a16..254c0f4b4 100644 --- a/test/uploads/e2e.spec.ts +++ b/test/uploads/e2e.spec.ts @@ -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')