@salesforce/webapp-template-feature-react-file-upload-experimental 1.90.0 → 1.90.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.
Files changed (45) hide show
  1. package/README.md +245 -219
  2. package/dist/CHANGELOG.md +16 -0
  3. package/dist/force-app/main/default/webapplications/feature-react-file-upload/package.json +3 -3
  4. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{api → features/fileupload/api}/fileUpload.ts +153 -0
  5. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{components → features/fileupload/components}/FileUploadDialog.tsx +2 -2
  6. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{components → features/fileupload/components}/FileUploadFileItem.tsx +1 -1
  7. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/index.ts +25 -39
  8. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/routes.tsx +2 -2
  9. package/dist/package.json +1 -1
  10. package/package.json +14 -9
  11. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/appLayout.tsx +9 -0
  12. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/__inherit__button.tsx +39 -0
  13. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/__inherit__dialog.tsx +102 -0
  14. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/api/fileUpload.ts +299 -0
  15. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/assets/icon-image-close.svg +3 -0
  16. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/assets/image.svg +3 -0
  17. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/assets/success.svg +3 -0
  18. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/assets/symbols.svg +1 -0
  19. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/components/FileUpload.tsx +100 -0
  20. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/components/FileUploadDialog.tsx +79 -0
  21. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/components/FileUploadDropZone.tsx +90 -0
  22. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/components/FileUploadFileItem.tsx +99 -0
  23. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/components/FileUploadIcons.tsx +90 -0
  24. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/hooks/useFileUpload.ts +312 -0
  25. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/hooks/useFileUploadDialog.ts +70 -0
  26. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/pages/UploadTest.tsx +56 -0
  27. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/types/fileUpload.ts +28 -0
  28. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/utils/fileUploadUtils.ts +54 -0
  29. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/features/fileupload/utils/labels.ts +23 -0
  30. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/index.ts +60 -0
  31. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/routes.tsx +22 -0
  32. package/src/force-app/main/default/webapplications/feature-react-file-upload/vite.config.ts +43 -0
  33. /package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{assets → features/fileupload/assets}/icon-image-close.svg +0 -0
  34. /package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{assets → features/fileupload/assets}/image.svg +0 -0
  35. /package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{assets → features/fileupload/assets}/success.svg +0 -0
  36. /package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{assets → features/fileupload/assets}/symbols.svg +0 -0
  37. /package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{components → features/fileupload/components}/FileUpload.tsx +0 -0
  38. /package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{components → features/fileupload/components}/FileUploadDropZone.tsx +0 -0
  39. /package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{components → features/fileupload/components}/FileUploadIcons.tsx +0 -0
  40. /package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{hooks → features/fileupload/hooks}/useFileUpload.ts +0 -0
  41. /package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{hooks → features/fileupload/hooks}/useFileUploadDialog.ts +0 -0
  42. /package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{pages → features/fileupload/pages}/UploadTest.tsx +0 -0
  43. /package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{types → features/fileupload/types}/fileUpload.ts +0 -0
  44. /package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{utils → features/fileupload/utils}/fileUploadUtils.ts +0 -0
  45. /package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/{utils → features/fileupload/utils}/labels.ts +0 -0
@@ -144,3 +144,156 @@ export async function getCurrentUserId(): Promise<string> {
144
144
  }
145
145
  return user.id;
146
146
  }
