implementing-ui-bundle-file-upload

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

File Upload API (workflow)

文件上传API(工作流程)

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.
当用户需要在React UI bundle中实现文件上传功能时,请遵循以下工作流程。此功能仅提供API——你必须使用提供的API自行构建UI组件。

CRITICAL: This is an API-only package

重要提示:这是一个仅提供API的包

The package exports programmatic APIs, not React components or hooks. You will:
  • Use the
    upload()
    function to handle file uploads with progress tracking
  • Build your own custom UI (file input, dropzone, progress bars, etc.)
  • Track upload progress through the
    onProgress
    callback
Do NOT:
  • Expect pre-built components like
    <FileUpload />
    — they are not exported
  • Try to import React hooks like
    useFileUpload
    — they are not exported
  • Look for dropzone components — they are not exported
The source code contains reference components for demonstration, but they are not available as imports. Use them as examples to build your own UI.
该包导出编程式API,而非React组件或钩子。你需要:
  • 使用
    upload()
    函数处理带进度跟踪的文件上传
  • 自行构建自定义UI(文件输入框、拖放区域、进度条等)
  • 通过
    onProgress
    回调跟踪上传进度
请勿:
  • 期望获取如
    <FileUpload />
    这类预构建组件——它们并未被导出
  • 尝试导入
    useFileUpload
    这类React钩子——它们并未被导出
  • 寻找拖放区域组件——它们并未被导出
源代码中包含用于演示的参考组件,但它们无法被导入。你可以将它们作为示例来构建自己的UI。

1. Install the package

1. 安装包

bash
npm install @salesforce/ui-bundle-template-feature-react-file-upload
Dependencies are automatically installed:
  • @salesforce/ui-bundle
    (API client)
  • @salesforce/sdk-data
    (data SDK)
bash
npm install @salesforce/ui-bundle-template-feature-react-file-upload
依赖会自动安装:
  • @salesforce/ui-bundle
    (API客户端)
  • @salesforce/sdk-data
    (数据SDK)

2. Understand the three upload patterns

2. 了解三种上传模式

Pattern A: Basic upload (no record linking)

模式A:基础上传(无记录关联)

Upload files to Salesforce and get back
contentBodyId
for each file. No ContentVersion record is created.
When to use:
  • User wants to upload files first, then create/link them to a record later
  • Building a multi-step form where the record doesn't exist yet
  • Deferred record linking scenarios
tsx
import { 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)
将文件上传至Salesforce,并获取每个文件的
contentBodyId
。不会创建ContentVersion记录。
适用场景:
  • 用户希望先上传文件,之后再创建/关联到记录
  • 构建多步骤表单,此时记录尚未存在
  • 延迟记录关联的场景
tsx
import { 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..."(始终可用)
// results[0].contentVersionId: undefined(未关联记录)

Pattern B: Upload with immediate record linking

模式B:即时关联记录的上传

Upload files and immediately link them to an existing Salesforce record by creating ContentVersion records.
When to use:
  • Record already exists (Account, Opportunity, Case, etc.)
  • User wants files immediately attached to the record
  • Direct upload-and-attach scenarios
tsx
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)
上传文件并立即通过创建ContentVersion记录,将其关联到现有的Salesforce记录。
适用场景:
  • 记录已存在(如客户、商机、案例等)
  • 用户希望文件立即附加到记录上
  • 直接上传并关联的场景
tsx
import { upload } from "@salesforce/ui-bundle-template-feature-react-file-upload";

const results = await upload({
  files: [file1, file2],
  recordId: "001xx000000yyyy", // 现有记录ID
  onProgress: (progress) => {
    console.log(`${progress.fileName}: ${progress.status} - ${progress.progress}%`);
  },
});

// results[0].contentBodyId: "069..."(始终可用)
// results[0].contentVersionId: "068..."(已关联到记录)

Pattern C: Deferred record linking (record creation flow)

模式C:延迟关联记录(记录创建流程)

Upload files without a record, then link them after the record is created.
When to use:
  • Building a "create record with attachments" form
  • Record doesn't exist until form submission
  • Need to upload files before knowing the final record ID
tsx
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,
  );
}
在无记录的情况下上传文件,待记录创建后再关联。
适用场景:
  • 构建“创建记录并附加文件”的表单
  • 记录在表单提交前不存在
  • 需要在知晓最终记录ID前上传文件
tsx
import {
  upload,
  createContentVersion,
} from "@salesforce/ui-bundle-template-feature-react-file-upload";

// 步骤1:上传文件(无recordId)
const uploadResults = await upload({
  files: [file1, file2],
  onProgress: (progress) => console.log(progress),
});

// 步骤2:创建记录
const newRecordId = await createRecord(formData);

// 步骤3:将上传的文件关联到新记录
for (const file of uploadResults) {
  const contentVersionId = await createContentVersion(
    new File([""], file.fileName),
    file.contentBodyId,
    newRecordId,
  );
}

3. Build your custom UI

3. 构建自定义UI

