From 48586f444dda1ce984ba6dc9a3c08fd2e07e6b14 Mon Sep 17 00:00:00 2001 From: DAC Date: Mon, 29 Dec 2025 21:09:41 -0800 Subject: [PATCH 01/10] exercise page split #132 split up the exercise form into two pages. the base page will only display the table information and the new form page will display the edit for the selected exercise. the new path is `/create/exercise/:exercise_id` and will handle both the create, update, and delete operations for a single exercise. additional modifications: - modified the index route for `/create` to redirect to `/create/demo` vs rendering the page - modified the `ReusableTable` to optionall accept the `onEdit` function as the new table for the exercises uses the title column to navigate to the edit form - removed `ExerciseForm.tsx` as it is no longer needed - modified `Create.tsx` to properly handle overflow and allow proper scrolling in the main content area --- codewit/client/src/app/app.tsx | 18 +- .../src/components/form/ReusableTable.tsx | 22 +- codewit/client/src/hooks/useExercise.ts | 24 +- codewit/client/src/pages/Create.tsx | 4 +- codewit/client/src/pages/ExerciseForm.tsx | 336 ----------------- codewit/client/src/pages/create/exercise.tsx | 90 +++++ .../src/pages/create/exercise/exercise_id.tsx | 340 ++++++++++++++++++ 7 files changed, 477 insertions(+), 357 deletions(-) delete mode 100644 codewit/client/src/pages/ExerciseForm.tsx create mode 100644 codewit/client/src/pages/create/exercise.tsx create mode 100644 codewit/client/src/pages/create/exercise/exercise_id.tsx diff --git a/codewit/client/src/app/app.tsx b/codewit/client/src/app/app.tsx index 6e17b1c..e695edd 100644 --- a/codewit/client/src/app/app.tsx +++ b/codewit/client/src/app/app.tsx @@ -7,7 +7,7 @@ import CourseView from "../pages/CourseView"; import Read from '../pages/Read'; import Create from '../pages/Create'; import NotFound from '../components/notfound/NotFound'; -import ExerciseForms from '../pages/ExerciseForm'; +import { ExerciseView } from '../pages/create/exercise'; import ModuleForm from '../pages/ModuleForm'; import ResourceForm from '../pages/ResourceForm'; import CourseForm from '../pages/CourseForm'; @@ -25,23 +25,23 @@ export function App() { const { user, loading, handleLogout } = useAuth(); const location = useLocation(); const [searchParams] = useSearchParams(); - + const isLandingPage = location.pathname === '/'; const isUserManagement = location.pathname.startsWith('/usermanagement'); - + const [courseTitle, setCourseTitle] = useState(() => localStorage.getItem('courseTitle') || '', ); - + const [courseId, setCourseId] = useState(() => localStorage.getItem('courseId') || '', - ); + ); useEffect(() => { if (courseTitle) { localStorage.setItem('courseTitle', courseTitle); } - if (courseId) { + if (courseId) { localStorage.setItem('courseId', courseId); } }, [courseTitle, courseId]); @@ -60,7 +60,7 @@ export function App() { .catch(() => {}); } }, [user?.isAdmin, courseId]); - + // Derive courseId from the current URL for students (and /read) useEffect(() => { const segs = location.pathname.split('/').filter(Boolean); @@ -137,9 +137,9 @@ export function App() { ) } > - } /> + }/> } /> - } /> + } /> } /> } /> } /> diff --git a/codewit/client/src/components/form/ReusableTable.tsx b/codewit/client/src/components/form/ReusableTable.tsx index 50f1daf..cd07240 100644 --- a/codewit/client/src/components/form/ReusableTable.tsx +++ b/codewit/client/src/components/form/ReusableTable.tsx @@ -14,7 +14,7 @@ interface ReusableTableProps { data: T[], className?: string, itemsPerPage?: number, - onEdit: (item: T) => void, + onEdit?: (item: T) => void, onDelete: (item: T) => void, } @@ -44,7 +44,7 @@ const paginationTheme: CustomFlowbiteTheme["pagination"] = { }, }; -const ReusableTable = ({ +export const ReusableTable = ({ columns, data, className, @@ -113,12 +113,16 @@ const ReusableTable = - + {onEdit != null ? + + : + null + } + + + refetch()}/> + {isLoading && isFetching ? +
Loading...
+ : + error != null ? +
Failed to load exercises.
+ : + deleteExercise.mutate({uid: record.uid})} + /> + } + ; +} + diff --git a/codewit/client/src/pages/create/exercise/exercise_id.tsx b/codewit/client/src/pages/create/exercise/exercise_id.tsx new file mode 100644 index 0000000..19920f3 --- /dev/null +++ b/codewit/client/src/pages/create/exercise/exercise_id.tsx @@ -0,0 +1,340 @@ +// codewit/client/src/pages/ExerciseForm.tsx +import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/24/solid"; +import { Editor } from "@monaco-editor/react"; +import { useQueryClient, useMutation } from '@tanstack/react-query'; +import MDEditor from "@uiw/react-markdown-editor"; +import axios from "axios"; +import { Button, Select, TextInput } from "flowbite-react"; +import React, { useState } from "react"; +import { Routes, Route, Link, useParams, useNavigate } from "react-router-dom"; +import { toast } from "react-toastify"; + +import { + ExerciseInput, + ExerciseResponse, +} from "@codewit/interfaces"; + +import TagSelect from "../../../components/form/TagSelect"; +import LanguageSelect from "../../../components/form/LanguageSelect"; +import ReusableModal from "../../../components/form/ReusableModal"; +import InputLabel from "../../../components/form/InputLabel"; +import { isFormValid } from "../../../utils/formValidationUtils"; +import { cn } from "../../../utils/styles"; +import { use_single_exercise_query, single_exercise_query_key } from "../../../hooks/useExercise"; + +type UiTag = { label: string; value: string }; +type Difficulty = 'easy' | 'hard' | 'worked example'; + +interface ExerciseFormState extends ExerciseInput { + /** UI helper: language in + setFormData(p => ({ + ...p, + difficulty: (e.target.value || undefined) as Difficulty | undefined, + })) + } + > + + + + + + + + Prompt + + Reference Test + + Starter Code + +
+ + +
+
+ +
+ + ; +} From 47ab37c4115a04c66c77c032d5abc79c28edc2a8 Mon Sep 17 00:00:00 2001 From: DAC Date: Mon, 5 Jan 2026 13:44:17 -0800 Subject: [PATCH 02/10] PR requests / additional fixes #132 PR specific: - add in missing call for the `on_deleted` prop when an exercise is deleted - updated `use_single_exercise_query` to better handle axios errors (mainly for handling not found by returning null) Additional - found a bug when requesting an exercise that does not exist. previousely if the database was not able to find the record it would return null but was still treated as valid and would cause errors when attempting to create the formatted response. a proper null check has been added and a new response has also been added to better indicate that the record was not found. - added in additional views for the exercise_id page to display something for "not found", "error", "loading", "invalid id", "missing param" --- codewit/api/src/controllers/exercise.ts | 9 +++- codewit/api/src/routes/exercise.ts | 26 ++++++----- codewit/client/src/components/error/Error.tsx | 14 +++--- codewit/client/src/hooks/useExercise.ts | 19 +++++--- .../src/pages/create/exercise/exercise_id.tsx | 44 ++++++++++++++++--- 5 files changed, 79 insertions(+), 33 deletions(-) diff --git a/codewit/api/src/controllers/exercise.ts b/codewit/api/src/controllers/exercise.ts index dd381b2..bcb357d 100644 --- a/codewit/api/src/controllers/exercise.ts +++ b/codewit/api/src/controllers/exercise.ts @@ -17,7 +17,12 @@ async function getExerciseById(uid: number): Promise { include: [Tag, Language], order: [[Tag, ExerciseTags, 'ordering', 'ASC']], }); - return formatExerciseResponse(exercise); + + if (exercise == null) { + return null; + } else { + return formatExerciseResponse(exercise); + } } async function getExercisesByIds(ids: number[]): Promise { @@ -110,7 +115,7 @@ async function updateExercise( const trimmed = title?.trim(); updates.title = trimmed ? trimmed : null; } - // allow update OR clear difficulty + // allow update OR clear difficulty if (typeof difficulty !== 'undefined') { updates.difficulty = difficulty ?? null; } diff --git a/codewit/api/src/routes/exercise.ts b/codewit/api/src/routes/exercise.ts index 6389f60..5bf2ab8 100644 --- a/codewit/api/src/routes/exercise.ts +++ b/codewit/api/src/routes/exercise.ts @@ -25,19 +25,21 @@ const upload = multer({ limits: { fileSize: 5 * 1024 * 1024 }, // 5MB }); -exerciseRouter.get('/:uid', async (req, res) => { - try { - const exercise = await getExerciseById(Number(req.params.uid)); - if (exercise) { - res.json(exercise); - } else { - res.status(404).json({ message: 'Exercise not found' }); - } - } catch (err) { - console.error(err); - res.status(500).json({ message: 'Server error' }); +exerciseRouter.get('/:uid', asyncHandle(async (req, res) => { + let parsed = parseInt(req.params.uid, 10); + + if (isNaN(parsed) || parsed < 0) { + res.status(400).json({error:"InvalidUid"}); } -}); + + const exercise = await getExerciseById(Number(req.params.uid)); + + if (exercise) { + res.json(exercise); + } else { + res.status(404).json({ message: 'Exercise not found' }); + } +})); exerciseRouter.get('/', async (req, res) => { try { diff --git a/codewit/client/src/components/error/Error.tsx b/codewit/client/src/components/error/Error.tsx index 265198c..be96c2d 100644 --- a/codewit/client/src/components/error/Error.tsx +++ b/codewit/client/src/components/error/Error.tsx @@ -4,7 +4,7 @@ import { PropsWithChildren, ReactNode } from "react"; interface ErrorProps { message?: string; - statusCode?: number; + statusCode?: number; } export const ErrorPage = ({ message = "Oops! Page does not exist. You can now return to the main page.", statusCode = 400 }: ErrorProps): JSX.Element => { @@ -19,7 +19,7 @@ export const ErrorPage = ({ message = "Oops! Page does not exist. You can now re