147
+
148
+ /**
149
+ * Upload status for a single file
150
+ */
151
+ export type UploadStatus = "pending" | "uploading" | "processing" | "success" | "error";
152
+
153
+ /**
154
+ * Progress callback for file upload
155
+ */
156
+ export interface FileUploadProgress {
157
+ fileName: string;
158
+ status: UploadStatus;
159
+ progress: number; // 0-100 for uploading, 0 for other states
160
+ error?: string;
161
+ }
162
+
163
+ /**
164
+ * Result of a file upload
165
+ */
166
+ export interface FileUploadResult {
167
+ fileName: string;
168
+ size: number;
169
+ contentBodyId: string;
170
+ contentVersionId?: string;
171
+ }
172
+
173
+ /**
174
+ * Options for the upload API
175
+ */
176
+ export interface UploadOptions {
177
+ /** Files to upload */
178
+ files: File[];
179
+ /** Record ID to link files to (FirstPublishLocationId). If null/undefined, ContentVersion is not created. */
180
+ recordId?: string | null;
181
+ /** Callback for upload progress of each file */
182
+ onProgress?: (progress: FileUploadProgress) => void;
183
+ /** Optional abort signal to cancel all uploads */
184
+ signal?: AbortSignal;
185
+ }
186
+
187
+ /**
188
+ * Unified upload API that combines all steps:
189
+ * 1. Get upload config (once for all files)
190
+ * 2. Upload each file with progress tracking
191
+ * 3. Create ContentVersion (if recordId is provided)
192
+ *
193
+ * Returns array of upload results with file info and IDs.
194
+ * Calls onProgress for each file as it progresses through stages.
195
+ *
196
+ * @example
197
+ * ```tsx
198
+ * const results = await upload({
199
+ * files: [file1, file2],
200
+ * recordId: accountId,
201
+ * onProgress: (progress) => {
202
+ * console.log(`${progress.fileName}: ${progress.status} ${progress.progress}%`);
203
+ * }
204
+ * });
205
+ * ```
206
+ */
207
+ export async function upload(options: UploadOptions): Promise<FileUploadResult[]> {
208
+ const { files, recordId, onProgress, signal } = options;
209
+
210
+ if (!files || files.length === 0) {
211
+ return [];
212
+ }
213
+
214
+ // Get upload config once for all files
215
+ const config = await getUploadConfig();
216
+
217
+ const results: FileUploadResult[] = [];
218
+
219
+ // Upload each file sequentially (can be made parallel if needed)
220
+ for (const file of files) {
221
+ try {
222
+ // Update status: pending
223
+ onProgress?.({
224
+ fileName: file.name,
225
+ status: "pending",
226
+ progress: 0,
227
+ });
228
+
229
+ // Update status: uploading
230
+ onProgress?.({
231
+ fileName: file.name,
232
+ status: "uploading",
233
+ progress: 0,
234
+ });
235
+
236
+ // Upload file with progress tracking
237
+ const contentBodyId = await uploadToUrl(
238
+ file,
239
+ config.token,
240
+ config.uploadUrl,
241
+ (percent) => {
242
+ onProgress?.({
243
+ fileName: file.name,
244
+ status: "uploading",
245
+ progress: percent,
246
+ });
247
+ },
248
+ signal,
249
+ );
250
+
251
+ // Create ContentVersion if recordId is provided
252
+ let contentVersionId: string | undefined;
253
+ if (recordId) {
254
+ // Update status: processing (creating ContentVersion)
255
+ onProgress?.({
256
+ fileName: file.name,
257
+ status: "processing",
258
+ progress: 0,
259
+ });
260
+
261
+ contentVersionId = await createContentVersion(file, contentBodyId, recordId);
262
+ }
263
+
264
+ // Update status: success
265
+ onProgress?.({
266
+ fileName: file.name,
267
+ status: "success",
268
+ progress: 100,
269
+ });
270
+
271
+ results.push({
272
+ fileName: file.name,
273
+ size: file.size,
274
+ contentBodyId,
275
+ contentVersionId,
276
+ });
277
+ } catch (error) {
278
+ // Update status: error
279
+ const errorMessage = error instanceof Error ? error.message : String(error);
280
+ onProgress?.({
281
+ fileName: file.name,
282
+ status: "error",
283
+ progress: 0,
284
+ error: errorMessage,
285
+ });
286
+
287
+ // Optionally continue with other files or rethrow
288
+ // For now, we'll continue with remaining files
289
+ results.push({
290
+ fileName: file.name,
291
+ size: file.size,
292
+ contentBodyId: "",
293
+ contentVersionId: undefined,
294
+ });
295
+ }
296
+ }
297
+
298
+ return results;
299
+ }
@@ -1,4 +1,4 @@
1
- import { Button } from "./ui/button";
1
+ import { Button } from "../../../components/ui/button";
2
2
  import {
3
3
  Dialog,
4
4
  DialogClose,
@@ -6,7 +6,7 @@ import {
6
6
  DialogFooter,
7
7
  DialogHeader,
8
8
  DialogTitle,
9
- } from "./ui/dialog";
9
+ } from "../../../components/ui/dialog";
10
10
  import { FileUploadFileItem } from "./FileUploadFileItem";