The package provides the backend — you build the frontend. Here's a minimal example:
tsx
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>
  );
}
该包提供后端支持——你需要构建前端。以下是一个最简示例:
tsx
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>
  );
}

4. Track upload progress

4. 跟踪上传进度

The
onProgress
callback fires multiple times for each file as it moves through stages:
StatusWhenProgress Value
"pending"
File queued for upload
0
"uploading"
Upload in progress (XHR)
0-100
(percentage)
"processing"
Creating ContentVersion (if recordId provided)
0
"success"
Upload complete
100
"error"
Upload failed
0
Always provide visual feedback:
  • Show file name
  • Display current status
  • Render progress bar for "uploading" status
  • Show error message if status is "error"
onProgress
回调会在每个文件进入不同阶段时多次触发:
状态触发时机进度值范围
"pending"
文件已排入上传队列
0
"uploading"
上传进行中(XHR)
0-100
(百分比)
"processing"
正在创建ContentVersion(若提供了recordId)
0
"success"
上传完成
100
"error"
上传失败
0
务必提供视觉反馈:
  • 显示文件名
  • 展示当前状态
  • 为"uploading"状态渲染进度条
  • 若状态为"error",显示错误信息

5. Cancel uploads (optional)

5. 取消上传(可选)

Use an
AbortController
to allow users to cancel uploads:
tsx
const 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();
};
使用
AbortController
允许用户取消上传:
tsx
const 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();
};

6. Link to current user (special case)

6. 关联到当前用户(特殊场景)

If the user wants to upload files to their own profile or personal library:
tsx
import {
  upload,
  getCurrentUserId,
} from "@salesforce/ui-bundle-template-feature-react-file-upload";

const userId = await getCurrentUserId();
await upload({ files, recordId: userId });
若用户希望将文件上传至自己的个人资料或私人库:
tsx
import {
  upload,
  getCurrentUserId,
} from "@salesforce/ui-bundle-template-feature-react-file-upload";

const userId = await getCurrentUserId();
await upload({ files, recordId: userId });

API Reference

API参考

upload(options)

upload(options)

Main upload API that handles complete flow with progress tracking.
typescript
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
}
Returns:
Promise<FileUploadResult[]>
处理完整上传流程并带进度跟踪的核心上传API。
typescript
interface UploadOptions {
  files: File[];
  recordId?: string | null; // 若提供,将创建ContentVersion
  onProgress?: (progress: FileUploadProgress) => void;
  signal?: AbortSignal; // 可选的取消信号
}

interface FileUploadProgress {
  fileName: string;
  status: "pending" | "uploading" | "processing" | "success" | "error";
  progress: number; // 上传中为0-100,其他状态为0
  error?: string;
}

interface FileUploadResult {
  fileName: string;
  size: number;
  contentBodyId: string; // 始终可用
  contentVersionId?: string; // 仅在提供recordId时返回
}
返回值:
Promise<FileUploadResult[]>

createContentVersion(file, contentBodyId, recordId)

createContentVersion(file, contentBodyId, recordId)

Manually create a ContentVersion record from a previously uploaded file.
typescript
async function createContentVersion(
  file: File,
  contentBodyId: string,
  recordId: string,
): Promise<string | undefined>;
Parameters:
  • file
    — File object (used for metadata like name)
  • contentBodyId
    — ContentBody ID from previous upload
  • recordId
    — Record ID for FirstPublishLocationId
Returns: ContentVersion ID if successful
从已上传的文件手动创建ContentVersion记录。
typescript
async function createContentVersion(
  file: File,
  contentBodyId: string,
  recordId: string,
): Promise<string | undefined>;
参数:
  • file
    — 文件对象(用于获取名称等元数据)
  • contentBodyId
    — 之前上传得到的ContentBody ID
  • recordId
    — FirstPublishLocationId对应的记录ID
返回值: 成功时返回ContentVersion ID

getCurrentUserId()

getCurrentUserId()

Get the current user's Salesforce ID.
typescript
async function getCurrentUserId(): Promise<string>;
Returns: Current user ID
获取当前用户的Salesforce ID。
typescript
async function getCurrentUserId(): Promise<string>;
返回值: 当前用户ID

Common UI patterns

常见UI模式

File input with button

带按钮的文件输入框

tsx
<input type="file" multiple accept=".pdf,.doc,.docx,.jpg,.png" onChange={handleFileSelect} />
tsx
<input type="file" multiple accept=".pdf,.doc,.docx,.jpg,.png" onChange={handleFileSelect} />

Drag-and-drop zone

拖放区域

Build your own dropzone using native events:
tsx
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>
  );
}
使用原生事件构建自己的拖放区域:
tsx
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" }}
    >
      拖放文件至此处
    </div>
  );
}

Progress bar

进度条

