diff options
| author | dan <[email protected]> | 2024-01-03 18:36:53 -0500 | 
|---|---|---|
| committer | dan <[email protected]> | 2024-01-03 18:36:53 -0500 | 
| commit | 98be17522ef0474a40b134a3ca4e0d2e8096d469 (patch) | |
| tree | 9ae3b31c03aa2b3cbd8c2f6942445a1c5a439a4e | |
| parent | 3baf3c177f86dcc01ce49e14daec7c5c895eefe6 (diff) | |
| download | draggable-form-demo-master.tar.gz draggable-form-demo-master.tar.bz2 draggable-form-demo-master.zip | |
| -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 | 
