@salesforce/webapp-template-feature-react-file-upload-experimental 1.55.0

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 (126) hide show
  1. package/LICENSE.txt +82 -0
  2. package/README.md +102 -0
  3. package/dist/.a4drules/README.md +35 -0
  4. package/dist/.a4drules/a4d-webapp-generate.md +27 -0
  5. package/dist/.a4drules/build-validation.md +78 -0
  6. package/dist/.a4drules/code-quality.md +137 -0
  7. package/dist/.a4drules/graphql/tools/knowledge/lds-explore-graphql-schema.md +227 -0
  8. package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-mutationquery.md +212 -0
  9. package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-readquery.md +185 -0
  10. package/dist/.a4drules/graphql/tools/knowledge/lds-guide-graphql.md +205 -0
  11. package/dist/.a4drules/graphql/tools/schemas/shared.graphqls +1150 -0
  12. package/dist/.a4drules/graphql.md +409 -0
  13. package/dist/.a4drules/images.md +13 -0
  14. package/dist/.a4drules/react.md +387 -0
  15. package/dist/.a4drules/react_image_processing.md +45 -0
  16. package/dist/.a4drules/typescript.md +224 -0
  17. package/dist/.a4drules/ui-layout.md +23 -0
  18. package/dist/.a4drules/webapp-nav-and-placeholders.md +33 -0
  19. package/dist/.a4drules/webapp-no-node-e.md +25 -0
  20. package/dist/.a4drules/webapp-ui-first.md +32 -0
  21. package/dist/.a4drules/webapp.md +75 -0
  22. package/dist/.forceignore +15 -0
  23. package/dist/.husky/pre-commit +4 -0
  24. package/dist/.prettierignore +11 -0
  25. package/dist/.prettierrc +17 -0
  26. package/dist/AGENT.md +75 -0
  27. package/dist/CHANGELOG.md +803 -0
  28. package/dist/README.md +18 -0
  29. package/dist/config/project-scratch-def.json +13 -0
  30. package/dist/force-app/main/default/webapplications/feature-react-file-upload/.graphqlrc.yml +2 -0
  31. package/dist/force-app/main/default/webapplications/feature-react-file-upload/.prettierignore +9 -0
  32. package/dist/force-app/main/default/webapplications/feature-react-file-upload/.prettierrc +11 -0
  33. package/dist/force-app/main/default/webapplications/feature-react-file-upload/build/vite.config.d.ts +2 -0
  34. package/dist/force-app/main/default/webapplications/feature-react-file-upload/build/vite.config.js +93 -0
  35. package/dist/force-app/main/default/webapplications/feature-react-file-upload/codegen.yml +94 -0
  36. package/dist/force-app/main/default/webapplications/feature-react-file-upload/e2e/app.spec.ts +17 -0
  37. package/dist/force-app/main/default/webapplications/feature-react-file-upload/eslint.config.js +141 -0
  38. package/dist/force-app/main/default/webapplications/feature-react-file-upload/feature-react-file-upload.webapplication-meta.xml +7 -0
  39. package/dist/force-app/main/default/webapplications/feature-react-file-upload/index.html +13 -0
  40. package/dist/force-app/main/default/webapplications/feature-react-file-upload/package-lock.json +18396 -0
  41. package/dist/force-app/main/default/webapplications/feature-react-file-upload/package.json +66 -0
  42. package/dist/force-app/main/default/webapplications/feature-react-file-upload/playwright.config.ts +24 -0
  43. package/dist/force-app/main/default/webapplications/feature-react-file-upload/scripts/get-graphql-schema.mjs +68 -0
  44. package/dist/force-app/main/default/webapplications/feature-react-file-upload/scripts/rewrite-e2e-assets.mjs +23 -0
  45. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/api/fileUpload.ts +154 -0
  46. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/api/graphql-operations-types.ts +116 -0
  47. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/api/utils/accounts.ts +41 -0
  48. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/api/utils/query/highRevenueAccountsQuery.graphql +29 -0
  49. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/app.tsx +22 -0
  50. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/appLayout.tsx +9 -0
  51. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/icons/book.svg +3 -0
  52. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/icons/copy.svg +4 -0
  53. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/icons/rocket.svg +3 -0
  54. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/icons/star.svg +3 -0
  55. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/images/codey-1.png +0 -0
  56. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/images/codey-2.png +0 -0
  57. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/images/codey-3.png +0 -0
  58. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/images/vibe-codey.svg +194 -0
  59. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/symbols.svg +1 -0
  60. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/utility.svg +1 -0
  61. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUpload.tsx +83 -0
  62. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadDialog.tsx +79 -0
  63. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadDropZone.tsx +82 -0
  64. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadFileItem.tsx +99 -0
  65. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadIcons.tsx +58 -0
  66. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/alert.tsx +69 -0
  67. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/button.tsx +67 -0
  68. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/card.tsx +92 -0
  69. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/dialog.tsx +143 -0
  70. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/field.tsx +222 -0
  71. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/index.ts +84 -0
  72. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/input.tsx +19 -0
  73. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/label.tsx +19 -0
  74. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/pagination.tsx +112 -0
  75. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/select.tsx +183 -0
  76. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/separator.tsx +26 -0
  77. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/skeleton.tsx +14 -0
  78. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/spinner.tsx +15 -0
  79. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/table.tsx +87 -0
  80. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/tabs.tsx +78 -0
  81. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components.json +18 -0
  82. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/hooks/useFileUpload.ts +299 -0
  83. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/hooks/useFileUploadDialog.ts +70 -0
  84. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/index.ts +56 -0
  85. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/lib/utils.ts +6 -0
  86. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/navigationMenu.tsx +80 -0
  87. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/pages/Home.tsx +25 -0
  88. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/pages/NotFound.tsx +18 -0
  89. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/router-utils.tsx +35 -0
  90. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/routes.tsx +22 -0
  91. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/styles/global.css +135 -0
  92. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/types/fileUpload.ts +26 -0
  93. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/utils/fileUploadUtils.ts +44 -0
  94. package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/utils/labels.ts +21 -0
  95. package/dist/force-app/main/default/webapplications/feature-react-file-upload/tsconfig.json +36 -0
  96. package/dist/force-app/main/default/webapplications/feature-react-file-upload/tsconfig.node.json +13 -0
  97. package/dist/force-app/main/default/webapplications/feature-react-file-upload/vite-env.d.ts +1 -0
  98. package/dist/force-app/main/default/webapplications/feature-react-file-upload/vite.config.ts +43 -0
  99. package/dist/force-app/main/default/webapplications/feature-react-file-upload/vitest-env.d.ts +2 -0
  100. package/dist/force-app/main/default/webapplications/feature-react-file-upload/vitest.config.ts +11 -0
  101. package/dist/force-app/main/default/webapplications/feature-react-file-upload/vitest.setup.ts +1 -0
  102. package/dist/force-app/main/default/webapplications/feature-react-file-upload/webapplication.json +7 -0
  103. package/dist/jest.config.js +6 -0
  104. package/dist/package.json +38 -0
  105. package/dist/scripts/apex/hello.apex +10 -0
  106. package/dist/scripts/soql/account.soql +6 -0
  107. package/dist/sfdx-project.json +12 -0
  108. package/package.json +53 -0
  109. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/api/fileUpload.ts +154 -0
  110. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/appLayout.tsx +9 -0
  111. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/assets/symbols.svg +1 -0
  112. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/assets/utility.svg +1 -0
  113. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUpload.tsx +83 -0
  114. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadDialog.tsx +79 -0
  115. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadDropZone.tsx +82 -0
  116. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadFileItem.tsx +99 -0
  117. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadIcons.tsx +58 -0
  118. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/hooks/useFileUpload.ts +299 -0
  119. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/hooks/useFileUploadDialog.ts +70 -0
  120. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/index.ts +56 -0
  121. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/pages/Home.tsx +25 -0
  122. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/routes.tsx +17 -0
  123. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/types/fileUpload.ts +26 -0
  124. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/utils/fileUploadUtils.ts +44 -0
  125. package/src/force-app/main/default/webapplications/feature-react-file-upload/src/utils/labels.ts +21 -0
  126. package/src/force-app/main/default/webapplications/feature-react-file-upload/vite.config.ts +43 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Hook: useFileUpload
