Skip to content

Commit

Permalink
fix(ui): disable save button until the upload file is uploaded / ready (
Browse files Browse the repository at this point in the history
#10083)

### What?

Previously, while uploading a file - if the uploading process took a bit
of time, users could still save the document prior to the upload fully
completing.

### Why?

During the uploading process - the save button should be disabled until
the upload is complete to prevent premature saving of an upload
document.

### How?

Now, we keep track of the state of the upload in a provider and disable
the save button until the file is fully uploaded.
  • Loading branch information
PatrikKozak authored Jan 2, 2025
1 parent b0ebd92 commit bd7f705
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 11 deletions.
18 changes: 15 additions & 3 deletions packages/ui/src/elements/PublishButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
setHasPublishedDoc,
setUnpublishedVersionCount,
unpublishedVersionCount,
uploadStatus,
} = useDocumentInfo()

const { config } = useConfig()
Expand Down Expand Up @@ -62,7 +63,10 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
entityConfig?.versions?.drafts.schedulePublish

const hasNewerVersions = unpublishedVersionCount > 0
const canPublish = hasPublishPermission && (modified || hasNewerVersions || !hasPublishedDoc)
const canPublish =
hasPublishPermission &&
(modified || hasNewerVersions || !hasPublishedDoc) &&
uploadStatus !== 'uploading'
const operation = useOperation()

const forceDisable = operation === 'update' && !modified
Expand Down Expand Up @@ -107,6 +111,10 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }
})

const publish = useCallback(() => {
if (uploadStatus === 'uploading') {
return
}

void submit({
overrides: {
_status: 'published',
Expand All @@ -115,10 +123,14 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }

setUnpublishedVersionCount(0)
setHasPublishedDoc(true)
}, [setHasPublishedDoc, submit, setUnpublishedVersionCount])
}, [setHasPublishedDoc, submit, setUnpublishedVersionCount, uploadStatus])

