Loading...
Loading...
MUST 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 feature provides programmatic APIs ONLY — build custom UI using the upload() API. ALWAYS use this instead of building file upload from scratch with FormData or XHR.
npx skill4agent add forcedotcom/afv-library implementing-ui-bundle-file-uploadupload()onProgress<FileUpload />useFileUploadnpm install @salesforce/ui-bundle-template-feature-react-file-upload@salesforce/ui-bundle@salesforce/sdk-datacontentBodyIdimport { upload } from "@salesforce/ui-bundle-template-feature-react-file-upload";
const results = await upload({
files: [file1, file2],
onProgress: (progress) => {
console.log(`${progress.fileName}: ${progress.status} - ${progress.progress}%`);
},
});
// results[0].contentBodyId: "069..." (always available)
// results[0].contentVersionId: undefined (no record linked)import { upload } from "@salesforce/ui-bundle-template-feature-react-file-upload";
const results = await upload({
files: [file1, file2],
recordId: "001xx000000yyyy", // Existing record ID
onProgress: (progress) => {
console.log(`${progress.fileName}: ${progress.status} - ${progress.progress}%`);
},
});
// results[0].contentBodyId: "069..." (always available)
// results[0].contentVersionId: "068..." (linked to record)import {
upload,
createContentVersion,
} from "@salesforce/ui-bundle-template-feature-react-file-upload";
// Step 1: Upload files (no recordId)
const uploadResults = await upload({
files: [file1, file2],
onProgress: (progress) => console.log(progress),
});
// Step 2: Create the record
const newRecordId = await createRecord(formData);
// Step 3: Link uploaded files to the new record
for (const file of uploadResults) {
const contentVersionId = await createContentVersion(
new File([""], file.fileName),
file.contentBodyId,
newRecordId,
);
}import {
upload,
type FileUploadProgress,
} from "@salesforce/ui-bundle-template-feature-react-file-upload";
import { useState } from "react";
function CustomFileUpload({ recordId }: { recordId?: string }) {
const [progress, setProgress] = useState<Map<string, FileUploadProgress>>(new Map());
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []);
await upload({
files,
recordId,
onProgress: (fileProgress) => {
setProgress((prev) => new Map(prev).set(fileProgress.fileName, fileProgress));
},
});
};
return (
<div>
<input type="file" multiple onChange={handleFileSelect} />
{Array.from(progress.entries()).map(([fileName, fileProgress]) => (
<div key={fileName}>
{fileName}: {fileProgress.status} - {fileProgress.progress}%
{fileProgress.error && <span>Error: {fileProgress.error}</span>}
</div>
))}
</div>
);
}onProgress| Status | When | Progress Value |
|---|---|---|
| File queued for upload | |
| Upload in progress (XHR) | |
| Creating ContentVersion (if recordId provided) | |
| Upload complete | |
| Upload failed | |
AbortControllerconst abortController = new AbortController();
const handleUpload = async (files: File[]) => {
try {
await upload({
files,
signal: abortController.signal,
onProgress: (progress) => console.log(progress),
});
} catch (error) {
console.error("Upload cancelled or failed:", error);
}
};
const cancelUpload = () => {
abortController.abort();
};import {
upload,
getCurrentUserId,
} from "@salesforce/ui-bundle-template-feature-react-file-upload";
const userId = await getCurrentUserId();
await upload({ files, recordId: userId });interface UploadOptions {
files: File[];
recordId?: string | null; // If provided, creates ContentVersion
onProgress?: (progress: FileUploadProgress) => void;
signal?: AbortSignal; // Optional cancellation
}
interface FileUploadProgress {
fileName: string;
status: "pending" | "uploading" | "processing" | "success" | "error";
progress: number; // 0-100 for uploading, 0 for other states
error?: string;
}
interface FileUploadResult {
fileName: string;
size: number;
contentBodyId: string; // Always available
contentVersionId?: string; // Only if recordId was provided
}Promise<FileUploadResult[]>async function createContentVersion(
file: File,
contentBodyId: string,
recordId: string,
): Promise<string | undefined>;filecontentBodyIdrecordIdasync function getCurrentUserId(): Promise<string>;<input type="file" multiple accept=".pdf,.doc,.docx,.jpg,.png" onChange={handleFileSelect} />function DropZone({ onDrop }: { onDrop: (files: File[]) => void }) {
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
const files = Array.from(e.dataTransfer.files);
onDrop(files);
};
return (
<div
onDrop={handleDrop}
onDragOver={(e) => e.preventDefault()}
style={{ border: "2px dashed #ccc", padding: "2rem" }}
>
Drop files here
</div>
);
}{
progress.status === "uploading" && (
<div style={{ width: "100%", background: "#eee" }}>
<div
style={{
width: `${progress.progress}%`,
background: "#0176d3",
height: "8px",
}}
/>
</div>
);
}recordIdgetCurrentUserId()contentBodyIdrecordIdcontentVersionIdsrc/features/fileupload/FileUpload.tsxFileUploadDialog.tsxFileUploadDropZone.tsxuseFileUpload.tslocalhostonProgressrecordIdupload()recordIdcontentVersionIdupload()<FileUpload />useFileUpload