tsx
{
  progress.status === "uploading" && (
    <div style={{ width: "100%", background: "#eee" }}>
      <div
        style={{
          width: `${progress.progress}%`,
          background: "#0176d3",
          height: "8px",
        }}
      />
    </div>
  );
}
tsx
{
  progress.status === "uploading" && (
    <div style={{ width: "100%", background: "#eee" }}>
      <div
        style={{
          width: `${progress.progress}%`,
          background: "#0176d3",
          height: "8px",
        }}
      />
    </div>
  );
}

Decision tree for agents

智能体决策树

User asks for file upload functionality:
  1. Ask about record context:
    • "Do you want to link uploaded files to a specific record, or upload them first and link later?"
  2. Based on response:
    • Link to existing record → Use Pattern B with
      recordId
    • Upload first, link later → Use Pattern A (no recordId), then Pattern C for linking
    • Link to current user → Use Pattern B with
      getCurrentUserId()
  3. Build the UI:
    • Create file input or dropzone (not provided by package)
    • Add progress display for each file (status + progress bar)
    • Handle errors in the UI
  4. Test the implementation:
    • Verify progress callbacks fire correctly
    • Check that
      contentBodyId
      is returned
    • If
      recordId
      was provided, verify
      contentVersionId
      is returned
用户询问文件上传功能:
  1. 询问记录上下文:
    • "你希望将上传的文件关联到特定记录,还是先上传之后再关联?"
  2. 根据回复选择方案:
    • 关联到现有记录 → 使用带
      recordId
      的模式B
    • 先上传,后关联 → 使用无recordId的模式A,之后使用模式C进行关联
    • 关联到当前用户 → 使用带
      getCurrentUserId()
      的模式B
  3. 构建UI:
    • 创建文件输入框或拖放区域(包未提供)
    • 为每个文件添加进度展示(状态+进度条)
    • 在UI中处理错误
  4. 测试实现:
    • 验证进度回调是否正确触发
    • 检查是否返回
      contentBodyId
    • 若提供了
      recordId
      ,验证是否返回
      contentVersionId

Reference implementation

参考实现

The package includes a reference implementation in
src/features/fileupload/
with:
  • FileUpload.tsx
    — Complete component with dropzone and dialog
  • FileUploadDialog.tsx
    — Progress tracking dialog
  • FileUploadDropZone.tsx
    — Drag-and-drop zone
  • useFileUpload.ts
    — React hook for state management
These are NOT exported but can be viewed as examples. Read the source files to understand patterns for building your own UI.
该包在
src/features/fileupload/
目录中包含参考实现,包括:
  • FileUpload.tsx
    — 完整的带拖放区域和对话框的组件
  • FileUploadDialog.tsx
    — 进度跟踪对话框
  • FileUploadDropZone.tsx
    — 拖放区域
  • useFileUpload.ts
    — 用于状态管理的React钩子
这些组件并未被导出,但可作为示例查看。阅读源文件以了解构建自定义UI的模式。

Troubleshooting

故障排除

Upload fails with CORS error:
  • Ensure the UI bundle is properly deployed to Salesforce or running on
    localhost
  • Check that the org allows the origin in CORS settings
No progress updates:
  • Verify
    onProgress
    callback is provided
  • Check that the callback function updates React state correctly
ContentVersion not created:
  • Verify
    recordId
    is provided to
    upload()
    function
  • Check that the record ID is valid and exists in the org
  • Ensure user has permissions to create ContentVersion records
Files upload but don't appear in record:
  • Verify
    recordId
    is correct
  • Check that ContentVersion was created (look for
    contentVersionId
    in results)
  • Confirm user has access to view files on the record
上传因CORS错误失败:
  • 确保UI bundle已正确部署到Salesforce或在
    localhost
    运行
  • 检查组织是否在CORS设置中允许该源
无进度更新:
  • 确认已提供
    onProgress
    回调
  • 检查回调函数是否正确更新React状态
未创建ContentVersion:
  • 确认已向
    upload()
    函数提供
    recordId
  • 检查记录ID是否有效且存在于组织中
  • 确保用户拥有创建ContentVersion记录的权限
文件已上传但未显示在记录中:
  • 确认
    recordId
    正确
  • 检查是否已创建ContentVersion(在结果中查看
    contentVersionId
  • 确认用户有权查看记录上的文件

DO NOT do these things

请勿执行以下操作

  • ❌ Build XHR/fetch upload logic from scratch — use the
    upload()
    API
  • ❌ Try to import
    <FileUpload />
    component — it's not exported
  • ❌ Try to import
    useFileUpload
    hook — it's not exported
  • ❌ Use third-party file upload libraries when this feature exists
  • ❌ Skip progress tracking — always provide user feedback
  • ❌ Ignore errors — always handle and display error messages
  • ❌ 从零开始构建XHR/fetch上传逻辑——请使用
    upload()
    API
  • ❌ 尝试导入
    <FileUpload />
    组件——它并未被导出
  • ❌ 尝试导入
    useFileUpload
    钩子——它并未被导出
  • ❌ 当此功能存在时使用第三方文件上传库
  • ❌ 跳过进度跟踪——务必向用户提供反馈
  • ❌ 忽略错误——务必处理并显示错误信息