const publishSpecificLocale = useCallback(
(locale) => {
if (uploadStatus === 'uploading') {
return
}

const params = qs.stringify({
publishSpecificLocale: locale,
})
Expand All @@ -136,7 +148,7 @@ export const PublishButton: React.FC<{ label?: string }> = ({ label: labelProp }

setHasPublishedDoc(true)
},
[api, collectionSlug, globalSlug, id, serverURL, setHasPublishedDoc, submit],
[api, collectionSlug, globalSlug, id, serverURL, setHasPublishedDoc, submit, uploadStatus],
)

if (!hasPublishPermission) {
Expand Down
16 changes: 12 additions & 4 deletions packages/ui/src/elements/SaveButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import React, { useRef } from 'react'
import { useForm, useFormModified } from '../../forms/Form/context.js'
import { FormSubmit } from '../../forms/Submit/index.js'
import { useHotkey } from '../../hooks/useHotkey.js'
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
import { useEditDepth } from '../../providers/EditDepth/index.js'
import { useOperation } from '../../providers/Operation/index.js'
import { useTranslation } from '../../providers/Translation/index.js'

export const SaveButton: React.FC<{ label?: string }> = ({ label: labelProp }) => {
const { uploadStatus } = useDocumentInfo()
const { t } = useTranslation()
const { submit } = useForm()
const modified = useFormModified()
Expand All @@ -18,7 +20,7 @@ export const SaveButton: React.FC<{ label?: string }> = ({ label: labelProp }) =
const editDepth = useEditDepth()
const operation = useOperation()

const forceDisable = operation === 'update' && !modified
const forceDisable = (operation === 'update' && !modified) || uploadStatus === 'uploading'

useHotkey({ cmdCtrlKey: true, editDepth, keyCodes: ['s'] }, (e) => {
if (forceDisable) {
Expand All @@ -32,13 +34,19 @@ export const SaveButton: React.FC<{ label?: string }> = ({ label: labelProp }) =
}
})

const handleSubmit = () => {
if (uploadStatus === 'uploading') {
return
}

return void submit()
}

return (
<FormSubmit
buttonId="action-save"
disabled={forceDisable}
onClick={() => {
return void submit()
}}
onClick={handleSubmit}
ref={ref}
size="medium"
type="button"
Expand Down
5 changes: 3 additions & 2 deletions packages/ui/src/elements/SaveDraftButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ export const SaveDraftButton: React.FC = () => {
serverURL,
},
} = useConfig()
const { id, collectionSlug, globalSlug, setUnpublishedVersionCount } = useDocumentInfo()
const { id, collectionSlug, globalSlug, setUnpublishedVersionCount, uploadStatus } =
useDocumentInfo()
const modified = useFormModified()
const { code: locale } = useLocale()
const ref = useRef<HTMLButtonElement>(null)
Expand All @@ -30,7 +31,7 @@ export const SaveDraftButton: React.FC = () => {
const { submit } = useForm()
const operation = useOperation()

const forceDisable = operation === 'update' && !modified
const forceDisable = (operation === 'update' && !modified) || uploadStatus === 'uploading'

const saveDraft = useCallback(async () => {
if (forceDisable) {
Expand Down
7 changes: 5 additions & 2 deletions packages/ui/src/elements/Upload/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import { Dropzone } from '../Dropzone/index.js'
import { EditUpload } from '../EditUpload/index.js'
import { FileDetails } from '../FileDetails/index.js'
import { PreviewSizes } from '../PreviewSizes/index.js'
import { Thumbnail } from '../Thumbnail/index.js'
import './index.scss'
import { Thumbnail } from '../Thumbnail/index.js'

const baseClass = 'file-field'
export const editDrawerSlug = 'edit-upload'
Expand Down Expand Up @@ -94,7 +94,7 @@ export const Upload: React.FC<UploadProps> = (props) => {
const { t } = useTranslation()
const { setModified } = useForm()
const { resetUploadEdits, updateUploadEdits, uploadEdits } = useUploadEdits()
const { docPermissions, savedDocumentData } = useDocumentInfo()
const { docPermissions, savedDocumentData, setUploadStatus } = useDocumentInfo()
const isFormSubmitting = useFormProcessing()
const { errorMessage, setValue, showError, value } = useField<File>({
path: 'file',
Expand Down Expand Up @@ -174,6 +174,7 @@ export const Upload: React.FC<UploadProps> = (props) => {

const handleUrlSubmit = async () => {
if (fileUrl) {
setUploadStatus('uploading')
try {
const response = await fetch(fileUrl)
const data = await response.blob()
Expand All @@ -184,8 +185,10 @@ export const Upload: React.FC<UploadProps> = (props) => {
// Create a new File object from the Blob data
const file = new File([data], fileName, { type: data.type })
handleFileChange(file)
setUploadStatus('idle')
} catch (e) {
toast.error(e.message)
setUploadStatus('failed')
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions packages/ui/src/providers/DocumentInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ const DocumentInfo: React.FC<
const [currentEditor, setCurrentEditor] = useState<ClientUser | null>(currentEditorFromProps)
const [lastUpdateTime, setLastUpdateTime] = useState<number>(lastUpdateTimeFromProps)
const [savedDocumentData, setSavedDocumentData] = useState(initialData)
const [uploadStatus, setUploadStatus] = useState<'failed' | 'idle' | 'uploading'>('idle')

const updateUploadStatus = useCallback((status: 'failed' | 'idle' | 'uploading') => {
setUploadStatus(status)
}, [])

const isInitializing = initialState === undefined || initialData === undefined

Expand Down Expand Up @@ -330,11 +335,13 @@ const DocumentInfo: React.FC<
setLastUpdateTime,
setMostRecentVersionIsAutosaved,
setUnpublishedVersionCount,
setUploadStatus: updateUploadStatus,
title: documentTitle,
unlockDocument,
unpublishedVersionCount,
updateDocumentEditor,
updateSavedDocumentData,
uploadStatus,
versionCount,
}

Expand Down
3 changes: 3 additions & 0 deletions packages/ui/src/providers/DocumentInfo/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,12 @@ export type DocumentInfoContext = {
setLastUpdateTime: React.Dispatch<React.SetStateAction<number>>
setMostRecentVersionIsAutosaved: React.Dispatch<React.SetStateAction<boolean>>
setUnpublishedVersionCount: React.Dispatch<React.SetStateAction<number>>
setUploadStatus?: (status: 'failed' | 'idle' | 'uploading') => void
title: string
unlockDocument: (docID: number | string, slug: string) => Promise<void>
unpublishedVersionCount: number
updateDocumentEditor: (docID: number | string, slug: string, user: ClientUser) => Promise<void>
updateSavedDocumentData: (data: Data) => void
uploadStatus?: 'failed' | 'idle' | 'uploading'
versionCount: number
} & DocumentInfoProps
32 changes: 32 additions & 0 deletions test/fields/collections/Upload/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,38 @@ describe('Upload', () => {
)
})

test('should disable save button during upload progress from remote URL', async () => {
await page.goto(url.create)

const pasteURLButton = page.locator('.file-field__upload button', {
hasText: 'Paste URL',
})
await pasteURLButton.click()

const remoteImage = 'https://payloadcms.com/images/og-image.jpg'

const inputField = page.locator('.file-field__upload .file-field__remote-file')
await inputField.fill(remoteImage)

// Intercept the upload request
await page.route(
'https://payloadcms.com/images/og-image.jpg',
(route) => setTimeout(() => route.continue(), 2000), // Artificial 2-second delay
)

const addFileButton = page.locator('.file-field__add-file')
await addFileButton.click()

const submitButton = page.locator('.form-submit .btn')
await expect(submitButton).toBeDisabled()

// Wait for the upload to complete
await page.waitForResponse('https://payloadcms.com/images/og-image.jpg')

// Assert the submit button is re-enabled after upload
await expect(submitButton).toBeEnabled()
})

// test that the image renders
test('should render uploaded image', async () => {
await uploadImage()
Expand Down

0 comments on commit bd7f705

Please sign in to comment.