Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ab8ad7d
porting over User import/export interface to extend to topics (SignUp…
nadkarnik Mar 21, 2026
ca5fc32
frontend fixes to see user roles and add them to teams manually.
nadkarnik Mar 22, 2026
bfd0e81
Merge branch 'expertiza:main' into kdnadkar/frontend-changes-for-imp-…
nadkarnik Mar 25, 2026
bec59d2
using ProjectTopic now instead of SignUpTopic
nadkarnik Mar 29, 2026
fd2b760
Revert package file changes
nadkarnik Mar 29, 2026
8bb5be7
Forcing topics tab to use backend assignments (from sister PR which w…
nadkarnik Mar 29, 2026
73a47f8
Updated handling to use backend data instead of frontend examples.
nadkarnik Mar 29, 2026
0a8227c
Several changes to frontend relating to teams. Specifically assignme…
nadkarnik Mar 29, 2026
acc9b49
Merge pull request #1 from kamatmihir2002/kdnadkar/frontend-changes-f…
nadkarnik Mar 30, 2026
195395a
Merge pull request #2 from kamatmihir2002/kdnadkar/fixing-teams-not-p…
kamatmihir2002 Mar 30, 2026
2d61ca9
Change exportmodal to download multiple files for graph-export
kamatmihir2002 Mar 30, 2026
80fe3a1
Add export button to questionnaire window
kamatmihir2002 Mar 30, 2026
5bb4c6c
fixing duplicate downloads of csv export.
nadkarnik Mar 30, 2026
12f0bd7
utilize useCallback for reset function in useAPI hook
nadkarnik Mar 30, 2026
b7ed663
Merge pull request #5 from kamatmihir2002/kdnadkar/fixing-duplicate-d…
nadkarnik Mar 30, 2026
7305824
Merge pull request #3 from kamatmihir2002/mskamat/graph-export-integr…
nadkarnik Mar 30, 2026
3e8ef78
Add questionnaire import button
kamatmihir2002 Mar 31, 2026
4ff7a87
Add questionnaire imports
kamatmihir2002 Mar 31, 2026
f2032ed
Added timer to ImportModal to allow for modal closing to only occur o…
nadkarnik Apr 3, 2026
9d3fd59
Merge pull request #6 from kamatmihir2002/mskamat/add-questionnaire-i…
kamatmihir2002 Apr 3, 2026
cdf817b
Merge branch 'main' into kdnadkar/fixing-spammed-GETs
kamatmihir2002 Apr 3, 2026
33c0021
Merge pull request #7 from kamatmihir2002/kdnadkar/fixing-spammed-GETs
kamatmihir2002 Apr 3, 2026
01299ce
Add option to download sample csv when importing
kamatmihir2002 Apr 3, 2026
814935a
Merge pull request #8 from kamatmihir2002/mskamat/add_blank_header_im…
nadkarnik Apr 3, 2026
529632e
Branch start for final changes
kamatmihir2002 Apr 3, 2026
2d1026d
Remove export-submodels button from frontend
kamatmihir2002 Apr 3, 2026
92030e4
Merge pull request #9 from kamatmihir2002/mskamat/final_changes
kamatmihir2002 Apr 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ function App() {

{
path: "assignments/edit/:assignmentId/participants",
element: <Participants type="student_tasks" id={1} />,
element: <Participants type="assignments" id={1} />,
children: [
{
path: "new",
Expand Down
216 changes: 144 additions & 72 deletions src/components/Modals/ExportModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// src/components/ExportModal.tsx
import React, { useEffect, useState, memo, useCallback } from "react";
import React, { useEffect, useState, memo, useCallback, useRef } from "react";
import { Modal, Button, Form, Row, Col, OverlayTrigger, Tooltip } from "react-bootstrap";
import useAPI from "../../hooks/useAPI";
import { HttpMethod } from "../../utils/httpMethods";
Expand Down Expand Up @@ -80,34 +80,85 @@ Icon.displayName = 'Icon';
type ExportModal = {
show: boolean;
onHide: () => void;
modelClass: string;
modelClass: string;
contextParams?: Record<string, string | number | undefined>;
};

type ExportFilePayload = {
name: string;
contents: string;
};

/* =============================================================================
Component (dummy mode – no backend)
============================================================================= */

const ExportModal: React.FC<ExportModal> = ({ show, onHide, modelClass }) => {
const [mandatoryFields, setMandatoryFields] = useState<string[]>([]);
const [optionalFields, setOptionalFields] = useState<string[]>([]);
const [externalFields, setExternalFields] = useState<string[]>([]);
const [allFields, setAllFields] = useState<string[]>([]);
const [selectedFields, setSelectedFields] = useState<string[]>([]);
const ExportModal: React.FC<ExportModal> = ({ show, onHide, modelClass, contextParams }) => {
const [mandatoryFields, setMandatoryFields] = useState<string[]>([]);
const [optionalFields, setOptionalFields] = useState<string[]>([]);
const [externalFields, setExternalFields] = useState<string[]>([]);
const [allFields, setAllFields] = useState<string[]>([]);
const [selectedFields, setSelectedFields] = useState<string[]>([]);
const [status, setStatus] = useState<string>('');
const { error, isLoading, data: exportResponse, sendRequest: fetchExports } = useAPI();
const { data: sendExportResponse, error: exportError, sendRequest: sendExport } = useAPI();

const fetchConfig = useCallback(async () => {
try {
fetchExports({ url: `/export/${modelClass}` });
// Handle the responses as needed
} catch (err) {
// Handle any errors that occur during the fetch
console.error("Error fetching data:", err);
const { isLoading, data: exportResponse, sendRequest: fetchExports } = useAPI();
const {
data: sendExportResponse,
error: exportError,
sendRequest: sendExport,
reset: resetExportState,
} = useAPI();
const hasHandledExportResponse = useRef(false);
const teamParticipantFields = useCallback(
(fields: string[]) => fields.filter((field) => field.startsWith("participant_")),
[]
);
const normalizeTeamFieldList = useCallback(
(fields: string[]) => {
if (modelClass !== "Team") return fields;

const participantFields = teamParticipantFields(fields);
if (participantFields.length === 0) return fields;

const nonParticipantFields = fields.filter((field) => !field.startsWith("participant_"));
return [...nonParticipantFields, "participant_ids"];
},
[modelClass, teamParticipantFields]
);
const expandTeamFieldSelection = useCallback(
(fields: string[], sourceFields: string[]) => {
if (modelClass !== "Team" || !fields.includes("participant_ids")) return fields;

const participantFields = teamParticipantFields(sourceFields);
const expandedFields = fields.flatMap((field) =>
field === "participant_ids" ? participantFields : [field]
);

return Array.from(new Set(expandedFields));
},
[modelClass, teamParticipantFields]
);

const fetchConfig = useCallback(async () => {
hasHandledExportResponse.current = false;
resetExportState(true, true);

try {
const params = new URLSearchParams();
Object.entries(contextParams || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
params.append(key, String(value));
}
}, [fetchExports]);
});

const url = params.toString() ? `/export/${modelClass}?${params.toString()}` : `/export/${modelClass}`;
fetchExports({ url });
} catch (err) {
console.error("Error fetching data:", err);
}
}, [contextParams, fetchExports, modelClass]);

const transformField = (field: string) => {
if (field === "participant_ids") return "Participant IDs";
let f = field.replace(/_/g, " ");
return f.charAt(0).toUpperCase() + f.slice(1);
};
Expand All @@ -124,27 +175,32 @@ const ExportModal: React.FC<ExportModal> = ({ show, onHide, modelClass }) => {
useEffect(() => {
if (!show) return;

fetchConfig()
}, [show]);
hasHandledExportResponse.current = false;
resetExportState(true, true);
fetchConfig();
}, [fetchConfig, resetExportState, show]);

useEffect(() => {
if (exportResponse) {
setMandatoryFields(exportResponse.data.mandatory_fields);
setOptionalFields(exportResponse.data.optional_fields);
const normalizedMandatoryFields = normalizeTeamFieldList(exportResponse.data.mandatory_fields);
const normalizedOptionalFields = normalizeTeamFieldList(exportResponse.data.optional_fields);

setMandatoryFields(normalizedMandatoryFields);
setOptionalFields(normalizedOptionalFields);
setExternalFields(exportResponse.data.external_fields);

const fields = [
...exportResponse.data.mandatory_fields,
...exportResponse.data.optional_fields,
...normalizedMandatoryFields,
...normalizedOptionalFields,
...exportResponse.data.external_fields
]
];

setAllFields(fields)
setSelectedFields(exportResponse.data.mandatory_fields)
setAllFields(Array.from(new Set(fields)));
setSelectedFields(normalizedMandatoryFields);

setStatus('');
}
}, [exportResponse]);
}, [exportResponse, normalizeTeamFieldList]);

const toggleField = (field: string) => {
setSelectedFields((prev) =>
Expand All @@ -170,35 +226,35 @@ const ExportModal: React.FC<ExportModal> = ({ show, onHide, modelClass }) => {
});
};

function getFormattedDateTimeForFilename() {
const getTimestampForFilename = () => {
const now = new Date();

// Get year, month, day
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed
const day = String(now.getDate()).padStart(2, '0');

// Get hours, minutes, seconds
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
const seconds = String(now.getSeconds()).padStart(2, "0");

// Combine into a string without invalid characters
return `${year}${month}${day}_${hours}${minutes}${seconds}`;
}

const downloadFile = (file) => {
const url = window.URL.createObjectURL(new Blob([file]))
const link = document.createElement('a')
link.href = url

const timestamp = Date.now().toLocaleString();
};

link.setAttribute('download', `${modelClass}_export_${getFormattedDateTimeForFilename()}.csv`)
document.body.appendChild(link)
link.click()
link.remove()
}
const downloadFiles = (files: ExportFilePayload[]) => {
const timestamp = getTimestampForFilename();

files.forEach((file) => {
const blob = new Blob([file.contents], { type: "text/csv;charset=utf-8;" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
const baseName = file.name.replace(/\.csv$/i, "");
const filename = `${baseName}_${timestamp}.csv`;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
});
};
const on_export = async () => {
if (selectedFields.length === 0) {
setStatus('Please select at least one field.');
Expand All @@ -210,22 +266,31 @@ const ExportModal: React.FC<ExportModal> = ({ show, onHide, modelClass }) => {
try {
const formData = new FormData();

const orderedFields = allFields.filter((f) => selectedFields.includes(f));
const orderedFields = expandTeamFieldSelection(
allFields.filter((f) => selectedFields.includes(f)),
exportResponse?.data
? [
...exportResponse.data.mandatory_fields,
...exportResponse.data.optional_fields,
...exportResponse.data.external_fields,
]
: allFields
);

formData.append("ordered_fields", JSON.stringify(orderedFields));

let url = `/export/${modelClass}`;
Object.entries(contextParams || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
formData.append(key, String(value));
}
});

await sendExport({
url,
url: `/export/${modelClass}`,
method: HttpMethod.POST,
data: formData,
headers: { "Content-Type": "multipart/form-data" },
});


console.log(sendExportResponse)

} catch (err: any) {
setStatus(err.message || "Unexpected error.");
}
Expand All @@ -234,17 +299,24 @@ const ExportModal: React.FC<ExportModal> = ({ show, onHide, modelClass }) => {


useEffect(() => {
if(sendExportResponse) {
setStatus(sendExportResponse.data.message);
downloadFile(sendExportResponse.data.file)
if (!show) return;

if (!exportError){
setTimeout(onHide, 1500);
if (sendExportResponse && !hasHandledExportResponse.current) {
hasHandledExportResponse.current = true;
setStatus(sendExportResponse.data.message);
const files = Array.isArray(sendExportResponse.data.file) ? sendExportResponse.data.file : [];
downloadFiles(files);

if (!exportError) {
setTimeout(() => {
resetExportState(true, true);
onHide();
}, 1500);
}
} else if (exportError) {
setStatus(exportError);
}
}, [sendExportResponse, exportError]);
}, [exportError, onHide, resetExportState, sendExportResponse, show]);

return (
<Modal
Expand Down Expand Up @@ -300,8 +372,8 @@ const ExportModal: React.FC<ExportModal> = ({ show, onHide, modelClass }) => {
</span>
</OverlayTrigger>
</div>
<div>
<strong>Optional fields:</strong>
<div>
<strong>External fields:</strong>
<OverlayTrigger
placement="right"
overlay={
Expand All @@ -310,11 +382,11 @@ const ExportModal: React.FC<ExportModal> = ({ show, onHide, modelClass }) => {
</Tooltip>
}
>
<span style={{ cursor: "help", marginLeft: 6 }}>
<Icon name="info" size={16} />
</span>
<span style={{ cursor: "help", marginLeft: 6 }}>
<Icon name="info" size={16} />
</span>
</OverlayTrigger>
</div>
</div>
</div>
</Col>
</Row>
Expand Down
Loading