@salesforce/webapp-template-feature-react-file-upload-experimental 1.60.1 → 1.60.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -11
- package/dist/CHANGELOG.md +8 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/package-lock.json +15 -15
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/api/fileUpload.ts +4 -12
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUpload.tsx +20 -3
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadDialog.tsx +2 -2
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadDropZone.tsx +48 -40
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadFileItem.tsx +1 -1
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadIcons.tsx +2 -2
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/hooks/useFileUpload.ts +36 -23
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/index.ts +18 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/pages/Home.tsx +10 -23
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/pages/UploadTest.tsx +56 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/routes.tsx +8 -3
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/types/fileUpload.ts +2 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/utils/fileUploadUtils.ts +12 -2
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/utils/labels.ts +2 -0
- package/dist/package.json +1 -1
- package/package.json +3 -3
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/api/fileUpload.ts +4 -12
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUpload.tsx +20 -3
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadDialog.tsx +2 -2
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadDropZone.tsx +48 -40
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadFileItem.tsx +1 -1
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadIcons.tsx +2 -2
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/__inherit__button.tsx +45 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/__inherit__dialog.tsx +102 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/hooks/useFileUpload.ts +36 -23
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/index.ts +18 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/pages/UploadTest.tsx +56 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/routes.tsx +8 -3
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/types/fileUpload.ts +2 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/utils/fileUploadUtils.ts +12 -2
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/utils/labels.ts +2 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/pages/Home.tsx +0 -25
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as React from "react";
|
|
15
15
|
import { flushSync } from "react-dom";
|
|
16
|
-
import {
|
|
17
|
-
getUploadConfig,
|
|
18
|
-
uploadToUrl,
|
|
19
|
-
createContentVersion,
|
|
20
|
-
getCurrentUserId,
|
|
21
|
-
} from "../api/fileUpload";
|
|
16
|
+
import { getUploadConfig, uploadToUrl, createContentVersion } from "../api/fileUpload";
|
|
22
17
|
import type { FileUploadItem, UploadedFile, UploadState } from "../types/fileUpload";
|
|
23
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
isFileTooLarge,
|
|
20
|
+
MAX_FILE_SIZE_BYTES,
|
|
21
|
+
bytesFromMB,
|
|
22
|
+
formatFileSize,
|
|
23
|
+
} from "../utils/fileUploadUtils";
|
|
24
24
|
import { LABELS } from "../utils/labels";
|
|
25
25
|
|
|
26
26
|
export interface UseFileUploadOptions {
|
|
@@ -28,12 +28,14 @@ export interface UseFileUploadOptions {
|
|
|
28
28
|
accept?: string;
|
|
29
29
|
/** Whether to allow multiple file selection. Default: false */
|
|
30
30
|
multiple?: boolean;
|
|
31
|
-
/** Record Id for FirstPublishLocationId (e.g. Account, Opportunity). When provided, files are linked to this record.
|
|
31
|
+
/** Record Id for FirstPublishLocationId (e.g. Account, Opportunity). When provided, files are linked to this record and ContentVersion is created. When null/undefined, only uploads file and returns contentBodyId without creating ContentVersion. */
|
|
32
32
|
recordId?: string;
|
|
33
|
-
/** Called when uploads complete. Receives array of successfully uploaded files with name, size, and contentVersionId. */
|
|
33
|
+
/** Called when uploads complete. Receives array of successfully uploaded files with name, size, contentBodyId, and contentVersionId (if ContentVersion was created). */
|
|
34
34
|
onUploadComplete?: (files: UploadedFile[]) => void;
|
|
35
35
|
/** Called when an upload fails. Receives the file and error message. */
|
|
36
36
|
onUploadError?: (file: File, error: string) => void;
|
|
37
|
+
/** Maximum file size in MB. Files exceeding this limit are rejected with an error. Omit for default (2 GB). */
|
|
38
|
+
maxFileSize?: number;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
function updateItem(
|
|
@@ -69,7 +71,16 @@ interface UseFileUploadReturn {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
export function useFileUpload(options: UseFileUploadOptions = {}): UseFileUploadReturn {
|
|
72
|
-
const {
|
|
74
|
+
const {
|
|
75
|
+
accept,
|
|
76
|
+
multiple = false,
|
|
77
|
+
recordId,
|
|
78
|
+
onUploadComplete,
|
|
79
|
+
onUploadError,
|
|
80
|
+
maxFileSize,
|
|
81
|
+
} = options;
|
|
82
|
+
|
|
83
|
+
const maxBytes = maxFileSize != null ? bytesFromMB(maxFileSize) : MAX_FILE_SIZE_BYTES;
|
|
73
84
|
|
|
74
85
|
const [fileItems, setFileItems] = React.useState<FileUploadItem[]>([]);
|
|
75
86
|
const [isDragging, setIsDragging] = React.useState(false);
|
|
@@ -96,11 +107,11 @@ export function useFileUpload(options: UseFileUploadOptions = {}): UseFileUpload
|
|
|
96
107
|
const files = e.target.files ? Array.from(e.target.files) : [];
|
|
97
108
|
if (files.length === 0) return;
|
|
98
109
|
|
|
99
|
-
const maxSizeLabel = formatFileSize(
|
|
110
|
+
const maxSizeLabel = formatFileSize(maxBytes);
|
|
100
111
|
const errorMessage = LABELS.fileTooLarge(maxSizeLabel);
|
|
101
112
|
|
|
102
113
|
const items: FileUploadItem[] = files.map((file) => {
|
|
103
|
-
if (isFileTooLarge(file)) {
|
|
114
|
+
if (isFileTooLarge(file, maxBytes)) {
|
|
104
115
|
onUploadError?.(file, errorMessage);
|
|
105
116
|
return {
|
|
106
117
|
file,
|
|
@@ -171,23 +182,24 @@ export function useFileUpload(options: UseFileUploadOptions = {}): UseFileUpload
|
|
|
171
182
|
continue;
|
|
172
183
|
}
|
|
173
184
|
|
|
174
|
-
if
|
|
175
|
-
|
|
185
|
+
// Only create ContentVersion if recordId is provided
|
|
186
|
+
let contentVersionId: string | undefined;
|
|
187
|
+
if (recordId) {
|
|
188
|
+
if (!publishLocationId) {
|
|
189
|
+
publishLocationId = recordId;
|
|
190
|
+
}
|
|
191
|
+
setFileItems((prev) =>
|
|
192
|
+
updateItem(prev, file.name, { state: "creating_record", progress: 100 }),
|
|
193
|
+
);
|
|
194
|
+
contentVersionId = await createContentVersion(file, contentBodyId, publishLocationId);
|
|
176
195
|
}
|
|
177
|
-
setFileItems((prev) =>
|
|
178
|
-
updateItem(prev, file.name, { state: "creating_record", progress: 100 }),
|
|
179
|
-
);
|
|
180
|
-
const contentVersionId = await createContentVersion(
|
|
181
|
-
file,
|
|
182
|
-
contentBodyId,
|
|
183
|
-
publishLocationId,
|
|
184
|
-
);
|
|
185
196
|
|
|
186
197
|
flushSync(() => {
|
|
187
198
|
setFileItems((prev) =>
|
|
188
199
|
updateItem(prev, file.name, {
|
|
189
200
|
state: "success",
|
|
190
201
|
progress: 100,
|
|
202
|
+
contentBodyId,
|
|
191
203
|
contentVersionId,
|
|
192
204
|
}),
|
|
193
205
|
);
|
|
@@ -195,6 +207,7 @@ export function useFileUpload(options: UseFileUploadOptions = {}): UseFileUpload
|
|
|
195
207
|
uploadedFiles.push({
|
|
196
208
|
name: file.name,
|
|
197
209
|
size: file.size,
|
|
210
|
+
contentBodyId,
|
|
198
211
|
contentVersionId,
|
|
199
212
|
});
|
|
200
213
|
} catch (err) {
|
|
@@ -222,7 +235,7 @@ export function useFileUpload(options: UseFileUploadOptions = {}): UseFileUpload
|
|
|
222
235
|
|
|
223
236
|
e.target.value = "";
|
|
224
237
|
},
|
|
225
|
-
[recordId, onUploadComplete, onUploadError],
|
|
238
|
+
[recordId, onUploadComplete, onUploadError, maxBytes],
|
|
226
239
|
);
|
|
227
240
|
|
|
228
241
|
const handleDragOver = React.useCallback((e: React.DragEvent) => {
|
|
@@ -54,3 +54,21 @@ export { useFileUpload } from "./hooks/useFileUpload";
|
|
|
54
54
|
* @see useFileUpload
|
|
55
55
|
*/
|
|
56
56
|
export type { UseFileUploadOptions } from "./hooks/useFileUpload";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create ContentVersion record programmatically. Useful when you need to create
|
|
60
|
+
* ContentVersion separately from the file upload process (e.g., after uploading
|
|
61
|
+
* file to a different location first).
|
|
62
|
+
*
|
|
63
|
+
* @param file - The file to create ContentVersion for
|
|
64
|
+
* @param contentBodyId - The ContentBody ID from a previous upload
|
|
65
|
+
* @param recordId - The record ID for FirstPublishLocationId (e.g., Account, Opportunity, or User ID)
|
|
66
|
+
* @returns ContentVersion ID if successful
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* const contentBodyId = await uploadToUrl(file, token, uploadUrl, onProgress);
|
|
71
|
+
* const contentVersionId = await createContentVersion(file, contentBodyId, recordId);
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export { createContentVersion, getCurrentUserId } from "./api/fileUpload";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { FileUpload } from "../components/FileUpload";
|
|
2
|
+
import { createContentVersion, getCurrentUserId } from "../api/fileUpload";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Upload test page for testing the file-upload feature standalone.
|
|
6
|
+
* Renders FileUpload for dialog-based file upload with progress.
|
|
7
|
+
* Demonstrates manual ContentVersion creation after file upload.
|
|
8
|
+
*/
|
|
9
|
+
export default function UploadTest() {
|
|
10
|
+
const handleUploadComplete = async (
|
|
11
|
+
files: { name: string; size: number; contentBodyId: string; contentVersionId?: string }[],
|
|
12
|
+
) => {
|
|
13
|
+
// eslint-disable-next-line no-console
|
|
14
|
+
console.log("Uploaded files:", JSON.stringify(files, null, 2));
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const userId = await getCurrentUserId();
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
19
|
+
console.log("Current user ID:", userId);
|
|
20
|
+
|
|
21
|
+
for (const uploadedFile of files) {
|
|
22
|
+
// Create a File object from the uploaded file info (for API signature)
|
|
23
|
+
const file = new File([""], uploadedFile.name);
|
|
24
|
+
const contentVersionId = await createContentVersion(
|
|
25
|
+
file,
|
|
26
|
+
uploadedFile.contentBodyId,
|
|
27
|
+
userId,
|
|
28
|
+
);
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.log(`Created ContentVersion for ${uploadedFile.name}:`, contentVersionId);
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.error("Error creating ContentVersion:", error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12 space-y-16">
|
|
40
|
+
<section>
|
|
41
|
+
<h2 className="text-2xl font-bold text-gray-900 mb-2">File Upload (Dialog + Progress)</h2>
|
|
42
|
+
<p className="text-gray-600 mb-8">
|
|
43
|
+
Choose files to open a dialog showing upload status with progress bar for each file.
|
|
44
|
+
ContentVersion is created manually after upload using current user ID.
|
|
45
|
+
</p>
|
|
46
|
+
<FileUpload
|
|
47
|
+
multiple
|
|
48
|
+
accept="image/jpeg,.jpg,.jpeg,application/pdf,.pdf"
|
|
49
|
+
maxFileSize={50}
|
|
50
|
+
formatHint="JPEG and PDF formats, up to 50MB"
|
|
51
|
+
onUploadComplete={handleUploadComplete}
|
|
52
|
+
/>
|
|
53
|
+
</section>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { RouteObject } from "react-router";
|
|
2
|
+
import { Navigate } from "react-router";
|
|
2
3
|
import AppLayout from "./appLayout";
|
|
3
|
-
import
|
|
4
|
+
import UploadTest from "./pages/UploadTest";
|
|
4
5
|
|
|
5
6
|
export const routes: RouteObject[] = [
|
|
6
7
|
{
|
|
@@ -9,8 +10,12 @@ export const routes: RouteObject[] = [
|
|
|
9
10
|
children: [
|
|
10
11
|
{
|
|
11
12
|
index: true,
|
|
12
|
-
element: <
|
|
13
|
-
|
|
13
|
+
element: <Navigate to="/upload-test" replace />,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
path: "upload-test",
|
|
17
|
+
element: <UploadTest />,
|
|
18
|
+
handle: { showInNavigation: true, label: "Upload Test" },
|
|
14
19
|
},
|
|
15
20
|
],
|
|
16
21
|
},
|
package/src/force-app/main/default/webapplications/feature-react-file-upload/src/types/fileUpload.ts
CHANGED
|
@@ -16,11 +16,13 @@ export interface FileUploadItem {
|
|
|
16
16
|
state: UploadState;
|
|
17
17
|
progress: number;
|
|
18
18
|
error?: string;
|
|
19
|
+
contentBodyId?: string;
|
|
19
20
|
contentVersionId?: string;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export interface UploadedFile {
|
|
23
24
|
name: string;
|
|
24
25
|
size: number;
|
|
26
|
+
contentBodyId: string;
|
|
25
27
|
contentVersionId?: string;
|
|
26
28
|
}
|
|
@@ -7,8 +7,18 @@ import type { UploadState } from "../types/fileUpload";
|
|
|
7
7
|
/** Maximum allowed file size: 2 GB */
|
|
8
8
|
export const MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024 * 1024;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/** Convert megabytes to bytes */
|
|
11
|
+
export function bytesFromMB(mb: number): number {
|
|
12
|
+
return mb * 1024 * 1024;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if file exceeds size limit.
|
|
17
|
+
* @param file - File to check
|
|
18
|
+
* @param maxBytes - Max size in bytes. Defaults to MAX_FILE_SIZE_BYTES when omitted.
|
|
19
|
+
*/
|
|
20
|
+
export function isFileTooLarge(file: File, maxBytes: number = MAX_FILE_SIZE_BYTES): boolean {
|
|
21
|
+
return file.size > maxBytes;
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
const UPLOADING_STATES: UploadState[] = ["loading_config", "uploading", "creating_record"];
|
package/src/force-app/main/default/webapplications/feature-react-file-upload/src/utils/labels.ts
CHANGED
|
@@ -4,6 +4,8 @@ export const LABELS = {
|
|
|
4
4
|
uploadFiles: "Upload Files",
|
|
5
5
|
dropFilesHere: "Drop files here",
|
|
6
6
|
orDropFiles: "Or drop files",
|
|
7
|
+
chooseFileOrDrop: "Choose a file or drag & drop for reference here.",
|
|
8
|
+
formatHint: "JPEG, PNG, PDF, and MP4 formats, up to 50MB",
|
|
7
9
|
uploadFilesDialogTitle: "Upload Files",
|
|
8
10
|
done: "Done",
|
|
9
11
|
cancelled: "Cancelled",
|
package/src/force-app/main/default/webapplications/feature-react-file-upload/src/pages/Home.tsx
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { FileUpload } from "../components/FileUpload";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Home page for testing the file-upload feature standalone.
|
|
5
|
-
* Renders FileUpload for dialog-based file upload with progress.
|
|
6
|
-
*/
|
|
7
|
-
export default function Home() {
|
|
8
|
-
return (
|
|
9
|
-
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12 space-y-16">
|
|
10
|
-
<section>
|
|
11
|
-
<h2 className="text-2xl font-bold text-gray-900 mb-2">File Upload (Dialog + Progress)</h2>
|
|
12
|
-
<p className="text-gray-600 mb-8">
|
|
13
|
-
Choose files to open a dialog showing upload status with progress bar for each file.
|
|
14
|
-
</p>
|
|
15
|
-
<FileUpload
|
|
16
|
-
multiple
|
|
17
|
-
onUploadComplete={(files) => {
|
|
18
|
-
// eslint-disable-next-line no-console
|
|
19
|
-
console.log("Uploaded files:", files);
|
|
20
|
-
}}
|
|
21
|
-
/>
|
|
22
|
-
</section>
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
25
|
-
}
|