diff options
-rw-r--r-- | src/App.js | 10 | ||||
-rw-r--r-- | src/components/FormBuilder/FinalForm.js | 12 | ||||
-rw-r--r-- | src/components/FormBuilder/index.js | 201 | ||||
-rw-r--r-- | src/hooks/useForm.js | 20 | ||||
-rw-r--r-- | src/hooks/useLocalStorage.js | 17 | ||||
-rw-r--r-- | src/pages/NewSurvey/index.js | 5 | ||||
-rw-r--r-- | src/pages/Survey/index.js | 7 | ||||
-rw-r--r-- | src/pages/SurveyAnswer/index.js | 27 | ||||
-rw-r--r-- | src/pages/SurveyResults/index.js | 58 | ||||
-rw-r--r-- | src/pages/Surveys/SurveysList.js | 36 |
10 files changed, 299 insertions, 94 deletions
@@ -7,7 +7,9 @@ import { import Login from "./pages/Login"; import Surveys from "./pages/Surveys"; import NewSurvey from "./pages/NewSurvey"; +import Survey from "./pages/Survey"; import SurveyResults from "./pages/SurveyResults"; +import SurveyAnswer from "./pages/SurveyAnswer"; import SurveyAssignees from "./pages/SurveyAssignees"; import Users from "./pages/Users"; import NavBar from "./components/NavBar"; @@ -53,10 +55,18 @@ function routes({ login, logout, isLoggedIn }) { element: withNavBar(<NewSurvey />), }, { + path: "/surveys/:formId", + element: withNavBar(<Survey />), + }, + { path: "/surveys/:surveyId/results", element: withNavBar(<SurveyResults />), }, { + path: "/surveys/:surveyId/results/:runId/answer", + element: withNavBar(<SurveyAnswer />), + }, + { path: "/surveys/:surveyId/assignees", element: withNavBar(<SurveyAssignees />), }, diff --git a/src/components/FormBuilder/FinalForm.js b/src/components/FormBuilder/FinalForm.js new file mode 100644 index 0000000..070a0fe --- /dev/null +++ b/src/components/FormBuilder/FinalForm.js @@ -0,0 +1,12 @@ +import { Form } from "./index"; + +export default function FinalForm({ formId, setResults, results }) { + return ( + <Form + formId={formId} + editable={false} + setResults={setResults} + results={results} + /> + ); +} diff --git a/src/components/FormBuilder/index.js b/src/components/FormBuilder/index.js index 5636b32..5c77c25 100644 --- a/src/components/FormBuilder/index.js +++ b/src/components/FormBuilder/index.js @@ -8,7 +8,9 @@ import { TextField, } from "@mui/material"; import React, { useRef, useState } from "react"; -export default function FormBuilder() { +import useForm from "../../hooks/useForm"; + +export default function FormBuilder({ formId }) { return ( <Stack direction={"column"}> <h3>FormBuilder</h3> @@ -18,7 +20,7 @@ export default function FormBuilder() { alignItems={"flex-start"} spacing={2} > - <Form initialForm={false} editable /> + <Form initialForm={false} editable formId={formId} /> <WidgetsLibrary /> </Stack> </Stack> @@ -28,43 +30,57 @@ export default function FormBuilder() { const availableWidgets = { label: { name: "Label", - element: ({ id, name, setName }) => ( - <TextField - id={id} - variant="standard" - value={name} - onChange={(e) => setName(e.target.value)} - fullWidth - /> - ), - finalElement: ({ id }) => <div>{id}</div>, + element: ({ id, name, setName, finalField }) => + finalField ? ( + <div>{name}</div> + ) : ( + <TextField + id={id} + variant="standard" + value={name} + onChange={(e) => setName(e.target.value)} + fullWidth + /> + ), }, number: { name: "Number", - element: ({ id }) => ( + element: ({ id, value, setValue }) => ( <TextField id={id} label={id ? `example field ${id}` : "Outlined"} variant="outlined" type={"number"} + value={value || ""} + onChange={(e) => { + if (setValue) { + setValue(e.target.value); + } + }} fullWidth /> ), }, text: { name: "Text", - element: ({ id }) => ( + element: ({ id, setValue, value }) => ( <TextField id={id} label={id ? `example field ${id}` : "Outlined"} variant="outlined" fullWidth + value={value || ""} + onChange={(e) => { + if (setValue) { + setValue(e.target.value); + } + }} /> ), }, multiline: { name: "Multiline", - element: ({ id }) => ( + element: ({ id, setValue, value }) => ( <TextField id={id} label={id ? `example field ${id}` : "Multiline"} @@ -72,6 +88,12 @@ const availableWidgets = { multiline minRows={4} fullWidth + value={value || ""} + onChange={(e) => { + if (setValue) { + setValue(e.target.value); + } + }} /> ), }, @@ -134,7 +156,14 @@ function WidgetCard({ name, element, type, finalElement }) { } } -function FormField({ formField, setFormField, deleteFormField }) { +function FormField({ + formField, + setFormField, + deleteFormField, + editable, + setResult, + result, +}) { const { id, type, name } = formField; function setName(name) { @@ -147,33 +176,47 @@ function FormField({ formField, setFormField, deleteFormField }) { const { element: Element } = availableWidgets[type]; const dragHandleRef = useRef(); const [target, setTarget] = useState(); - return ( - <Stack - direction={"row"} - sx={{ width: "100%" }} - alignItems={"center"} - justifyContent={"space-between"} - draggable - onMouseDown={(e) => { - setTarget(e.target); - }} - onDragStart={(e) => { - if (dragHandleRef.current.contains(target)) { - e.dataTransfer.setData("application/formwidgetid", id); - } else { - e.preventDefault(); - } - }} - > - <span ref={dragHandleRef} style={{ cursor: "grab" }}> - <DragIndicator /> - </span> - <Element id={id} name={name} setName={setName} type={type} /> - <IconButton onClick={deleteFormField}> - <Clear /> - </IconButton> - </Stack> - ); + if (!editable) { + return ( + <Element + id={id} + name={name} + setName={setName} + type={type} + finalField={!editable} + setValue={setResult} + value={result?.result} + /> + ); + } else { + return ( + <Stack + direction={"row"} + sx={{ width: "100%" }} + alignItems={"center"} + justifyContent={"space-between"} + draggable + onMouseDown={(e) => { + setTarget(e.target); + }} + onDragStart={(e) => { + if (dragHandleRef.current.contains(target)) { + e.dataTransfer.setData("application/formwidgetid", id); + } else { + e.preventDefault(); + } + }} + > + <span ref={dragHandleRef} style={{ cursor: "grab" }}> + <DragIndicator /> + </span> + <Element id={id} name={name} setName={setName} type={type} /> + <IconButton onClick={deleteFormField}> + <Clear /> + </IconButton> + </Stack> + ); + } } function capture(e) { @@ -214,8 +257,9 @@ function DropZone({ insertField, index, moveField }) { ); } -function Form({ initialForm, editable }) { - const [form, setForm] = useState({ fields: [], ...initialForm }); +export function Form({ editable, formId, setResults, results }) { + // const [form, setForm] = useState({ fields: [], ...initialForm }); + const [form, setForm] = useForm(formId); function updateFormField(newField) { setForm({ @@ -254,41 +298,42 @@ function Form({ initialForm, editable }) { setForm({ ...form }); } - if (!editable && !initialForm) { - return; - // else if (!initialForm) { - // return <FormInitialPrompt />; - // } - } else { - return ( - <Stack sx={{ width: "100%" }}> - <FormHeader form={form} /> - {editable && ( - <DropZone - moveField={moveField} - insertField={insertField} - index={-1} + function setResult(fieldId, result) { + const fieldResult = { + fieldId, + result, + fieldName: form?.fields?.find(({ id }) => id === fieldId)?.name, + }; + setResults({ ...results, [fieldId]: fieldResult }); + } + + return ( + <Stack sx={{ width: "100%" }} spacing={editable ? 0 : 1}> + <FormHeader form={form} /> + {editable && ( + <DropZone moveField={moveField} insertField={insertField} index={-1} /> + )} + {form?.fields?.map((formField, i) => ( + <Grouping key={formField.id}> + <FormField + setFormField={(field) => updateFormField(field)} + deleteFormField={() => deleteFormField(formField.id)} + formField={formField} + editable={editable} + setResult={results && ((result) => setResult(formField.id, result))} + result={results && results[formField.id]} /> - )} - {form?.fields?.map((formField, i) => ( - <Grouping key={formField.id}> - <FormField - setFormField={(field) => updateFormField(field)} - deleteFormField={() => deleteFormField(formField.id)} - formField={formField} + {editable && ( + <DropZone + moveField={moveField} + insertField={insertField} + index={i} /> - {editable && ( - <DropZone - moveField={moveField} - insertField={insertField} - index={i} - /> - )} - </Grouping> - ))} - </Stack> - ); - } + )} + </Grouping> + ))} + </Stack> + ); } function FormHeader() { diff --git a/src/hooks/useForm.js b/src/hooks/useForm.js new file mode 100644 index 0000000..6270c1e --- /dev/null +++ b/src/hooks/useForm.js @@ -0,0 +1,20 @@ +import useLocalStorage from "./useLocalStorage"; + +export default function useForm(formId) { + const [forms, setForms] = useLocalStorage(`forms`, {}); + + const form = forms[formId] || { fields: [], id: formId }; + + function setForm(newForm) { + console.log( + "setForm", + newForm, + { ...forms, [newForm.id]: newForm }, + { form: forms[formId], formId, forms }, + ); + setForms({ ...forms, [newForm.id]: newForm }); + } + + console.log("useForm", formId, form); + return [form, setForm]; +} diff --git a/src/hooks/useLocalStorage.js b/src/hooks/useLocalStorage.js new file mode 100644 index 0000000..6dbed36 --- /dev/null +++ b/src/hooks/useLocalStorage.js @@ -0,0 +1,17 @@ +import { useState } from "react"; + +const useLocalStorage = (key, initialValue) => { + const storedVal = localStorage[key] + ? JSON.parse(localStorage[key]) || initialValue + : initialValue; + const [value, setValue] = useState(storedVal); + + function update(newValue) { + localStorage[key] = JSON.stringify(newValue); + setValue(newValue); + } + + return [value, update]; +}; + +export default useLocalStorage; diff --git a/src/pages/NewSurvey/index.js b/src/pages/NewSurvey/index.js index f1d4b89..335eb93 100644 --- a/src/pages/NewSurvey/index.js +++ b/src/pages/NewSurvey/index.js @@ -1,4 +1,5 @@ -import FormBuilder from "../../components/FormBuilder"; +import { Navigate } from "react-router-dom"; export default function NewSurvey() { - return <FormBuilder />; + const id = crypto.randomUUID(); + return <Navigate to={{ pathname: `/surveys/${id}` }} />; } diff --git a/src/pages/Survey/index.js b/src/pages/Survey/index.js new file mode 100644 index 0000000..41bac5e --- /dev/null +++ b/src/pages/Survey/index.js @@ -0,0 +1,7 @@ +import FormBuilder from "../../components/FormBuilder"; +import { useParams } from "react-router-dom"; + +export default function Survey() { + const { formId } = useParams(); + return <FormBuilder formId={formId} />; +} diff --git a/src/pages/SurveyAnswer/index.js b/src/pages/SurveyAnswer/index.js new file mode 100644 index 0000000..d1676c1 --- /dev/null +++ b/src/pages/SurveyAnswer/index.js @@ -0,0 +1,27 @@ +import { useState } from "react"; +import { useParams } from "react-router-dom"; +import Form from "../../components/FormBuilder/FinalForm"; +import useLocalStorage from "../../hooks/useLocalStorage"; + +export default function SurveyAnswers() { + const { runId, surveyId } = useParams(); + const [resultsSet, setResultsSet] = useLocalStorage( + `results-${surveyId}-${runId}`, + {}, + ); + const [answerSetId] = useState(crypto.randomUUID()); + const results = (resultsSet && resultsSet[answerSetId]) || { + datetime: new Date().toISOString(), + }; + + function setResults(newResults) { + setResultsSet({ ...resultsSet, [answerSetId]: newResults }); + } + + return ( + <div> + {runId} + <Form formId={surveyId} results={results} setResults={setResults} /> + </div> + ); +} diff --git a/src/pages/SurveyResults/index.js b/src/pages/SurveyResults/index.js index 82d41c6..dec182e 100644 --- a/src/pages/SurveyResults/index.js +++ b/src/pages/SurveyResults/index.js @@ -1,3 +1,59 @@ +import { Button } from "@mui/material"; +import { DataGrid } from "@mui/x-data-grid"; +import { Link, useParams } from "react-router-dom"; +import useLocalStorage from "../../hooks/useLocalStorage"; +import useForm from "../../hooks/useForm"; + export default function SurveyResults() { - return <>SurveyResults</>; + const { surveyId } = useParams(); + const [runs, setRuns] = useLocalStorage(`runs-${surveyId}`, []); + const [form] = useForm(surveyId); + + const columns = form?.fields?.map(({ id, name }) => ({ + field: id, + headerName: name, + })); + + return ( + <> + <h3>SurveyRuns</h3> + {surveyId} + <Button onClick={() => setRuns([{ id: crypto.randomUUID() }, ...runs])}> + New Run + </Button> + + {runs && + runs.map((r) => { + const runRs = runResults(surveyId, r.id); + + console.log(runRs); + + return ( + <div key={r.id}> + <Link to={`${r.id}/answer`}>Add Answer</Link> {JSON.stringify(r)} + Total Results: {Object.keys(runRs).length} + <DataGrid + rows={[]} + columns={columns} + initialState={{ + pagination: { + paginationModel: { + pageSize: 25, + }, + }, + }} + pageSizeOptions={[25, 50, 100]} + checkboxSelection + disableRowSelectionOnClick + /> + </div> + ); + })} + </> + ); +} + +function runResults(surveyId, runId) { + const str = localStorage[`results-${surveyId}-${runId}`]; + return !str ? {} : JSON.parse(str); } diff --git a/src/pages/Surveys/SurveysList.js b/src/pages/Surveys/SurveysList.js index c9c5b90..a320716 100644 --- a/src/pages/Surveys/SurveysList.js +++ b/src/pages/Surveys/SurveysList.js @@ -1,9 +1,10 @@ import React from "react"; import Box from "@mui/material/Box"; import { DataGrid } from "@mui/x-data-grid"; +import useLocalStorage from "../../hooks/useLocalStorage"; +import { Link } from "react-router-dom"; const columns = [ - { field: "id", headerName: "ID", width: 90 }, { field: "name", headerName: "Name", @@ -14,21 +15,30 @@ const columns = [ headerName: "Next Run", width: 150, }, -]; - -const rows = [ - { id: 1, name: "survey", next_run_at: "Mon 28 Aug 16:35:36 EDT 2023" }, - { id: 2, name: "survey", next_run_at: "Mon 28 Aug 16:35:36 EDT 2023" }, - { id: 3, name: "survey", next_run_at: "Mon 28 Aug 16:35:36 EDT 2023" }, - { id: 4, name: "survey", next_run_at: "Mon 28 Aug 16:35:36 EDT 2023" }, - { id: 5, name: "survey", next_run_at: "Mon 28 Aug 16:35:36 EDT 2023" }, - { id: 6, name: "survey", next_run_at: "Mon 28 Aug 16:35:36 EDT 2023" }, - { id: 7, name: "survey", next_run_at: "Mon 28 Aug 16:35:36 EDT 2023" }, - { id: 8, name: "survey", next_run_at: "Mon 28 Aug 16:35:36 EDT 2023" }, - { id: 9, name: "survey", next_run_at: "Mon 28 Aug 16:35:36 EDT 2023" }, + { + field: "edit_survey_link", + headerName: "", + renderCell: ({ row }) => <Link to={`${row?.path}`}>Edit</Link>, + width: 150, + }, + { + field: "results_survey_link", + headerName: "", + renderCell: ({ row }) => <Link to={`${row?.path}/results`}>Results</Link>, + width: 150, + }, ]; export default function SurveysList() { + const [forms] = useLocalStorage(`forms`, {}); + + const rows = Object.keys(forms).map((id, i) => ({ + id, + name: `Survey ${i}`, + next_run_at: "Mon 28 Aug 16:35:36 EDT 2023", + path: `/surveys/${id}`, + })); + return ( <Box sx={{ minHeight: "100%", width: "100%" }}> <DataGrid |