11
11
  import { formatUploadSummary } from "../utils/fileUploadUtils";
12
12
  import { LABELS } from "../utils/labels";
@@ -1,4 +1,4 @@
1
- import { Button } from "./ui/button";
1
+ import { Button } from "../../../components/ui/button";
2
2
  import { FileTypeIcon, IconImageClose, SuccessIcon } from "./FileUploadIcons";
3
3
  import {
4
4
  formatFileSize,
@@ -1,59 +1,45 @@
1
1
  /**
2
- * feature-react-file-upload – File upload component
2
+ * feature-react-file-upload – File upload API
3
3
  *
4
- * Provides a React file upload experience with drag-and-drop, progress tracking,
5
- * and Salesforce ContentVersion integration. Supports single or multiple files,
6
- * optional record linking (FirstPublishLocationId), and custom accept filters.
4
+ * Provides programmatic APIs for file upload with progress tracking
5
+ * and Salesforce ContentVersion integration.
7
6
  *
8
7
  * @packageDocumentation
9
8
  */
10
9
 
11
10
  /**
12
- * File upload component. Renders a drop zone for selecting files, a modal dialog
13
- * showing upload progress, and a list of successfully uploaded files. Supports
14
- * click-to-select and drag-and-drop.
11
+ * Unified upload API that handles the complete upload flow:
12
+ * 1. Gets upload config
13
+ * 2. Uploads files with progress tracking
14
+ * 3. Creates ContentVersion records (if recordId provided)
15
15
  *
16
- * @example
17
- * ```tsx
18
- * <FileUpload
19
- * accept="image/*"
20
- * multiple
21
- * recordId={accountId}
22
- * onUploadComplete={(files) => console.log('Uploaded:', files)}
23
- * />
24
- * ```
25
- */
26
- export { FileUpload } from "./components/FileUpload";
27
-
28
- /**
29
- * Props for the FileUpload component.
30
- *
31
- * @see FileUpload
32
- */
33
- export type { FileUploadProps } from "./components/FileUpload";
34
-
35
- /**
36
- * Hook that manages file upload state and logic: config fetch, upload to URL,
37
- * and ContentVersion creation. Use for custom upload UIs or with the FileUpload
38
- * component. Returns fileItems, getInputProps, getDropZoneProps, and helpers
39
- * for progress, cancel, and reset.
16
+ * Returns array of results with file names, sizes, and IDs.
17
+ * Provides progress callbacks for each file through all stages.
40
18
  *
41
19
  * @example
42
20
  * ```tsx
43
- * const { fileItems, getDropZoneProps, openFilePicker } = useFileUpload({
44
- * accept: '.pdf',
45
- * onUploadComplete: (files) => handleComplete(files),
21
+ * const results = await upload({
22
+ * files: [file1, file2],
23
+ * recordId: accountId,
24
+ * onProgress: (progress) => {
25
+ * console.log(`${progress.fileName}: ${progress.status} ${progress.progress}%`);
26
+ * }
46
27
  * });
47
28
  * ```
48
29
  */
49
- export { useFileUpload } from "./hooks/useFileUpload";
30
+ export { upload } from "./features/fileupload/api/fileUpload";
50
31
 
51
32
  /**
52
- * Options for the useFileUpload hook.
33
+ * Options for the upload API.
53
34
  *
54
- * @see useFileUpload
35
+ * @see upload
55
36
  */
56
- export type { UseFileUploadOptions } from "./hooks/useFileUpload";
37
+ export type {
38
+ UploadOptions,
39
+ FileUploadResult,
40
+ FileUploadProgress,
41
+ UploadStatus,
42
+ } from "./features/fileupload/api/fileUpload";
57
43
 
58
44
  /**
59
45
  * Create ContentVersion record programmatically. Useful when you need to create
@@ -71,4 +57,4 @@ export type { UseFileUploadOptions } from "./hooks/useFileUpload";
71
57
  * const contentVersionId = await createContentVersion(file, contentBodyId, recordId);
72
58
  * ```
73
59
  */
74
- export { createContentVersion, getCurrentUserId } from "./api/fileUpload";
60
+ export { createContentVersion, getCurrentUserId } from "./features/fileupload/api/fileUpload";
@@ -2,7 +2,7 @@ import type { RouteObject } from 'react-router';
2
2
  import AppLayout from './appLayout';
3
3
  import NotFound from './pages/NotFound';
4
4
  import { Navigate } from "react-router";
5
- import UploadTest from "./pages/UploadTest";
5
+ import UploadTest from "./features/fileupload/pages/UploadTest";
6
6
 
7
7
  export const routes: RouteObject[] = [
8
8
  {
@@ -20,7 +20,7 @@ export const routes: RouteObject[] = [
20
20
  {
21
21
  path: "upload-test",
22
22
  element: <UploadTest />,
23
- handle: { showInNavigation: true, label: "Upload Test" }
23
+ handle: { showInNavigation: true, label: "Upload Test (Component)" }
24
24
  }
25
25
  ]
26
26
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-base-sfdx-project-experimental",
3
- "version": "1.90.0",
3
+ "version": "1.90.2",
4
4
  "description": "Base SFDX project template",
5
5
  "private": true,
6
6
  "files": [
package/package.json CHANGED
@@ -1,17 +1,22 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-feature-react-file-upload-experimental",
3
- "version": "1.90.0",
3
+ "version": "1.90.2",
4
4
  "description": "File upload feature with a component to upload files to core",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "",
7
- "type": "module",
8
- "main": "index.js",
7
+ "main": "src/force-app/main/default/webapplications/feature-react-file-upload/src/index.ts",
8
+ "types": "src/force-app/main/default/webapplications/feature-react-file-upload/src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/force-app/main/default/webapplications/feature-react-file-upload/src/index.ts",
12
+ "import": "./src/force-app/main/default/webapplications/feature-react-file-upload/src/index.ts",
13
+ "default": "./src/force-app/main/default/webapplications/feature-react-file-upload/src/index.ts"
14
+ }
15
+ },
9
16
  "files": [
10
- "dist"
17
+ "dist",
18
+ "src"
11
19
  ],
12
- "publishConfig": {
13
- "access": "public"
14
- },
15
20
  "scripts": {
16
21
  "clean": "rm -rf dist"
17
22
  },
@@ -26,13 +31,13 @@
26
31
  "build:dist-app": {
27
32
  "executor": "@salesforce/webapp-template-cli-experimental:build-dist-app"
28
33
  },
29
- "start": {
34
+ "dev": {
30
35
  "executor": "@salesforce/webapp-template-cli-experimental:dev-server"
31
36
  }
32
37
  }
33
38
  },
34
39
  "devDependencies": {
35
- "@salesforce/webapp-experimental": "^1.90.0",
40
+ "@salesforce/webapp-experimental": "^1.90.2",
36
41
  "@types/react": "^19.2.7",
37
42
  "@types/react-dom": "^19.2.3",
38
43
  "nodemon": "^3.1.0",
@@ -0,0 +1,9 @@
1
+ import { Outlet } from "react-router";
2
+
3
+ export default function AppLayout() {
4
+ return (
5
+ <>
6
+ <Outlet />
7
+ </>
8
+ );
9
+ }
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ const buttonVariants = cva(
5
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
6
+ {
7
+ variants: {
8
+ variant: {
9
+ default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
10
+ destructive:
11
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
12
+ outline:
13
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
14
+ secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
15
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
16
+ link: "text-primary underline-offset-4 hover:underline",
17
+ },
18
+ size: {
19
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
20
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
21
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
22
+ icon: "size-9",
23
+ },
24
+ },
25
+ defaultVariants: {
26
+ variant: "default",
27
+ size: "default",
28
+ },
29
+ },
30
+ );
31
+
32
+ function Button({}: React.ComponentProps<"button"> &
33
+ VariantProps<typeof buttonVariants> & {
34
+ asChild?: boolean;
35
+ }) {
36
+ return <> </>;
37
+ }
38
+
39
+ export { Button, buttonVariants };
@@ -0,0 +1,102 @@
1
+ import * as React from "react";
2
+ import { Dialog as DialogPrimitive } from "radix-ui";
3
+
4
+ import { Button } from "./__inherit__button";
5
+ import { XIcon } from "lucide-react";
6
+
7
+ function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
8
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />;
9
+ }
10
+
11
+ function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
12
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
13
+ }
14
+
15
+ function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
16
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
17
+ }
18
+
19
+ function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
20
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
21
+ }
22
+
23
+ function DialogOverlay({
24
+ className,
25
+ ...props
26
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
27
+ return <DialogPrimitive.Overlay data-slot="dialog-overlay" {...props} />;
28
+ }
29
+
30
+ function DialogContent({
31
+ className,
32
+ children,
33
+ showCloseButton = true,
34
+ ...props
35
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
36
+ showCloseButton?: boolean;
37
+ }) {
38
+ return (
39
+ <DialogPortal>
40
+ <DialogOverlay />
41
+ <DialogPrimitive.Content data-slot="dialog-content" {...props}>
42
+ {children}
43
+ {showCloseButton && (
44
+ <DialogPrimitive.Close data-slot="dialog-close" asChild>
45
+ <Button variant="ghost" className="absolute top-2 right-2" size="icon">
46
+ <XIcon />
47
+ <span className="sr-only">Close</span>
48
+ </Button>
49
+ </DialogPrimitive.Close>
50
+ )}
51
+ </DialogPrimitive.Content>
52
+ </DialogPortal>
53
+ );
54
+ }
55
+
56
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
57
+ return <div data-slot="dialog-header" {...props} />;
58
+ }
59
+
60
+ function DialogFooter({
61
+ className,
62
+ showCloseButton = false,
63
+ children,
64
+ ...props
65
+ }: React.ComponentProps<"div"> & {
66
+ showCloseButton?: boolean;
67
+ }) {
68
+ return (
69
+ <div data-slot="dialog-footer" {...props}>
70
+ {children}
71
+ {showCloseButton && (
72
+ <DialogPrimitive.Close asChild>
73
+ <Button variant="outline">Close</Button>
74
+ </DialogPrimitive.Close>
75
+ )}
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
81
+ return <DialogPrimitive.Title data-slot="dialog-title" {...props} />;
82
+ }
83
+
84
+ function DialogDescription({
85
+ className,
86
+ ...props
87
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
88
+ return <DialogPrimitive.Description data-slot="dialog-description" {...props} />;
89
+ }
90
+
91
+ export {
92
+ Dialog,
93
+ DialogClose,
94
+ DialogContent,
95
+ DialogDescription,
96
+ DialogFooter,
97
+ DialogHeader,
98
+ DialogOverlay,
99
+ DialogPortal,
100
+ DialogTitle,
101
+ DialogTrigger,
102
+ };