Skip to content

Commit 4da5d4f

Browse files
FWF-5276:[V8Feature] - Form Edit Actions MF
1 parent c049c99 commit 4da5d4f

File tree

10 files changed

+386
-119
lines changed

10 files changed

+386
-119
lines changed

forms-flow-components/src/components/CustomComponents/FileUploadArea.tsx

Lines changed: 96 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface FileUploadAreaProps extends Omit<React.ComponentPropsWithoutRef<"div">
2525
onDone?: () => void;
2626
/** Currently selected file */
2727
file: File | null;
28+
primaryButtonText: string;
2829
/** Upload progress (0-100) */
2930
progress: number;
3031
/** Error message if upload fails */
@@ -62,6 +63,7 @@ const FileUploadAreaComponent = forwardRef<HTMLDivElement, FileUploadAreaProps>(
6263
dataTestId,
6364
className = "",
6465
maxFileSizeMB = 20,
66+
primaryButtonText = "Done",
6567
...restProps
6668
}, ref) => {
6769
const { t } = useTranslation();
@@ -172,115 +174,111 @@ const FileUploadAreaComponent = forwardRef<HTMLDivElement, FileUploadAreaProps>(
172174
</>
173175
), [fileInputId, handleChange, fileType, t, maxFileSizeMB]);
174176