3
+ *
4
+ * Manages file upload state and logic: config fetch, upload to URL, ContentVersion creation.
5
+ * Separates upload logic from UI. Use for custom upload UIs or with the FileUpload component.
6
+ *
7
+ * @param options - Upload options (accept, multiple, recordId, onUploadComplete, onUploadError)
8
+ * @returns Object containing fileItems, allDone, cancelFile, getInputProps, getDropZoneProps, openFilePicker, isDragging, reset
9
+ *
10
+ * @remarks
11
+ * Coordinates: (1) getUploadConfig for token/URL, (2) uploadToUrl for file body, (3) createContentVersion
12
+ * for record creation. Supports cancel, progress tracking, and sequential multi-file uploads.
13
+ */
14
+ import * as React from "react";
15
+ import { flushSync } from "react-dom";
16
+ import {
17
+ getUploadConfig,
18
+ uploadToUrl,
19
+ createContentVersion,
20
+ getCurrentUserId,
21
+ } from "../api/fileUpload";
22
+ import type { FileUploadItem, UploadedFile, UploadState } from "../types/fileUpload";
23
+ import { isFileTooLarge, MAX_FILE_SIZE_BYTES, formatFileSize } from "../utils/fileUploadUtils";
24
+ import { LABELS } from "../utils/labels";
25
+
26
+ export interface UseFileUploadOptions {
27
+ /** MIME types or file extensions to accept (e.g. image/*, .pdf). Omit for all files. */
28
+ accept?: string;
29
+ /** Whether to allow multiple file selection. Default: false */
30
+ multiple?: boolean;
31
+ /** Record Id for FirstPublishLocationId (e.g. Account, Opportunity). When provided, files are linked to this record. Otherwise, current user Id is used. */
32
+ recordId?: string;
33
+ /** Called when uploads complete. Receives array of successfully uploaded files with name, size, and contentVersionId. */
34
+ onUploadComplete?: (files: UploadedFile[]) => void;
35
+ /** Called when an upload fails. Receives the file and error message. */
36
+ onUploadError?: (file: File, error: string) => void;
37
+ }
38
+
39
+ function updateItem(
40
+ items: FileUploadItem[],
41
+ fileName: string,
42
+ update: Partial<FileUploadItem>,
43
+ ): FileUploadItem[] {
44
+ return items.map((item) => (item.file.name === fileName ? { ...item, ...update } : item));
45
+ }
46
+
47
+ interface UseFileUploadReturn {
48
+ fileItems: FileUploadItem[];
49
+ allDone: boolean;
50
+ cancelFile: (fileName: string) => void;
51
+ getInputProps: () => {
52
+ ref: React.RefObject<HTMLInputElement | null>;
53
+ type: "file";
54
+ accept?: string;
55
+ multiple: boolean;
56
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
57
+ };
58
+ getDropZoneProps: () => {
59
+ onClick: () => void;
60
+ onDragOver: (e: React.DragEvent) => void;
61
+ onDragLeave: (e: React.DragEvent) => void;
62
+ onDrop: (e: React.DragEvent) => void;
63
+ onKeyDown: (e: React.KeyboardEvent) => void;
64
+ };
65
+ /** Programmatically open the native file picker. Use with a custom trigger (e.g. button). */
66
+ openFilePicker: () => void;
67
+ isDragging: boolean;
68
+ reset: () => void;
69
+ }
70
+
71
+ export function useFileUpload(options: UseFileUploadOptions = {}): UseFileUploadReturn {
72
+ const { accept, multiple = false, recordId, onUploadComplete, onUploadError } = options;
73
+
74
+ const [fileItems, setFileItems] = React.useState<FileUploadItem[]>([]);
75
+ const [isDragging, setIsDragging] = React.useState(false);
76
+ const inputRef = React.useRef<HTMLInputElement>(null);
77
+ const cancelledFilesRef = React.useRef<Set<string>>(new Set());
78
+ const currentAbortControllerRef = React.useRef<AbortController | null>(null);
79
+ const currentFileNameRef = React.useRef<string | null>(null);
80
+
81
+ const cancelFile = React.useCallback((fileName: string) => {
82
+ cancelledFilesRef.current.add(fileName);
83
+ if (currentFileNameRef.current === fileName && currentAbortControllerRef.current) {
84
+ currentAbortControllerRef.current.abort();
85
+ }
86
+ }, []);
87
+
88
+ React.useEffect(() => {
89
+ return () => {
90
+ currentAbortControllerRef.current?.abort();
91
+ };
92
+ }, []);
93
+
94
+ const handleChange = React.useCallback(
95
+ async (e: React.ChangeEvent<HTMLInputElement>) => {
96
+ const files = e.target.files ? Array.from(e.target.files) : [];
97
+ if (files.length === 0) return;
98
+
99
+ const maxSizeLabel = formatFileSize(MAX_FILE_SIZE_BYTES);
100
+ const errorMessage = LABELS.fileTooLarge(maxSizeLabel);
101
+
102
+ const items: FileUploadItem[] = files.map((file) => {
103
+ if (isFileTooLarge(file)) {
104
+ onUploadError?.(file, errorMessage);
105
+ return {
106
+ file,
107
+ state: "error" as UploadState,
108
+ progress: 0,
109
+ error: errorMessage,
110
+ };
111
+ }
112
+ return {
113
+ file,
114
+ state: "loading_config" as UploadState,
115
+ progress: 0,
116
+ };
117
+ });
118
+ setFileItems(items);
119
+ cancelledFilesRef.current.clear();
120
+
121
+ const itemsToUpload = items.filter((item) => item.state !== "error");
122
+ if (itemsToUpload.length === 0) {
123
+ e.target.value = "";
124
+ return;
125
+ }
126
+
127
+ let config: Awaited<ReturnType<typeof getUploadConfig>> | null = null;
128
+ let publishLocationId: string | null = null;
129
+ const uploadedFiles: UploadedFile[] = [];
130
+
131
+ for (const item of itemsToUpload) {
132
+ const { file } = item;
133
+
134
+ if (cancelledFilesRef.current.has(file.name)) {
135
+ setFileItems((prev) => updateItem(prev, file.name, { state: "cancelled", progress: 0 }));
136
+ continue;
137
+ }
138
+
139
+ try {
140
+ if (!config) {
141
+ config = await getUploadConfig();
142
+ }
143
+ setFileItems((prev) => updateItem(prev, file.name, { state: "loading_config" }));
144
+
145
+ if (cancelledFilesRef.current.has(file.name)) {
146
+ setFileItems((prev) =>
147
+ updateItem(prev, file.name, { state: "cancelled", progress: 0 }),
148
+ );
149
+ continue;
150
+ }
151
+
152
+ const abortController = new AbortController();
153
+ currentAbortControllerRef.current = abortController;
154
+ currentFileNameRef.current = file.name;
155
+
156
+ setFileItems((prev) => updateItem(prev, file.name, { state: "uploading", progress: 0 }));
157
+ const contentBodyId = await uploadToUrl(
158
+ file,
159
+ config.token,
160
+ config.uploadUrl,
161
+ (percent) => {
162
+ setFileItems((prev) => updateItem(prev, file.name, { progress: percent }));
163
+ },
164
+ abortController.signal,
165
+ );
166
+
167
+ currentAbortControllerRef.current = null;
168
+ currentFileNameRef.current = null;
169
+
170
+ if (cancelledFilesRef.current.has(file.name)) {
171
+ continue;
172
+ }
173
+
174
+ if (!publishLocationId) {
175
+ publishLocationId = recordId ?? (await getCurrentUserId());
176
+ }
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
+
186
+ flushSync(() => {
187
+ setFileItems((prev) =>
188
+ updateItem(prev, file.name, {
189
+ state: "success",
190
+ progress: 100,
191
+ contentVersionId,
192
+ }),
193
+ );
194
+ });
195
+ uploadedFiles.push({
196
+ name: file.name,
197
+ size: file.size,
198
+ contentVersionId,
199
+ });
200
+ } catch (err) {
201
+ currentAbortControllerRef.current = null;
202
+ currentFileNameRef.current = null;
203
+ const message = err instanceof Error ? err.message : String(err);
204
+ const isCancelled =
205
+ message === "Upload aborted" || cancelledFilesRef.current.has(file.name);
206
+ setFileItems((prev) =>
207
+ updateItem(prev, file.name, {
208
+ state: isCancelled ? "cancelled" : "error",
209
+ error: isCancelled ? undefined : message,
210
+ }),
211
+ );
212
+ if (!isCancelled) {
213
+ onUploadError?.(file, message);
214
+ }
215
+ // Continue with remaining files; do not return early
216
+ }
217
+ }
218
+
219
+ if (uploadedFiles.length > 0) {
220
+ onUploadComplete?.(uploadedFiles);
221
+ }
222
+
223
+ e.target.value = "";
224
+ },
225
+ [recordId, onUploadComplete, onUploadError],
226
+ );
227
+
228
+ const handleDragOver = React.useCallback((e: React.DragEvent) => {
229
+ e.preventDefault();
230
+ e.stopPropagation();
231
+ setIsDragging(true);
232
+ }, []);
233
+
234
+ const handleDragLeave = React.useCallback((e: React.DragEvent) => {
235
+ e.preventDefault();
236
+ e.stopPropagation();
237
+ setIsDragging(false);
238
+ }, []);
239
+
240
+ const handleDrop = React.useCallback(
241
+ (e: React.DragEvent) => {
242
+ e.preventDefault();
243
+ e.stopPropagation();
244
+ setIsDragging(false);
245
+ const files = e.dataTransfer.files ? Array.from(e.dataTransfer.files) : [];
246
+ if (files.length > 0 && inputRef.current) {
247
+ const dt = new DataTransfer();
248
+ files.forEach((f) => dt.items.add(f));
249
+ inputRef.current.files = dt.files;
250
+ handleChange({ target: inputRef.current } as React.ChangeEvent<HTMLInputElement>);
251
+ }
252
+ },
253
+ [handleChange],
254
+ );
255
+
256
+ const handleClick = React.useCallback(() => {
257
+ inputRef.current?.click();
258
+ }, []);
259
+
260
+ const handleKeyDown = React.useCallback((e: React.KeyboardEvent) => {
261
+ if (e.key === "Enter" || e.key === " ") {
262
+ e.preventDefault();
263
+ inputRef.current?.click();
264
+ }
265
+ }, []);
266
+
267
+ const reset = React.useCallback(() => {
268
+ setFileItems([]);
269
+ }, []);
270
+
271
+ const allDone =
272
+ fileItems.length > 0 &&
273
+ fileItems.every(
274
+ (item) => item.state === "success" || item.state === "error" || item.state === "cancelled",
275
+ );
276
+
277
+ return {
278
+ fileItems,
279
+ allDone,
280
+ isDragging,
281
+ reset,
282
+ cancelFile,
283
+ openFilePicker: handleClick,
284
+ getInputProps: () => ({
285
+ ref: inputRef,
286
+ type: "file" as const,
287
+ accept,
288
+ multiple,
289
+ onChange: handleChange,
290
+ }),
291
+ getDropZoneProps: () => ({
292
+ onClick: handleClick,
293
+ onDragOver: handleDragOver,
294
+ onDragLeave: handleDragLeave,
295
+ onDrop: handleDrop,
296
+ onKeyDown: handleKeyDown,
297
+ }),
298
+ };
299
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Hook: useFileUploadDialog
3
+ *
4
+ * Manages file upload dialog state: open/close and list of successfully uploaded file names.
5
+ * Powers the FileUpload component internally. Opens dialog when fileItems has items; on close,
6
+ * captures success names for display and resets upload state.
7
+ *
8
+ * Not exported from the package. Use the FileUpload component for file upload UIs.
9
+ *
10
+ * @param options - Dialog options (fileItems, reset)
11
+ * @returns Object containing dialogOpen, uploadedFileNames, handleOpenChange
12
+ *
13
+ * @remarks
14
+ * Coordinates with useFileUpload: opens when fileItems.length > 0, on close calls reset and
15
+ * accumulates successfully uploaded file names for the summary list below the drop zone.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * const { dialogOpen, uploadedFileNames, handleOpenChange } = useFileUploadDialog({
20
+ * fileItems,
21
+ * reset,
22
+ * });
23
+ * ```
24
+ */
25
+ import * as React from "react";
26
+ import type { FileUploadItem } from "../types/fileUpload";
27
+
28
+ interface UseFileUploadDialogOptions {
29
+ fileItems: FileUploadItem[];
30
+ reset: () => void;
31
+ }
32
+
33
+ interface UseFileUploadDialogReturn {
34
+ dialogOpen: boolean;
35
+ uploadedFileNames: string[];
36
+ handleOpenChange: (open: boolean) => void;
37
+ }
38
+
39
+ export function useFileUploadDialog({
40
+ fileItems,
41
+ reset,
42
+ }: UseFileUploadDialogOptions): UseFileUploadDialogReturn {
43
+ const [dialogOpen, setDialogOpen] = React.useState(false);
44
+ const [uploadedFileNames, setUploadedFileNames] = React.useState<string[]>([]);
45
+
46
+ React.useEffect(() => {
47
+ if (fileItems.length > 0) {
48
+ setDialogOpen(true);
49
+ setUploadedFileNames([]);
50
+ }
51
+ }, [fileItems.length]);
52
+
53
+ const handleOpenChange = React.useCallback(
54
+ (open: boolean) => {
55
+ if (!open) {
56
+ const successNames = fileItems
57
+ .filter((item) => item.state === "success")
58
+ .map((item) => item.file.name);
59
+ if (successNames.length > 0) {
60
+ setUploadedFileNames((prev) => [...prev, ...successNames]);
61
+ }
62
+ reset();
63
+ }
64
+ setDialogOpen(open);
65
+ },
66
+ [fileItems, reset],
67
+ );
68
+
69
+ return { dialogOpen, uploadedFileNames, handleOpenChange };
70
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * feature-react-file-upload – File upload component
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.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ /**
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.
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.
40
+ *
41
+ * @example
42
+ * ```tsx
43
+ * const { fileItems, getDropZoneProps, openFilePicker } = useFileUpload({
44
+ * accept: '.pdf',
45
+ * onUploadComplete: (files) => handleComplete(files),
46
+ * });
47
+ * ```
48
+ */
49
+ export { useFileUpload } from "./hooks/useFileUpload";
50
+
51
+ /**
52
+ * Options for the useFileUpload hook.
53
+ *
54
+ * @see useFileUpload
55
+ */
56
+ export type { UseFileUploadOptions } from "./hooks/useFileUpload";
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,80 @@
1
+ import { Link, useLocation } from 'react-router';
2
+ import { getAllRoutes } from './router-utils';
3
+ import { useState } from 'react';
4
+
5
+ export default function NavigationMenu() {
6
+ const [isOpen, setIsOpen] = useState(false);
7
+ const location = useLocation();
8
+
9
+ const isActive = (path: string) => location.pathname === path;
10
+
11
+ const toggleMenu = () => setIsOpen(!isOpen);
12
+
13
+ const navigationRoutes: { path: string; label: string }[] = getAllRoutes()
14
+ .filter(
15
+ route =>
16
+ route.handle?.showInNavigation === true &&
17
+ route.fullPath !== undefined &&
18
+ route.handle?.label !== undefined
19
+ )
20
+ .map(
21
+ route =>
22
+ ({
23
+ path: route.fullPath,
24
+ label: route.handle?.label,
25
+ }) as { path: string; label: string }
26
+ );
27
+
28
+ return (
29
+ <nav className="bg-white border-b border-gray-200">
30
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
31
+ <div className="flex justify-between items-center h-16">
32
+ <Link to="/" className="text-xl font-semibold text-gray-900">
33
+ React App
34
+ </Link>
35
+ <button
36
+ onClick={toggleMenu}
37
+ className="p-2 rounded-md text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
38
+ aria-label="Toggle menu"
39
+ >
40
+ <div className="w-6 h-6 flex flex-col justify-center space-y-1.5">
41
+ <span
42
+ className={`block h-0.5 w-6 bg-current transition-all ${
43
+ isOpen ? 'rotate-45 translate-y-2' : ''
44
+ }`}
45
+ />
46
+ <span
47
+ className={`block h-0.5 w-6 bg-current transition-all ${isOpen ? 'opacity-0' : ''}`}
48
+ />
49
+ <span
50
+ className={`block h-0.5 w-6 bg-current transition-all ${
51
+ isOpen ? '-rotate-45 -translate-y-2' : ''
52
+ }`}
53
+ />
54
+ </div>
55
+ </button>
56
+ </div>
57
+ {isOpen && (
58
+ <div className="pb-4">
59
+ <div className="flex flex-col space-y-2">
60
+ {navigationRoutes.map(item => (
61
+ <Link
62
+ key={item.path}
63
+ to={item.path}
64
+ onClick={() => setIsOpen(false)}
65
+ className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
66
+ isActive(item.path)
67
+ ? 'bg-blue-100 text-blue-700'
68
+ : 'text-gray-700 hover:bg-gray-100'
69
+ }`}
70
+ >
71
+ {item.label}
72
+ </Link>
73
+ ))}
74
+ </div>
75
+ </div>
76
+ )}
77
+ </div>
78
+ </nav>
79
+ );
80
+ }
@@ -0,0 +1,25 @@
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
+ }
@@ -0,0 +1,18 @@
1
+ import { Link } from 'react-router';
2
+
3
+ export default function NotFound() {
4
+ return (
5
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
6
+ <div className="text-center">
7
+ <h1 className="text-4xl font-bold text-gray-900 mb-4">404</h1>
8
+ <p className="text-lg text-gray-600 mb-8">Page not found</p>
9
+ <Link
10
+ to="/"
11
+ className="inline-block px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
12
+ >
13
+ Go to Home
14
+ </Link>
15
+ </div>
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,35 @@
1
+ import type { RouteObject } from 'react-router';
2
+ import { routes } from './routes';
3
+
4
+ export type RouteWithFullPath = RouteObject & { fullPath: string };
5
+
6
+ const flatMapRoutes = (
7
+ route: RouteObject,
8
+ parentPath: string = ''
9
+ ): RouteWithFullPath[] => {
10
+ let fullPath: string;
11
+
12
+ if (route.index) {
13
+ fullPath = parentPath || '/';
14
+ } else if (route.path) {
15
+ if (route.path.startsWith('/')) {
16
+ fullPath = route.path;
17
+ } else {
18
+ fullPath =
19
+ parentPath === '/' ? `/${route.path}` : `${parentPath}/${route.path}`;
20
+ }
21
+ } else {
22
+ fullPath = parentPath;
23
+ }
24
+
25
+ const routeWithPath = { ...route, fullPath };
26
+
27
+ const childRoutes =
28
+ route.children?.flatMap(child => flatMapRoutes(child, fullPath)) || [];
29
+
30
+ return [routeWithPath, ...childRoutes];
31
+ };
32
+
33
+ export const getAllRoutes = (): RouteWithFullPath[] => {
34
+ return routes.flatMap(route => flatMapRoutes(route));
35
+ };
@@ -0,0 +1,22 @@
1
+ import type { RouteObject } from 'react-router';
2
+ import AppLayout from './appLayout';
3
+ import Home from './pages/Home';
4
+ import NotFound from './pages/NotFound';
5
+
6
+ export const routes: RouteObject[] = [
7
+ {
8
+ path: "/",
9
+ element: <AppLayout />,
10
+ children: [
11
+ {
12
+ index: true,
13
+ element: <Home />,
14
+ handle: { showInNavigation: true, label: "Home" }
15
+ },
16
+ {
17
+ path: '*',
18
+ element: <NotFound />
19
+ }
20
+ ]
21
+ }
22
+ ];