From d6f1619cf2890edde3c3ad90a7062eb32bff2d2e Mon Sep 17 00:00:00 2001 From: Martina Viola Date: Fri, 4 Apr 2025 09:41:40 -0400 Subject: [PATCH 01/11] Created QuestionnaireUtils.ts with interfaces for questionnaire responses and requests and fetching data from the back end --- package-lock.json | 45 ++++++- src/App.tsx | 33 +++++- src/components/Alert.tsx | 2 +- src/pages/Questionnaires/Question.tsx | 85 ++++++++++++++ .../Questionnaire.css | 0 .../Questionnaires/QuestionnaireColumns.tsx | 66 +++++++++++ .../Questionnaires/QuestionnaireEditor.tsx | 29 +++++ .../Questionnaires/QuestionnaireUtils.ts | 111 ++++++++++++++++++ .../dummyData.json | 0 .../questionnaire.tsx | 0 src/utils/interfaces.ts | 1 + 11 files changed, 365 insertions(+), 7 deletions(-) create mode 100644 src/pages/Questionnaires/Question.tsx rename src/pages/{Questionnaire => Questionnaires}/Questionnaire.css (100%) create mode 100644 src/pages/Questionnaires/QuestionnaireColumns.tsx create mode 100644 src/pages/Questionnaires/QuestionnaireEditor.tsx create mode 100644 src/pages/Questionnaires/QuestionnaireUtils.ts rename src/pages/{Questionnaire => Questionnaires}/dummyData.json (100%) rename src/pages/{Questionnaire => Questionnaires}/questionnaire.tsx (100%) diff --git a/package-lock.json b/package-lock.json index 3ef4561e..ec5213eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@types/react-router-dom": "^5.3.3", "axios": "^1.4.0", "bootstrap": "^5.3.3", - "chart.js": "^3.7.0", + "chart.js": "^4.1.1", "formik": "^2.2.9", "jquery": "^3.7.1", "jwt-decode": "^3.1.2", @@ -38,7 +38,7 @@ "react-redux": "^8.0.5", "react-router-dom": "^6.11.1", "react-scripts": "^5.0.1", - "recharts": "^2.12.3", + "recharts": "^2.0.0", "redux-persist": "^6.0.0", "sass": "^1.62.1", "save": "^2.9.0", @@ -3045,6 +3045,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -5760,9 +5766,16 @@ } }, "node_modules/chart.js": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz", - "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg==" + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz", + "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } }, "node_modules/check-types": { "version": "11.2.3", @@ -16603,6 +16616,28 @@ "node": ">= 0.8" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", diff --git a/src/App.tsx b/src/App.tsx index 27736ba3..ba5a21ac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,7 +23,8 @@ import UserEditor from "./pages/Users/UserEditor"; import Users from "./pages/Users/User"; import { loadUserDataRolesAndInstitutions } from "./pages/Users/userUtil"; import Home from "pages/Home"; -import Questionnaire from "pages/EditQuestionnaire/Questionnaire"; +// import Questionnaire from "pages/EditQuestionnaire/Questionnaire"; +import Questionnaire from "pages/Questionnaires/Question"; import Courses from "pages/Courses/Course"; import CourseEditor from "pages/Courses/CourseEditor"; import { loadCourseInstructorDataAndInstitutions } from "pages/Courses/CourseUtil"; @@ -40,6 +41,14 @@ import ViewSubmissions from "pages/Assignments/ViewSubmissions"; import ViewScores from "pages/Assignments/ViewScores"; import ViewReports from "pages/Assignments/ViewReports"; import ViewDelayedJobs from "pages/Assignments/ViewDelayedJobs"; + + +// FIXME: Project 4 Additions +import QuestionnaireEditor from "pages/Questionnaires/QuestionnaireEditor"; +import { loadQuestionnaire } from "pages/Questionnaires/QuestionnaireUtils"; + + + function App() { const router = createBrowserRouter([ { @@ -289,7 +298,29 @@ function App() { ], }, { path: "*", element: }, + + // FIXME: REMOVE { path: "questionnaire", element: }, // Added the Questionnaire route + + + // FIxME: Project 4 E2538 + { + path: "questionnaires", + element: } leastPrivilegeRole={ROLE.INSTRUCTOR} />, + loader: loadQuestionnaire, + children: [ + { + path: "new", + element: , + loader: loadQuestionnaire, + }, + { + path: "edit/:id", + element: , + loader: loadQuestionnaire, + }, + ], + }, ], }, ]); diff --git a/src/components/Alert.tsx b/src/components/Alert.tsx index 820b73ca..faf29a50 100644 --- a/src/components/Alert.tsx +++ b/src/components/Alert.tsx @@ -7,7 +7,7 @@ import { alertActions } from "store/slices/alertSlice"; * @author Ankur Mundra on May, 2023 */ -interface IAlertProps { +export interface IAlertProps { variant: string; title?: string; message: string; diff --git a/src/pages/Questionnaires/Question.tsx b/src/pages/Questionnaires/Question.tsx new file mode 100644 index 00000000..b711fc9e --- /dev/null +++ b/src/pages/Questionnaires/Question.tsx @@ -0,0 +1,85 @@ +import { Button, Col, Container, Row } from "react-bootstrap"; +import { Outlet, useLoaderData, useNavigate } from "react-router-dom"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { questionnaireColumns } from "./QuestionnaireColumns"; +import { BsFileText } from "react-icons/bs"; +import { QuestionnaireResponse, getQuestionnaireTypes } from "./QuestionnaireUtils"; +import { Row as TRow } from "@tanstack/react-table"; +import Table from "components/Table/Table"; + + +// FIXME: This is just a table of questionnaires and their corresponding details. +// The reimplemenentation back end doesn't appear to have a predefined list of questionnaire types. +// getQuestionnaireTypes in QuestionnaireUtils.tsx can be used to extract unique types from the +// list of all questionnaires. Alternatively, a constant list of types is included in the current +// Expertiza questionnaire model that can be used as a guide to define a constant list allowed types. + + +const Questionnaires = () => { + const navigate = useNavigate(); + + // loader option + const quest :any = useLoaderData(); + console.log(quest); + + const questionnaireTypes = getQuestionnaireTypes(quest); + + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ + visible: boolean; + data?: QuestionnaireResponse; + }>({ visible: false }); + + const onDeleteQuestionnaireHandler = useCallback(() => setShowDeleteConfirmation({ visible: false }), []); + + const onEditHandle = useCallback( + (row: TRow) => navigate(`edit/${row.original.id}`), + [navigate] + ); + + const onDeleteHandle = useCallback( + (row: TRow) => setShowDeleteConfirmation({ visible: true, data: row.original }), + [] + ); + + const tableColumns = useMemo( + () => questionnaireColumns(onEditHandle, onDeleteHandle), + [onDeleteHandle, onEditHandle] + ); + + + return ( + <> + +
+ + + +

Manage Questionnaires

+ +
+
+ + + + + + + + + + + + ); +}; + +export default Questionnaires; \ No newline at end of file diff --git a/src/pages/Questionnaire/Questionnaire.css b/src/pages/Questionnaires/Questionnaire.css similarity index 100% rename from src/pages/Questionnaire/Questionnaire.css rename to src/pages/Questionnaires/Questionnaire.css diff --git a/src/pages/Questionnaires/QuestionnaireColumns.tsx b/src/pages/Questionnaires/QuestionnaireColumns.tsx new file mode 100644 index 00000000..30d09c21 --- /dev/null +++ b/src/pages/Questionnaires/QuestionnaireColumns.tsx @@ -0,0 +1,66 @@ +import { BsPencilFill, BsPersonXFill } from "react-icons/bs"; +import { Row, createColumnHelper } from "@tanstack/react-table"; + +import { Button } from "react-bootstrap"; +import { QuestionnaireResponse as IQuestionnaire } from "./QuestionnaireUtils"; + +type Fn = (row: Row) => void; +const columnHelper = createColumnHelper(); + + +export const questionnaireColumns = (handleEdit: Fn, handleDelete: Fn) => [ + columnHelper.accessor("id", { + header: "ID", + }), + columnHelper.accessor("name", { + header: "Name", + }), + columnHelper.accessor("private", { + header: "Private", + }), + columnHelper.accessor("min_question_score", { + header: "Min. Question Score", + }), + columnHelper.accessor("max_question_score", { + header: "Max. Question Score", + }), + columnHelper.accessor("questionnaire_type", { + header: "Type", + }), + columnHelper.accessor("instructor_id", { + header: "Instructor ID", + }), + columnHelper.accessor("instructor.name", { + header: "Instructor Name", + }), + columnHelper.accessor("instructor.email", { + header: "Instructor Email", + }), + + + + columnHelper.display({ + id: "actions", + header: "Actions", + cell: ({ row }) => ( + <> + + + + + ), + }), +]; \ No newline at end of file diff --git a/src/pages/Questionnaires/QuestionnaireEditor.tsx b/src/pages/Questionnaires/QuestionnaireEditor.tsx new file mode 100644 index 00000000..25dec6cd --- /dev/null +++ b/src/pages/Questionnaires/QuestionnaireEditor.tsx @@ -0,0 +1,29 @@ +import * as Yup from "yup"; +import axiosClient from "../../utils/axios_client"; +import { IEditor } from "../../utils/interfaces"; +import { QuestionnaireFormValues } from "./QuestionnaireUtils"; +import React, { useEffect, useState } from "react"; +import { useLoaderData } from "react-router-dom"; +import useAPI from "hooks/useAPI"; + +interface IAlertProps { + variant: string; + title?: string; + message: string; +} + +const QuestionnaireEditor: React.FC = ({ mode }) => { + const [alert, setAlert] = useState(null); + const token = localStorage.getItem("token"); + const questionnaire :any = useLoaderData(); + + + // FIXME: Implement form for editing/creating an assignment + return ( +
+ +
+ ) +}; + +export default QuestionnaireEditor; \ No newline at end of file diff --git a/src/pages/Questionnaires/QuestionnaireUtils.ts b/src/pages/Questionnaires/QuestionnaireUtils.ts new file mode 100644 index 00000000..043ec97f --- /dev/null +++ b/src/pages/Questionnaires/QuestionnaireUtils.ts @@ -0,0 +1,111 @@ +import axiosClient from "../../utils/axios_client"; +import { IInstructor } from "../../utils/interfaces"; + +export const QuestionnaireTypes = [ + "Metareview", + "Author Feedback", + "Teammate Review", + "Survey", + "Assignment Survey", + "Global Survey", + "Course Survey", + "Bookmark Rating", + "Quiz", +]; + +export interface QuestionnaireFormValues { + id?: number; + name: string; + questionnaire_type:string; + private:boolean; + min_question_score: number; + max_question_score: number; + instructor_id: number; + instructor: IInstructor; +} + +export interface QuestionnaireResponse { + id?: number; + name: string; + private:boolean; + questionnaire_type:string; + min_question_score: number; + max_question_score: number; + + instructor_id: number; + instructor: IInstructor; +} + +export interface QuestionnaireRequest { + id?: number; + name: string; + private:boolean; + questionnaire_type:string; + min_question_score: number; + max_question_score: number; + + instructor_id: number; + instructor: IInstructor; +} + +export function getQuestionnaireTypes(quest: QuestionnaireResponse[]): string[] { + return Array.from( + new Set( + quest + .map((q) => q.questionnaire_type) + .filter((type): type is string => type !== null) + ) + ); +} + +export const transformQuestionnaireRequest = (values: QuestionnaireFormValues) => { + const questionnaire: QuestionnaireRequest = { + name: values.name, + + questionnaire_type:values.questionnaire_type, + private:values.private, + min_question_score: values.min_question_score, + max_question_score:values.max_question_score, + instructor_id:values.instructor_id, + instructor:values.instructor, + }; + console.log(questionnaire); + return JSON.stringify(questionnaire); +}; + +export const transformQuestionnaireResponse = (questionnaireResponse: string) => { + const questionnaire: QuestionnaireResponse = JSON.parse(questionnaireResponse); + const questionnaireValues: QuestionnaireFormValues = { + id: questionnaire.id, + name: questionnaire.name, + private:questionnaire.private, + questionnaire_type:questionnaire.questionnaire_type, + min_question_score: questionnaire.min_question_score, + max_question_score:questionnaire.max_question_score, + instructor_id:questionnaire.instructor_id, + instructor:questionnaire.instructor, + }; + return questionnaireValues; +}; + +export async function loadQuestionnaire({ params }: any) { + let questionnaireData = {}; + // if params contains id, then we are editing a user, so we need to load the user data + if (params.id) { + const questionnaireResponse = await axiosClient.get(`/questionnaires/${params.id}`, { + transformResponse: transformQuestionnaireResponse, + }); + questionnaireData = await questionnaireResponse.data; + } + else { + // Fetch all questionnaires + const questionnaireResponse = await axiosClient.get(`/questionnaires`); + questionnaireData = questionnaireResponse.data.map((q: any) => + transformQuestionnaireResponse(JSON.stringify(q)) + ); + } + + console.log(questionnaireData); + return questionnaireData; +} + diff --git a/src/pages/Questionnaire/dummyData.json b/src/pages/Questionnaires/dummyData.json similarity index 100% rename from src/pages/Questionnaire/dummyData.json rename to src/pages/Questionnaires/dummyData.json diff --git a/src/pages/Questionnaire/questionnaire.tsx b/src/pages/Questionnaires/questionnaire.tsx similarity index 100% rename from src/pages/Questionnaire/questionnaire.tsx rename to src/pages/Questionnaires/questionnaire.tsx diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 213909c9..99e70c19 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -20,6 +20,7 @@ export interface IInstitution { export interface IInstructor { id?: number; name: string; + email?: string; } export interface ITA { From c9dd633a5cf218dde2c42142e0e158e2ccbe5ec7 Mon Sep 17 00:00:00 2001 From: Martina Viola Date: Fri, 4 Apr 2025 10:56:13 -0400 Subject: [PATCH 02/11] Create table of questionnaire types --- src/App.tsx | 4 +- src/pages/Questionnaires/Question.tsx | 4 ++ .../Questionnaires/QuestionnaireTypes.tsx | 62 +++++++++++++++++++ .../Questionnaires/QuestionnaireUtils.ts | 29 ++++++++- 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/pages/Questionnaires/QuestionnaireTypes.tsx diff --git a/src/App.tsx b/src/App.tsx index ba5a21ac..5d32ad4a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -300,7 +300,9 @@ function App() { { path: "*", element: }, // FIXME: REMOVE - { path: "questionnaire", element: }, // Added the Questionnaire route + { path: "questionnaire", + element: , + loader: loadQuestionnaire,}, // Added the Questionnaire route // FIxME: Project 4 E2538 diff --git a/src/pages/Questionnaires/Question.tsx b/src/pages/Questionnaires/Question.tsx index b711fc9e..a319f37e 100644 --- a/src/pages/Questionnaires/Question.tsx +++ b/src/pages/Questionnaires/Question.tsx @@ -6,6 +6,7 @@ import { BsFileText } from "react-icons/bs"; import { QuestionnaireResponse, getQuestionnaireTypes } from "./QuestionnaireUtils"; import { Row as TRow } from "@tanstack/react-table"; import Table from "components/Table/Table"; +import QuestionnaireTypeTable from "./QuestionnaireTypes"; // FIXME: This is just a table of questionnaires and their corresponding details. @@ -65,6 +66,9 @@ const Questionnaires = () => { + + +
{ + const data: TableRow[] = QuestionnaireTypes.map((type) => ({ type })); + const navigate = useNavigate(); + + const onCreate = (type: QuestionnaireType) => { + // FIXME: Navigate to the Questionnaire's new form + navigate(`/questionnaires/new?type=${encodeURIComponent(type)}`); + }; + + const columns: ColumnDef[] = [ + { + header: "Questionnaire Type", + accessorKey: "type", + cell: (info) => info.getValue(), + size: 500, + minSize: 80, + maxSize: 600, + }, + { + header: "Action", + id: "action", + cell: ({ row }) => { + const type = row.original.type; + return ( + + // FIXME: Use "+" button instead (search assets) + + ); + }, + }, + ]; + + + // FIXME: Change the width of the table to span the container + return ( +
= 10} + /> + ); +}; + +export default QuestionnaireTypeTable; \ No newline at end of file diff --git a/src/pages/Questionnaires/QuestionnaireUtils.ts b/src/pages/Questionnaires/QuestionnaireUtils.ts index 043ec97f..8308ec38 100644 --- a/src/pages/Questionnaires/QuestionnaireUtils.ts +++ b/src/pages/Questionnaires/QuestionnaireUtils.ts @@ -1,7 +1,32 @@ import axiosClient from "../../utils/axios_client"; import { IInstructor } from "../../utils/interfaces"; -export const QuestionnaireTypes = [ +// export const QuestionnaireTypes = [ +// "Metareview", +// "Author Feedback", +// "Teammate Review", +// "Survey", +// "Assignment Survey", +// "Global Survey", +// "Course Survey", +// "Bookmark Rating", +// "Quiz", +// ]; + + +export type QuestionnaireType = + | "Metareview" + | "Author Feedback" + | "Teammate Review" + | "Survey" + | "Assignment Survey" + | "Global Survey" + | "Course Survey" + | "Bookmark Rating" + | "Quiz"; + + +export const QuestionnaireTypes: QuestionnaireType[] = [ "Metareview", "Author Feedback", "Teammate Review", @@ -13,6 +38,7 @@ export const QuestionnaireTypes = [ "Quiz", ]; + export interface QuestionnaireFormValues { id?: number; name: string; @@ -58,6 +84,7 @@ export function getQuestionnaireTypes(quest: QuestionnaireResponse[]): string[] ); } + export const transformQuestionnaireRequest = (values: QuestionnaireFormValues) => { const questionnaire: QuestionnaireRequest = { name: values.name, From bd87992e69d6a4991e69e524aeea0baafafa0b2a Mon Sep 17 00:00:00 2001 From: Martina Viola Date: Fri, 4 Apr 2025 11:38:39 -0400 Subject: [PATCH 03/11] add form to create a questionnaire of a particular type --- src/App.tsx | 1 + .../Questionnaires/QuestionnaireEditor.tsx | 131 +++++++++++++++++- .../Questionnaires/QuestionnaireUtils.ts | 8 +- 3 files changed, 132 insertions(+), 8 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 5d32ad4a..f10742b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -294,6 +294,7 @@ function App() { { path: "questionnaire", element: , + loader: loadQuestionnaire, }, ], }, diff --git a/src/pages/Questionnaires/QuestionnaireEditor.tsx b/src/pages/Questionnaires/QuestionnaireEditor.tsx index 25dec6cd..febd86bd 100644 --- a/src/pages/Questionnaires/QuestionnaireEditor.tsx +++ b/src/pages/Questionnaires/QuestionnaireEditor.tsx @@ -3,8 +3,10 @@ import axiosClient from "../../utils/axios_client"; import { IEditor } from "../../utils/interfaces"; import { QuestionnaireFormValues } from "./QuestionnaireUtils"; import React, { useEffect, useState } from "react"; -import { useLoaderData } from "react-router-dom"; +import { Outlet, useLoaderData, useLocation, useNavigate, useSearchParams } from "react-router-dom"; import useAPI from "hooks/useAPI"; +import { Formik, Field, Form } from "formik"; +import { Button, Container, Row, Col, Modal } from 'react-bootstrap'; interface IAlertProps { variant: string; @@ -12,18 +14,139 @@ interface IAlertProps { message: string; } + const QuestionnaireEditor: React.FC = ({ mode }) => { const [alert, setAlert] = useState(null); const token = localStorage.getItem("token"); const questionnaire :any = useLoaderData(); + const [searchParams] = useSearchParams(); + const location = useLocation(); + const navigate = useNavigate(); + const type = searchParams.get("type"); + + // Can view the decoded type in browser console + console.log("Type:", type); + + const validationSchema = Yup.object({ + name: Yup.string().required("Name is required"), + questionnaire_type: Yup.string().required("Type is required"), + min_question_score: Yup.number().required("Minimum score is required"), + max_question_score: Yup.number() + .moreThan(Yup.ref("min_question_score"), "Max score must be greater than min score") + .required("Maximum score is required"), + }); + + // FIXME: Implement HTTP method to submit form values + const handleSubmit = async (values: QuestionnaireFormValues) => { + console.log("Submit:", values); + }; + + + // Define initial form values + const initialValues: QuestionnaireFormValues = { + id: questionnaire?.id ?? undefined, + name: questionnaire?.name ?? "", + questionnaire_type: questionnaire?.questionnaire_type ?? type ?? "", + private: questionnaire?.private ?? false, + min_question_score: questionnaire?.min_question_score ?? 0, + max_question_score: questionnaire?.max_question_score ?? 10, + }; + + const handleClose = () => navigate(location.state?.from ? location.state.from : "/questionnaires"); // FIXME: Implement form for editing/creating an assignment return ( -
+ + + + {mode === "update" + ? `Update Questionnaire - ${questionnaire.name}` + : `Create ${type} Questionnaire`} + + + + + {({ values, handleChange, errors, touched }) => ( +
+
Name
+ + {errors.name && touched.name &&
{errors.name}
} + +
+ + + {errors.questionnaire_type && touched.questionnaire_type && ( +
{errors.questionnaire_type}
+ )} + +
+ +
Private
+ + + +
+ +
Minimum Question Score
+ + {errors.min_question_score && touched.min_question_score && ( +
{errors.min_question_score}
+ )} + +
+ +
Maximum Question Score
+ + {errors.max_question_score && touched.max_question_score && ( +
{errors.max_question_score}
+ )} -
- ) +
+ + + )} + + + + ); }; export default QuestionnaireEditor; \ No newline at end of file diff --git a/src/pages/Questionnaires/QuestionnaireUtils.ts b/src/pages/Questionnaires/QuestionnaireUtils.ts index 8308ec38..91220660 100644 --- a/src/pages/Questionnaires/QuestionnaireUtils.ts +++ b/src/pages/Questionnaires/QuestionnaireUtils.ts @@ -46,8 +46,8 @@ export interface QuestionnaireFormValues { private:boolean; min_question_score: number; max_question_score: number; - instructor_id: number; - instructor: IInstructor; + instructor_id?: number; + instructor?: IInstructor; } export interface QuestionnaireResponse { @@ -70,8 +70,8 @@ export interface QuestionnaireRequest { min_question_score: number; max_question_score: number; - instructor_id: number; - instructor: IInstructor; + instructor_id?: number; + instructor?: IInstructor; } export function getQuestionnaireTypes(quest: QuestionnaireResponse[]): string[] { From 98d795692718e1fc5e1d90cb33810257b22eac6b Mon Sep 17 00:00:00 2001 From: Martina Viola Date: Fri, 4 Apr 2025 17:53:05 -0400 Subject: [PATCH 04/11] created field array for adding a variable number of items to a questionnaire --- .../Questionnaires/QuestionnaireEditor.tsx | 86 ++++++++-- .../QuestionnaireItemsFieldArray.tsx | 157 ++++++++++++++++++ 2 files changed, 230 insertions(+), 13 deletions(-) create mode 100644 src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx diff --git a/src/pages/Questionnaires/QuestionnaireEditor.tsx b/src/pages/Questionnaires/QuestionnaireEditor.tsx index febd86bd..a4c6cb71 100644 --- a/src/pages/Questionnaires/QuestionnaireEditor.tsx +++ b/src/pages/Questionnaires/QuestionnaireEditor.tsx @@ -1,12 +1,13 @@ import * as Yup from "yup"; -import axiosClient from "../../utils/axios_client"; import { IEditor } from "../../utils/interfaces"; import { QuestionnaireFormValues } from "./QuestionnaireUtils"; import React, { useEffect, useState } from "react"; import { Outlet, useLoaderData, useLocation, useNavigate, useSearchParams } from "react-router-dom"; import useAPI from "hooks/useAPI"; -import { Formik, Field, Form } from "formik"; +import { Formik, Field, Form, FieldArray } from "formik"; import { Button, Container, Row, Col, Modal } from 'react-bootstrap'; +import QuestionnaireItemsFieldArray from "./QuestionnaireItemsFieldArray"; + interface IAlertProps { variant: string; @@ -14,6 +15,17 @@ interface IAlertProps { message: string; } +interface QuestionnaireFormWithItems extends QuestionnaireFormValues { + items: { + txt: string; + question_type: string; + weight: number; + break_before: boolean; + alternatives?: string; + min_label?: string; + max_label?: string; + }[]; +} const QuestionnaireEditor: React.FC = ({ mode }) => { const [alert, setAlert] = useState(null); @@ -27,29 +39,71 @@ const QuestionnaireEditor: React.FC = ({ mode }) => { // Can view the decoded type in browser console console.log("Type:", type); - const validationSchema = Yup.object({ + const itemFields = Yup.object().shape({ + txt: Yup.string().required("Question text is required"), + question_type: Yup.string().required("Question type is required"), + weight: Yup.number() + .required("Weight is required") + .positive("Weight must be a positive number"), + + alternatives: Yup.string().when("question_type", ([questionType], schema) => { + if (questionType === "dropdown" || questionType === "multiple_choice") { + return schema + .required("Options are required") + .test( + "min-2-options", + "Enter at least two options, separated by commas.", + (value) => { + if (!value) return false; + const options = value + .split(",") + .map((opt) => opt.trim()) + .filter((opt) => opt !== ""); + return options.length >= 2; + } + ); + } + return schema.notRequired(); + }), + + min_label: Yup.string().when("question_type", ([question_type], schema) => { + return question_type === "scale" + ? schema.required("Minimum label is required") + : schema.notRequired(); + }), + + max_label: Yup.string().when("question_type", ([question_type], schema) => { + return question_type === "scale" + ? schema.required("Maximum label is required") + : schema.notRequired(); + }), + }); + + const validationSchema = Yup.object().shape({ name: Yup.string().required("Name is required"), - questionnaire_type: Yup.string().required("Type is required"), - min_question_score: Yup.number().required("Minimum score is required"), - max_question_score: Yup.number() - .moreThan(Yup.ref("min_question_score"), "Max score must be greater than min score") - .required("Maximum score is required"), + questionnaire_type: Yup.string().required("Questionnaire type is required"), + private: Yup.boolean(), + min_question_score: Yup.number().required("Minimum question score is required"), + max_question_score: Yup.number().required("Maximum question score is required"), + items: Yup.array().of(itemFields).min(1, "At least one question is required"), }); + // FIXME: Implement HTTP method to submit form values - const handleSubmit = async (values: QuestionnaireFormValues) => { + const handleSubmit = async (values: QuestionnaireFormWithItems) => { console.log("Submit:", values); }; - // Define initial form values - const initialValues: QuestionnaireFormValues = { + // initial form values + const initialValues: QuestionnaireFormWithItems = { id: questionnaire?.id ?? undefined, name: questionnaire?.name ?? "", questionnaire_type: questionnaire?.questionnaire_type ?? type ?? "", private: questionnaire?.private ?? false, min_question_score: questionnaire?.min_question_score ?? 0, max_question_score: questionnaire?.max_question_score ?? 10, + items: questionnaire?.items ?? [{ text: "" }], }; @@ -83,8 +137,7 @@ const QuestionnaireEditor: React.FC = ({ mode }) => { /> {errors.name && touched.name &&
{errors.name}
} -
- + = ({ mode }) => {
{errors.max_question_score}
)} +
+ + + {/* FIXME: Implement additional fields to add items to the questionnaire */} +
Items
+ +
- - )} - + /> ); diff --git a/src/pages/Questionnaires/QuestionnaireForm.tsx b/src/pages/Questionnaires/QuestionnaireForm.tsx new file mode 100644 index 00000000..522b4b32 --- /dev/null +++ b/src/pages/Questionnaires/QuestionnaireForm.tsx @@ -0,0 +1,143 @@ +import { QuestionnaireFormValues } from "./QuestionnaireUtils"; +import React from "react"; +import { Formik, Field, Form, FieldArray, ErrorMessage } from "formik"; +import { Button } from 'react-bootstrap'; +import QuestionnaireItemsFieldArray from "./QuestionnaireItemsFieldArray"; +import * as Yup from "yup"; + + +// const QuestionnaireForm = ({ initialValues, validationSchema, handleSubmit }: any) => { +const QuestionnaireForm = ({ initialValues, handleSubmit }: any) => { + + const itemFields = Yup.object().shape({ + txt: Yup.string().required("Question text is required"), + question_type: Yup.string().required("Question type is required"), + weight: Yup.number() + .required("Weight is required") + .positive("Weight must be a positive number"), + + alternatives: Yup.string().when("question_type", ([questionType], schema) => { + if (questionType === "dropdown" || questionType === "multiple_choice") { + return schema + .required("Options are required") + .test( + "min-2-options", + "Enter at least two options, separated by commas.", + (value) => { + if (!value) return false; + const options = value + .split(",") + .map((opt) => opt.trim()) + .filter((opt) => opt !== ""); + return options.length >= 2; + } + ); + } + return schema.notRequired(); + }), + + min_label: Yup.string().when("question_type", ([question_type], schema) => { + return question_type === "scale" + ? schema.required("Minimum label is required") + : schema.notRequired(); + }), + + max_label: Yup.string().when("question_type", ([question_type], schema) => { + return question_type === "scale" + ? schema.required("Maximum label is required") + : schema.notRequired(); + }), + }); + + const validationSchema = Yup.object().shape({ + name: Yup.string().required("Name is required"), + questionnaire_type: Yup.string().required("Questionnaire type is required"), + private: Yup.boolean(), + min_question_score: Yup.number().required("Minimum question score is required"), + max_question_score: Yup.number().required("Maximum question score is required"), + items: Yup.array().of(itemFields).min(1, "At least one question is required"), + }); + + + return ( + + {({ values, handleChange, errors, touched }) => ( +
+
Name
+ + + + + + + +
+ +
Private
+ + + +
+ +
Minimum Question Score
+ + + +
+ +
Maximum Question Score
+ + + +
+ + {/* FIXME: Implement additional fields to add items to the questionnaire */} +
Items
+ + +
+ + + )} +
+ ); +}; + +export default QuestionnaireForm; \ No newline at end of file diff --git a/src/pages/Questionnaires/QuestionnaireTypes.tsx b/src/pages/Questionnaires/QuestionnaireTypes.tsx index 92cc943e..d59d51c5 100644 --- a/src/pages/Questionnaires/QuestionnaireTypes.tsx +++ b/src/pages/Questionnaires/QuestionnaireTypes.tsx @@ -3,6 +3,7 @@ import Table from "components/Table/Table"; import { QuestionnaireTypes, QuestionnaireType } from "./QuestionnaireUtils"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { useNavigate } from "react-router-dom"; +import { IoIosAddCircle } from "react-icons/io"; interface TableRow { @@ -35,12 +36,44 @@ const QuestionnaireTypeTable: React.FC = () => { return ( // FIXME: Use "+" button instead (search assets) - + // + // onCreate(type)} + // className="text-blue-500 hover:text-blue-600 cursor-pointer" + // size={24} + // /> + // onCreate(type)} + // className="text-blue-500 hover:text-blue-600 cursor-pointer transition-all" + // size={24} + // /> + onCreate(type)} - className="bg-blue-500 hover:bg-blue-600 text-black px-3 py-1 rounded" - > - Create - + style={{ + cursor: "pointer", + transition: "all 0.2s", + color: "#3b82f6", // Tailwind blue-500 + }} + onMouseEnter={(e) => { + e.currentTarget.style.color = "#2563eb"; // Tailwind blue-600 + }} + onMouseLeave={(e) => { + e.currentTarget.style.color = "#3b82f6"; // Tailwind blue-500 + }} + size={24} + /> + ); }, }, From 2ad44e96c65e83f24330ea1b74c972b210ab6e7a Mon Sep 17 00:00:00 2001 From: Martina Viola Date: Thu, 10 Apr 2025 16:08:17 -0400 Subject: [PATCH 07/11] change handleSubmit to onSubmit to fix formik error --- src/pages/Questionnaires/QuestionnaireEditor.tsx | 8 ++++---- src/pages/Questionnaires/QuestionnaireForm.tsx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/Questionnaires/QuestionnaireEditor.tsx b/src/pages/Questionnaires/QuestionnaireEditor.tsx index 27eb35e4..5ecf5145 100644 --- a/src/pages/Questionnaires/QuestionnaireEditor.tsx +++ b/src/pages/Questionnaires/QuestionnaireEditor.tsx @@ -89,7 +89,7 @@ const QuestionnaireEditor: React.FC = ({ mode }) => { // FIXME: See note below - // const handleSubmit = async (values: QuestionnaireFormWithItems) => { + // const onSubmit = async (values: QuestionnaireFormWithItems) => { // console.log("Submit:", values); // console.log("Submit:", values.items); // }; @@ -100,9 +100,9 @@ const QuestionnaireEditor: React.FC = ({ mode }) => { // allows the items belonging to a questionnaire to be saved at the same time. This also // requires updating the questionnaire_params in questionnaire_controller.rb to include item_attributes. // Including this method without theses changes will result in a failure to submit the form. - // Comment out this version of handleSubmit, and include the implementation above to simply print + // Comment out this version of onSubmit, and include the implementation above to simply print // the form values to the browser console. - const handleSubmit = async (values: any) => { + const onSubmit = async (values: any) => { const endpoint = mode === "create" ? "/questionnaires" // creating questionnaires : `/questionnaires/edit/${values.id}`; // updating questionnaires @@ -151,7 +151,7 @@ const QuestionnaireEditor: React.FC = ({ mode }) => { diff --git a/src/pages/Questionnaires/QuestionnaireForm.tsx b/src/pages/Questionnaires/QuestionnaireForm.tsx index 522b4b32..f27c410b 100644 --- a/src/pages/Questionnaires/QuestionnaireForm.tsx +++ b/src/pages/Questionnaires/QuestionnaireForm.tsx @@ -6,8 +6,8 @@ import QuestionnaireItemsFieldArray from "./QuestionnaireItemsFieldArray"; import * as Yup from "yup"; -// const QuestionnaireForm = ({ initialValues, validationSchema, handleSubmit }: any) => { -const QuestionnaireForm = ({ initialValues, handleSubmit }: any) => { +// const QuestionnaireForm = ({ initialValues, validationSchema, onSubmit }: any) => { +const QuestionnaireForm = ({ initialValues, onSubmit }: any) => { const itemFields = Yup.object().shape({ txt: Yup.string().required("Question text is required"), @@ -63,7 +63,7 @@ const QuestionnaireForm = ({ initialValues, handleSubmit }: any) => { {({ values, handleChange, errors, touched }) => (
From 1677dab68d68eed3cb4299deb31f63fd2671f3b1 Mon Sep 17 00:00:00 2001 From: Martina Viola Date: Thu, 10 Apr 2025 16:15:03 -0400 Subject: [PATCH 08/11] move validationschema to form file since validation doesn't change --- .../Questionnaires/QuestionnaireEditor.tsx | 51 +------------------ .../Questionnaires/QuestionnaireForm.tsx | 1 - 2 files changed, 1 insertion(+), 51 deletions(-) diff --git a/src/pages/Questionnaires/QuestionnaireEditor.tsx b/src/pages/Questionnaires/QuestionnaireEditor.tsx index 5ecf5145..35a82171 100644 --- a/src/pages/Questionnaires/QuestionnaireEditor.tsx +++ b/src/pages/Questionnaires/QuestionnaireEditor.tsx @@ -38,56 +38,7 @@ const QuestionnaireEditor: React.FC = ({ mode }) => { // Can view the decoded type in browser console console.log("Type:", type); - // const itemFields = Yup.object().shape({ - // txt: Yup.string().required("Question text is required"), - // question_type: Yup.string().required("Question type is required"), - // weight: Yup.number() - // .required("Weight is required") - // .positive("Weight must be a positive number"), - // - // alternatives: Yup.string().when("question_type", ([questionType], schema) => { - // if (questionType === "dropdown" || questionType === "multiple_choice") { - // return schema - // .required("Options are required") - // .test( - // "min-2-options", - // "Enter at least two options, separated by commas.", - // (value) => { - // if (!value) return false; - // const options = value - // .split(",") - // .map((opt) => opt.trim()) - // .filter((opt) => opt !== ""); - // return options.length >= 2; - // } - // ); - // } - // return schema.notRequired(); - // }), - // - // min_label: Yup.string().when("question_type", ([question_type], schema) => { - // return question_type === "scale" - // ? schema.required("Minimum label is required") - // : schema.notRequired(); - // }), - // - // max_label: Yup.string().when("question_type", ([question_type], schema) => { - // return question_type === "scale" - // ? schema.required("Maximum label is required") - // : schema.notRequired(); - // }), - // }); - // - // const validationSchema = Yup.object().shape({ - // name: Yup.string().required("Name is required"), - // questionnaire_type: Yup.string().required("Questionnaire type is required"), - // private: Yup.boolean(), - // min_question_score: Yup.number().required("Minimum question score is required"), - // max_question_score: Yup.number().required("Maximum question score is required"), - // items: Yup.array().of(itemFields).min(1, "At least one question is required"), - // }); - - + // FIXME: See note below // const onSubmit = async (values: QuestionnaireFormWithItems) => { // console.log("Submit:", values); diff --git a/src/pages/Questionnaires/QuestionnaireForm.tsx b/src/pages/Questionnaires/QuestionnaireForm.tsx index f27c410b..d171b5fd 100644 --- a/src/pages/Questionnaires/QuestionnaireForm.tsx +++ b/src/pages/Questionnaires/QuestionnaireForm.tsx @@ -6,7 +6,6 @@ import QuestionnaireItemsFieldArray from "./QuestionnaireItemsFieldArray"; import * as Yup from "yup"; -// const QuestionnaireForm = ({ initialValues, validationSchema, onSubmit }: any) => { const QuestionnaireForm = ({ initialValues, onSubmit }: any) => { const itemFields = Yup.object().shape({ From 9e00a2eb849b367663d119192fde333e31a5be07 Mon Sep 17 00:00:00 2001 From: Martina Viola Date: Tue, 22 Apr 2025 09:22:49 -0400 Subject: [PATCH 09/11] updates to form submission for backend integration, ui updates --- src/App.tsx | 10 +- src/components/Table/Table.tsx | 6 +- src/layout/Header.tsx | 2 +- src/pages/Questionnaires/Questionnaire.css | 11 +- .../Questionnaires/QuestionnaireColumns.tsx | 3 - .../Questionnaires/QuestionnaireEditor.tsx | 56 +++--- .../Questionnaires/QuestionnaireForm.tsx | 2 +- .../QuestionnaireItemsFieldArray.tsx | 19 +- .../Questionnaires/QuestionnaireTypes.tsx | 35 +--- .../Questionnaires/QuestionnaireUtils.ts | 39 ++-- .../{Question.tsx => Questionnaires.tsx} | 29 ++- src/pages/Questionnaires/questionnaire.tsx | 167 ------------------ 12 files changed, 96 insertions(+), 283 deletions(-) rename src/pages/Questionnaires/{Question.tsx => Questionnaires.tsx} (67%) delete mode 100644 src/pages/Questionnaires/questionnaire.tsx diff --git a/src/App.tsx b/src/App.tsx index f10742b6..9b46aff0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,8 +23,7 @@ import UserEditor from "./pages/Users/UserEditor"; import Users from "./pages/Users/User"; import { loadUserDataRolesAndInstitutions } from "./pages/Users/userUtil"; import Home from "pages/Home"; -// import Questionnaire from "pages/EditQuestionnaire/Questionnaire"; -import Questionnaire from "pages/Questionnaires/Question"; +import Questionnaire from "pages/Questionnaires/Questionnaires"; import Courses from "pages/Courses/Course"; import CourseEditor from "pages/Courses/CourseEditor"; import { loadCourseInstructorDataAndInstitutions } from "pages/Courses/CourseUtil"; @@ -43,7 +42,7 @@ import ViewReports from "pages/Assignments/ViewReports"; import ViewDelayedJobs from "pages/Assignments/ViewDelayedJobs"; -// FIXME: Project 4 Additions +// E2538 Additions import QuestionnaireEditor from "pages/Questionnaires/QuestionnaireEditor"; import { loadQuestionnaire } from "pages/Questionnaires/QuestionnaireUtils"; @@ -300,11 +299,6 @@ function App() { }, { path: "*", element: }, - // FIXME: REMOVE - { path: "questionnaire", - element: , - loader: loadQuestionnaire,}, // Added the Questionnaire route - // FIxME: Project 4 E2538 { diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 8be9ca9c..ef7607ac 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -146,7 +146,6 @@ const Table: React.FC = ({ return ( <> -
{isGlobalFilterVisible && ( @@ -158,11 +157,9 @@ const Table: React.FC = ({ {isGlobalFilterVisible ? " Hide" : " Show"} {" "} - - - + {getHeaderGroups().map((headerGroup) => ( @@ -226,7 +223,6 @@ const Table: React.FC = ({ )} - ); }; diff --git a/src/layout/Header.tsx b/src/layout/Header.tsx index 5b278dd8..949750db 100644 --- a/src/layout/Header.tsx +++ b/src/layout/Header.tsx @@ -128,7 +128,7 @@ const Header: React.FC = () => { Assignments - + Questionnaire diff --git a/src/pages/Questionnaires/Questionnaire.css b/src/pages/Questionnaires/Questionnaire.css index 5362989a..19f7f4cd 100644 --- a/src/pages/Questionnaires/Questionnaire.css +++ b/src/pages/Questionnaires/Questionnaire.css @@ -4,12 +4,13 @@ align-items: center; text-align: center; min-height: 100vh; - padding-top: 20px; /* Add padding to the top */ + padding-top: 20px; } + .questionnaire-table { border-collapse: collapse; - width: 70%; /* Adjust the width as needed */ - margin-top: 20px; /* Add some spacing from the button and label */ + width: 70%; + margin-top: 20px; } .questionnaire-table th, .questionnaire-table td { @@ -29,11 +30,11 @@ .questionnaire-table tr:hover { background-color: #ddd; } + .centered-table { display: flex; justify-content: center; align-items: center; text-align: center; height: 100%; - } - \ No newline at end of file + } \ No newline at end of file diff --git a/src/pages/Questionnaires/QuestionnaireColumns.tsx b/src/pages/Questionnaires/QuestionnaireColumns.tsx index 30d09c21..0a4604e2 100644 --- a/src/pages/Questionnaires/QuestionnaireColumns.tsx +++ b/src/pages/Questionnaires/QuestionnaireColumns.tsx @@ -36,9 +36,6 @@ export const questionnaireColumns = (handleEdit: Fn, handleDelete: Fn) => [ columnHelper.accessor("instructor.email", { header: "Instructor Email", }), - - - columnHelper.display({ id: "actions", header: "Actions", diff --git a/src/pages/Questionnaires/QuestionnaireEditor.tsx b/src/pages/Questionnaires/QuestionnaireEditor.tsx index 35a82171..72c76e16 100644 --- a/src/pages/Questionnaires/QuestionnaireEditor.tsx +++ b/src/pages/Questionnaires/QuestionnaireEditor.tsx @@ -1,7 +1,7 @@ import axiosClient from "../../utils/axios_client"; import * as Yup from "yup"; import { IEditor } from "../../utils/interfaces"; -import { QuestionnaireFormValues } from "./QuestionnaireUtils"; +import { QuestionnaireFormValues , transformQuestionnaireRequest } from "./QuestionnaireUtils"; import React, { useState } from "react"; import { useLoaderData, useLocation, useNavigate, useSearchParams } from "react-router-dom"; import { Modal } from 'react-bootstrap'; @@ -14,17 +14,6 @@ interface IAlertProps { message: string; } -interface QuestionnaireFormWithItems extends QuestionnaireFormValues { - items: { - txt: string; - question_type: string; - weight: number; - break_before: boolean; - alternatives?: string; - min_label?: string; - max_label?: string; - }[]; -} const QuestionnaireEditor: React.FC = ({ mode }) => { const [alert, setAlert] = useState(null); @@ -38,9 +27,10 @@ const QuestionnaireEditor: React.FC = ({ mode }) => { // Can view the decoded type in browser console console.log("Type:", type); + console.log(questionnaire); // FIXME: See note below - // const onSubmit = async (values: QuestionnaireFormWithItems) => { + // const onSubmit = async (values: QuestionnaireFormValues) => { // console.log("Submit:", values); // console.log("Submit:", values.items); // }; @@ -54,22 +44,27 @@ const QuestionnaireEditor: React.FC = ({ mode }) => { // Comment out this version of onSubmit, and include the implementation above to simply print // the form values to the browser console. const onSubmit = async (values: any) => { + console.log("hello"); + console.log(values.items); + // transform the values before submitting + const transformedValues = transformQuestionnaireRequest(values); + + // determine the endpoint based on whether we're creating or updating const endpoint = mode === "create" ? "/questionnaires" // creating questionnaires - : `/questionnaires/edit/${values.id}`; // updating questionnaires + : `/questionnaires/${values.id}`; // updating questionnaires try { - const response = await axiosClient[mode === "create" ? "post" : "put"](endpoint, values, - { - headers: { - Authorization: `Bearer ${token}`, - }, - } - ); + // submit the transformed values to the API + const response = await axiosClient[mode === "create" ? "post" : "put"](endpoint, transformedValues, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); console.log("Server Response:", response.data); - // Navigate back to questionnaire management dashboard - navigate(`/questionnaires`) + // navigate back to the questionnaire management dashboard + navigate(`/questionnaires`); } catch (error) { console.error("Error submitting form:", error); } @@ -77,14 +72,24 @@ const QuestionnaireEditor: React.FC = ({ mode }) => { // initial form values - const initialValues: QuestionnaireFormWithItems = { + const initialValues: QuestionnaireFormValues = { id: questionnaire?.id ?? undefined, name: questionnaire?.name ?? "", questionnaire_type: questionnaire?.questionnaire_type ?? type ?? "", private: questionnaire?.private ?? false, min_question_score: questionnaire?.min_question_score ?? 0, max_question_score: questionnaire?.max_question_score ?? 10, - items: questionnaire?.items ?? [{ text: "" }], + items: questionnaire?.items ?? [ { + txt: "", + weight: 1, + question_type: "", + break_before: 1, + alternatives: "", + min_label: 0, + max_label: 10, + seq: 1, + }, + ], }; const handleClose = () => navigate(location.state?.from ? location.state.from : "/questionnaires"); @@ -101,7 +106,6 @@ const QuestionnaireEditor: React.FC = ({ mode }) => { diff --git a/src/pages/Questionnaires/QuestionnaireForm.tsx b/src/pages/Questionnaires/QuestionnaireForm.tsx index d171b5fd..6e15f86b 100644 --- a/src/pages/Questionnaires/QuestionnaireForm.tsx +++ b/src/pages/Questionnaires/QuestionnaireForm.tsx @@ -125,7 +125,7 @@ const QuestionnaireForm = ({ initialValues, onSubmit }: any) => {
- {/* FIXME: Implement additional fields to add items to the questionnaire */} + {/* Allows users to input a variable number of questions / items */}
Items
diff --git a/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx b/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx index 4bca598c..e75ecdb0 100644 --- a/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx +++ b/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx @@ -22,11 +22,15 @@ const QuestionnaireItemsFieldArray: React.FC = ({ values, errors, touched remove(index)} + onClick={() => { values.items[index]._destroy = true; remove(index)}} title="Remove question" /> + + + + = ({ values, errors, touched /> )} - - - - - - ))}
@@ -139,10 +137,11 @@ const QuestionnaireItemsFieldArray: React.FC = ({ values, errors, touched txt: "", weight: 1, question_type: "", - break_before: false, + break_before: 1, alternatives: "", - min_label: "", - max_label: "" + min_label: 0, + max_label: 10, + seq: values.items.length + 1, }) } title="Add question" diff --git a/src/pages/Questionnaires/QuestionnaireTypes.tsx b/src/pages/Questionnaires/QuestionnaireTypes.tsx index d59d51c5..5d8769c3 100644 --- a/src/pages/Questionnaires/QuestionnaireTypes.tsx +++ b/src/pages/Questionnaires/QuestionnaireTypes.tsx @@ -15,7 +15,7 @@ const QuestionnaireTypeTable: React.FC = () => { const navigate = useNavigate(); const onCreate = (type: QuestionnaireType) => { - // FIXME: Navigate to the Questionnaire's new form + // Navigate to the Questionnaire's new form navigate(`/questionnaires/new?type=${encodeURIComponent(type)}`); }; @@ -34,53 +34,26 @@ const QuestionnaireTypeTable: React.FC = () => { cell: ({ row }) => { const type = row.original.type; return ( - - // FIXME: Use "+" button instead (search assets) - // - // - // onCreate(type)} - // className="text-blue-500 hover:text-blue-600 cursor-pointer" - // size={24} - // /> - // onCreate(type)} - // className="text-blue-500 hover:text-blue-600 cursor-pointer transition-all" - // size={24} - // /> onCreate(type)} style={{ cursor: "pointer", transition: "all 0.2s", - color: "#3b82f6", // Tailwind blue-500 + color: "#3b82f6", }} onMouseEnter={(e) => { - e.currentTarget.style.color = "#2563eb"; // Tailwind blue-600 + e.currentTarget.style.color = "#2563eb"; }} onMouseLeave={(e) => { - e.currentTarget.style.color = "#3b82f6"; // Tailwind blue-500 + e.currentTarget.style.color = "#3b82f6"; }} size={24} /> - ); }, }, ]; - - // FIXME: Change the width of the table to span the container return (
{ const questionnaire: QuestionnaireRequest = { name: values.name, - questionnaire_type:values.questionnaire_type, private:values.private, min_question_score: values.min_question_score, max_question_score:values.max_question_score, instructor_id:values.instructor_id, instructor:values.instructor, + items_attributes:values.items ? values.items : [], }; - console.log(questionnaire); + console.log("Transformed Questionnaire Request:", questionnaire); return JSON.stringify(questionnaire); }; @@ -111,6 +115,7 @@ export const transformQuestionnaireResponse = (questionnaireResponse: string) => max_question_score:questionnaire.max_question_score, instructor_id:questionnaire.instructor_id, instructor:questionnaire.instructor, + items:questionnaire.items, }; return questionnaireValues; }; diff --git a/src/pages/Questionnaires/Question.tsx b/src/pages/Questionnaires/Questionnaires.tsx similarity index 67% rename from src/pages/Questionnaires/Question.tsx rename to src/pages/Questionnaires/Questionnaires.tsx index a319f37e..e9e96829 100644 --- a/src/pages/Questionnaires/Question.tsx +++ b/src/pages/Questionnaires/Questionnaires.tsx @@ -1,30 +1,31 @@ -import { Button, Col, Container, Row } from "react-bootstrap"; -import { Outlet, useLoaderData, useNavigate } from "react-router-dom"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { Button, Col, Container, Modal, Row } from "react-bootstrap"; +import { Outlet, useLoaderData, useLocation, useNavigate } from "react-router-dom"; +import React, { useCallback, useMemo, useState } from "react"; import { questionnaireColumns } from "./QuestionnaireColumns"; import { BsFileText } from "react-icons/bs"; -import { QuestionnaireResponse, getQuestionnaireTypes } from "./QuestionnaireUtils"; +import { QuestionnaireResponse } from "./QuestionnaireUtils"; import { Row as TRow } from "@tanstack/react-table"; import Table from "components/Table/Table"; import QuestionnaireTypeTable from "./QuestionnaireTypes"; -// FIXME: This is just a table of questionnaires and their corresponding details. +// This is just a table of questionnaires and their corresponding details. // The reimplemenentation back end doesn't appear to have a predefined list of questionnaire types. // getQuestionnaireTypes in QuestionnaireUtils.tsx can be used to extract unique types from the // list of all questionnaires. Alternatively, a constant list of types is included in the current // Expertiza questionnaire model that can be used as a guide to define a constant list allowed types. +// Because using getQuestionnaireTypes will only extract the type from existing questionnaires, +//. types with no instantiations would be missing using this method. const Questionnaires = () => { const navigate = useNavigate(); + const [showTypeModal, setShowTypeModal] = useState(false); // loader option const quest :any = useLoaderData(); console.log(quest); - const questionnaireTypes = getQuestionnaireTypes(quest); - const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ visible: boolean; data?: QuestionnaireResponse; @@ -47,6 +48,7 @@ const Questionnaires = () => { [onDeleteHandle, onEditHandle] ); + const handleClose = () => setShowTypeModal(false); return ( <> @@ -61,13 +63,22 @@ const Questionnaires = () => { - - + {showTypeModal && ( + + + Select Questionnaire Type + + + + + + )}
(null); - const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | 'default' | null>(null); - - const questionnaireItems = dummyData; // Use dummy data for items - - const handleAddButtonClick = () => { - console.log('Add button clicked'); - // Add your logic for adding questionnaire items here - }; - type QuestionnaireItem = { - name: string; - creationDate: string; - updatedDate: string; - }; - - - const handleItemClick = (index: number) => { - if (expandedItem === index) { - setExpandedItem(null); - } else { - setExpandedItem(index); - } - }; - - const handleDelete = (item: QuestionnaireItem) => { - console.log(`Delete button clicked for item:`, item); - // Add your logic for deleting the item here - }; - - const handleEdit = (item: QuestionnaireItem) => { - console.log(`Edit button clicked for item:`, item); - // Add your logic for editing the item here - }; - - const handleShow = (item: QuestionnaireItem) => { - console.log(`Show button clicked for item:`, item); - // Add your logic for showing the item here - }; - - const handleSortByName = () => { - if (sortOrder === 'asc') { - setSortOrder('desc'); - } else { - setSortOrder('asc'); - } - }; - - const sortedQuestionnaireItems = [...questionnaireItems]; - - if (sortOrder === 'asc') { - sortedQuestionnaireItems.sort(); - } else if (sortOrder === 'desc') { - sortedQuestionnaireItems.sort().reverse(); - } - - return ( -
-

Questionnaire List

- - -
- - - -
- - - - - - - - {sortedQuestionnaireItems.map((item, index) => ( - - - - - - {expandedItem === index && ( - - - -)} - - ))} - -
- Name {sortOrder === 'asc' && '↑'} {sortOrder === 'desc' && '↓'} {sortOrder === null && '↑↓'} - Action
handleItemClick(index)}>{item.name} - - - -
- - - - - - - - - - - - - - - - - - - - - - - - -
Name:Creation Date:Updated Date:Actions:
{item.name}{item.creationDate}{item.updatedDate} - - - - - - - - - -
-
- - ); -} - -export default Questionnaire; \ No newline at end of file From b7361aa438e428c7c14066f2fbece302ebda8794 Mon Sep 17 00:00:00 2001 From: Martina Viola Date: Tue, 22 Apr 2025 09:45:17 -0400 Subject: [PATCH 10/11] remove unused imports and other cleanup --- src/pages/Questionnaires/QuestionnaireEditor.tsx | 12 +----------- src/pages/Questionnaires/QuestionnaireForm.tsx | 3 +-- .../Questionnaires/QuestionnaireItemsFieldArray.tsx | 2 -- src/pages/Questionnaires/QuestionnaireTypes.tsx | 4 ++-- 4 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/pages/Questionnaires/QuestionnaireEditor.tsx b/src/pages/Questionnaires/QuestionnaireEditor.tsx index 72c76e16..95bdc29e 100644 --- a/src/pages/Questionnaires/QuestionnaireEditor.tsx +++ b/src/pages/Questionnaires/QuestionnaireEditor.tsx @@ -1,22 +1,13 @@ import axiosClient from "../../utils/axios_client"; -import * as Yup from "yup"; import { IEditor } from "../../utils/interfaces"; import { QuestionnaireFormValues , transformQuestionnaireRequest } from "./QuestionnaireUtils"; -import React, { useState } from "react"; +import React from "react"; import { useLoaderData, useLocation, useNavigate, useSearchParams } from "react-router-dom"; import { Modal } from 'react-bootstrap'; import QuestionnaireForm from "./QuestionnaireForm"; -interface IAlertProps { - variant: string; - title?: string; - message: string; -} - - const QuestionnaireEditor: React.FC = ({ mode }) => { - const [alert, setAlert] = useState(null); const token = localStorage.getItem("token"); const questionnaire :any = useLoaderData(); const [searchParams] = useSearchParams(); @@ -27,7 +18,6 @@ const QuestionnaireEditor: React.FC = ({ mode }) => { // Can view the decoded type in browser console console.log("Type:", type); - console.log(questionnaire); // FIXME: See note below // const onSubmit = async (values: QuestionnaireFormValues) => { diff --git a/src/pages/Questionnaires/QuestionnaireForm.tsx b/src/pages/Questionnaires/QuestionnaireForm.tsx index 6e15f86b..dbc56536 100644 --- a/src/pages/Questionnaires/QuestionnaireForm.tsx +++ b/src/pages/Questionnaires/QuestionnaireForm.tsx @@ -1,6 +1,5 @@ -import { QuestionnaireFormValues } from "./QuestionnaireUtils"; import React from "react"; -import { Formik, Field, Form, FieldArray, ErrorMessage } from "formik"; +import { Formik, Field, Form, ErrorMessage } from "formik"; import { Button } from 'react-bootstrap'; import QuestionnaireItemsFieldArray from "./QuestionnaireItemsFieldArray"; import * as Yup from "yup"; diff --git a/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx b/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx index e75ecdb0..b5df4ee0 100644 --- a/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx +++ b/src/pages/Questionnaires/QuestionnaireItemsFieldArray.tsx @@ -1,7 +1,6 @@ import React from "react"; import { Field, FieldArray, FieldArrayRenderProps, ErrorMessage } from "formik"; import { IoIosRemoveCircleOutline, IoIosAddCircleOutline } from "react-icons/io"; -import * as Yup from "yup"; interface Props { values: any; @@ -9,7 +8,6 @@ interface Props { touched: any; } - const QuestionnaireItemsFieldArray: React.FC = ({ values, errors, touched }) => { return ( diff --git a/src/pages/Questionnaires/QuestionnaireTypes.tsx b/src/pages/Questionnaires/QuestionnaireTypes.tsx index 5d8769c3..f899c30d 100644 --- a/src/pages/Questionnaires/QuestionnaireTypes.tsx +++ b/src/pages/Questionnaires/QuestionnaireTypes.tsx @@ -1,7 +1,7 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React from "react"; import Table from "components/Table/Table"; import { QuestionnaireTypes, QuestionnaireType } from "./QuestionnaireUtils"; -import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; +import { ColumnDef } from "@tanstack/react-table"; import { useNavigate } from "react-router-dom"; import { IoIosAddCircle } from "react-icons/io"; From 7d262b843113c79dd0ee81e568339de75f421e2f Mon Sep 17 00:00:00 2001 From: Martina Viola Date: Tue, 22 Apr 2025 10:29:03 -0400 Subject: [PATCH 11/11] automatically close types pop-up when a type is selected --- src/pages/Questionnaires/Questionnaires.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/Questionnaires/Questionnaires.tsx b/src/pages/Questionnaires/Questionnaires.tsx index e9e96829..371ef080 100644 --- a/src/pages/Questionnaires/Questionnaires.tsx +++ b/src/pages/Questionnaires/Questionnaires.tsx @@ -1,6 +1,6 @@ import { Button, Col, Container, Modal, Row } from "react-bootstrap"; import { Outlet, useLoaderData, useLocation, useNavigate } from "react-router-dom"; -import React, { useCallback, useMemo, useState } from "react"; +import React, { useCallback, useMemo, useState, useEffect } from "react"; import { questionnaireColumns } from "./QuestionnaireColumns"; import { BsFileText } from "react-icons/bs"; import { QuestionnaireResponse } from "./QuestionnaireUtils"; @@ -20,12 +20,17 @@ import QuestionnaireTypeTable from "./QuestionnaireTypes"; const Questionnaires = () => { const navigate = useNavigate(); + const location = useLocation(); const [showTypeModal, setShowTypeModal] = useState(false); // loader option const quest :any = useLoaderData(); console.log(quest); + useEffect(() => { + setShowTypeModal(false); + }, [location]); + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<{ visible: boolean; data?: QuestionnaireResponse;