175-
// Memoized upload status component
176-
const renderUploadStatus = useCallback(() => {
177-
// Map states to config
178-
const stateConfig = {
179-
uploading: {
180-
status: t(`Importing ${file?.name}`),
181-
label: t("Cancel"),
182-
onClick: () => onCancel?.(),
183-
},
184-
error: {
185-
status: t("There was an error in the upload"),
186-
label: t("Try Again"),
187-
onClick: () => file && onRetry?.(file),
188-
},
189-
completed: {
190-
status: t("Upload Complete!"),
191-
label: t("Done"),
192-
onClick: () => onDone?.(),
193-
},
194-
};
177+
const renderUploadStatus = useCallback(() => {
178+
return (
179+
<div
180+
className="upload-progress-section"
181+
aria-label={t("File upload progress")}
182+
data-testid="file-upload-progress"
183+
>
184+
<FileUploadIcon aria-hidden="true" />
185+
186+
<div
187+
className="upload-progress-bar"
188+
aria-label={t("Upload progress bar")}
189+
data-testid="file-upload-progress-bar"
190+
>
191+
<CustomProgressBar
192+
progress={progress}
193+
color={uploadState.isError ? "error" : undefined}
194+
/>
195+
</div>
196+
197+
<p className="upload-status" aria-live="polite">
198+
{uploadState.isUploading
199+
? t(`Importing ${file?.name}`)
200+
: uploadState.isError
201+
? t("There was an error in the upload")
202+
: t("Upload Complete!")}
203+
</p>
204+
</div>
205+
);
206+
}, [uploadState, file, progress, t]);
195207

196-
// Determine current state
197-
const current =
198-
uploadState.isUploading
199-
? stateConfig.uploading
200-
: uploadState.isError && file
201-
? stateConfig.error
202-
: uploadState.isCompleted
203-
? stateConfig.completed
204-
: null;
208+
const containerClassName = useMemo(
209+
() =>
210+
buildClassNames(
211+
"file-upload",
212+
uploadState.hasFile && "file-upload-progress",
213+
isDragOver && "file-upload-dragover",
214+
className
215+
),
216+
[uploadState.hasFile, isDragOver, className]
217+
);
205218

206219
return (
207220
<div
208-
className="upload-progress"
209-
aria-label={t("File upload progress")}
210-
data-testid="file-upload-progress"
221+
ref={ref}
222+
role="button"
223+
data-testid={dataTestId || "file-upload-container"}
224+
className={containerClassName}
225+
tabIndex={0}
226+
onDragOver={handleDragOver}
227+
onDragLeave={handleDragLeave}
228+
onDrop={handleDrop}
229+
onClick={handleAreaClick}
230+
onKeyDown={handleKeyDown}
231+
aria-label={ariaLabel || t("Upload file area")}
232+
{...restProps}
211233
>
212-
<FileUploadIcon aria-hidden="true" />
213-
214-
<div
215-
className="upload-progress-bar"
216-
aria-label={t("Upload progress bar")}
217-
data-testid="file-upload-progress-bar"
218-
>
219-
<CustomProgressBar
220-
progress={progress}
221-
color={uploadState.isError ? "error" : undefined}
222-
/>
223-
</div>
234+
{!uploadState.hasFile ? (
235+
renderUploadPrompt()
236+
) : (
237+
<div className="upload-status-section">
238+
{renderUploadStatus()}
224239

225-
{/* Status text */}
226-
{current?.status && (
227-
<p
228-
className="upload-status"
229-
aria-live="polite"
230-
data-testid="file-upload-status"
231-
>
232-
{current.status}
233-
</p>
234-
)}
240+
{(uploadState.isUploading ||
241+
uploadState.isCompleted ||
242+
uploadState.isError) && (
243+
<div className="upload-action-row">
235244

236-
{/* Action button */}
237-
{current && (
238-
<V8CustomButton
239-
className="file-upload-action-btn"
240-
label={current.label}
241-
onClick={current.onClick}
242-
ariaLabel={current.label}
243-
dataTestId="file-upload-action-btn"
244-
variant="secondary"
245-
/>
245+
{uploadState.isError && file && (
246+
<V8CustomButton
247+
className="file-upload-action-btn"
248+
label={t("Try Again")}
249+
onClick={() => onRetry?.(file)}
250+
ariaLabel={t("Try Again")}
251+
dataTestId="file-upload-retry-btn"
252+
variant="primary"
253+
/>
254+
)}
255+
{uploadState.isCompleted && (
256+
<V8CustomButton
257+
className="file-upload-action-btn"
258+
label={t(primaryButtonText)}
259+
onClick={onDone}
260+
ariaLabel={t(primaryButtonText)}
261+
dataTestId="file-upload-action-btn"
262+
variant="primary"
263+
/>
264+
)}
265+
{(uploadState.isUploading || uploadState.isCompleted) && (<V8CustomButton
266+
className="file-upload-action-btn"
267+
label="Cancel"
268+
onClick={onCancel}
269+
ariaLabel="Cancel File Upload"
270+
dataTestId="file-upload-cancel-btn"
271+
variant="secondary"
272+
/>)}
273+
</div>
274+
)}
275+
</div>
246276
)}
247277
</div>
248278
);
249-
}, [uploadState, file, progress, t, onCancel, onRetry, onDone]);
250-
251-
// Memoized container className
252-
const containerClassName = useMemo(() => buildClassNames(
253-
"file-upload",
254-
uploadState.hasFile && "file-upload-progress",
255-
isDragOver && "file-upload-dragover",
256-
className
257-
), [uploadState.hasFile, isDragOver, className]);
258-
259-
return (
260-
<div
261-
ref={ref}
262-
role="button"
263-
data-testid={dataTestId || "file-upload-container"}
264-
className={containerClassName}
265-
tabIndex={0}
266-
onDragOver={handleDragOver}
267-
onDragLeave={handleDragLeave}
268-
onDrop={handleDrop}
269-
onClick={handleAreaClick}
270-
onKeyDown={handleKeyDown}
271-
aria-label={ariaLabel || t("Upload file area")}
272-
{...restProps}
273-
>
274-
{!uploadState.hasFile ? renderUploadPrompt() : renderUploadStatus()}
275-
</div>
276-
);
277-
});
279+
}
280+
);
278281

