Skip to content

Commit af9b4ab

Browse files
authored
Merge pull request #890 from AOT-Technologies/V8Feature/FWF-5276-V8-Edit-Settings-MF
FWF-5276:[V8Feature] - Form Edit Actions MF
2 parents 383e0dd + 395ca55 commit af9b4ab

File tree

10 files changed

+388
-119
lines changed

10 files changed

+388
-119
lines changed

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

Lines changed: 99 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,114 @@ 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+
let statusText = "";
179+
if (uploadState.isUploading) {
180+
statusText = t(`Importing ${file?.name}`);
181+
} else if (uploadState.isError) {
182+
statusText = t("There was an error in the upload");
183+
} else {
184+
statusText = t("Upload Complete!");
185+
}
186+
return (
187+
<div
188+
className="upload-progress-section"
189+
aria-label={t("File upload progress")}
190+
data-testid="file-upload-progress"
191+
>
192+
<FileUploadIcon aria-hidden="true" />
193+
194+
<div
195+
className="upload-progress-bar"
196+
aria-label={t("Upload progress bar")}
197+
data-testid="file-upload-progress-bar"
198+
>
199+
<CustomProgressBar
200+
progress={progress}
201+
color={uploadState.isError ? "error" : undefined}
202+
/>
203+
</div>
204+
<p className="upload-status" aria-live="polite">
205+
{statusText}
206+
</p>
207+
</div>
208+
);
209+
}, [uploadState, file, progress, t]);
195210

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;
211+
const containerClassName = useMemo(
212+
() =>
213+
buildClassNames(
214+
"file-upload",
215+
uploadState.hasFile && "file-upload-progress",
216+
isDragOver && "file-upload-dragover",
217+
className
218+
),
219+
[uploadState.hasFile, isDragOver, className]
220+
);
205221

206222
return (
207223
<div
208-
className="upload-progress"
209-
aria-label={t("File upload progress")}
210-
data-testid="file-upload-progress"
224+
ref={ref}
225+
role="button"
226+
data-testid={dataTestId || "file-upload-container"}
227+
className={containerClassName}
228+
tabIndex={0}
229+
onDragOver={handleDragOver}
230+
onDragLeave={handleDragLeave}
231+
onDrop={handleDrop}
232+
onClick={handleAreaClick}
233+
onKeyDown={handleKeyDown}
234+
aria-label={ariaLabel || t("Upload file area")}
235+
{...restProps}
211236
>
212-
<FileUploadIcon aria-hidden="true" />
237+
{!uploadState.hasFile ? (
238+
renderUploadPrompt()
239+
) : (
240+
<div className="upload-status-section">
241+
{renderUploadStatus()}
213242

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>
224-
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-
)}
243+
{(uploadState.isUploading ||
244+
uploadState.isCompleted ||
245+
uploadState.isError) && (
246+
<div className="upload-action-row">
235247

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-
/>
248+
{uploadState.isError && file && (
249+
<V8CustomButton
250+
className="file-upload-action-btn"
251+
label={t("Try Again")}
252+
onClick={() => onRetry?.(file)}
253+
ariaLabel={t("Try Again")}
254+
dataTestId="file-upload-retry-btn"
255+
variant="primary"
256+
/>
257+
)}
258+
{uploadState.isCompleted && (
259+
<V8CustomButton
260+
className="file-upload-action-btn"
261+
label={t(primaryButtonText)}
262+
onClick={onDone}
263+
ariaLabel={t(primaryButtonText)}
264+
dataTestId="file-upload-action-btn"
265+
variant="primary"
266+
/>
267+
)}
268+
{(uploadState.isUploading || uploadState.isCompleted) && (<V8CustomButton
269+
className="file-upload-action-btn"
270+
label="Cancel"
271+
onClick={onCancel}
272+
ariaLabel="Cancel File Upload"
273+
dataTestId="file-upload-cancel-btn"
274+
variant="secondary"
275+
/>)}
276+
</div>
277+
)}
278+
</div>
246279
)}
247280
</div>
248281
);
249-
}, [uploadState, file, progress, t, onCancel, onRetry, onDone]);
282+
}
283+
);
250284

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-
});
278-
279-
// Set display name for better debugging
280285
FileUploadAreaComponent.displayName = "FileUploadArea";
281-
282-
// Export memoized component for performance optimization
283286
export const FileUploadArea = memo(FileUploadAreaComponent);
284-
285-
// Export types for consumers
286287
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)