- {displayStatusCode} + {displayStatusCode}

Error

{stateMessage || message}

@@ -42,16 +42,16 @@ type ErrorViewProps = PropsWithChildren<{ }>; export function ErrorView({title = "Error", children}: ErrorViewProps) { - return
-
+ return
+
{typeof title === "string" ? -

+

{title}

: title } - {children}
+ {children}
-} \ No newline at end of file +} diff --git a/codewit/client/src/hooks/useExercise.ts b/codewit/client/src/hooks/useExercise.ts index 8f9cc5a..ca7a394 100644 --- a/codewit/client/src/hooks/useExercise.ts +++ b/codewit/client/src/hooks/useExercise.ts @@ -82,14 +82,21 @@ export function use_single_exercise_query(exercise_id: number) { return useQuery({ queryKey: single_exercise_query_key(exercise_id), queryFn: async () => { - let result = await axios.get(`/api/exercises/${exercise_id}`); + try { + let result = await axios.get(`/api/exercises/${exercise_id}`); - if (result.status === 200) { return result.data; - } else if (result.status === 404) { - return null; - } else { - throw new Error("ApiError"); + } catch(err) { + if (axios.isAxiosError(err)) { + if (err.response.status === 404) { + // only in the event that the thing we are looking for does not + // exist do we return null + return null; + } + } + + // everything else is no explicitly handled by this catch + throw err; } } }); diff --git a/codewit/client/src/pages/create/exercise/exercise_id.tsx b/codewit/client/src/pages/create/exercise/exercise_id.tsx index 19920f3..e0056c5 100644 --- a/codewit/client/src/pages/create/exercise/exercise_id.tsx +++ b/codewit/client/src/pages/create/exercise/exercise_id.tsx @@ -14,6 +14,8 @@ import { ExerciseResponse, } from "@codewit/interfaces"; +import LoadingPage from "../../../components/loading/LoadingPage"; +import { ErrorView } from "../../../components/error/Error"; import TagSelect from "../../../components/form/TagSelect"; import LanguageSelect from "../../../components/form/LanguageSelect"; import ReusableModal from "../../../components/form/ReusableModal"; @@ -42,7 +44,12 @@ export function ExerciseIdView() { const client = useQueryClient(); if (params.exercise_id == null) { - return
no exercise id provided
; + return +

No exercise uid was provided to the page.

+ + + +
; } if (params.exercise_id === "new") { @@ -56,7 +63,12 @@ export function ExerciseIdView() { if (!isNaN(parsed)) { return ; } else { - return
Invalid Exercise Id Provided
; + return +

The provided exercise uid is not valid. Make sure that the uid is a valid whole number greater than 0

+ + + +
} } } @@ -72,15 +84,25 @@ export function ValidExerciseIdView({exercise_id}: ValidExerciseIdViewProps) { const { data, isLoading, isFetching, error } = use_single_exercise_query(exercise_id); if (isLoading && isFetching) { - return
Loading...
; + return ; } if (error != null) { - return
Failed to retrieve exercise
; + return +

There was an error when attempting to load the requested exercise.

+ + + +
; } if (data == null) { - return
Exercise Not Found
; + return +

The exercise was not found.

+ + + +
; } return { + toast.success("Exercise Deleted"); + + on_deleted(); + }, + onError: (err, vars, ctx) => { + toast.error("Failed to delete Exercise"); + + console.error("failed to delete exercise:", err); + }, }); const handleSubmit = async () => { From 633623207e6c10c6959a2cb270ccdcf5a4449eca Mon Sep 17 00:00:00 2001 From: DAC Date: Mon, 5 Jan 2026 14:09:56 -0800 Subject: [PATCH 03/10] fix ts error #132 there is a ts error when checking if the error caught is an axios error. ts will still think that it can be undefined eventhough the check before should indicate that it is not. --- codewit/client/src/hooks/useExercise.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/codewit/client/src/hooks/useExercise.ts b/codewit/client/src/hooks/useExercise.ts index ca7a394..fb5525a 100644 --- a/codewit/client/src/hooks/useExercise.ts +++ b/codewit/client/src/hooks/useExercise.ts @@ -1,7 +1,7 @@ // codewit/client/src/hooks/useExercise.ts import { useQuery } from "@tanstack/react-query"; import { useState, useEffect } from 'react'; -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; import { ExerciseResponse, Exercise, ExerciseInput } from '@codewit/interfaces'; // Hook to handle fetching data with axios @@ -87,8 +87,10 @@ export function use_single_exercise_query(exercise_id: number) { return result.data; } catch(err) { - if (axios.isAxiosError(err)) { - if (err.response.status === 404) { + if (err instanceof AxiosError) { + // ts still thinks that this is undefined even though we just checked + // that it is an instance of AxiosError? + if (err?.response?.status === 404) { // only in the event that the thing we are looking for does not // exist do we return null return null; From cd52d6f6852c2a5c5d71307fcd0afd1faeaf4314 Mon Sep 17 00:00:00 2001 From: DAC Date: Mon, 5 Jan 2026 15:03:53 -0800 Subject: [PATCH 04/10] resolved stash conflict --- .../src/pages/create/exercise/exercise_id.tsx | 250 +++++++++++++++++- 1 file changed, 242 insertions(+), 8 deletions(-) diff --git a/codewit/client/src/pages/create/exercise/exercise_id.tsx b/codewit/client/src/pages/create/exercise/exercise_id.tsx index e0056c5..b866222 100644 --- a/codewit/client/src/pages/create/exercise/exercise_id.tsx +++ b/codewit/client/src/pages/create/exercise/exercise_id.tsx @@ -1,12 +1,15 @@ // codewit/client/src/pages/ExerciseForm.tsx import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/24/solid"; import { Editor } from "@monaco-editor/react"; +import { useField, useForm } from "@tanstack/react-form"; import { useQueryClient, useMutation } from '@tanstack/react-query'; import MDEditor from "@uiw/react-markdown-editor"; import axios from "axios"; -import { Button, Select, TextInput } from "flowbite-react"; +import { Button, Select, TextInput, Label } from "flowbite-react"; import React, { useState } from "react"; import { Routes, Route, Link, useParams, useNavigate } from "react-router-dom"; +import { default as ReactSelect } from "react-select"; +import CreatableSelect from 'react-select/creatable'; import { toast } from "react-toastify"; import { @@ -16,22 +19,22 @@ import { import LoadingPage from "../../../components/loading/LoadingPage"; import { ErrorView } from "../../../components/error/Error"; -import TagSelect from "../../../components/form/TagSelect"; +import TagSelect, { topic_options } from "../../../components/form/TagSelect"; import LanguageSelect from "../../../components/form/LanguageSelect"; import ReusableModal from "../../../components/form/ReusableModal"; import InputLabel from "../../../components/form/InputLabel"; import { isFormValid } from "../../../utils/formValidationUtils"; -import { cn } from "../../../utils/styles"; +import { cn, SelectStyles } from "../../../utils/styles"; import { use_single_exercise_query, single_exercise_query_key } from "../../../hooks/useExercise"; -type UiTag = { label: string; value: string }; +type UITag = { label: string; value: string }; type Difficulty = 'easy' | 'hard' | 'worked example'; interface ExerciseFormState extends ExerciseInput { /** UI helper: language in field.handleChange(ev.target.value)} + > + + + + + +
+ }}/> +
+
+ { + return
+ + field.handleChange(value?.value ?? "")} + /> +
+ }}/> + { + return
+ + +
+ }}/> +
+ { + return
+ + field.handleChange(value)} + styles={SelectStyles} + /> +
+ }}/> + { + return
+ + field.handleChange(value)} + height="300px" + data-testid="prompt" + /> +
+ }}/> + { + let language = field.form.getFieldValue("language"); + + console.log(language); + + return
+ + field.handleChange(value)} + theme="vs-dark" + /> +
+ }}/> +
+
; +} From a1a297b14da55258a3052ca13874ba6567495c8d Mon Sep 17 00:00:00 2001 From: DAC Date: Thu, 8 Jan 2026 10:55:17 -0800 Subject: [PATCH 05/10] id check and err check #132 - updated the uid checks for exercises to exclude 0 - changed axios error check back to `axios.isAxiosError` --- codewit/api/src/routes/exercise.ts | 2 +- codewit/client/src/hooks/useExercise.ts | 4 ++-- codewit/client/src/pages/create/exercise/exercise_id.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/codewit/api/src/routes/exercise.ts b/codewit/api/src/routes/exercise.ts index 5bf2ab8..69fc968 100644 --- a/codewit/api/src/routes/exercise.ts +++ b/codewit/api/src/routes/exercise.ts @@ -28,7 +28,7 @@ const upload = multer({ exerciseRouter.get('/:uid', asyncHandle(async (req, res) => { let parsed = parseInt(req.params.uid, 10); - if (isNaN(parsed) || parsed < 0) { + if (isNaN(parsed) || parsed <= 0) { res.status(400).json({error:"InvalidUid"}); } diff --git a/codewit/client/src/hooks/useExercise.ts b/codewit/client/src/hooks/useExercise.ts index fb5525a..b951661 100644 --- a/codewit/client/src/hooks/useExercise.ts +++ b/codewit/client/src/hooks/useExercise.ts @@ -1,7 +1,7 @@ // codewit/client/src/hooks/useExercise.ts import { useQuery } from "@tanstack/react-query"; import { useState, useEffect } from 'react'; -import axios, { AxiosError } from 'axios'; +import axios from 'axios'; import { ExerciseResponse, Exercise, ExerciseInput } from '@codewit/interfaces'; // Hook to handle fetching data with axios @@ -87,7 +87,7 @@ export function use_single_exercise_query(exercise_id: number) { return result.data; } catch(err) { - if (err instanceof AxiosError) { + if (axios.isAxiosError(err)) { // ts still thinks that this is undefined even though we just checked // that it is an instance of AxiosError? if (err?.response?.status === 404) { diff --git a/codewit/client/src/pages/create/exercise/exercise_id.tsx b/codewit/client/src/pages/create/exercise/exercise_id.tsx index e0056c5..3a465e2 100644 --- a/codewit/client/src/pages/create/exercise/exercise_id.tsx +++ b/codewit/client/src/pages/create/exercise/exercise_id.tsx @@ -60,7 +60,7 @@ export function ExerciseIdView() { } else { let parsed = parseInt(params.exercise_id, 10); - if (!isNaN(parsed)) { + if (!isNaN(parsed) && parsed > 0) { return ; } else { return From 787f28906990c149b315210bc2390f8bf19daf68 Mon Sep 17 00:00:00 2001 From: DAC Date: Wed, 14 Jan 2026 15:45:30 -0800 Subject: [PATCH 06/10] app form #138 this provides a codewit specific form hook that can be used instead of the global one that is provided by tanstack/react-form. it will allow us to create reusable components and hook into the state to provide common logic. currently there is a set of buttons available to to help with confirming user actions but more things can be added. --- codewit/client/src/form.ts | 28 ++++ codewit/client/src/form/button.tsx | 203 +++++++++++++++++++++++++++++ codewit/client/src/form/context.ts | 6 + 3 files changed, 237 insertions(+) create mode 100644 codewit/client/src/form.ts create mode 100644 codewit/client/src/form/button.tsx create mode 100644 codewit/client/src/form/context.ts diff --git a/codewit/client/src/form.ts b/codewit/client/src/form.ts new file mode 100644 index 0000000..ca20288 --- /dev/null +++ b/codewit/client/src/form.ts @@ -0,0 +1,28 @@ +import { createFormHook } from "@tanstack/react-form"; + +import { + fieldContext, + formContext, + useFieldContext, + useFormContext +} from "./form/context"; +import { + SubmitButton, + ConfirmReset, + ConfirmDelete, + ConfirmAway +} from "./form/button"; + +const { useAppForm } = createFormHook({ + fieldContext, + formContext, + fieldComponents: {}, + formComponents: { + SubmitButton, + ConfirmReset, + ConfirmDelete, + ConfirmAway, + }, +}); + +export { useAppForm, useFieldContext, useFormContext }; diff --git a/codewit/client/src/form/button.tsx b/codewit/client/src/form/button.tsx new file mode 100644 index 0000000..4410372 --- /dev/null +++ b/codewit/client/src/form/button.tsx @@ -0,0 +1,203 @@ +import { ArrowLeftIcon, TrashIcon, ArrowPathIcon } from "@heroicons/react/24/solid"; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from "flowbite-react"; +import { useState } from "react"; + +import { useFormContext } from "../form"; + +interface SubmitButtonProps {} + +export function SubmitButton({}) { + const form = useFormContext(); + + return ({ + submitting: state.isSubmitting, + dirty: state.isDirty, + can_submit: state.canSubmit, + })}> + {({submitting, dirty, can_submit}) => ( + + )} + +} + +interface ConfirmResetProps { + /** + * will be called when the user requests the reset + */ + on_reset: () => void, + /** + * will be called when the user requests to cancel the reset + */ + on_cancel?: () => void, +} + +/** + * a simple reset confirmation modal that will trigger the `on_reset` when the + * user confirms the reset action. requires a form context as it will enable + * and disable based on if the form is `submitting` or is `dirty`. + */ +export function ConfirmReset({ + on_reset, + on_cancel = () => {}, +}: ConfirmResetProps) { + const form = useFormContext(); + + const [open, set_open] = useState(false); + + const cancel_cb = () => { + set_open(false); + on_cancel(); + }; + + const reset_cb = () => { + set_open(false); + on_reset(); + }; + + return ({ + submitting: state.isSubmitting, + dirty: state.isDirty, + })}> + {({submitting, dirty}) => <> + + + Reset Data + +

