Skip to content

Commit 13e5fda

Browse files
MyOpenCRE: add CSV import preview, confirmation step, and improved UX safeguards (#685)
feat(myopencre): add CSV import preview and confirmation flow
1 parent 59d7386 commit 13e5fda

2 files changed

Lines changed: 125 additions & 5 deletions

File tree

application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@
1212

1313
.myopencre-disabled {
1414
opacity: 0.7;
15+
}
16+
17+
.myopencre-preview {
18+
margin-top: 1rem;
1519
}

application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import './MyOpenCRE.scss';
22

3-
import React, { useState } from 'react';
3+
import React, { useRef, useState } from 'react';
44
import { Button, Container, Form, Header, Message } from 'semantic-ui-react';
55

66
import { useEnvironment } from '../../hooks';
@@ -19,6 +19,13 @@ type ImportErrorResponse = {
1919
errors?: RowValidationError[];
2020
};
2121

22+
type CsvPreview = {
23+
rows: number;
24+
creMappings: number;
25+
uniqueSections: number;
26+
creColumns: string[];
27+
};
28+
2229
export const MyOpenCRE = () => {
2330
const { apiUrl } = useEnvironment();
2431

@@ -30,6 +37,10 @@ export const MyOpenCRE = () => {
3037
const [error, setError] = useState<ImportErrorResponse | null>(null);
3138
const [success, setSuccess] = useState<any | null>(null);
3239
const [info, setInfo] = useState<string | null>(null);
40+
const [preview, setPreview] = useState<CsvPreview | null>(null);
41+
const [confirmedImport, setConfirmedImport] = useState(false);
42+
43+
const fileInputRef = useRef<HTMLInputElement>(null);
3344

3445
/* ------------------ CSV DOWNLOAD ------------------ */
3546

@@ -64,12 +75,60 @@ export const MyOpenCRE = () => {
6475
}
6576
};
6677

78+
/* ------------------ CSV PREVIEW ------------------ */
79+
80+
const generateCsvPreview = async (file: File) => {
81+
const text = await file.text();
82+
const lines = text.split('\n').filter(Boolean);
83+
84+
if (lines.length < 2) {
85+
setPreview(null);
86+
return;
87+
}
88+
89+
const headers = lines[0].split(',').map((h) => h.trim());
90+
const rows = lines.slice(1);
91+
92+
const creColumns = headers.filter((h) => h.startsWith('CRE'));
93+
let creMappings = 0;
94+
const sectionSet = new Set<string>();
95+
96+
rows.forEach((line) => {
97+
const values = line.split(',');
98+
const rowObj: Record<string, string> = {};
99+
100+
headers.forEach((h, i) => {
101+
rowObj[h] = (values[i] || '').trim();
102+
});
103+
104+
const name = (rowObj['standard|name'] || '').trim();
105+
const id = (rowObj['standard|id'] || '').trim();
106+
107+
if (name || id) {
108+
sectionSet.add(`${name}|${id}`);
109+
}
110+
111+
creColumns.forEach((col) => {
112+
if (rowObj[col]) creMappings += 1;
113+
});
114+
});
115+
116+
setPreview({
117+
rows: rows.length,
118+
creMappings,
119+
uniqueSections: sectionSet.size,
120+
creColumns,
121+
});
122+
};
123+
67124
/* ------------------ FILE SELECTION ------------------ */
68125

69126
const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
70127
setError(null);
71128
setSuccess(null);
72129
setInfo(null);
130+
setPreview(null);
131+
setConfirmedImport(false);
73132

74133
if (!e.target.files || e.target.files.length === 0) return;
75134

@@ -87,12 +146,13 @@ export const MyOpenCRE = () => {
87146
}
88147

89148
setSelectedFile(file);
149+
generateCsvPreview(file);
90150
};
91151

92152
/* ------------------ CSV UPLOAD ------------------ */
93153

94154
const uploadCsv = async () => {
95-
if (!selectedFile) return;
155+
if (!selectedFile || !confirmedImport) return;
96156

97157
setLoading(true);
98158
setError(null);
@@ -125,16 +185,25 @@ export const MyOpenCRE = () => {
125185
setInfo(
126186
'Import completed successfully, but no new CREs or standards were added because all mappings already exist.'
127187
);
188+
} else if (payload.import_type === 'empty') {
189+
setInfo('The uploaded CSV did not contain any importable rows. No changes were made.');
128190
} else {
129191
setSuccess(payload);
130192
}
131-
setSelectedFile(null);
193+
194+
setConfirmedImport(false);
195+
setPreview(null);
196+
if (fileInputRef.current) {
197+
fileInputRef.current.value = '';
198+
}
132199
} catch (err: any) {
133200
setError({
134201
success: false,
135202
type: 'CLIENT_ERROR',
136203
message: err.message || 'Unexpected error during import',
137204
});
205+
setPreview(null);
206+
setConfirmedImport(false);
138207
} finally {
139208
setLoading(false);
140209
}
@@ -211,15 +280,62 @@ export const MyOpenCRE = () => {
211280
</Message>
212281
)}
213282

283+
{confirmedImport && !loading && !success && !error && (
284+
<Message positive>
285+
CSV validated successfully. Click <strong>Upload CSV</strong> to start importing.
286+
</Message>
287+
)}
288+
289+
{preview && (
290+
<Message info className="myopencre-preview">
291+
<strong>Import Preview</strong>
292+
<ul>
293+
<li>Rows detected: {preview.rows}</li>
294+
<li>CRE mappings found: {preview.creMappings}</li>
295+
<li>Unique standard sections: {preview.uniqueSections}</li>
296+
<li>CRE columns detected: {preview.creColumns.join(', ')}</li>
297+
</ul>
298+
299+
<Button
300+
primary
301+
size="small"
302+
onClick={() => {
303+
setPreview(null);
304+
setConfirmedImport(true);
305+
}}
306+
>
307+
Confirm Import
308+
</Button>
309+
310+
<Button
311+
size="small"
312+
onClick={() => {
313+
setPreview(null);
314+
setConfirmedImport(false);
315+
setSelectedFile(null);
316+
if (fileInputRef.current) fileInputRef.current.value = '';
317+
}}
318+
>
319+
Cancel
320+
</Button>
321+
</Message>
322+
)}
323+
214324
<Form>
215325
<Form.Field>
216-
<input type="file" accept=".csv" disabled={!isUploadEnabled || loading} onChange={onFileChange} />
326+
<input
327+
ref={fileInputRef}
328+
type="file"
329+
accept=".csv"
330+
disabled={!isUploadEnabled || loading || !!preview}
331+
onChange={onFileChange}
332+
/>
217333
</Form.Field>
218334

219335
<Button
220336
primary
221337
loading={loading}
222-
disabled={!isUploadEnabled || !selectedFile || loading}
338+
disabled={!isUploadEnabled || !selectedFile || !confirmedImport || loading}
223339
onClick={uploadCsv}
224340
>
225341
Upload CSV

0 commit comments

Comments
 (0)