$npx -y skills add forcedotcom/sf-skills --skill experience-ui-bundle-file-upload-generateMUST activate when the project contains a uiBundles/*/src/ directory and the task involves uploading, attaching, or dropping files. Use this skill when adding file upload functionality to a UI bundle app. Provides progress tracking and Salesforce ContentVersion integration. This
| 1 | # File Upload API (workflow) |
| 2 | |
| 3 | When the user wants file upload functionality in a React UI bundle, follow this workflow. This feature provides **APIs only** — you must build the UI components yourself using the provided APIs. |
| 4 | |
| 5 | ## CRITICAL: This is an API-only package |
| 6 | |
| 7 | The package exports **programmatic APIs**, not React components or hooks. You will: |
| 8 | |
| 9 | - Use the `upload()` function to handle file uploads with progress tracking |
| 10 | - Build your own custom UI (file input, dropzone, progress bars, etc.) |
| 11 | - Track upload progress through the `onProgress` callback |
| 12 | |
| 13 | **Do NOT:** |
| 14 | |
| 15 | - Expect pre-built components like `<FileUpload />` — they are not exported |
| 16 | - Try to import React hooks like `useFileUpload` — they are not exported |
| 17 | - Look for dropzone components — they are not exported |
| 18 | |
| 19 | The source code contains reference components for demonstration, but they are **not available** as imports. Use them as examples to build your own UI. |
| 20 | |
| 21 | ## 1. Install the package |
| 22 | |
| 23 | ```bash |
| 24 | npm install @salesforce/ui-bundle-template-feature-react-file-upload |
| 25 | ``` |
| 26 | |
| 27 | Dependencies are automatically installed: |
| 28 | |
| 29 | - `@salesforce/ui-bundle` (API client) |
| 30 | - `@salesforce/platform-sdk` (data SDK; the old `@salesforce/sdk-data` name is dead — see the `experience-ui-bundle-salesforce-data-access` skill) |
| 31 | |
| 32 | ## 2. Understand the three upload patterns |
| 33 | |
| 34 | ### Pattern A: Basic upload (no record linking) |
| 35 | |
| 36 | Upload files to Salesforce and get back `contentBodyId` for each file. No ContentVersion record is created. |
| 37 | |
| 38 | **When to use:** |
| 39 | |
| 40 | - User wants to upload files first, then create/link them to a record later |
| 41 | - Building a multi-step form where the record doesn't exist yet |
| 42 | - Deferred record linking scenarios |
| 43 | |
| 44 | ```tsx |
| 45 | import { upload } from "@salesforce/ui-bundle-template-feature-react-file-upload"; |
| 46 | |
| 47 | const results = await upload({ |
| 48 | files: [file1, file2], |
| 49 | onProgress: (progress) => { |
| 50 | console.log(`${progress.fileName}: ${progress.status} - ${progress.progress}%`); |
| 51 | }, |
| 52 | }); |
| 53 | |
| 54 | // results[0].contentBodyId: "069..." (always available) |
| 55 | // results[0].contentVersionId: undefined (no record linked) |
| 56 | ``` |
| 57 | |
| 58 | ### Pattern B: Upload with immediate record linking |
| 59 | |
| 60 | Upload files and immediately link them to an existing Salesforce record by creating ContentVersion records. |
| 61 | |
| 62 | **When to use:** |
| 63 | |
| 64 | - Record already exists (Account, Opportunity, Case, etc.) |
| 65 | - User wants files immediately attached to the record |
| 66 | - Direct upload-and-attach scenarios |
| 67 | |
| 68 | ```tsx |
| 69 | import { upload } from "@salesforce/ui-bundle-template-feature-react-file-upload"; |
| 70 | |
| 71 | const results = await upload({ |
| 72 | files: [file1, file2], |
| 73 | recordId: "001xx000000yyyy", // Existing record ID |
| 74 | onProgress: (progress) => { |
| 75 | console.log(`${progress.fileName}: ${progress.status} - ${progress.progress}%`); |
| 76 | }, |
| 77 | }); |
| 78 | |
| 79 | // results[0].contentBodyId: "069..." (always available) |
| 80 | // results[0].contentVersionId: "068..." (linked to record) |
| 81 | ``` |
| 82 | |
| 83 | ### Pattern C: Deferred record linking (record creation flow) |
| 84 | |
| 85 | Upload files without a record, then link them after the record is created. |
| 86 | |
| 87 | **When to use:** |
| 88 | |
| 89 | - Building a "create record with attachments" form |
| 90 | - Record doesn't exist until form submission |
| 91 | - Need to upload files before knowing the final record ID |
| 92 | |
| 93 | ```tsx |
| 94 | import { |
| 95 | upload, |
| 96 | createContentVersion, |
| 97 | } from "@salesforce/ui-bundle-template-feature-react-file-upload"; |
| 98 | |
| 99 | // Step 1: Upload files (no recordId) |
| 100 | const uploadResults = await upload({ |
| 101 | files: [file1, file2], |
| 102 | onProgress: (progress) => console.log(progress), |
| 103 | }); |
| 104 | |
| 105 | // Step 2: Create the record |
| 106 | const newRecordId = await createRecord(formData); |
| 107 | |
| 108 | // Step 3: Link uploaded files to the new record |
| 109 | for (const file of uploadResults) { |
| 110 | const contentVersionId = await createContentVersion( |
| 111 | new File([""], file.fileName), |
| 112 | file.contentBodyId, |
| 113 | newRecordId, |
| 114 | ); |
| 115 | } |
| 116 | ``` |
| 117 | |
| 118 | ## 3. Build your custom UI |
| 119 | |
| 120 | The package provides the backend — you build the frontend. Here's a minimal example: |
| 121 | |
| 122 | ```tsx |
| 123 | import { |
| 124 | upload, |
| 125 | type FileUploadProgress, |
| 126 | } from "@salesforce/ui-bundle-template-feature-react-file-upload"; |
| 127 | import { useState } from "react"; |
| 128 | |
| 129 | function CustomFileUpload({ recordId }: { recordId?: string }) { |
| 130 | const [progress, setProgress] = useState<Map<string, FileUploadProgress>>(new Map()); |
| 131 | |
| 132 | const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => { |
| 133 | const files = Array.from(event.target.files || []); |
| 134 | |
| 135 | await upload({ |
| 136 | files, |
| 137 | recordId, |
| 138 | onProgress: (fileProgress) => { |
| 139 | setProgress((prev) => new Map(prev).set(fileProgress.fileName, file |