From 916a834f13224598fcb30eccf7cf43325960cb16 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Sat, 9 Nov 2019 23:52:01 +0200 Subject: [PATCH] Implement signup page using jsonschema form --- package-lock.json | 54 ++++--- package.json | 4 +- src/models/Signup.ts | 26 ++++ src/pages/SignUpPage/SignUpPage.scss | 24 +++ src/pages/SignUpPage/SignUpPage.tsx | 211 +++++++++++++++++++-------- 5 files changed, 242 insertions(+), 77 deletions(-) create mode 100644 src/models/Signup.ts diff --git a/package-lock.json b/package-lock.json index a005af4..662c93a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3041,6 +3041,7 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, "requires": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -11669,8 +11670,7 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash-id": { "version": "0.14.0", @@ -11708,11 +11708,6 @@ "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", "dev": true }, - "lodash.topath": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz", - "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=" - }, "lodash.unescape": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", @@ -15001,27 +14996,51 @@ "integrity": "sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA==" }, "react-jsonschema-form": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/react-jsonschema-form/-/react-jsonschema-form-1.5.0.tgz", - "integrity": "sha512-SsldN37+5dDLRAGmwNO6cKb9AH2zhgkhIST9+UVaBqQ/KONl4jj1KFerXiEySGGDFBe81CjMGapZV5Ydrdp4pg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/react-jsonschema-form/-/react-jsonschema-form-1.8.0.tgz", + "integrity": "sha512-3rZZ1tCG+vtXRXEdX751t5L1c1TUGk1pvSY/4LzBrUo5FlwulnwJpkosE4BqwSRxvfMckP8YoHFQV4KjzaHGgQ==", "requires": { + "@babel/runtime-corejs2": "^7.4.5", "ajv": "^6.7.0", - "babel-runtime": "^6.26.0", "core-js": "^2.5.7", - "lodash.topath": "^4.5.2", - "prop-types": "^15.5.8" + "lodash": "^4.17.15", + "prop-types": "^15.5.8", + "react-is": "^16.8.4", + "react-lifecycles-compat": "^3.0.4", + "shortid": "^2.2.14" }, "dependencies": { + "@babel/runtime-corejs2": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.7.2.tgz", + "integrity": "sha512-GfVnHchOBvIMsweQ13l4jd9lT4brkevnavnVOej5g2y7PpTRY+R4pcQlCjWMZoUla5rMLFzaS/Ll2s59cB1TqQ==", + "requires": { + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.2" + }, + "dependencies": { + "core-js": { + "version": "2.6.10", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.10.tgz", + "integrity": "sha512-I39t74+4t+zau64EN1fE5v2W31Adtc/REhzWN+gWRRXg6WH5qAsZm62DHpQ1+Yhe4047T55jvzz7MUqF/dBBlA==" + } + } + }, "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } + }, + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" } } }, @@ -15310,7 +15329,8 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true }, "regenerator-transform": { "version": "0.10.1", diff --git a/package.json b/package.json index 9b8e304..3f6574c 100644 --- a/package.json +++ b/package.json @@ -120,11 +120,11 @@ "query-string": "6.5.0", "react-beautiful-dnd": "10.1.1", "react-helmet": "5.2.1", - "react-jsonschema-form": "1.5.0", + "react-jsonschema-form": "^1.8.0", "react-router": "4.3.1", "react-router-dom": "4.3.1", "react-router-hash-link": "1.2.1", "shortid": "2.2.14" }, "postcss": {} -} \ No newline at end of file +} diff --git a/src/models/Signup.ts b/src/models/Signup.ts new file mode 100644 index 0000000..62f0591 --- /dev/null +++ b/src/models/Signup.ts @@ -0,0 +1,26 @@ +import axios from "axios"; +import { getAuthHeader } from "../auth"; +import * as qs from "query-string"; +const url = `${process.env.API_URL}/signup/`; +import { Question } from "../components/SignupQuestionsWidget"; + +export interface Signup { + id?: number; + time: string; + signupForm_id: number; + answer: string; +} + +export async function createSignup(data: Signup): Promise { + try { + const resp = await axios.post(url, data, { + headers: { + "Authorization": getAuthHeader(), + }, + }); + return resp.data; + } catch (err) { + console.error(err); + throw err; + } +} diff --git a/src/pages/SignUpPage/SignUpPage.scss b/src/pages/SignUpPage/SignUpPage.scss index 66bcb97..f1e0e94 100644 --- a/src/pages/SignUpPage/SignUpPage.scss +++ b/src/pages/SignUpPage/SignUpPage.scss @@ -1,6 +1,26 @@ .sign-up-page { display: flex; + fieldset { + border: none; + padding: 0; + margin: 1rem 0; + + .form-group > label { + margin: 0.5rem 0; + font-weight: 600; + display: block; + } + + input[type="text"] { + display: block; + } + + input[type="radio"], input[type="checkbox"] { + margin-right: 4px; + } + } + .question { display: flex; flex-flow: column nowrap; @@ -9,6 +29,10 @@ input { margin-right: 1rem; } + + h3 { + margin: 0.5rem 0; + } } } } diff --git a/src/pages/SignUpPage/SignUpPage.tsx b/src/pages/SignUpPage/SignUpPage.tsx index 16f9f92..db6fbc0 100644 --- a/src/pages/SignUpPage/SignUpPage.tsx +++ b/src/pages/SignUpPage/SignUpPage.tsx @@ -1,7 +1,9 @@ import * as React from "react"; import Helmet from "react-helmet"; +import Form from "react-jsonschema-form"; import "./SignUpPage.scss"; import { getForm, SignupForm } from "../../models/SignupForm"; +import { createSignup, Signup } from "../../models/Signup"; import PageSection from "../../components/PageSection"; import { ColorEnum } from "../../components/ColorDiv/ColorDiv"; import { Question, optionTypes } from "../../components/SignupQuestionsWidget"; @@ -16,36 +18,22 @@ export interface SignUpPageProps { export interface SignUpPageState { signUpForm: SignupForm | null; + formData: any; + statusMessage: string; + error: string; } class SignUpPage extends React.Component { constructor(props) { super(props); - const { staticContext } = props; + this.state = { + signUpForm: null, + formData: {}, + statusMessage: "", + error: "", + }; - if (staticContext) { - /* The static context is an object that manages promises when - rendering on the server. If staticContext exists, that means - we have to store all promises in it. Otherwise, operate - normally. See server/index.ts. */ - if (staticContext.resolutions.getSignUpForm) { - const signUpForm = staticContext.resolutions.getSignUpForm as SignupForm; - this.state = { - signUpForm, - }; - } else { - this.state = { - signUpForm: null, - }; - const promiseSignUpForm = this.fetchSignUp(); - staticContext.promises.getSignUpForm = promiseSignUpForm; - } - } else { - this.state = { - signUpForm: null, - }; - this.fetchSignUp(); - } + this.fetchSignUp(); } fetchSignUp = (): Promise => { @@ -60,56 +48,163 @@ class SignUpPage extends React.Component { return formPromise; } - renderAnswerInput = (question: Question) => { - if (!optionTypes.includes(question.type)) { - throw new Error("Unknown question type"); + questionToSchemaProp = (question: Question): {} => { + let obj; + if (question.type === "text") { + obj = { + type: "string", + title: question.name, + default: "", + }; + } + else if (question.type === "radiobutton") { + obj = { + type: "string", + title: question.name, + enum: question.options, + }; + } + else if (question.type === "checkbox") { + obj = { + type: "array", + title: question.name, + items: { + type: "string", + enum: question.options, + }, + uniqueItems: true, + }; + } + else { + throw new Error(`No mapping to schema prop for question type ${question.type}`); } - if (question.type === "text") { - return ; + return { + [question.id]: obj, } - if (question.type === "radiobutton") { - const { options } = question; - return options.map((opt, index) => ( - - - {opt} - - )); + } + + questionToUISchemaProp = (question: Question): {} => { + let obj = {}; + if (question.type == "checkbox") { + obj = { + "ui:widget": "checkboxes", + }; } - if (question.type === "checkbox") { - const { options } = question; - return options.map((opt, index) => { - const optSlug = opt.replace(/\s/g, "_").toLocaleLowerCase(); - return ( - - {opt}
-
- ) + else if (question.type == "radiobutton") { + obj = { + "ui:widget": "radio", + } + } + // else { + // throw new Error(`No mapping to UI schema prop for question type ${question.type}`); + // } + return { + [question.id]: obj, + }; + } + + buildSchema = () => { + const { error, formData, signUpForm } = this.state; + const questions: Question[] = JSON.parse(signUpForm.questions); + const schemaPropsArray = questions.map(this.questionToSchemaProp); + let schemaProps = {}; + schemaPropsArray.forEach((schemaProp) => { + schemaProps = { + ...schemaProps, + ...schemaProp, + }; + }); + + const schema = { + title: signUpForm.id ? signUpForm.title : "Loading...", + type: "object", + required: [], + properties: schemaProps, + }; + + return schema; + } + + buildUISchema = () => { + const { error, formData, signUpForm } = this.state; + const questions: Question[] = JSON.parse(signUpForm.questions); + const uiSchemaPropsArray = questions.map(this.questionToUISchemaProp); + let uiSchemaProps = {}; + uiSchemaPropsArray.forEach((uiSchemaProp) => { + uiSchemaProps = { + ...uiSchemaProps, + ...uiSchemaProp, + }; + }); + + const uiSchema = { + ...uiSchemaProps, + }; + return uiSchema; + } + + onSubmit = async (data) => { + const { signUpForm } = this.state; + console.log("submitted, data:"); + console.log(data); + + try { + const payload: Signup = { + time: new Date().toISOString(), + signupForm_id: signUpForm.id, + answer: JSON.stringify(data.formData), + }; + // payload.questions = JSON.stringify(payload.questions); + + if (payload.id === undefined) { + const resp = await createSignup(payload); + this.setState({ + formData: resp, + statusMessage: "Sign-up submitted successfully", + }); + } + else { + debugger; + // const resp = await updateSignup(payload); + // this.setState({ + // formData: resp, + // statusMessage: "Sign-up submission updated successfully.", + // }); + } + } catch (err) { + this.setState({ + error: String(err), }); } } - renderQuestion = (question: Question) => { - return ( -
- {question.name} - {this.renderAnswerInput(question)} -
- ); + onChange = (data) => { + console.log(data); } renderForm() { - const { signUpForm } = this.state; - const questions: Question[] = JSON.parse(signUpForm.questions); - const content = questions.map(this.renderQuestion); + const { signUpForm, formData } = this.state; + // const questions: Question[] = JSON.parse(signUpForm.questions); + // const content = questions.map(this.renderQuestion); + const schema = this.buildSchema(); + const uiSchema = this.buildUISchema(); return (

Title: {signUpForm.title}

-
+ {/*
{content} -
+ + */} +
); }