@salesforce/webapp-template-feature-react-file-upload-experimental 1.60.0 → 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 +16 -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
package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/pages/Home.tsx
CHANGED
|
@@ -1,25 +1,12 @@
|
|
|
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
1
|
export default function Home() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// eslint-disable-next-line no-console
|
|
19
|
-
console.log("Uploaded files:", files);
|
|
20
|
-
}}
|
|
21
|
-
/>
|
|
22
|
-
</section>
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
2
|
+
return (
|
|
3
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
4
|
+
<div className="text-center">
|
|
5
|
+
<h1 className="text-4xl font-bold text-gray-900 mb-4">Home</h1>
|
|
6
|
+
<p className="text-lg text-gray-600 mb-8">
|
|
7
|
+
Welcome to your React application.
|
|
8
|
+
</p>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
25
12
|
}
|
|
@@ -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
|
+
}
|
package/dist/force-app/main/default/webapplications/feature-react-file-upload/src/routes.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { RouteObject } from 'react-router';
|
|
2
2
|
import AppLayout from './appLayout';
|
|
3
|
-
import Home from './pages/Home';
|
|
4
3
|
import NotFound from './pages/NotFound';
|
|
4
|
+
import { Navigate } from "react-router";
|
|
5
|
+
import UploadTest from "./pages/UploadTest";
|
|
5
6
|
|
|
6
7
|
export const routes: RouteObject[] = [
|
|
7
8
|
{
|
|
@@ -10,12 +11,16 @@ export const routes: RouteObject[] = [
|
|
|
10
11
|
children: [
|
|
11
12
|
{
|
|
12
13
|
index: true,
|
|
13
|
-
element: <
|
|
14
|
-
handle: { showInNavigation: true, label: "Home" }
|
|
14
|
+
element: <Navigate to="/upload-test" replace />
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
path: '*',
|
|
18
18
|
element: <NotFound />
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: "upload-test",
|
|
22
|
+
element: <UploadTest />,
|
|
23
|
+
handle: { showInNavigation: true, label: "Upload Test" }
|
|
19
24
|
}
|
|
20
25
|
]
|
|
21
26
|
}
|
|
@@ -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/dist/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/dist/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/webapp-template-feature-react-file-upload-experimental",
|
|
3
|
-
"version": "1.60.
|
|
3
|
+
"version": "1.60.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": "",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@salesforce/webapp-experimental": "^1.60.
|
|
44
|
+
"@salesforce/webapp-experimental": "^1.60.2",
|
|
45
45
|
"@types/react": "^19.2.7",
|
|
46
46
|
"@types/react-dom": "^19.2.3",
|
|
47
47
|
"nodemon": "^3.1.0",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"react-router": "^7.10.1",
|
|
50
50
|
"vite": "^7.3.1"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "abc41ea9a3be79240ce212be95208320d197d14a"
|
|
53
53
|
}
|
package/src/force-app/main/default/webapplications/feature-react-file-upload/src/api/fileUpload.ts
CHANGED
|
@@ -3,16 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { createRecord, getCurrentUser } from "@salesforce/webapp-experimental/api";
|
|
6
|
-
import { createDataSDK, type DataSDK } from "@salesforce/sdk-data";
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
let sdkInstance: DataSDK | null = null;
|
|
10
|
-
async function getSDK() {
|
|
11
|
-
if (!sdkInstance) {
|
|
12
|
-
sdkInstance = await createDataSDK();
|
|
13
|
-
}
|
|
14
|
-
return sdkInstance;
|
|
15
|
-
}
|
|
7
|
+
import { getDataSDK } from "@salesforce/sdk-data";
|
|
16
8
|
|
|
17
9
|
declare const __SF_API_VERSION__: string;
|
|
18
10
|
|
|
@@ -25,7 +17,7 @@ export interface UploadConfig {
|
|
|
25
17
|
* Get upload config (token, uploadUrl) from /connect/file/upload/config.
|
|
26
18
|
*/
|
|
27
19
|
export async function getUploadConfig(): Promise<UploadConfig> {
|
|
28
|
-
const sdk = await
|
|
20
|
+
const sdk = await getDataSDK();
|
|
29
21
|
if (!sdk?.fetch) {
|
|
30
22
|
throw new Error("Failed to initialize SDK");
|
|
31
23
|
}
|
|
@@ -120,10 +112,10 @@ function parseJsonResponse(text: string): Record<string, unknown> {
|
|
|
120
112
|
export async function createContentVersion(
|
|
121
113
|
file: File,
|
|
122
114
|
contentBodyId: string,
|
|
123
|
-
|
|
115
|
+
recordId: string,
|
|
124
116
|
): Promise<string | undefined> {
|
|
125
117
|
const fields = {
|
|
126
|
-
FirstPublishLocationId:
|
|
118
|
+
FirstPublishLocationId: recordId,
|
|
127
119
|
Title: fileNameWithoutExtension(file.name),
|
|
128
120
|
PathOnClient: file.name,
|
|
129
121
|
ContentBodyId: contentBodyId,
|
|
@@ -9,13 +9,21 @@ export interface FileUploadProps {
|
|
|
9
9
|
accept?: string;
|
|
10
10
|
/** Whether to allow multiple file selection. Default: false */
|
|
11
11
|
multiple?: boolean;
|
|
12
|
-
/** Record Id for FirstPublishLocationId (e.g. Account, Opportunity). When provided, files are linked to this record.
|
|
12
|
+
/** 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. */
|
|
13
13
|
recordId?: string;
|
|
14
|
-
/** Called when uploads complete. Receives array of successfully uploaded files with name, size, and contentVersionId. */
|
|
15
|
-
onUploadComplete?: (
|
|
14
|
+
/** Called when uploads complete. Receives array of successfully uploaded files with name, size, contentBodyId, and contentVersionId (if ContentVersion was created). */
|
|
15
|
+
onUploadComplete?: (
|
|
16
|
+
files: { name: string; size: number; contentBodyId: string; contentVersionId?: string }[],
|
|
17
|
+
) => void;
|
|
16
18
|
onUploadError?: (file: File, error: string) => void;
|
|
17
19
|
/** Optional CSS class for the wrapper div */
|
|
18
20
|
className?: string;
|
|
21
|
+
/** Optional CSS class for the drop zone (e.g. "h-full" for flex layouts) */
|
|
22
|
+
dropZoneClassName?: string;
|
|
23
|
+
/** Optional format hint for drop zone (e.g. "JPEG, PNG, PDF, and MP4 formats, up to 50MB") */
|
|
24
|
+
formatHint?: string;
|
|
25
|
+
/** Maximum file size in MB. Files exceeding this limit are rejected with an error. Omit for default (2 GB). */
|
|
26
|
+
maxFileSize?: number;
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
/**
|
|
@@ -30,6 +38,9 @@ export function FileUpload({
|
|
|
30
38
|
onUploadComplete,
|
|
31
39
|
onUploadError,
|
|
32
40
|
className = "",
|
|
41
|
+
dropZoneClassName = "",
|
|
42
|
+
formatHint,
|
|
43
|
+
maxFileSize,
|
|
33
44
|
}: FileUploadProps) {
|
|
34
45
|
const { fileItems, getInputProps, getDropZoneProps, isDragging, reset, cancelFile, allDone } =
|
|
35
46
|
useFileUpload({
|
|
@@ -38,6 +49,7 @@ export function FileUpload({
|
|
|
38
49
|
recordId,
|
|
39
50
|
onUploadComplete,
|
|
40
51
|
onUploadError,
|
|
52
|
+
maxFileSize,
|
|
41
53
|
});
|
|
42
54
|
|
|
43
55
|
const { dialogOpen, uploadedFileNames, handleOpenChange } = useFileUploadDialog({
|
|
@@ -45,6 +57,9 @@ export function FileUpload({
|
|
|
45
57
|
reset,
|
|
46
58
|
});
|
|
47
59
|
|
|
60
|
+
const effectiveFormatHint =
|
|
61
|
+
formatHint ?? (maxFileSize != null ? `Up to ${maxFileSize}MB` : undefined);
|
|
62
|
+
|
|
48
63
|
const inputProps = getInputProps();
|
|
49
64
|
const dropZoneProps = getDropZoneProps();
|
|
50
65
|
|
|
@@ -55,6 +70,8 @@ export function FileUpload({
|
|
|
55
70
|
inputProps={inputProps}
|
|
56
71
|
dropZoneProps={dropZoneProps}
|
|
57
72
|
isDragging={isDragging}
|
|
73
|
+
formatHint={effectiveFormatHint}
|
|
74
|
+
className={dropZoneClassName}
|
|
58
75
|
/>
|
|
59
76
|
|
|
60
77
|
<FileUploadDialog
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import { Button } from "./ui/__inherit__button";
|
|
1
2
|
import {
|
|
2
|
-
Button,
|
|
3
3
|
Dialog,
|
|
4
4
|
DialogClose,
|
|
5
5
|
DialogContent,
|
|
6
6
|
DialogFooter,
|
|
7
7
|
DialogHeader,
|
|
8
8
|
DialogTitle,
|
|
9
|
-
} from "
|
|
9
|
+
} from "./ui/__inherit__dialog";
|
|
10
10
|
import { FileUploadFileItem } from "./FileUploadFileItem";
|
|
11
11
|
import { formatUploadSummary } from "../utils/fileUploadUtils";
|
|
12
12
|
import { LABELS } from "../utils/labels";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { UtilityIcon } from "./FileUploadIcons";
|
|
2
1
|
import { LABELS } from "../utils/labels";
|
|
3
2
|
|
|
3
|
+
import { UtilityIcon } from "./FileUploadIcons";
|
|
4
|
+
|
|
4
5
|
export interface FileUploadDropZoneProps {
|
|
5
6
|
/** Props for the hidden file input (ref, type, accept, multiple, onChange) */
|
|
6
7
|
inputProps: {
|
|
@@ -20,63 +21,70 @@ export interface FileUploadDropZoneProps {
|
|
|
20
21
|
};
|
|
21
22
|
/** Whether the user is currently dragging over the drop zone */
|
|
22
23
|
isDragging: boolean;
|
|
24
|
+
/** Optional format hint (e.g. "JPEG, PNG, PDF, and MP4 formats, up to 50MB"). Defaults to LABELS.formatHint. */
|
|
25
|
+
formatHint?: string;
|
|
26
|
+
/** Optional CSS class for the drop zone (e.g. "h-full" for flex layouts) */
|
|
27
|
+
className?: string;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
const DROP_ZONE_BASE_CLASSES =
|
|
26
|
-
"mb-2 flex cursor-pointer flex-
|
|
31
|
+
"mb-2 flex cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed bg-white px-6 pt-4 pb-8 transition-colors";
|
|
27
32
|
const DROP_ZONE_DRAGGING_CLASSES = "border-blue-500 bg-blue-50";
|
|
28
|
-
const DROP_ZONE_IDLE_CLASSES = "hover:bg-gray-50";
|
|
29
|
-
const BORDER_COLOR_IDLE = "rgb(116, 116, 116)";
|
|
33
|
+
const DROP_ZONE_IDLE_CLASSES = "border-gray-300 hover:border-gray-400 hover:bg-gray-50";
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
|
-
* Drop zone for file selection. Renders a dashed border area with
|
|
33
|
-
*
|
|
36
|
+
* Drop zone for file selection. Renders a dashed border area with document icon,
|
|
37
|
+
* primary instruction text, and format hint. Accepts click and drag-and-drop.
|
|
34
38
|
* Uses a hidden file input; props come from useFileUpload.
|
|
35
39
|
*/
|
|
36
40
|
export function FileUploadDropZone({
|
|
37
41
|
inputProps,
|
|
38
42
|
dropZoneProps,
|
|
39
43
|
isDragging,
|
|
44
|
+
formatHint = LABELS.formatHint,
|
|
45
|
+
className = "",
|
|
40
46
|
}: FileUploadDropZoneProps) {
|
|
41
47
|
const dropZoneClassName = [
|
|
42
48
|
DROP_ZONE_BASE_CLASSES,
|
|
43
49
|
isDragging ? DROP_ZONE_DRAGGING_CLASSES : DROP_ZONE_IDLE_CLASSES,
|
|
44
|
-
|
|
50
|
+
className,
|
|
51
|
+
]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join(" ");
|
|
45
54
|
|
|
46
55
|
return (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
<div
|
|
57
|
+
role="button"
|
|
58
|
+
tabIndex={0}
|
|
59
|
+
onClick={dropZoneProps.onClick}
|
|
60
|
+
onDragOver={dropZoneProps.onDragOver}
|
|
61
|
+
onDragLeave={dropZoneProps.onDragLeave}
|
|
62
|
+
onDrop={dropZoneProps.onDrop}
|
|
63
|
+
onKeyDown={dropZoneProps.onKeyDown}
|
|
64
|
+
className={dropZoneClassName}
|
|
65
|
+
aria-label={LABELS.dropZone}
|
|
66
|
+
data-testid="file-upload-drop-zone"
|
|
67
|
+
>
|
|
68
|
+
<input
|
|
69
|
+
id="file-upload-input-id"
|
|
70
|
+
ref={inputProps.ref}
|
|
71
|
+
type="file"
|
|
72
|
+
accept={inputProps.accept}
|
|
73
|
+
multiple={inputProps.multiple}
|
|
74
|
+
onChange={inputProps.onChange}
|
|
75
|
+
className="sr-only"
|
|
76
|
+
aria-hidden
|
|
77
|
+
/>
|
|
78
|
+
<span
|
|
79
|
+
className="inline-flex h-12 w-12 shrink-0 items-center justify-center text-gray-500"
|
|
80
|
+
aria-hidden
|
|
61
81
|
>
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
className="sr-only"
|
|
70
|
-
aria-hidden
|
|
71
|
-
/>
|
|
72
|
-
<span className="inline-flex items-center gap-2 rounded-[4px] border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-blue-600 hover:bg-gray-50">
|
|
73
|
-
<UtilityIcon id="upload" size="sm" />
|
|
74
|
-
{LABELS.uploadFiles}
|
|
75
|
-
</span>
|
|
76
|
-
<span className="text-sm text-gray-900">
|
|
77
|
-
{isDragging ? LABELS.dropFilesHere : LABELS.orDropFiles}
|
|
78
|
-
</span>
|
|
79
|
-
</div>
|
|
80
|
-
</>
|
|
82
|
+
<UtilityIcon id="image" />
|
|
83
|
+
</span>
|
|
84
|
+
<p className="text-center text-sm font-medium text-gray-900">
|
|
85
|
+
{isDragging ? LABELS.dropFilesHere : LABELS.chooseFileOrDrop}
|
|
86
|
+
</p>
|
|
87
|
+
<p className="text-center text-xs text-gray-500">{formatHint}</p>
|
|
88
|
+
</div>
|
|
81
89
|
);
|
|
82
90
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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({
|
|
33
|
+
className,
|
|
34
|
+
variant,
|
|
35
|
+
size,
|
|
36
|
+
asChild = false,
|
|
37
|
+
...props
|
|
38
|
+
}: React.ComponentProps<"button"> &
|
|
39
|
+
VariantProps<typeof buttonVariants> & {
|
|
40
|
+
asChild?: boolean;
|
|
41
|
+
}) {
|
|
42
|
+
return <> </>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
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-sm">
|
|
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
|
+
};
|