Skip to content

Commit

Permalink
Merge pull request #35 from CarnegieLearningWeb/dev
Browse files Browse the repository at this point in the history
improvements to validation logic
  • Loading branch information
emoltz authored Dec 4, 2024
2 parents 715a245 + ce3ce3e commit 60ddb30
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 136 deletions.
40 changes: 24 additions & 16 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import './App.css';
import {useContext, useEffect, useState} from 'react';
import {GlobalDataType, GraphData} from './lib/types';
import DirectedGraph from './components/DirectedGraph';
import DropZone from './components/DropZone';
// import { NavBar } from './components/NavBar';

import {Button} from './components/ui/button';
import {Context} from './Context';
import {processDataShopData} from './lib/dataProcessingUtils';
import Loading from './components/Loading';

function App() {

const {resetData, setGraphData, setLoading, data, setData, graphData, loading} = useContext(Context)
const [showDropZone, setShowDropZone] = useState<boolean>(true)

const {resetData, setGraphData, setLoading, data, setData, loading, error, setError} = useContext(Context);
const [showDropZone, setShowDropZone] = useState<boolean>(true);
const handleData = (data: GlobalDataType[]) => {
setData(data)
setShowDropZone(false)
Expand All @@ -23,6 +21,10 @@ function App() {
setLoading(loading)
}

const handleError = (errorMessage: string) => {
setError(errorMessage);
}

useEffect(() => {
if (data) {
const graphData: GraphData = processDataShopData(data)
Expand All @@ -45,7 +47,13 @@ function App() {
>
Reset
</Button>

{error && (
<div className="text-red-500 p-4 m-4 bg-red-50 rounded-md">
{error.split('\n').map((errorLine, index) => (
<p key={index} className="mb-1">{errorLine}</p>
))}
</div>
)}
<div className=" flex items-center justify-center pt-20">
{
loading ?
Expand All @@ -54,23 +62,23 @@ function App() {
(
showDropZone && (
<div className="">
<DropZone afterDrop={handleData} onLoadingChange={handleLoading}/>
<DropZone afterDrop={handleData} onLoadingChange={handleLoading}
onError={handleError}/>
</div>
)

)

}


{
graphData && (
<>
{/* TODO: Swap DirectedGraph for your new component */}
<DirectedGraph graphData={graphData}/>
</>
)
}
{/*{*/}
{/* graphData && (*/}
{/* <>*/}
{/* /!* TODO: Swap DirectedGraph for your new component *!/*/}
{/* <DirectedGraph graphData={graphData}/>*/}
{/* </>*/}
{/* )*/}
{/*}*/}

</div>
</div>
Expand Down
29 changes: 24 additions & 5 deletions src/Context.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createContext, useState } from 'react';
import {createContext, ReactNode, useState} from 'react';
import { GlobalDataType, GraphData } from './lib/types';
// import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
interface ContextInterface {
data: GlobalDataType[] | null;
error: string | null;
setError: (error: string | null) => void;
graphData: GraphData | null;
loading: boolean;
setLoading: (loading: boolean) => void;
Expand All @@ -18,18 +21,32 @@ const initialState = {
}

interface ProviderProps {
children: React.ReactNode;
children: ReactNode;
}
export const Provider = ({ children }: ProviderProps) => {
const [data, setData] = useState<GlobalDataType[] | null>(initialState.data)
const [graphData, setGraphData] = useState<GraphData | null>(initialState.graphData)
const [loading, setLoading] = useState<boolean>(initialState.loading)

const [error, setError] = useState<string | null>(null)
// const queryClient = useQueryClient();

// const { data: uploadedData } = useQuery<GlobalDataType[]>({
// queryKey: ['uploadedData'],
// staleTime: Infinity,
// });
//
// const { mutate: uploadData } = useMutation({
// mutationKey: ['uploadedData'],
// mutationFn: async (data: GlobalDataType[]) => data,
// onSuccess: (data) => {
// queryClient.setQueryData(['uploadedData'], data);
// },
// });

const resetData = () => {
setData(null)
setError(null)
setGraphData(null)
console.log("Data reset");

}

return (
Expand All @@ -38,6 +55,8 @@ export const Provider = ({ children }: ProviderProps) => {
data,
graphData,
loading,
error,
setError,
setLoading,
setData,
setGraphData,
Expand Down
12 changes: 0 additions & 12 deletions src/components/Debug.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
import { GlobalDataType } from "@/lib/types";
import { useState } from "react";
import DropZone from "./DropZone";

export default function Debug() {
const [data, setData] = useState<GlobalDataType[]>([])
const handleData = (data: GlobalDataType[]) => {
setData(data)
console.log("Data from file: ", data);

}

const handleLoading = (loading: boolean) => {
}

return (
<>

<DropZone afterDrop={handleData} onLoadingChange={handleLoading} />
</>
)
}
119 changes: 54 additions & 65 deletions src/components/DropZone.tsx
Original file line number Diff line number Diff line change
@@ -1,100 +1,90 @@
import { useCallback, useState } from 'react';
import { Accept, useDropzone } from 'react-dropzone';
import { GlobalDataType } from '@/lib/types';
import { parseData } from '@/lib/utils';
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import toast from 'react-hot-toast';
import {useCallback, useState} from 'react';
import {Accept, useDropzone} from 'react-dropzone';
import {GlobalDataType, ParseResult} from '@/lib/types';
import {parseData} from '@/lib/utils';
import {Label} from "@/components/ui/label"
import {RadioGroup, RadioGroupItem} from "@/components/ui/radio-group"

interface DropZoneProps {
afterDrop: (data: GlobalDataType[]) => void,
onLoadingChange: (loading: boolean) => void
onLoadingChange: (loading: boolean) => void,
onError: (error: string) => void,
}
// TODO move this up to App.tsx so I can better handle errors

export default function DropZone({ afterDrop, onLoadingChange }: DropZoneProps) {
const delimiters = ["tsv", "csv", "pipe"];
const [errorMessage, setErrorMessage] = useState<string>("");
export default function DropZone({afterDrop, onLoadingChange, onError}: DropZoneProps) {
const delimiters = ["csv", "tsv"];

const [fileType, setFileType] = useState<string>(delimiters[0])
const [fileType, setFileType] = useState<string>(delimiters[1])

const onDrop = useCallback((acceptedFiles: File[]) => {
onLoadingChange(true);

acceptedFiles.forEach((file: File) => {
// Add this file type detection
const fileExtension = file.name.split('.').pop()?.toLowerCase() || '';
const detectedFileType = fileExtension === 'json' ? 'json' :
fileExtension === 'tsv' ? 'tsv' :
fileExtension === 'csv' ? 'csv' : fileType;

const reader = new FileReader();

reader.onabort = () => console.warn('file reading was aborted');
reader.onerror = () => console.error('file reading has failed');
reader.onload = () => {
const textStr = reader.result;
let delimiter: string;
switch (fileType) {
// Use detectedFileType instead of fileType
switch (detectedFileType) {
case 'json':
delimiter = '';
break;
case 'tsv':
delimiter = '\t';
break;
case 'csv':
delimiter = ',';
break;
case 'pipe':
delimiter = '|';
break;
default:
delimiter = '\t';
delimiter = ',';
break;
}
const array: GlobalDataType[] | null = parseData(textStr, delimiter);
console.log("Array from file: ", array);
// array is null when there is an error in the file structure or content
if (!array) {

toast.error("Invalid file structure or content")
console.log("Error state before: ", errorMessage);
setErrorMessage("Invalid file structure or content");
console.log("Error state after: ", errorMessage);

// the below prints, but the above does not execute. Why?
// console.error("!!!Invalid file structure or content");
}
else {
afterDrop(array);
}

const array: ParseResult = parseData(textStr, delimiter);
if (!array.data) {
onError(array.error?.details.join('\n') || 'Error parsing file');
} else {
afterDrop(array.data);
}

onLoadingChange(false);
};
reader.readAsText(file);
onLoadingChange(false);
// console.log("File: ", file);

});
}, [fileType, afterDrop, onLoadingChange]);

const acceptedFileTypes: Accept = {
'text/tab-separated-values': ['.tsv'],
'text/csv': ['.csv'],
'text/plain': ['.txt', '.csv', '.tsv', '.json', '.pipe']
'text/plain': ['.txt', '.csv', '.tsv', '.json']
};



const { getRootProps, getInputProps, isDragActive, isFocused, isDragReject } = useDropzone({
const {getRootProps, getInputProps, isDragActive, isFocused, isDragReject} = useDropzone({
onDrop,
accept: acceptedFileTypes,
validator: (file) => {
// returns FileError | Array.<FileError> | null
if (!acceptedFileTypes[file.type]) {
return {
code: 'file-invalid-type',
message: 'Invalid file type',
}
}
return null;
}
// validator: (file) => {
// // returns FileError | Array.<FileError> | null
// if (!acceptedFileTypes[file.type]) {

// return {
// code: 'file-invalid-type',
// message: 'Invalid file type',
// }
// }
// return null;
// }
});



const fileTypeOptions = [
{
Expand All @@ -105,14 +95,14 @@ export default function DropZone({ afterDrop, onLoadingChange }: DropZoneProps)
label: 'Comma Separated',
value: delimiters.find((delimiter) => delimiter === 'csv') as string
},
{
label: 'Pipe Separated',
value: delimiters.find((delimiter) => delimiter === 'pipe') as string
},
// {
// label: 'JSON',
// value: delimiters.find((delimiter) => delimiter === 'json') as string
// }
// label: 'Pipe Separated',
// value: delimiters.find((delimiter) => delimiter === 'pipe') as string
// },
{
label: 'JSON',
value: delimiters.find((delimiter) => delimiter === 'json') as string
}
]
return (
<>
Expand All @@ -126,7 +116,7 @@ export default function DropZone({ afterDrop, onLoadingChange }: DropZoneProps)
}}>
{fileTypeOptions.map((option, index) => (
<div className="flex items-center space-x-2" key={index}>
<RadioGroupItem value={option.value} key={option.value} />
<RadioGroupItem value={option.value} key={option.value}/>
<Label htmlFor={option.value}>{option.label}</Label>
</div>
))}
Expand All @@ -143,15 +133,14 @@ export default function DropZone({ afterDrop, onLoadingChange }: DropZoneProps)
<p className={""}>Drag 'n' drop some files here, or click to select files</p>
</div>
:
<div className={`flex items-center h-full w-[fitcontent] justify-center bg-slate-100 rounded-lg p-2`}>
<div
className={`flex items-center h-full w-[fitcontent] justify-center bg-slate-100 rounded-lg p-2`}>
<p className={""}>Drag 'n' drop some files here, or click to select files</p>
</div>
}
{isDragReject && <p className="text-red-500">Invalid file type</p>}

<div className="">
{errorMessage && <p className="text-red-500 pt-10">{errorMessage}</p>}
</div>

</div>


Expand Down
10 changes: 9 additions & 1 deletion src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,12 @@ export type StudentPath = {
currentStep: number;
problemId?: string;
sectionId?: string;
}
}

export type ParseResult = {
data: GlobalDataType[] | null;
error?: {
type: 'parsing' | 'validation';
details: string[];
};
}
Loading

0 comments on commit 60ddb30

Please sign in to comment.