279-
// Set display name for better debugging
280282
FileUploadAreaComponent.displayName = "FileUploadArea";
281-
282-
// Export memoized component for performance optimization
283283
export const FileUploadArea = memo(FileUploadAreaComponent);
284-
285-
// Export types for consumers
286284
export type { FileUploadAreaProps };
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import React, { useEffect, useState } from "react";
2+
import { useTranslation } from "react-i18next";
3+
import { FileUploadArea } from "./FileUploadArea";
4+
5+
interface FileItem {
6+
form?: {
7+
majorVersion: number;
8+
minorVersion: number;
9+
};
10+
workflow?: {
11+
majorVersion: number;
12+
minorVersion: number;
13+
};
14+
}
15+
16+
interface CustomProgressBarProps {
17+
progress: number;
18+
color: string;
19+
}
20+
21+
interface ProcessVersion {
22+
majorVersion: number;
23+
minorVersion: number;
24+
type: string;
25+
}
26+
27+
interface FileUploadPanelProps {
28+
onClose: () => void;
29+
uploadActionType: {
30+
IMPORT: string;
31+
VALIDATE: string;
32+
};
33+
importError: string | null;
34+
importLoader: boolean;
35+
formName: string;
36+
description: string;
37+
handleImport: (
38+
file: File,
39+
uploadActionType: string,
40+
layoutVersion: string | null,
41+
flowVersion: string | null
42+
) => void;
43+
fileItems: FileItem | null;
44+
fileType: string;
45+
primaryButtonText: string;
46+
headerText: string;
47+
processVersion: ProcessVersion | null;
48+
CustomProgressBarProps?: CustomProgressBarProps | null;
49+
}
50+
51+
const FileUploadPanel: React.FC<FileUploadPanelProps> = React.memo(
52+
({
53+
uploadActionType,
54+
importError,
55+
handleImport,
56+
fileItems,
57+
fileType,
58+
primaryButtonText,
59+
}) => {
60+
const { t } = useTranslation();
61+
const [selectedFile, setSelectedFile] = useState<File | null>(null);
62+
const [uploadProgress, setUploadProgress] = useState(0);
63+
64+
const selectedLayoutVersion = {
65+
value: "minor",
66+
label: t(
67+
`import as version ${fileItems?.form?.majorVersion}.${
68+
fileItems?.form?.minorVersion + 1
69+
} (impacts previous and new submissions)`
70+
),
71+
};
72+
73+
const selectedFlowVersion = { value: "true", label: t("Skip, do not import") };
74+
const resetState = () => {
75+
setSelectedFile(null);
76+
setUploadProgress(0);
77+
};
78+
79+
const onImport = () => {
80+
if (selectedFile) {
81+
handleImport(
82+
selectedFile,
83+
uploadActionType.IMPORT,
84+
selectedLayoutVersion?.value,
85+
selectedFlowVersion?.value
86+
);
87+
}
88+
};
89+
90+
const handleRetry = () => {
91+
if (selectedFile) {
92+
handleImport(
93+
selectedFile,
94+
uploadActionType.VALIDATE,
95+
selectedLayoutVersion?.value ?? null,
96+
selectedFlowVersion?.value ?? null
97+
);
98+
}
99+
};
100+
101+
useEffect(() => {
102+
let isMounted = true;
103+
if (selectedFile) {
104+
handleRetry();
105+
let start: number | null = null;
106+
const duration = 2000;
107+
const maxProgress = importError ? 50 : 100;
108+
109+
const animateProgress = (timestamp: number) => {
110+
if (!isMounted) return;
111+
if (!start) start = timestamp;
112+
const progress = Math.min(
113+
((timestamp - start) / duration) * maxProgress,
114+
maxProgress
115+
);
116+
setUploadProgress(progress);
117+
if (progress < maxProgress) {
118+
requestAnimationFrame(animateProgress);
119+
}
120+
};
121+
122+
const animation = requestAnimationFrame(animateProgress);
123+
return () => {
124+
isMounted = false;
125+
cancelAnimationFrame(animation);
126+
};
127+
}
128+
}, [selectedFile, importError]);
129+
return (
130+
<div className="import-section-container p-4">
131+
132+
<div className="import-section-body">
133+
<div className="d-flex justify-content-center mb-3">
134+
<FileUploadArea
135+
primaryButtonText={primaryButtonText}
136+
fileType={fileType}
137+
onFileSelect={setSelectedFile}
138+
file={selectedFile}
139+
progress={uploadProgress}
140+
error={importError}
141+
onRetry={handleRetry}
142+
onCancel={resetState}
143+
onDone={onImport}
144+
/>
145+
</div>
146+
</div>
147+
</div>
148+
);
149+
}
150+
);
151+
152+
export default FileUploadPanel;

forms-flow-components/src/components/CustomComponents/ImportModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ export const ImportModal: React.FC<ImportModalProps> = React.memo(
461461
<Modal.Body className="p-5">
462462
<div className="d-flex justify-content-center">
463463
<FileUploadArea
464+
primaryButtonText={primaryButtonText}
464465
fileType={fileType}
465466
onFileSelect={setSelectedFile}
466467
file={selectedFile}

0 commit comments

Comments
 (0)