Skip to content

Commit

Permalink
fix(ui): perf improvements in bulk upload (#8944)
Browse files Browse the repository at this point in the history
  • Loading branch information
paulpopus authored Oct 30, 2024
1 parent d64946c commit 03331de
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 3 deletions.
5 changes: 2 additions & 3 deletions packages/ui/src/elements/BulkUpload/FileSidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function FileSidebar() {
isInitializing,
removeFile,
setActiveIndex,
thumbnailUrls,
totalErrorCount,
} = useFormsManager()
const { initialFiles, maxFiles } = useBulkUpload()
Expand Down Expand Up @@ -156,9 +157,7 @@ export function FileSidebar() {
>
<Thumbnail
className={`${baseClass}__thumbnail`}
fileSrc={
isImage(currentFile.type) ? URL.createObjectURL(currentFile) : undefined
}
fileSrc={isImage(currentFile.type) ? thumbnailUrls[index] : undefined}
/>
<div className={`${baseClass}__fileDetails`}>
<p className={`${baseClass}__fileName`} title={currentFile.name}>
Expand Down
38 changes: 38 additions & 0 deletions packages/ui/src/elements/BulkUpload/FormsManager/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useTranslation } from '../../../providers/Translation/index.js'
import { getFormState } from '../../../utilities/getFormState.js'
import { hasSavePermission as getHasSavePermission } from '../../../utilities/hasSavePermission.js'
import { useLoadingOverlay } from '../../LoadingOverlay/index.js'
import { createThumbnail } from '../../Thumbnail/createThumbnail.js'
import { useBulkUpload } from '../index.js'
import { createFormData } from './createFormData.js'
import { formsManagementReducer } from './reducer.js'
Expand All @@ -41,6 +42,7 @@ type FormsManagerContext = {
errorCount: number
index: number
}) => void
readonly thumbnailUrls: string[]
readonly totalErrorCount?: number
}

Expand All @@ -59,6 +61,7 @@ const Context = React.createContext<FormsManagerContext>({
saveAllDocs: () => Promise.resolve(),
setActiveIndex: () => 0,
setFormTotalErrorCount: () => {},
thumbnailUrls: [],
totalErrorCount: 0,
})

Expand Down Expand Up @@ -90,6 +93,40 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
const [state, dispatch] = React.useReducer(formsManagementReducer, initialState)
const { activeIndex, forms, totalErrorCount } = state

const formsRef = React.useRef(forms)
formsRef.current = forms
const formsCount = forms.length

const thumbnailUrlsRef = React.useRef<string[]>([])
const processedFiles = React.useRef(new Set()) // Track already-processed files
const [renderedThumbnails, setRenderedThumbnails] = React.useState<string[]>([])

React.useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
;(async () => {
const newThumbnails = [...thumbnailUrlsRef.current]

for (let i = 0; i < formsCount; i++) {
const file = formsRef.current[i].formState.file.value as File

// Skip if already processed
if (processedFiles.current.has(file) || !file) {
continue
}
processedFiles.current.add(file)

// Generate thumbnail and update ref
const thumbnailUrl = await createThumbnail(file)
newThumbnails[i] = thumbnailUrl
thumbnailUrlsRef.current = newThumbnails

// Trigger re-render in batches
setRenderedThumbnails([...newThumbnails])
await new Promise((resolve) => setTimeout(resolve, 100))
}
})()
}, [formsCount, createThumbnail])

const { toggleLoadingOverlay } = useLoadingOverlay()
const { closeModal } = useModal()
const { collectionSlug, drawerSlug, initialFiles, onSuccess } = useBulkUpload()
Expand Down Expand Up @@ -378,6 +415,7 @@ export function FormsManagerProvider({ children }: FormsManagerProps) {
saveAllDocs,
setActiveIndex,
setFormTotalErrorCount,
thumbnailUrls: renderedThumbnails,
totalErrorCount,
}}
>
Expand Down
52 changes: 52 additions & 0 deletions packages/ui/src/elements/Thumbnail/createThumbnail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Create a thumbnail from a File object by drawing it onto an OffscreenCanvas
*/
export const createThumbnail = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const img = new Image()
img.src = URL.createObjectURL(file) // Use Object URL directly

img.onload = () => {
const maxDimension = 280
let drawHeight: number, drawWidth: number

// Calculate aspect ratio
const aspectRatio = img.width / img.height

// Determine dimensions to fit within maxDimension while maintaining aspect ratio
if (aspectRatio > 1) {
// Image is wider than tall
drawWidth = maxDimension
drawHeight = maxDimension / aspectRatio
} else {
// Image is taller than wide, or square
drawWidth = maxDimension * aspectRatio
drawHeight = maxDimension
}

const canvas = new OffscreenCanvas(drawWidth, drawHeight) // Create an OffscreenCanvas
const ctx = canvas.getContext('2d')

// Draw the image onto the OffscreenCanvas with calculated dimensions
ctx.drawImage(img, 0, 0, drawWidth, drawHeight)

// Convert the OffscreenCanvas to a Blob and free up memory
canvas
.convertToBlob({ type: 'image/jpeg', quality: 0.25 })
.then((blob) => {
URL.revokeObjectURL(img.src) // Release the Object URL
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string) // Resolve as data URL
reader.onerror = reject
reader.readAsDataURL(blob)
})
.catch(reject)
}

img.onerror = (error) => {
URL.revokeObjectURL(img.src) // Release Object URL on error
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
reject(error)
}
})
}
3 changes: 3 additions & 0 deletions packages/ui/src/elements/Thumbnail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const baseClass = 'thumbnail'
import type { SanitizedCollectionConfig } from 'payload'

import { File } from '../../graphics/File/index.js'
import { useIntersect } from '../../hooks/useIntersect.js'
import { ShimmerEffect } from '../ShimmerEffect/index.js'

export type ThumbnailProps = {
Expand All @@ -28,6 +29,7 @@ export const Thumbnail: React.FC<ThumbnailProps> = (props) => {

React.useEffect(() => {
if (!fileSrc) {
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
setFileExists(false)
return
}
Expand Down Expand Up @@ -72,6 +74,7 @@ export function ThumbnailComponent(props: ThumbnailComponentProps) {

React.useEffect(() => {
if (!fileSrc) {
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
setFileExists(false)
return
}
Expand Down

0 comments on commit 03331de

Please sign in to comment.