This will reset any changes you made to its original state. Are you sure?

+
+ + + + +
+ } +
+} + +interface ConfirmDeleteProps { + /** + * will be called when the user requests the delete + */ + on_delete: () => void, + /** + * will be called when the user reqests to cancel the delete + */ + on_cancel?: () => void, +} + +/** + * a simple delete confirmation modal that will trigger the `on_delete` when the + * user confirms the delete action. requires a form context as it will enable + * and disable based on if the form is `submitting` + */ +export function ConfirmDelete({ + on_delete, + on_cancel = () => {}, +}: ConfirmDeleteProps) { + const form = useFormContext(); + + const [open, set_open] = useState(false); + + const cancel_cb = () => { + set_open(false); + on_cancel(); + }; + + const delete_cb = () => { + set_open(false); + on_delete(); + }; + + return ({ + submitting: state.isSubmitting, + })}> + {({submitting}) => <> + + + Delete Data + +

This will delete the current data from the server. Are you sure?

+
+ + + + +
+ } +
+} + +interface ConfirmAwayProps { + /** + * will be called when the user reqests to away + */ + on_away: () => void, + /** + * will be called when the user requests to cancel the away + */ + on_cancel?: () => void, +} + +/** + * a simple away confirmation modal that will trigger the `on_away` when the + * user confirms the awayt action. requires a form context as it will enable + * and disable based on if the form is `submitting`. the modal will only appear + * if the form is marked as dirty otherwise it will not prompt the user to + * confirm the away request. + */ +export function ConfirmAway({ + on_away, + on_cancel = () => {}, +}: ConfirmAwayProps) { + const form = useFormContext(); + + const [open, set_open] = useState(false); + + const away_cb = () => { + set_open(false); + on_away(); + }; + + const cancel_cb = () => { + set_open(false); + on_cancel(); + }; + + return ({ + submitting: state.isSubmitting, + dirty: state.isDirty, + })}> + {({submitting, dirty}) => <> + + + Cancel Changes + +

You have made changes to the data and did not save. Are you sure?

+
+ + + + +
+ } +
+} diff --git a/codewit/client/src/form/context.ts b/codewit/client/src/form/context.ts new file mode 100644 index 0000000..2485739 --- /dev/null +++ b/codewit/client/src/form/context.ts @@ -0,0 +1,6 @@ +import { createFormHookContexts, createFormHook } from "@tanstack/react-form"; + +const { fieldContext, formContext, useFieldContext, useFormContext } = createFormHookContexts(); + +export { fieldContext, formContext, useFieldContext, useFormContext }; + From 586ac69b7902f90be3483941fc4bdcd3fc3c1102 Mon Sep 17 00:00:00 2001 From: DAC Date: Wed, 14 Jan 2026 15:48:13 -0800 Subject: [PATCH 07/10] tanstack form devtools #138 this adds the devtools for tanstack form. --- codewit/client/src/main.tsx | 19 +- codewit/package-lock.json | 458 ++++++++++++++++++++++++++++++++---- codewit/package.json | 2 + 3 files changed, 425 insertions(+), 54 deletions(-) diff --git a/codewit/client/src/main.tsx b/codewit/client/src/main.tsx index 5c9153e..626c6a0 100644 --- a/codewit/client/src/main.tsx +++ b/codewit/client/src/main.tsx @@ -3,8 +3,10 @@ import { StrictMode } from 'react'; import * as ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; import { ToastContainer } from "react-toastify"; +import { TanStackDevtools } from "@tanstack/react-devtools"; +import { FormDevtoolsPanel } from "@tanstack/react-form-devtools"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools"; import "react-toastify/dist/ReactToastify.css"; import App from './app/app'; @@ -22,7 +24,20 @@ root.render( - + , + defaultOpen: true, + }, + { + name: "TanStack Form", + render: , + defaultOpen: false, + }, + ]} + /> // ); diff --git a/codewit/package-lock.json b/codewit/package-lock.json index 4da8827..502ec6c 100644 --- a/codewit/package-lock.json +++ b/codewit/package-lock.json @@ -14,7 +14,9 @@ "@heroicons/react": "^2.1.1", "@monaco-editor/react": "^4.6.0", "@nrwl/node": "^18.0.1", + "@tanstack/react-devtools": "^0.9.2", "@tanstack/react-form": "^1.14.1", + "@tanstack/react-form-devtools": "^0.2.11", "@tanstack/react-query": "^5.83.0", "@tanstack/react-query-devtools": "^5.91.1", "@types/express-session": "^1.18.0", @@ -226,6 +228,7 @@ "version": "7.23.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -2457,6 +2460,7 @@ "version": "6.24.1", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.24.1.tgz", "integrity": "sha512-sBfP4rniPBRQzNakwuQEqjEuiJDWJyF2kqLLqij4WXRoVwPPJfjx966Eq3F7+OPQxDtMt/Q9MWLoZLWjeveBlg==", + "peer": true, "dependencies": { "@codemirror/state": "^6.4.0", "style-mod": "^4.1.0", @@ -2565,6 +2569,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -4844,6 +4849,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -5328,6 +5334,80 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@solid-primitives/event-listener": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@solid-primitives/event-listener/-/event-listener-2.4.3.tgz", + "integrity": "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg==", + "license": "MIT", + "dependencies": { + "@solid-primitives/utils": "^6.3.2" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/keyboard": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@solid-primitives/keyboard/-/keyboard-1.3.3.tgz", + "integrity": "sha512-9dQHTTgLBqyAI7aavtO+HnpTVJgWQA1ghBSrmLtMu1SMxLPDuLfuNr+Tk5udb4AL4Ojg7h9JrKOGEEDqsJXWJA==", + "license": "MIT", + "dependencies": { + "@solid-primitives/event-listener": "^2.4.3", + "@solid-primitives/rootless": "^1.5.2", + "@solid-primitives/utils": "^6.3.2" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/resize-observer": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@solid-primitives/resize-observer/-/resize-observer-2.1.3.tgz", + "integrity": "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ==", + "license": "MIT", + "dependencies": { + "@solid-primitives/event-listener": "^2.4.3", + "@solid-primitives/rootless": "^1.5.2", + "@solid-primitives/static-store": "^0.1.2", + "@solid-primitives/utils": "^6.3.2" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/rootless": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@solid-primitives/rootless/-/rootless-1.5.2.tgz", + "integrity": "sha512-9HULb0QAzL2r47CCad0M+NKFtQ+LrGGNHZfteX/ThdGvKIg2o2GYhBooZubTCd/RTu2l2+Nw4s+dEfiDGvdrrQ==", + "license": "MIT", + "dependencies": { + "@solid-primitives/utils": "^6.3.2" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/static-store": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@solid-primitives/static-store/-/static-store-0.1.2.tgz", + "integrity": "sha512-ReK+5O38lJ7fT+L6mUFvUr6igFwHBESZF+2Ug842s7fvlVeBdIVEdTCErygff6w7uR6+jrr7J8jQo+cYrEq4Iw==", + "license": "MIT", + "dependencies": { + "@solid-primitives/utils": "^6.3.2" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/utils": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.3.2.tgz", + "integrity": "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ==", + "license": "MIT", + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -5487,6 +5567,7 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -5659,6 +5740,7 @@ "resolved": "https://registry.npmjs.org/@swc-node/register/-/register-1.6.8.tgz", "integrity": "sha512-74ijy7J9CWr1Z88yO+ykXphV29giCrSpANQPQRooE0bObpkTO1g4RzQovIfbIaniBiGDDVsYwDoQ3FIrCE8HcQ==", "devOptional": true, + "peer": true, "dependencies": { "@swc-node/core": "^1.10.6", "@swc-node/sourcemap-support": "^0.3.0", @@ -5752,6 +5834,7 @@ "integrity": "sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==", "devOptional": true, "hasInstallScript": true, + "peer": true, "dependencies": { "@swc/counter": "^0.1.1", "@swc/types": "^0.1.5" @@ -5953,7 +6036,8 @@ "version": "0.1.5", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", - "devOptional": true + "devOptional": true, + "peer": true }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", @@ -5995,13 +6079,182 @@ "node": ">=4" } }, + "node_modules/@tanstack/devtools": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tanstack/devtools/-/devtools-0.10.3.tgz", + "integrity": "sha512-M2HnKtaNf3Z8JDTNDq+X7/1gwOqSwTnCyC0GR+TYiRZM9mkY9GpvTqp6p6bx3DT8onu2URJiVxgHD9WK2e3MNQ==", + "license": "MIT", + "dependencies": { + "@solid-primitives/event-listener": "^2.4.3", + "@solid-primitives/keyboard": "^1.3.3", + "@solid-primitives/resize-observer": "^2.1.3", + "@tanstack/devtools-client": "0.0.5", + "@tanstack/devtools-event-bus": "0.4.0", + "@tanstack/devtools-ui": "0.4.4", + "clsx": "^2.1.1", + "goober": "^2.1.16", + "solid-js": "^1.9.9" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "solid-js": ">=1.9.7" + } + }, + "node_modules/@tanstack/devtools-client": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-client/-/devtools-client-0.0.5.tgz", + "integrity": "sha512-hsNDE3iu4frt9cC2ppn1mNRnLKo2uc1/1hXAyY9z4UYb+o40M2clFAhiFoo4HngjfGJDV3x18KVVIq7W4Un+zA==", + "license": "MIT", + "dependencies": { + "@tanstack/devtools-event-client": "^0.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/devtools-event-bus": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-bus/-/devtools-event-bus-0.4.0.tgz", + "integrity": "sha512-1t+/csFuDzi+miDxAOh6Xv7VDE80gJEItkTcAZLjV5MRulbO/W8ocjHLI2Do/p2r2/FBU0eKCRTpdqvXaYoHpQ==", + "license": "MIT", + "dependencies": { + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/devtools-event-client": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-event-client/-/devtools-event-client-0.4.0.tgz", + "integrity": "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/devtools-ui": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-ui/-/devtools-ui-0.4.4.tgz", + "integrity": "sha512-5xHXFyX3nom0UaNfiOM92o6ziaHjGo3mcSGe2HD5Xs8dWRZNpdZ0Smd0B9ddEhy0oB+gXyMzZgUJb9DmrZV0Mg==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "goober": "^2.1.16", + "solid-js": "^1.9.9" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "solid-js": ">=1.9.7" + } + }, + "node_modules/@tanstack/devtools-utils": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@tanstack/devtools-utils/-/devtools-utils-0.2.4.tgz", + "integrity": "sha512-6Y6JJHxoerWuq/dVLOPLZ1iSpZie1C+3whGjDFCK126qI8dA+Gui3s5jZUD12kvzs7vhRqY+uUlA5S6f85FHqg==", + "license": "MIT", + "dependencies": { + "@tanstack/devtools-ui": "^0.4.4" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@types/react": ">=17.0.0", + "preact": ">=10.0.0", + "react": ">=17.0.0", + "solid-js": ">=1.9.7", + "vue": ">=3.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "preact": { + "optional": true + }, + "react": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, "node_modules/@tanstack/form-core": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.14.0.tgz", - "integrity": "sha512-uAOW3IxkT/Cmy8JlznK8S/LSpvtHjpUQi2wyuPqVfJ04y95WuV90SO+VKtb9TrNp51QLrrTFBR8tMEuzqp5wmA==", + "version": "1.27.7", + "resolved": "https://registry.npmjs.org/@tanstack/form-core/-/form-core-1.27.7.tgz", + "integrity": "sha512-nvogpyE98fhb0NDw1Bf2YaCH+L7ZIUgEpqO9TkHucDn6zg3ni521boUpv0i8HKIrmmFwDYjWZoCnrgY4HYWTkw==", + "license": "MIT", + "dependencies": { + "@tanstack/devtools-event-client": "^0.4.0", + "@tanstack/pacer-lite": "^0.1.1", + "@tanstack/store": "^0.7.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/form-devtools": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@tanstack/form-devtools/-/form-devtools-0.2.11.tgz", + "integrity": "sha512-wCQ5uicGfxs34ytZmVhppKELijrx4OU5zRj4PYs0RbBJH3bJYOj5MATsj+rkdDLi1FeVrwLgK2qX4a6c/hWF4g==", "license": "MIT", "dependencies": { - "@tanstack/store": "^0.7.2" + "@tanstack/devtools-ui": "^0.4.4", + "@tanstack/devtools-utils": "^0.2.3", + "@tanstack/form-core": "1.27.7", + "clsx": "^2.1.1", + "dayjs": "^1.11.18", + "goober": "^2.1.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "solid-js": ">=1.9.9" + } + }, + "node_modules/@tanstack/pacer-lite": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@tanstack/pacer-lite/-/pacer-lite-0.1.1.tgz", + "integrity": "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==", + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { "type": "github", @@ -6028,43 +6281,75 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/react-devtools": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-devtools/-/react-devtools-0.9.2.tgz", + "integrity": "sha512-JNXvBO3jgq16GzTVm7p65n5zHNfMhnqF6Bm7CawjoqZrjEakxbM6Yvy63aKSIpbrdf+Wun2Xn8P0qD+vp56e1g==", + "license": "MIT", + "dependencies": { + "@tanstack/devtools": "0.10.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "@types/react-dom": ">=16.8", + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@tanstack/react-form": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.14.1.tgz", - "integrity": "sha512-Ioja3zcLZj082OdCH6pFNv15fD4UTfnJgKIXxY7Iumio8EcYLXSuxzanqNWewFvftshUFHknSEa7QtyOAkFs0Q==", + "version": "1.27.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-form/-/react-form-1.27.7.tgz", + "integrity": "sha512-xTg4qrUY0fuLaSnkATLZcK3BWlnwLp7IuAb6UTbZKngiDEvvDCNTvVvHgPlgef1O2qN4klZxInRyRY6oEkXZ2A==", "license": "MIT", "dependencies": { - "@tanstack/form-core": "1.14.0", - "@tanstack/react-store": "^0.7.3", - "decode-formdata": "^0.9.0", - "devalue": "^5.1.1" + "@tanstack/form-core": "1.27.7", + "@tanstack/react-store": "^0.8.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-start": "^1.112.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "vinxi": "^0.5.0" + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@tanstack/react-start": { "optional": true - }, - "vinxi": { - "optional": true } } }, + "node_modules/@tanstack/react-form-devtools": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-form-devtools/-/react-form-devtools-0.2.11.tgz", + "integrity": "sha512-Hv6aZqH2dfamFPWwonA20xKj61xU3omilOYEGdihldOddl+cFLY6P7q66Zqs0p9a9mOQvBS+/i0zBcZjRzvupg==", + "license": "MIT", + "dependencies": { + "@tanstack/devtools-utils": "^0.2.3", + "@tanstack/form-devtools": "0.2.11" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@tanstack/react-form/node_modules/@tanstack/react-store": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.3.tgz", - "integrity": "sha512-3Dnqtbw9P2P0gw8uUM8WP2fFfg8XMDSZCTsywRPZe/XqqYW8PGkXKZTvP0AHkE4mpqP9Y43GpOg9vwO44azu6Q==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.8.0.tgz", + "integrity": "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==", "license": "MIT", "dependencies": { - "@tanstack/store": "0.7.2", - "use-sync-external-store": "^1.5.0" + "@tanstack/store": "0.8.0", + "use-sync-external-store": "^1.6.0" }, "funding": { "type": "github", @@ -6075,11 +6360,22 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@tanstack/react-form/node_modules/@tanstack/store": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.8.0.tgz", + "integrity": "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/react-query": { "version": "5.90.11", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.11.tgz", "integrity": "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.11" }, @@ -6109,9 +6405,9 @@ } }, "node_modules/@tanstack/store": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.2.tgz", - "integrity": "sha512-RP80Z30BYiPX2Pyo0Nyw4s1SJFH2jyM6f9i3HfX4pA+gm5jsnYryscdq2aIQLnL4TaGuQMO+zXmN9nh1Qck+Pg==", + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.7.tgz", + "integrity": "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==", "license": "MIT", "funding": { "type": "github", @@ -6599,6 +6895,7 @@ "version": "18.2.33", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.33.tgz", "integrity": "sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==", + "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -6609,7 +6906,7 @@ "version": "18.2.14", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.14.tgz", "integrity": "sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==", - "dev": true, + "peer": true, "dependencies": { "@types/react": "*" } @@ -6783,6 +7080,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz", "integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.20.0", "@typescript-eslint/types": "6.20.0", @@ -7352,6 +7650,7 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7412,6 +7711,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -8312,6 +8612,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -8677,6 +8978,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "peer": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -9244,7 +9546,8 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "peer": true }, "node_modules/csv-parse": { "version": "6.1.0", @@ -9479,9 +9782,10 @@ } }, "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" }, "node_modules/debounce": { "version": "2.1.0", @@ -9517,12 +9821,6 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, - "node_modules/decode-formdata": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/decode-formdata/-/decode-formdata-0.9.0.tgz", - "integrity": "sha512-q5uwOjR3Um5YD+ZWPOF/1sGHVW9A5rCrRwITQChRXlmPkxDFBqCm4jNTIVdGHNH9OnR+V9MoZVgRhsFb+ARbUw==", - "license": "MIT" - }, "node_modules/decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -9732,12 +10030,6 @@ "detect-port": "bin/detect-port.js" } }, - "node_modules/devalue": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", - "license": "MIT" - }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -10062,6 +10354,7 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "peer": true, "dependencies": { "ansi-colors": "^4.1.1" }, @@ -10234,6 +10527,7 @@ "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "dev": true, "hasInstallScript": true, + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -10291,6 +10585,7 @@ "version": "8.48.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -10345,6 +10640,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -11104,6 +11400,7 @@ "version": "1.18.0", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz", "integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==", + "peer": true, "dependencies": { "cookie": "0.6.0", "cookie-signature": "1.0.7", @@ -12010,6 +12307,15 @@ "node": ">=8.6.0" } }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -13395,6 +13701,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -13528,7 +13835,6 @@ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "optional": true, - "peer": true, "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -14116,6 +14422,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -14133,6 +14440,7 @@ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", "dev": true, + "peer": true, "dependencies": { "abab": "^2.0.6", "cssstyle": "^3.0.0", @@ -15673,7 +15981,8 @@ "node_modules/monaco-editor": { "version": "0.46.0", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.46.0.tgz", - "integrity": "sha512-ADwtLIIww+9FKybWscd7OCfm9odsFYHImBRI1v9AviGce55QY8raT+9ihH8jX/E/e6QVSGM+pKj4jSUSRmALNQ==" + "integrity": "sha512-ADwtLIIww+9FKybWscd7OCfm9odsFYHImBRI1v9AviGce55QY8raT+9ihH8jX/E/e6QVSGM+pKj4jSUSRmALNQ==", + "peer": true }, "node_modules/mrmime": { "version": "2.0.0", @@ -15869,6 +16178,7 @@ "integrity": "sha512-AtcM7JmBC82O17WMxuu9JJxEKTcsMII1AMgxCeiCWcW22wHd3EhIn5Hg1iSFv9ftkSSd8YgHeqTciRbdTqbxpA==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nrwl/tao": "18.0.1", "@yarnpkg/lockfile": "^1.1.0", @@ -16519,6 +16829,7 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "peer": true, "dependencies": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", @@ -16767,6 +17078,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -17222,6 +17534,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -17233,6 +17546,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -18080,6 +18394,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.6.tgz", "integrity": "sha512-05lzkCS2uASX0CiLFybYfVkwNbKZG5NFQ6Go0VWyogFTXXbR039UVsegViTntkk4OglHBdF54ccApXRRuXRbsg==", "devOptional": true, + "peer": true, "dependencies": { "@types/estree": "1.0.5" }, @@ -18470,6 +18785,28 @@ "node": ">= 10.0.0" } }, + "node_modules/seroval": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", + "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.3.tgz", + "integrity": "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -18668,6 +19005,18 @@ "tslib": "^2.0.3" } }, + "node_modules/solid-js": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz", + "integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.1.0", + "seroval": "~1.3.0", + "seroval-plugins": "~1.3.0" + } + }, "node_modules/sort-keys": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", @@ -19251,6 +19600,7 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -19880,6 +20230,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20204,9 +20555,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -20336,6 +20687,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", "integrity": "sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==", "dev": true, + "peer": true, "dependencies": { "esbuild": "^0.19.3", "postcss": "^8.4.32", @@ -20479,6 +20831,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz", "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==", "dev": true, + "peer": true, "dependencies": { "@vitest/expect": "1.2.2", "@vitest/runner": "1.2.2", @@ -20898,10 +21251,10 @@ } }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", - "dev": true, + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -21049,6 +21402,7 @@ "version": "3.22.4", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/codewit/package.json b/codewit/package.json index b2211e2..8b95443 100644 --- a/codewit/package.json +++ b/codewit/package.json @@ -23,7 +23,9 @@ "@heroicons/react": "^2.1.1", "@monaco-editor/react": "^4.6.0", "@nrwl/node": "^18.0.1", + "@tanstack/react-devtools": "^0.9.2", "@tanstack/react-form": "^1.14.1", + "@tanstack/react-form-devtools": "^0.2.11", "@tanstack/react-query": "^5.83.0", "@tanstack/react-query-devtools": "^5.91.1", "@types/express-session": "^1.18.0", From 596b3c7edbe4bf7db93e4c7873678f00bbe1594f Mon Sep 17 00:00:00 2001 From: DAC Date: Wed, 14 Jan 2026 15:51:41 -0800 Subject: [PATCH 08/10] test exercise and form rewrite #138 this is a rewrite of the exercise_id form to use the app form from tanstack react-form. the page has been reformated to have the larger inputs at the bottom while having the smaller ones near the top so that they are easier to access. all other logic should still be the same. this also adds in the ability to test an exercise without having to follow the student workflow. currently it will just display the results from a direct call to codeval with the api acting as a proxy. if we need to hide information then we can but right now it will display whatever is returned from codeval. --- codewit/api/src/models/exercise.ts | 6 +- codewit/api/src/routes/exercise.ts | 30 + codewit/api/src/typings/response.types.ts | 2 + codewit/api/src/utils/responseFormatter.ts | 2 + .../src/pages/create/exercise/exercise_id.tsx | 1017 ++++++++--------- .../shared/interfaces/src/lib/interfaces.ts | 3 +- 6 files changed, 541 insertions(+), 519 deletions(-) diff --git a/codewit/api/src/models/exercise.ts b/codewit/api/src/models/exercise.ts index 43d9da1..23b096b 100644 --- a/codewit/api/src/models/exercise.ts +++ b/codewit/api/src/models/exercise.ts @@ -37,6 +37,9 @@ class Exercise extends Model< declare title?: string | null; declare difficulty?: Difficulty | null; + declare createdAt?: Date; + declare updatedAt?: Date; + declare getTags: BelongsToManyGetAssociationsMixin; declare addTag: BelongsToManyAddAssociationMixin; declare addTags: BelongsToManyAddAssociationsMixin; @@ -84,7 +87,7 @@ class Exercise extends Model< key: 'uid', }, }, - title: { + title: { type: DataTypes.STRING, allowNull: true, }, @@ -92,7 +95,6 @@ class Exercise extends Model< type: DataTypes.ENUM('easy', 'hard', 'worked example'), allowNull: true, } - }, { sequelize, diff --git a/codewit/api/src/routes/exercise.ts b/codewit/api/src/routes/exercise.ts index 69fc968..d2602ea 100644 --- a/codewit/api/src/routes/exercise.ts +++ b/codewit/api/src/routes/exercise.ts @@ -1,4 +1,5 @@ import { Router } from 'express'; +import { z } from "zod"; import { createExercise, @@ -17,6 +18,7 @@ import { checkAdmin } from '../middleware/auth'; import multer from 'multer'; import { asyncHandle } from '../middleware/catch'; import { importExercisesCsv } from '../controllers/exerciseImport'; +import { executeCodeEvaluation } from "../utils/codeEvalService"; const exerciseRouter = Router(); @@ -161,4 +163,32 @@ exerciseRouter.delete('/:uid', checkAdmin, async (req, res) => { } }); +const test_schema = z.object({ + code: z.string(), + language: z.string(), + reference_test: z.string(), +}); + +exerciseRouter.post("/test", checkAdmin, asyncHandle(async (req, res) => { + const valid = test_schema.safeParse(req.body); + + // ts does not like it if you try to do `!valid.success` as it will consider + // `valid.error` to not exist + if (valid.success === false) { + return res.status(400).json({ + error: "Validation", + payload: fromZodError(valid.error) + }); + } + + let result = await executeCodeEvaluation({ + language: valid.data.language, + code: valid.data.code, + testCode: valid.data.reference_test, + runTests: true, + }); + + return res.status(200).json(result); +})); + export default exerciseRouter; diff --git a/codewit/api/src/typings/response.types.ts b/codewit/api/src/typings/response.types.ts index fe77e6f..3097760 100644 --- a/codewit/api/src/typings/response.types.ts +++ b/codewit/api/src/typings/response.types.ts @@ -26,6 +26,8 @@ export interface ExerciseResponse { starterCode: string; title?: string; difficulty?: Difficulty | null; + createdAt: string, + updatedAt: string, } export interface CourseResponse { diff --git a/codewit/api/src/utils/responseFormatter.ts b/codewit/api/src/utils/responseFormatter.ts index 17da9e2..e5576cc 100644 --- a/codewit/api/src/utils/responseFormatter.ts +++ b/codewit/api/src/utils/responseFormatter.ts @@ -46,6 +46,8 @@ export function formatSingleExercise( starterCode: exercise.starterCode, title: exercise.title, difficulty: exercise.difficulty, + createdAt: exercise.createdAt.toJSON(), + updatedAt: exercise.updatedAt.toJSON(), }; } diff --git a/codewit/client/src/pages/create/exercise/exercise_id.tsx b/codewit/client/src/pages/create/exercise/exercise_id.tsx index 6b2a20f..99e5178 100644 --- a/codewit/client/src/pages/create/exercise/exercise_id.tsx +++ b/codewit/client/src/pages/create/exercise/exercise_id.tsx @@ -1,22 +1,24 @@ -// codewit/client/src/pages/ExerciseForm.tsx -import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/24/solid"; import { Editor } from "@monaco-editor/react"; -import { useField, useForm } from "@tanstack/react-form"; import { useQueryClient, useMutation } from '@tanstack/react-query'; import MDEditor from "@uiw/react-markdown-editor"; import axios from "axios"; -import { Button, Select, TextInput, Label } from "flowbite-react"; -import React, { useState } from "react"; +import { Button, Select, TextInput, Label, ButtonGroup } from "flowbite-react"; +import { useState, useEffect } from "react"; import { Routes, Route, Link, useParams, useNavigate } from "react-router-dom"; import { default as ReactSelect } from "react-select"; import CreatableSelect from 'react-select/creatable'; import { toast } from "react-toastify"; import { - ExerciseInput, - ExerciseResponse, + ExerciseInput, + ExerciseResponse, } from "@codewit/interfaces"; +import { + createExerciseSchema, + updateExerciseSchema, +} from "@codewit/validations"; +import { useAppForm } from "../../../form"; import LoadingPage from "../../../components/loading/LoadingPage"; import { ErrorView } from "../../../components/error/Error"; import TagSelect, { topic_options } from "../../../components/form/TagSelect"; @@ -30,409 +32,172 @@ import { use_single_exercise_query, single_exercise_query_key } from "../../../h type UITag = { label: string; value: string }; type Difficulty = 'easy' | 'hard' | 'worked example'; -interface ExerciseFormState extends ExerciseInput { - /** UI helper: language in - setFormData(p => ({ - ...p, - difficulty: (e.target.value || undefined) as Difficulty | undefined, - })) - } - > - - - - - - - - Prompt - - Reference Test - - Starter Code - -
- - -
-
- -
- - ; + + return client.setQueryData( + single_exercise_query_key(record.uid), + record, + )} + on_deleted={() => navigate("/create/exercise")} + on_cancel={() => navigate("/create/exercise")} + />; } interface ExerciseForm { - uid: number, - title: string, - difficulty: Difficulty, - prompt: string - topic: string - tags: UITag[], - language: string, - referenceTest: string, - starterCode: string, + uid: number, + title: string, + difficulty: string, + prompt: string, + topic: string, + tags: string[], + language: string, + referenceTest: string, + starterCode: string, } -function blank_form(): ExerciseForm { - return { - uid: -1, - prompt: "", - topic: "", - tags: [], - language: "java", - referenceTest: "", - starterCode: "", - title: "", - difficulty: "", - } +type FormAction = "send" | "delete"; + +interface FormMeta { + action: FormAction, } -function exercise_to_form2(exercise: ExerciseResponse): ExerciseResponse { - return { - uid: exercise.uid, - title: exercise.title ?? "", - difficulty: exercise.difficulty ?? "", - prompt: exercise.prompt, - topic: exercise.topic, - tags: exercise.tags.map(tag => ({ label: tag, value: tag })), - language: exercise.language, - referenceTest: exercise.referenceTest, - starterCode: exercise.starterCode, - }; +function blank_form(): ExerciseForm { + return { + uid: -1, + prompt: "", + topic: "", + tags: [], + language: "java", + referenceTest: "", + starterCode: "", + title: "", + difficulty: "", + }; } -interface ExerciseEdit2Props { - exercise: ExerciseResponse | null, - on_created?: (exercise: ExerciseResponse) => void, - on_updated?: (exercise: ExerciseResponse) => void, - on_deleted?: () => void, +function exercise_to_form(exercise: ExerciseResponse): ExerciseForm { + return { + uid: exercise.uid, + title: exercise.title?.slice() ?? "", + difficulty: exercise.difficulty?.slice() ?? "", + prompt: exercise.prompt.slice(), + topic: exercise.topic.slice(), + tags: exercise.tags.map(tag => tag.slice()), + language: exercise.language.slice(), + referenceTest: exercise.referenceTest.slice(), + starterCode: exercise.starterCode?.slice() ?? "", + }; } -function ExerciseEdit2({ +interface ExerciseEditProps { + exercise: ExerciseResponse | null, + on_created?: (exercise: ExerciseResponse) => void, + on_updated?: (exercise: ExerciseResponse) => void, + on_deleted?: () => void, + on_cancel?: () => void, +} +/* + * there are multiple ts-ignores in this component as for some reason TS is + * unable to deduce the types for them when they should be defined. not sure + * what is the cause as I have done this exact same thing in another project and + * TS did not complain. + * + * TODO: figure out the reason for TS not be able to deduce the types for the + * various form components below + */ +function ExerciseEdit({ exercise, on_created = () => {}, on_updated = () => {}, on_deleted = () => {}, -}: ExerciseEdit2Props) { + on_cancel = () => {}, +}: ExerciseEditProps) { const create_exercise = useMutation({ mutationFn: async (payload: any) => { let result = await axios.post("/api/exercises", payload, { withCredentials: true }); return result.data; }, + onSuccess: (data, vars, ctx) => { + toast.success(`Created Exercise: ${data.title ?? data.uid}`); + + on_created(data); + }, + onError: (err, vars, ctx) => { + toast.error(`Failed to create exericse`); + }, }); const update_exercise = useMutation({ @@ -441,6 +206,14 @@ function ExerciseEdit2({ return result.data; }, + onSuccess: (data, vars, ctx) => { + toast.success(`Updated Exercise: ${data.title ?? data.uid}`); + + on_updated(data); + }, + onError: (err, vars, ctx) => { + toast.error(`Failed to update exercise`); + }, }); const delete_exercise = useMutation({ @@ -448,159 +221,371 @@ function ExerciseEdit2({ let result = await axios.delete(`/api/exercises/${uid}`, { withCredentials: true }); return result.data; + }, + onSuccess: (data, vars, ctx) => { + toast.success(`Deleted Exercise: ${data.title ?? data.uid}`); + + on_deleted(); + }, + onError: (error, vars, ctx) => { + toast.error("Failed to delete exercise"); } }); - const form = useForm({ - defaultValues: exercise != null ? exercise_to_form2(exercise) : blank_form(), - onSubmit: async ({value}) => { + const form = useAppForm({ + defaultValues: exercise != null ? exercise_to_form(exercise) : blank_form(), + onSubmitMeta: { + action: "send" + } as FormMeta, + onSubmit: async ({value, meta, formApi}) => { + if (meta.action === "send") { + let body = { + prompt: value.prompt, + topic: value.topic, + title: value.title.length === 0 ? undefined : value.title, + language: value.language, + difficulty: value.difficulty.length === 0 ? undefined : value.difficulty, + tags: value.tags, + referenceTest: value.referenceTest, + starterCode: value.starterCode, + }; + + if (exercise == null) { + await create_exercise.mutateAsync(body); + } else { + await update_exercise.mutateAsync({uid: exercise.uid, payload: body}); + } + } else if (meta.action === "delete") { + if (exercise != null) { + await delete_exercise.mutateAsync({uid: exercise.uid}); + } + } }, validators: {}, }); - return
-
- - - -

- {exercise != null ? "Edit Exercise" : "Create Exercise"} -

-
- - {exercise != null ? - - : - null - } -
-
-
- { - return
- - field.handleChange(ev.target.value)} - /> -
- }}/> - { - return
- - -
- }}/>
-
- { - return
- - field.handleChange(value?.value ?? "")} - /> -
- }}/> - { - return
- - -
- }}/> +

Results

+
+ {data != null ? + <> +
state: {data.state}
+
tests
+
+
passed: {data.passed}
+
failed: {data.failed}
+
total: {data.tests_run}
+
+
exceeded execution time: {data.execution_time_exceeded ? "true" : "false"}
+
exceeded memory: {data.memory_exceeded ? "true" : "false"}
+ {data.failure_details?.length !== 0 ? + <> +
failure details
+
+ {data.failure_details.map((v: any) => { + return ; + })} +
+ + : + null + } + {data.compilation_error?.length !== 0 ? + <> +
compilation error
+
{data.compilation_error}
+ + : + null + } + {data.runtime_error?.length !== 0 ? + <> +
runtime error
+
{data.runtime_error}
+ + : + null + } + + : + no data + }
- { - return
- - field.handleChange(value)} - styles={SelectStyles} - /> -
- }}/> - { - return
- - field.handleChange(value)} - height="300px" - data-testid="prompt" - /> -
- }}/> - { - let language = field.form.getFieldValue("language"); - - console.log(language); - - return
- - field.handleChange(value)} - theme="vs-dark" - /> -
- }}/>
; } + +interface FailureDetailProps { + test_case: string, + expected: string, + received: string, + error_message: string, + rawout: string, +} + +function FailureDetail({ + test_case, + expected, + received, + error_message, + rawout +}: FailureDetailProps) { + return <> +
{test_case}
+
+
expected: {expected} recieved: {received}
+
error message
+
{error_message}
+
+ ; +} diff --git a/codewit/lib/shared/interfaces/src/lib/interfaces.ts b/codewit/lib/shared/interfaces/src/lib/interfaces.ts index 9fa38d5..8c97331 100644 --- a/codewit/lib/shared/interfaces/src/lib/interfaces.ts +++ b/codewit/lib/shared/interfaces/src/lib/interfaces.ts @@ -22,9 +22,10 @@ interface ExerciseResponse { referenceTest: string; starterCode: string; uid: number; - title?: string; difficulty?: 'easy' | 'hard' | 'worked example'; + createdAt: string, + updatedAt: string, } interface ExerciseFormData{ From f6da7520dcde425fe4fbf0e67c51bcc6b80ae691 Mon Sep 17 00:00:00 2001 From: DAC Date: Wed, 11 Feb 2026 20:21:12 -0800 Subject: [PATCH 09/10] error handling and checks #138 - added validation checks when submitting the form that will use the zod validators. the error messages are currently being ad hoc set so it may not be the best for future use. newer versions of zod may help with this since tanstack form makes comments about it. - added in field error messages that will be displayed if the field has an error attached to it. no styling right now so it will just be straight text with not much to it. - added in some better checks on the codeval response. --- codewit/client/src/form/button.tsx | 2 +- .../src/pages/create/exercise/exercise_id.tsx | 52 +++++++++++++++---- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/codewit/client/src/form/button.tsx b/codewit/client/src/form/button.tsx index 4410372..6aa54b2 100644 --- a/codewit/client/src/form/button.tsx +++ b/codewit/client/src/form/button.tsx @@ -2,7 +2,7 @@ import { ArrowLeftIcon, TrashIcon, ArrowPathIcon } from "@heroicons/react/24/sol import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from "flowbite-react"; import { useState } from "react"; -import { useFormContext } from "../form"; +import { useFormContext } from "./context"; interface SubmitButtonProps {} diff --git a/codewit/client/src/pages/create/exercise/exercise_id.tsx b/codewit/client/src/pages/create/exercise/exercise_id.tsx index 99e5178..bce7356 100644 --- a/codewit/client/src/pages/create/exercise/exercise_id.tsx +++ b/codewit/client/src/pages/create/exercise/exercise_id.tsx @@ -261,7 +261,28 @@ function ExerciseEdit({ } } }, - validators: {}, + validators: { + onSubmit: ({value, formApi}) => { + let result = exercise == null ? + createExerciseSchema.safeParse(value) : + updateExerciseSchema.safeParse(value); + + if (!result.success) { + let rtn = { + fields: {}, + }; + + for (let issue of result.error.issues) { + // TODO: this is going to be a little ad hoc for now and + // will need to double check this at some point for + // something better + rtn.fields[issue.path.join('.')] = issue.message; + } + + return rtn; + } + } + }, }); useEffect(() => { @@ -296,29 +317,32 @@ function ExerciseEdit({
- + { + fieldApi.setValue(value.trim()); + } + }}> {/*@ts-ignore*/} {field =>
- + field.handleChange(ev.target.value.trim())} + onChange={ev => field.handleChange(ev.target.value)} /> + {field.state.meta.errors.map(err =>
{err}
)}
}
{/*@ts-ignore*/} {field =>
- + + {field.state.meta.errors.map(err =>
{err}
)}
}
@@ -354,6 +379,7 @@ function ExerciseEdit({ } }} /> + {field.state.meta.errors.map(err =>
{err}
)}
}
@@ -364,7 +390,6 @@ function ExerciseEdit({ id={field.name} name={field.name} value={field.state.value} - disabled={field.form.state.isSubmitting} onBlur={field.handleBlur} onChange={ev => field.handleChange(ev.target.value)} > @@ -372,6 +397,7 @@ function ExerciseEdit({ + {field.state.meta.errors.map(err =>
{err}
)}
}
@@ -395,6 +421,7 @@ function ExerciseEdit({ styles={SelectStyles} isMulti /> + {field.state.meta.errors.map(err =>
{err}
)}
}
@@ -410,6 +437,7 @@ function ExerciseEdit({ height="300px" data-testid="prompt" /> + {field.state.meta.errors.map(err =>
{err}
)}
} {/*@ts-ignore*/} @@ -427,6 +455,7 @@ function ExerciseEdit({ onChange={value => field.handleChange(value ?? "")} theme="vs-dark" /> + {field.state.meta.errors.map(err =>
{err}
)} } @@ -440,6 +469,7 @@ function ExerciseEdit({ onChange={value => field.handleChange(value ?? "")} theme="vs-dark" /> + {field.state.meta.errors.map(err =>
{err}
)} }
} @@ -528,7 +558,7 @@ function ExerciseTest({
exceeded execution time: {data.execution_time_exceeded ? "true" : "false"}
exceeded memory: {data.memory_exceeded ? "true" : "false"}
- {data.failure_details?.length !== 0 ? + {(data.failure_details?.length ?? 0) !== 0 ? <>
failure details
@@ -540,7 +570,7 @@ function ExerciseTest({ : null } - {data.compilation_error?.length !== 0 ? + {(data.compilation_error?.length ?? 0) !== 0 ? <>
compilation error
{data.compilation_error}
@@ -548,7 +578,7 @@ function ExerciseTest({ : null } - {data.runtime_error?.length !== 0 ? + {(data.runtime_error?.length ?? 0) !== 0 ? <>
runtime error
{data.runtime_error}
From 1e53df5de08550adc59f8d89920d99eb8a59ae68 Mon Sep 17 00:00:00 2001 From: DAC Date: Wed, 11 Feb 2026 20:35:56 -0800 Subject: [PATCH 10/10] silence ts errors #138 this needs to be fixed as there should be no reason for the typescript to be complaining about these errors. --- .../client/src/pages/create/exercise/exercise_id.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/codewit/client/src/pages/create/exercise/exercise_id.tsx b/codewit/client/src/pages/create/exercise/exercise_id.tsx index bce7356..26b4b73 100644 --- a/codewit/client/src/pages/create/exercise/exercise_id.tsx +++ b/codewit/client/src/pages/create/exercise/exercise_id.tsx @@ -276,6 +276,7 @@ function ExerciseEdit({ // TODO: this is going to be a little ad hoc for now and // will need to double check this at some point for // something better + //@ts-ignore rtn.fields[issue.path.join('.')] = issue.message; } @@ -318,7 +319,9 @@ function ExerciseEdit({
{ + //@ts-ignore fieldApi.setValue(value.trim()); } }}> @@ -332,6 +335,7 @@ function ExerciseEdit({ onBlur={field.handleBlur} onChange={ev => field.handleChange(ev.target.value)} /> + {/*@ts-ignore*/} {field.state.meta.errors.map(err =>
{err}
)}
} @@ -351,6 +355,7 @@ function ExerciseEdit({ + {/*@ts-ignore*/} {field.state.meta.errors.map(err =>
{err}
)}
} @@ -379,6 +384,7 @@ function ExerciseEdit({ } }} /> + {/*@ts-ignore*/} {field.state.meta.errors.map(err =>
{err}
)}
} @@ -397,6 +403,7 @@ function ExerciseEdit({ + {/*@ts-ignore*/} {field.state.meta.errors.map(err =>
{err}
)} } @@ -421,6 +428,7 @@ function ExerciseEdit({ styles={SelectStyles} isMulti /> + {/*@ts-ignore*/} {field.state.meta.errors.map(err =>
{err}
)} } @@ -437,6 +445,7 @@ function ExerciseEdit({ height="300px" data-testid="prompt" /> + {/*@ts-ignore*/} {field.state.meta.errors.map(err =>
{err}
)} } @@ -455,6 +464,7 @@ function ExerciseEdit({ onChange={value => field.handleChange(value ?? "")} theme="vs-dark" /> + {/*@ts-ignore*/} {field.state.meta.errors.map(err =>
{err}
)} } @@ -469,6 +479,7 @@ function ExerciseEdit({ onChange={value => field.handleChange(value ?? "")} theme="vs-dark" /> + {/*@ts-ignore*/} {field.state.meta.errors.map(err =>
{err}
)} }