From 14006ccc2d17fff7d7eb6578a2934049124fa50d Mon Sep 17 00:00:00 2001 From: Aarni Halinen Date: Wed, 1 Sep 2021 20:20:30 +0300 Subject: [PATCH] Update signup question types --- .../Widgets/SignupQuestionsWidget/common.ts | 6 +- src/i18n/index.tsx | 2 +- src/models/Signup.ts | 21 +- src/pages/admin/signups/[id].tsx | 4 +- src/views/SignUpPage/FormUtils.tsx | 54 +- src/views/SignUpPage/SignUpPageView.tsx | 8 +- .../__snapshots__/formUtils.test.ts.snap | 550 +++++++++++++++++- src/views/SignUpPage/formUtils.test.ts | 319 +++++++--- 8 files changed, 843 insertions(+), 121 deletions(-) diff --git a/src/components/Widgets/SignupQuestionsWidget/common.ts b/src/components/Widgets/SignupQuestionsWidget/common.ts index 4a3f18f..753a8fd 100644 --- a/src/components/Widgets/SignupQuestionsWidget/common.ts +++ b/src/components/Widgets/SignupQuestionsWidget/common.ts @@ -2,7 +2,9 @@ export interface Question { id: string; name: string; type: OptionTypes; - options: string[]; + enum?: string[]; + enumNames?: string[]; + description?: string; required?: boolean; } @@ -13,7 +15,7 @@ export interface InputProps { type: string; } -type OptionTypes = +export type OptionTypes = "text" | "info" | "integer" | diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx index 184a565..b53b326 100644 --- a/src/i18n/index.tsx +++ b/src/i18n/index.tsx @@ -4,7 +4,7 @@ import React, { import fi from "./locales/fi/common.json"; import en from "./locales/en/common.json"; -type Lang = "fi" | "en"; +export type Lang = "fi" | "en"; const LOCAL_STORAGE_KEY = "locale"; type TranslateFunc = (key: string) => string; diff --git a/src/models/Signup.ts b/src/models/Signup.ts index 6e301e1..a4e91c7 100644 --- a/src/models/Signup.ts +++ b/src/models/Signup.ts @@ -1,4 +1,4 @@ -import { Question } from "@components/Widgets/SignupQuestionsWidget/common"; +import { OptionTypes } from "@components/Widgets/SignupQuestionsWidget/common"; export interface Signup { id?: number; @@ -6,14 +6,31 @@ export interface Signup { answer: string; } +// Describes how forms are stored in backend +export interface SignupFormQuestion { + id: string; + title_fi: string; + title_en: string; + description_fi?: string; + description_en?: string; + type: OptionTypes; + options?: { + enum: string[]; + enumNames_fi: string[]; + enumNames_en: string[]; + }; + required?: boolean; +} export interface SignupForm { id?: number; title_fi: string; title_en: string; visible: boolean; + isOpen: boolean; start_time: string; end_time: string; - questions: Question[]; + email_content: string; + questions: SignupFormQuestion[]; signups: string[]; quota: number; schema: { diff --git a/src/pages/admin/signups/[id].tsx b/src/pages/admin/signups/[id].tsx index c47f982..ae9b2d9 100644 --- a/src/pages/admin/signups/[id].tsx +++ b/src/pages/admin/signups/[id].tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { NextPage } from "next"; import { useRouter } from "next/router"; import AdminCreateCommon from "@views/admin/AdminCreateCommon"; -import { SignupForm } from "@models/Signup"; +import { SignupForm, SignupFormQuestion } from "@models/Signup"; import SignupApi from "@api/signupApi"; import DatetimeWidget from "@components/Widgets/DatetimeWidget"; import SignupQuestionsWidget from "@components/Widgets/SignupQuestionsWidget/SignupQuestionsWidget"; @@ -124,7 +124,7 @@ const SignupCreatePage: NextPage = () => { const onSubmit = async (data: any) => { try { - const questions = JSON.parse(data.formData.questions); + const questions: SignupFormQuestion[] = JSON.parse(data.formData.questions); const payload: SignupForm = { ...data.formData, questions, diff --git a/src/views/SignUpPage/FormUtils.tsx b/src/views/SignUpPage/FormUtils.tsx index f6d189b..943339c 100644 --- a/src/views/SignUpPage/FormUtils.tsx +++ b/src/views/SignUpPage/FormUtils.tsx @@ -1,7 +1,8 @@ import { Question } from "@components/Widgets/SignupQuestionsWidget/common"; -import { SignupForm } from "@models/Signup"; +import { SignupFormQuestion } from "@models/Signup"; import { EMAIL_REGEX } from "@utils/regexes"; import escapeRegExp from "lodash/escapeRegExp"; +import { Lang } from "src/i18n"; const questionToUISchemaProp = (question: Question) => { let obj: Record<"ui:widget", string>; @@ -30,7 +31,7 @@ const questionToValidationSchema = (question: Question) => { obj = { type: "null", title: question.name, - description: question.options, + description: question.description, }; } else if (question.type === "email") { // Format is just a "FYI" field, so we also have pattern. @@ -45,37 +46,37 @@ const questionToValidationSchema = (question: Question) => { obj = { type: "string", title: question.name, - pattern: question.options.map((x) => `^${escapeRegExp(x)}$`).join("|"), - enum: question.options, + pattern: question.enum.map((x) => `^${escapeRegExp(x)}$`).join("|"), + enum: question.enum, }; } else if (question.type === "checkbox") { obj = { type: "array", title: question.name, uniqueItems: true, - maxItems: question.options.length, + maxItems: question.enum.length, items: { type: "string", - enum: question.options, - pattern: question.options.map((x) => `^${escapeRegExp(x)}$`).join("|"), + enum: question.enum, + pattern: question.enum.map((x) => `^${escapeRegExp(x)}$`).join("|"), }, }; } else if (question.type === "integer") { // https://json-schema.org/understanding-json-schema/reference/numeric.html - if (question.options.length === 1) { + if (question.enum.length === 1) { obj = { type: "number", - title: `${question.name} (Max: ${question.options[0]})`, + title: `${question.name} (Max: ${question.enum[0]})`, multipleOf: 1.0, - maximum: Number(question.options[0]), + maximum: Number(question.enum[0]), }; - } else if (question.options.length === 2) { + } else if (question.enum.length === 2) { obj = { type: "number", - title: `${question.name} (${question.options[0]} -- ${question.options[1]})`, + title: `${question.name} (${question.enum[0]} -- ${question.enum[1]})`, multipleOf: 1.0, - minimum: Number(question.options[0]), - maximum: Number(question.options[1]), + minimum: Number(question.enum[0]), + maximum: Number(question.enum[1]), }; } else { obj = { @@ -92,9 +93,8 @@ const questionToValidationSchema = (question: Question) => { }; }; -export const buildFormSchema = (signUpForm: SignupForm) => { +export const buildFormSchema = (questions: Question[], title: string) => { let schemaProps = {}; - const { questions } = signUpForm; const requiredIds = questions.filter((q) => q.required).map((q) => q.id); const schemaPropsArray = questions.map(questionToValidationSchema); schemaPropsArray.forEach((schemaProp) => { @@ -105,7 +105,7 @@ export const buildFormSchema = (signUpForm: SignupForm) => { }); const schema = { - title: signUpForm.id ? signUpForm.title_fi : "Loading...", + title, type: "object", required: requiredIds, properties: schemaProps, @@ -114,9 +114,24 @@ export const buildFormSchema = (signUpForm: SignupForm) => { return schema; }; -export const buildValidationSchema = (questions: Question[]) => { +export const signupFormQuestionToQuestion = ({ + id, title_fi, title_en, description_fi, description_en, type, options, required, +}: SignupFormQuestion, language: Lang): Question => ({ + id, + type, + name: language === "fi" ? title_fi : title_en, + enum: options?.enum, + enumNames: language === "fi" ? options?.enumNames_fi : options?.enumNames_en, + description: language === "fi" ? description_fi : description_en, + required, +}); + +export const buildValidationSchema = (sfQuestions: SignupFormQuestion[]) => { let schemaProps = {}; + // Remove translations. We use Finnish translations as values for validation + const questions = sfQuestions.map((q) => signupFormQuestionToQuestion(q, "fi")); + // Force every radiobutton to be required field questions.forEach((q) => { if (q.type === "radiobutton") { @@ -144,8 +159,7 @@ export const buildValidationSchema = (questions: Question[]) => { return validationSchema; }; -export const buildUISchema = (signUpForm: SignupForm) => { - const { questions } = signUpForm; +export const buildUISchema = (questions: Question[]) => { const uiSchemaPropsArray = questions.map(questionToUISchemaProp); let uiSchemaProps = {}; uiSchemaPropsArray.forEach((uiSchemaProp) => { diff --git a/src/views/SignUpPage/SignUpPageView.tsx b/src/views/SignUpPage/SignUpPageView.tsx index 30ca454..b07fc7e 100644 --- a/src/views/SignUpPage/SignUpPageView.tsx +++ b/src/views/SignUpPage/SignUpPageView.tsx @@ -8,7 +8,7 @@ import { TextSection, ChangeLanguageButton } from "@components/index"; import colors from "@theme/colors"; import FormWrapper from "@views/common/FormWrapper"; import Loader from "@components/Loader"; -import { buildFormSchema, buildUISchema } from "./FormUtils"; +import { buildFormSchema, buildUISchema, signupFormQuestionToQuestion } from "./FormUtils"; import { useTranslation } from "../../i18n"; const customWidgets = { @@ -106,12 +106,14 @@ const SignUpPageView: React.FC = ({ ); signups = renderList(); } else { + const formTitle = signUpForm.id ? signUpForm.title_fi : "Loading..."; + const questions = signUpForm.questions.map((q) => signupFormQuestionToQuestion(q, i18n.language)); form = ( <>

{`${t("Ilmoittauminen sulkeutuu")} ${endDateStr}`}.

signupFormQuestionToQuestion(q, "fi")); +const englishQuestions = signupForm.questions.map((q) => signupFormQuestionToQuestion(q, "en")); + +describe("signupFormQuestionToQuestion", () => { + it("mathces snapshot in Finnish", () => { + expect(finnishQuestions).toMatchSnapshot(); + }); + it("mathces snapshot in English", () => { + expect(englishQuestions).toMatchSnapshot(); + }); +}); + describe("buildFormSchema", () => { it("matches snapshot", () => { - expect(buildFormSchema(signupForm)).toMatchSnapshot(); + expect(buildFormSchema(finnishQuestions, signupForm.title_fi)).toMatchSnapshot(); + }); +}); + +describe("buildUISchema", () => { + it("matches snapshot", () => { + expect(buildUISchema(finnishQuestions)).toMatchSnapshot(); }); }); @@ -384,9 +531,3 @@ describe("buildValidationSchema", () => { expect(buildValidationSchema(signupForm.questions)).toMatchSnapshot(); }); }); - -describe("buildUISchema", () => { - it("matches snapshot", () => { - expect(buildUISchema(signupForm)).toMatchSnapshot(); - }); -});