@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.
- package/LICENSE.txt +82 -0
- package/README.md +102 -0
- package/dist/.a4drules/README.md +35 -0
- package/dist/.a4drules/a4d-webapp-generate.md +27 -0
- package/dist/.a4drules/build-validation.md +78 -0
- package/dist/.a4drules/code-quality.md +137 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-explore-graphql-schema.md +227 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-mutationquery.md +212 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-readquery.md +185 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-guide-graphql.md +205 -0
- package/dist/.a4drules/graphql/tools/schemas/shared.graphqls +1150 -0
- package/dist/.a4drules/graphql.md +409 -0
- package/dist/.a4drules/images.md +13 -0
- package/dist/.a4drules/react.md +387 -0
- package/dist/.a4drules/react_image_processing.md +45 -0
- package/dist/.a4drules/typescript.md +224 -0
- package/dist/.a4drules/ui-layout.md +23 -0
- package/dist/.a4drules/webapp-nav-and-placeholders.md +33 -0
- package/dist/.a4drules/webapp-no-node-e.md +25 -0
- package/dist/.a4drules/webapp-ui-first.md +32 -0
- package/dist/.a4drules/webapp.md +75 -0
- package/dist/.forceignore +15 -0
- package/dist/.husky/pre-commit +4 -0
- package/dist/.prettierignore +11 -0
- package/dist/.prettierrc +17 -0
- package/dist/AGENT.md +75 -0
- package/dist/CHANGELOG.md +803 -0
- package/dist/README.md +18 -0
- package/dist/config/project-scratch-def.json +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/.graphqlrc.yml +2 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/.prettierignore +9 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/.prettierrc +11 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/build/vite.config.d.ts +2 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/build/vite.config.js +93 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/codegen.yml +94 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/e2e/app.spec.ts +17 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/eslint.config.js +141 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/feature-react-file-upload.webapplication-meta.xml +7 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/index.html +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/package-lock.json +18396 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/package.json +66 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/playwright.config.ts +24 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/scripts/get-graphql-schema.mjs +68 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/scripts/rewrite-e2e-assets.mjs +23 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/api/fileUpload.ts +154 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/api/graphql-operations-types.ts +116 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/api/utils/accounts.ts +41 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/api/utils/query/highRevenueAccountsQuery.graphql +29 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/app.tsx +22 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/appLayout.tsx +9 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/icons/book.svg +3 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/icons/copy.svg +4 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/icons/rocket.svg +3 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/icons/star.svg +3 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/images/codey-1.png +0 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/images/codey-2.png +0 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/images/codey-3.png +0 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/images/vibe-codey.svg +194 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/symbols.svg +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/assets/utility.svg +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUpload.tsx +83 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadDialog.tsx +79 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadDropZone.tsx +82 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadFileItem.tsx +99 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadIcons.tsx +58 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/alert.tsx +69 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/button.tsx +67 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/card.tsx +92 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/dialog.tsx +143 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/field.tsx +222 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/index.ts +84 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/input.tsx +19 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/label.tsx +19 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/pagination.tsx +112 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/select.tsx +183 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/separator.tsx +26 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/skeleton.tsx +14 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/spinner.tsx +15 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/table.tsx +87 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components/ui/tabs.tsx +78 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/components.json +18 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/hooks/useFileUpload.ts +299 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/hooks/useFileUploadDialog.ts +70 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/index.ts +56 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/lib/utils.ts +6 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/navigationMenu.tsx +80 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/pages/Home.tsx +25 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/pages/NotFound.tsx +18 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/router-utils.tsx +35 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/routes.tsx +22 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/styles/global.css +135 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/types/fileUpload.ts +26 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/utils/fileUploadUtils.ts +44 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/utils/labels.ts +21 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/tsconfig.json +36 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/tsconfig.node.json +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/vite-env.d.ts +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/vite.config.ts +43 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/vitest-env.d.ts +2 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/vitest.config.ts +11 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/vitest.setup.ts +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-file-upload/webapplication.json +7 -0
- package/dist/jest.config.js +6 -0
- package/dist/package.json +38 -0
- package/dist/scripts/apex/hello.apex +10 -0
- package/dist/scripts/soql/account.soql +6 -0
- package/dist/sfdx-project.json +12 -0
- package/package.json +53 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/api/fileUpload.ts +154 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/appLayout.tsx +9 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/assets/symbols.svg +1 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/assets/utility.svg +1 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUpload.tsx +83 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadDialog.tsx +79 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadDropZone.tsx +82 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadFileItem.tsx +99 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/components/FileUploadIcons.tsx +58 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/hooks/useFileUpload.ts +299 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/hooks/useFileUploadDialog.ts +70 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/index.ts +56 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/pages/Home.tsx +25 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/routes.tsx +17 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/types/fileUpload.ts +26 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/utils/fileUploadUtils.ts +44 -0
- package/src/force-app/main/default/webapplications/feature-react-file-upload/src/utils/labels.ts +21 -0
- 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";
|
package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/navigationMenu.tsx
ADDED
|
@@ -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
|
+
}
|
package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/pages/Home.tsx
ADDED
|
@@ -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
|
+
}
|
package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/pages/NotFound.tsx
ADDED
|
@@ -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
|
+
}
|
package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/router-utils.tsx
ADDED
|
@@ -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
|
+
];
|