Merge branch 'master' into 'production'

Prod deploy: Translated signup questions & preparation for PoTa100 signup

See merge request sahkoinsinoorikilta/vtmk/web2.0-frontend!84
This commit is contained in:
Aarni Halinen
2021-09-06 10:37:48 +00:00
25 changed files with 6851 additions and 140 deletions
+20
View File
@@ -0,0 +1,20 @@
module.exports = {
roots: ["<rootDir>/src"],
testMatch: ["**/*.test.ts"],
transform: {
"^.+\\.tsx?$": "ts-jest",
},
preset: "ts-jest",
verbose: true,
moduleNameMapper: {
"^@api/(.*)$": "<rootDir>/src/api/$1",
"^@components/(.*)$": "<rootDir>/src/components/$1",
"^@hooks/(.*)$": "<rootDir>/src/hooks/$1",
"^@models/(.*)$": "<rootDir>/src/models/$1",
"^@pages/(.*)$": "<rootDir>/src/pages/$1",
"^@theme/(.*)$": "<rootDir>/src/theme/$1",
"^@views/(.*)$": "<rootDir>/src/views/$1",
"^@utils/(.*)$": "<rootDir>/src/utils/$1",
},
};
+5009
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -27,12 +27,14 @@
"start": "next dev",
"start-prod": "next start --port ${SERVER_PORT:=80}",
"serve": "next start --port 3000",
"test:unit": "jest --coverage",
"test": "npm run testcafe",
"testcafe": "testcafe --config-file testcafe.json",
"build-analyze": "ANALYZE=true npm run build",
"prepare": "husky install"
},
"devDependencies": {
"@types/jest": "^27.0.1",
"@types/js-cookie": "^2.2.7",
"@types/react": "^17.0.19",
"@types/react-beautiful-dnd": "^13.1.1",
@@ -48,12 +50,14 @@
"eslint-config-airbnb-typescript": "^13.0.0",
"eslint-config-next": "^11.1.0",
"husky": "^7.0.1",
"jest": "^27.1.0",
"next-sitemap": "^1.6.162",
"npm-run-all": "^4.1.5",
"stylelint": "^13.13.1",
"stylelint-config-standard": "^22.0.0",
"stylelint-config-styled-components": "^0.1.1",
"testcafe": "^1.15.3",
"ts-jest": "^27.0.5",
"typescript": "^4.3.5"
},
"dependencies": {
@@ -4,7 +4,14 @@ import { WidgetProps } from "@rjsf/core";
import RadioButton from "./RadioButton";
type RadioButtonWidgetProps = Omit<WidgetProps, "options"> & {
options: any;
options: {
enumOptions: {
value: string;
label: string;
}[];
enumDisabled: string[];
inline: boolean;
};
};
const RadioButtonContainer = styled.div`
@@ -1,49 +1,72 @@
import React from "react";
import Checkbox from "@components/Widgets/Checkbox/Checkbox";
import { SignupFormQuestion } from "@models/Signup";
import { Lang } from "../../../i18n";
import {
Question, InputProps, optionTypes, SignupQuestionError,
InputProps, optionTypes, SignupQuestionError,
} from "./common";
interface OptionsWidgetProps {
inputProps: InputProps;
onChange: (value: Question[]) => void;
onChange: (value: SignupFormQuestion[]) => void;
}
class OptionsWidget extends React.Component<OptionsWidgetProps> {
handleListOptionsChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
handleListOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const { onChange } = this.props;
const val = event.target.value;
const lst = val.split(";").map((p) => p.trimLeft());
// eslint-disable-next-line no-param-reassign
questions[index].options = lst;
if (lang === "fi") {
// eslint-disable-next-line no-param-reassign
questions[index].options = {
...questions[index].options,
enumNames_fi: lst,
enum: lst,
};
}
if (lang === "en") {
// eslint-disable-next-line no-param-reassign
questions[index].options = {
...questions[index].options,
enumNames_en: lst,
};
}
onChange(questions);
};
handleTextOptionsChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
handleInfoTextOptionsChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const { onChange } = this.props;
const val = event.target.value;
// eslint-disable-next-line no-param-reassign
questions[index].options = val as unknown as string[]; // TODO: Check type
if (lang === "fi") {
// eslint-disable-next-line no-param-reassign
questions[index].description_fi = val;
}
if (lang === "en") {
// eslint-disable-next-line no-param-reassign
questions[index].description_en = val;
}
onChange(questions);
};
handleIntegerOptionsChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
handleIntegerOptionsChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const { onChange } = this.props;
const val = event.target.value;
if (val !== "") {
const lst = val.split(";").map((p) => p.trimLeft());
// Ignore everything else but the two first values
// eslint-disable-next-line no-param-reassign
questions[index].options = lst.splice(0, 2);
questions[index].options.enum = lst.splice(0, 2);
} else {
// eslint-disable-next-line no-param-reassign
questions[index].options = [];
questions[index].options.enum = [];
}
onChange(questions);
};
handleRequiredChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
handleRequiredChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const { onChange } = this.props;
const val: boolean = event.target.checked;
// eslint-disable-next-line no-param-reassign
@@ -67,7 +90,7 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
render(): JSX.Element {
const { inputProps } = this.props;
const {
type, value, questions, index,
value, type, questions, index,
} = inputProps;
if (!optionTypes.includes(type)) {
throw new SignupQuestionError(`Question widget type "${type}" not in types array.`);
@@ -82,25 +105,29 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
<>
<input
type="text"
placeholder="Write something informative"
value={questions[index].options}
onChange={this.handleTextOptionsChange(questions, index)}
placeholder="Write something informative in Finnish"
value={questions[index].description_fi}
onChange={this.handleInfoTextOptionsChange(questions, index, "fi")}
required
/>
<input
type="text"
placeholder="Write something informative in English"
value={questions[index].description_en}
onChange={this.handleInfoTextOptionsChange(questions, index, "en")}
required
/>
{this.requiredField()}
</>
);
}
if (type === "integer") {
const lst = value as string[];
const joinedValue = lst.join(";");
return (
<>
<input
type="text"
placeholder="Minimum;Maximum"
value={joinedValue}
value={value.enum.join(";")}
onChange={this.handleIntegerOptionsChange(questions, index)}
/>
{this.requiredField()}
@@ -109,15 +136,20 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
}
if (type === "radiobutton") {
const lst = value as string[];
const joinedValue = lst.join(";");
return (
<>
<input
type="text"
placeholder="Kyllä;ei;ehkä"
value={value.enumNames_fi.join(";")}
onChange={this.handleListOptionsChange(questions, index, "fi")}
required
/>
<input
type="text"
placeholder="Yes;no;maybe"
value={joinedValue}
onChange={this.handleListOptionsChange(questions, index)}
value={value.enumNames_en.join(";")}
onChange={this.handleListOptionsChange(questions, index, "en")}
required
/>
</>
@@ -125,15 +157,20 @@ class OptionsWidget extends React.Component<OptionsWidgetProps> {
}
if (type === "checkbox") {
const lst = value as string[];
const joinedValue = lst.join(";");
return (
<>
<input
type="text"
placeholder="A;B;C"
value={joinedValue}
onChange={this.handleListOptionsChange(questions, index)}
placeholder="Yksi;Kaksi;Kolme"
value={value.enumNames_fi.join(";")}
onChange={this.handleListOptionsChange(questions, index, "fi")}
required
/>
<input
type="text"
placeholder="One;Two;Three"
value={value.enumNames_en.join(";")}
onChange={this.handleListOptionsChange(questions, index, "en")}
required
/>
{this.requiredField()}
@@ -2,7 +2,8 @@ import React, { ReactNode } from "react";
import styled from "styled-components";
import { Draggable } from "react-beautiful-dnd";
import colors from "@theme/colors";
import { Question, InputProps } from "./common";
import { SignupFormQuestion } from "@models/Signup";
import { Lang } from "../../../i18n";
import OptionsWidget from "./OptionsWidget";
import TypeWidget from "./TypeWidget";
import QuestionElement from "./Question";
@@ -16,26 +17,28 @@ const WidgetRow = styled.div`
`;
interface QuestionListProps {
questions: Question[];
questions: SignupFormQuestion[];
innerRef: React.Ref<HTMLDivElement>;
placeholder: ReactNode;
onChange: (value: Question[]) => void;
onChange: (value: SignupFormQuestion[]) => void;
}
class QuestionList extends React.Component<QuestionListProps> {
renderTextWidget = ({ questions, value, index }: InputProps): JSX.Element => (
<input type="text" value={value} onChange={this.handleNameInputChange(questions, index)} />
);
handleNameInputChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLInputElement> => (event) => {
handleNameInputChange = (questions: SignupFormQuestion[], index: number, lang: Lang): React.ChangeEventHandler<HTMLInputElement> => (event) => {
const { onChange } = this.props;
const val = event.target.value;
// eslint-disable-next-line no-param-reassign
questions[index].name = val;
if (lang === "fi") {
// eslint-disable-next-line no-param-reassign
questions[index].title_fi = val;
}
if (lang === "en") {
// eslint-disable-next-line no-param-reassign
questions[index].title_en = val;
}
onChange(questions);
};
handleElementRemove = (questions: Question[], index: number) => (): void => {
handleElementRemove = (questions: SignupFormQuestion[], index: number) => (): void => {
const { onChange } = this.props;
const newQuestions = [...questions];
newQuestions.splice(index, 1);
@@ -45,11 +48,6 @@ class QuestionList extends React.Component<QuestionListProps> {
renderQuestions(): JSX.Element[] {
const { questions, onChange } = this.props;
return questions.map((q, index) => {
const nameWidgetProps = {
value: q.name, type: "text", questions, index,
};
const nameWidget = this.renderTextWidget(nameWidgetProps);
const dataProps = {
value: q.options, type: q.type, questions, index,
};
@@ -66,7 +64,8 @@ class QuestionList extends React.Component<QuestionListProps> {
<QuestionElement
onClick={this.handleElementRemove(questions, index)}
>
{nameWidget}
<input type="text" value={q.title_fi} onChange={this.handleNameInputChange(questions, index, "fi")} />
<input type="text" value={q.title_en} onChange={this.handleNameInputChange(questions, index, "en")} />
{typeSelectWidget}
{optionsWidget}
</QuestionElement>
@@ -4,8 +4,8 @@ import shortid from "shortid";
import { DragDropContext, Droppable } from "react-beautiful-dnd";
import colors from "@theme/colors";
import AddIcon from "@components/AddIcon";
import { SignupFormQuestion } from "@models/Signup";
import QuestionList from "./QuestionList";
import { Question } from "./common";
const Widget = styled.div`
& > button {
@@ -40,24 +40,29 @@ interface SignupQuestionsWidgetProps {
}
const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, onFocus, onChange }) => {
const onValueChange = (questions: Question[]) => {
const onValueChange = (questions: SignupFormQuestion[]) => {
const newValue = JSON.stringify(questions);
onChange(newValue);
};
const handleNewRowClick = (questions) => () => {
const newRow: Question = {
const newRow: SignupFormQuestion = {
id: shortid.generate(),
name: `Question #${questions.length + 1}`,
options: [],
title_fi: `Kysymys #${questions.length + 1}`,
title_en: `Question #${questions.length + 1}`,
options: {
enum: [],
enumNames_fi: [],
enumNames_en: [],
},
type: "text",
};
const newQuestions: Question[] = questions.concat([newRow]);
const newQuestions: SignupFormQuestion[] = questions.concat([newRow]);
onValueChange(newQuestions);
};
const handleDragEnd = (questions: Question[]) => (result) => {
const handleDragEnd = (questions: SignupFormQuestion[]) => (result) => {
const srcIndex = result.source.index;
const dstIndex = result.destination.index;
const srcCopy = { ...questions[srcIndex] };
@@ -66,7 +71,7 @@ const SignupQuestionsWidget: React.FC<SignupQuestionsWidgetProps> = ({ value, on
onValueChange(questions);
};
const questions = JSON.parse(value) as Question[];
const questions: SignupFormQuestion[] = JSON.parse(value);
return (
<Widget>
@@ -1,32 +1,29 @@
import React from "react";
import { Question, InputProps, optionTypes } from "./common";
import { SignupFormQuestion } from "@models/Signup";
import { InputProps, optionTypes } from "./common";
interface TypeWidgetProps {
inputProps: InputProps;
onChange: (value: Question[]) => void;
onChange: (value: SignupFormQuestion[]) => void;
}
class TypeWidget extends React.Component<TypeWidgetProps> {
handleTypeChange = (questions: Question[], index: number): React.ChangeEventHandler<HTMLSelectElement> => (event) => {
const { onChange } = this.props;
const val = event.target.value as Question["type"];
const TypeWidget = ({ onChange, inputProps }: TypeWidgetProps): JSX.Element => {
const handleTypeChange = (questions: SignupFormQuestion[], index: number): React.ChangeEventHandler<HTMLSelectElement> => (event) => {
const val = event.target.value as SignupFormQuestion["type"];
// eslint-disable-next-line no-param-reassign
questions[index].type = val;
onChange(questions);
};
render(): JSX.Element {
const { inputProps } = this.props;
const { type, questions, index } = inputProps;
const options = optionTypes.map((t) => (
<option key={t} value={t}>{t}</option>
));
return (
<select onChange={this.handleTypeChange(questions, index)} value={type} name="type">
{options}
</select>
);
}
}
const { questions, type, index } = inputProps;
const options = optionTypes.map((t) => (
<option key={t} value={t}>{t}</option>
));
return (
<select onChange={handleTypeChange(questions, index)} value={type} name="type">
{options}
</select>
);
};
export default TypeWidget;
@@ -1,19 +1,23 @@
import type { SignupFormQuestion } from "@models/Signup";
export interface Question {
id: string;
name: string;
type: OptionTypes;
options: string[];
enum?: string[];
enumNames?: string[];
description?: string;
required?: boolean;
}
export interface InputProps {
index: number;
value: string | string[];
questions: Question[];
value: SignupFormQuestion["options"];
questions: SignupFormQuestion[];
type: string;
}
type OptionTypes =
export type OptionTypes =
"text" |
"info" |
"integer" |
+1 -1
View File
@@ -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;
+19 -2
View File
@@ -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: {
+4 -4
View File
@@ -180,11 +180,11 @@ const EventCreatePage: NextPage = () => {
useEffect(() => {
TagApi.getTags()
.then((res) => setTags(res))
.catch((err) => setError(err));
.catch((err) => setError(err.message));
SignupApi.getForms(true)
.then((res) => setSignupForms(res))
.catch((err) => setError(err));
.catch((err) => setError(err.message));
const eventId = id && Number(id);
if (eventId !== undefined) {
@@ -194,7 +194,7 @@ const EventCreatePage: NextPage = () => {
tags: (res.tags).map((inst) => inst.id) as any,
signupForm: (res.signupForm).map((inst) => inst.id) as any,
}))
.catch((err) => setError(err));
.catch((err) => setError(err.message));
}
}, [id]);
@@ -230,7 +230,7 @@ const EventCreatePage: NextPage = () => {
}
} catch (err) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
setError(err);
setError(err.message);
}
};
+3 -3
View File
@@ -146,7 +146,7 @@ const FeedCreatePage: NextPage = () => {
useEffect(() => {
TagApi.getTags()
.then((res) => setTags(res))
.catch((err) => setError(err));
.catch((err) => setError(err.message));
const feedId = id && Number(id);
if (feedId !== undefined) {
@@ -155,7 +155,7 @@ const FeedCreatePage: NextPage = () => {
...res,
tags: (res.tags).map((inst) => inst.id) as any,
}))
.catch((err) => setError(err));
.catch((err) => setError(err.message));
}
}, [id]);
@@ -179,7 +179,7 @@ const FeedCreatePage: NextPage = () => {
}
} catch (err) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
setError(err);
setError(err.message);
}
};
+2 -2
View File
@@ -123,7 +123,7 @@ const JobAdCreatePage: NextPage = () => {
if (jobId !== undefined) {
JobAdApi.getJobAd(jobId, true)
.then((res) => setFormData(res))
.catch((err) => setError(err));
.catch((err) => setError(err.message));
}
}, [id]);
@@ -143,7 +143,7 @@ const JobAdCreatePage: NextPage = () => {
}
} catch (err) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
setError(err);
setError(err.message);
}
};
+4 -4
View File
@@ -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";
@@ -118,13 +118,13 @@ const SignupCreatePage: NextPage = () => {
questions: JSON.stringify(res.questions) as any,
});
})
.catch((err) => setError(err));
.catch((err) => setError(err.message));
}
}, [id]);
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,
@@ -150,7 +150,7 @@ const SignupCreatePage: NextPage = () => {
}
} catch (err) {
toast.error("Uh oh! Something went wrong! Try again later. 😟");
setError(err);
setError(err.message);
}
};
+1 -1
View File
@@ -71,7 +71,7 @@ const SignupEmailPage: NextPage = () => {
await SignupApi.signupFormSendEmail(payload, Number(id));
toast.success("Email sent successfully 😎");
} catch (err) {
setError(err);
setError(err.message);
toast.error("Uh oh! Something went wrong! Try again later. 😟");
}
};
+1 -1
View File
@@ -49,7 +49,7 @@ const SignupEmailPage: NextPage = () => {
// TODO: ATM we filter 'info' questions from table here. Maybe remove them from answer JSON altogether?
const questions = signupForm ? signupForm.questions.filter((q) => q.type !== "info").map((q) => ({
title: q.name,
title: q.title_fi,
id: q.id,
})) : [];
+36 -20
View File
@@ -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 "../../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,39 @@ 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,
enumNames: question.enumNames,
};
} 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,
enumNames: question.enumNames,
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 +95,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 +107,7 @@ export const buildFormSchema = (signUpForm: SignupForm) => {
});
const schema = {
title: signUpForm.id ? signUpForm.title_fi : "Loading...",
title,
type: "object",
required: requiredIds,
properties: schemaProps,
@@ -114,9 +116,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 +161,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) => {
+5 -3
View File
@@ -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<SignUpPageViewProps> = ({
);
signups = renderList();
} else {
const formTitle = signUpForm.id ? signUpForm.title_fi : "Loading...";
const questions = signUpForm.questions.map((q) => signupFormQuestionToQuestion(q, i18n.language));
form = (
<>
<p>{`${t("Ilmoittauminen sulkeutuu")} ${endDateStr}`}.</p>
<FormWrapper
schema={buildFormSchema(signUpForm) as unknown}
uiSchema={buildUISchema(signUpForm)}
schema={buildFormSchema(questions, formTitle) as unknown}
uiSchema={buildUISchema(questions)}
formData={formData}
widgets={customWidgets}
idPrefix="rjsf"
File diff suppressed because it is too large Load Diff
+533
View File
@@ -0,0 +1,533 @@
import { SignupForm } from "@models/Signup";
import {
signupFormQuestionToQuestion, buildFormSchema, buildValidationSchema, buildUISchema,
} from "./FormUtils";
const signupForm: SignupForm = {
id: 250,
title_fi: "Potentiaalin Tasaus 100 ilmoittautuminen - deviversio",
title_en: "Pota100 dev",
visible: true,
isOpen: true,
start_time: "2021-08-17T16:45:15+03:00",
end_time: "2021-09-30T23:59:59+03:00",
email_content: "Hei, \r\n\r\nIlmoittautumisesi on saapunut perille!\r\n\r\nMaksutiedot lähetetään sinulle vasta kun ilmoittautuminen on sulkeutunut. \r\n\r\nJos ilmoittautumisessa ilmenee ongelmia tai pääjuhlasta nousee kysymyksiä, olethan yhetydessä Potentiaalin Tasaus 100 pääjuhlavastaaviin:\r\n\r\nEmmaleena Ahonen ja Jonna Tammikivi \r\npota@sik100.fi\r\n\r\nPS. Jos tulet juhlaan avecin kanssa, muista, että hänen tulee myös ilmoittautua juhlaan erikseen!\r\n\r\nPoTassa nähdään!",
questions: [
{
id: "yigh6mhd4",
title_fi: "Ilmoittautuminen",
title_en: "EN-Ilmoittautuminen",
type: "info",
description_fi: "Tämä ilmoittautuminen kustantaa 120€ opiskelijoille ja 180€ alumneille. Ilmoittautuminen on sitova.",
description_en: "EN - Tämä ilmoittautuminen kustantaa 120€ opiskelijoille ja 180€ alumneille. Ilmoittautuminen on sitova.",
},
{
id: "WRflgsBe_",
title_fi: "Nimi",
title_en: "Name",
type: "name",
required: true,
},
{
id: "OF55WBbOx",
title_fi: "Sähköposti",
title_en: "Email",
type: "email",
required: true,
},
{
id: "ZY5UpArqx",
title_fi: "Olen ",
title_en: "I am ",
type: "radiobutton",
options: {
enum: [
"Killan jäsen",
"Killan alumni",
"Jäsenen avec",
"Alumnin avec",
],
enumNames_fi: [
"Killan jäsen",
"Killan alumni",
"Jäsenen avec",
"Alumnin avec",
],
enumNames_en: [
"Member",
"Alumni",
"Member avec",
"Alumni avec",
],
},
required: true,
},
{
id: "dUzh31kag",
title_fi: "Fuksivuosi (yyyy)",
title_en: "Freshman year (yyyy)",
type: "text",
},
{
id: "1LaFnZ-Of",
title_fi: "Erikoisruokavaliot / Allergiat",
title_en: "Allergies",
type: "text",
},
{
id: "PajprpSLa",
title_fi: "Liha ja kala menuvaihtoehto tarkoittaa sitä että menuun kuuluu molempia alku- tai pääruokana.",
title_en: "EN - Liha ja kala menuvaihtoehto tarkoittaa sitä että menuun kuuluu molempia alku- tai pääruokana.",
type: "info",
description_fi: "Huomioimme allergiat menuvalinnan lisäksi. Esimerkiksi jos on allerginen kalalle tämä otetaan huomioon jos on valinnut \"liha ja kala\" vaihtoehdon.",
description_en: "EN - Huomioimme allergiat menuvalinnan lisäksi. Esimerkiksi jos on allerginen kalalle tämä otetaan huomioon jos on valinnut \"liha ja kala\" vaihtoehdon.",
},
{
id: "0GMtDu46R",
title_fi: "Pääjuhlan ruokatarjoilut",
title_en: "EN - Pääjuhlan ruokatarjoilut",
type: "radiobutton",
options: {
enum: [
"Vegaaninen",
"Liha ja Kala",
],
enumNames_fi: [
"Vegaaninen",
"Liha ja Kala",
],
enumNames_en: [
"Vegan",
"Meat and fish",
],
},
required: true,
},
{
id: "MMghazOPT",
title_fi: "Pääjuhlan juomatarjoilut",
title_en: "EN - Pääjuhlan juomatarjoilut",
type: "radiobutton",
options: {
enum: [
"Alkoholillinen",
"Alkoholiton",
],
enumNames_fi: [
"Alkoholillinen",
"Alkoholiton",
],
enumNames_en: [
"Alcoholic",
"Alcohol free",
],
},
required: true,
},
{
id: "fCYJxDSrL",
title_fi: "Haluan tilata pöytään pullon viiniä tai kuohuvaa",
title_en: "EN - Haluan tilata pöytään pullon viiniä tai kuohuvaa",
type: "checkbox",
options: {
enum: [
"Punaviini (42€)",
"Valkoviini (42€)",
"Kuohuviini (42€)",
"Shamppanja (68€)",
],
enumNames_fi: [
"Punaviini (42€)",
"Valkoviini (42€)",
"Kuohuviini (42€)",
"Shamppanja (68€)",
],
enumNames_en: [
"Red wine (42€)",
"White wine (42€)",
"Sparkling wine (42€)",
"Champagne (68€)",
],
},
},
{
id: "0q74weKci",
title_fi: "Avec",
title_en: "Avec",
type: "text",
},
{
id: "kqPI12VK_",
title_fi: "Pöytäseurue",
title_en: "EN - Pöytäseurue",
type: "text",
},
{
id: "ofKH9GhFg",
title_fi: "Annan lahjan lahjanantotilaisuudessa",
title_en: "EN - Annan lahjan lahjanantotilaisuudessa",
type: "radiobutton",
options: {
enum: [
"Kyllä",
"Ei",
],
enumNames_fi: [
"Kyllä",
"Ei",
],
enumNames_en: [
"Yes",
"No",
],
},
required: true,
},
{
id: "AsYHmSz2V",
title_fi: "Jos annat lahjan, mitä tahoa edustat?",
title_en: "EN - Jos annat lahjan, mitä tahoa edustat?",
type: "text",
},
{
id: "hA3b8X6P4",
title_fi: "Haluan osallistua jatkoille",
title_en: "EN - Haluan osallistua jatkoille",
type: "radiobutton",
options: {
enum: [
"Kyllä",
"Ei",
],
enumNames_fi: [
"Kyllä",
"Ei",
],
enumNames_en: [
"Yes",
"No",
],
},
required: true,
},
{
id: "rf34jMWSe",
title_fi: "Sillikselle on rajattu määrä paikkoja, jolloin emme voi varmistaa kaikille pääsyä.",
title_en: "EN - Sillikselle on rajattu määrä paikkoja, jolloin emme voi varmistaa kaikille pääsyä.",
type: "info",
description_fi: "Ilmoitamme sähköpostilla siinä tapauksessa jos olet jonossa tai et maahtunut silliksen kiintiöön. ",
description_en: "EN - Ilmoitamme sähköpostilla siinä tapauksessa jos olet jonossa tai et maahtunut silliksen kiintiöön. ",
},
{
id: "PnzuTUxZH",
title_fi: "Haluan osallistua sillikselle seuraavana päivänä (25€)",
title_en: "EN - Haluan osallistua sillikselle seuraavana päivänä (25€)",
type: "radiobutton",
options: {
enum: [
"Kyllä",
"Ei",
],
enumNames_fi: [
"Kyllä",
"Ei",
],
enumNames_en: [
"Yes",
"No",
],
},
required: true,
},
{
id: "aM8Xjhsqs",
title_fi: "Haluan kuulla lisää SIK100-historiateoksesta ja mahdollisuudesta ostaa teoksen",
title_en: "EN - Haluan kuulla lisää SIK100-historiateoksesta ja mahdollisuudesta ostaa teoksen",
type: "radiobutton",
options: {
enum: [
"Kyllä",
"Ei",
],
enumNames_fi: [
"Kyllä",
"Ei",
],
enumNames_en: [
"Yes",
"No",
],
},
required: true,
},
{
id: "m2aKUikfI",
title_fi: "Vapaaehtoinen kannatusmaksu",
title_en: "EN - Vapaaehtoinen kannatusmaksu",
type: "checkbox",
options: {
enum: [
"15€",
"25€",
"50€",
],
enumNames_fi: [
"15€",
"25€",
"50€",
],
enumNames_en: [
"15€",
"25€",
"50€",
],
},
},
{
id: "13qShsW03",
title_fi: "Haluan saada sähköpostiini lisää tietoa SIK100-vuodesta",
title_en: "EN - Haluan saada sähköpostiini lisää tietoa SIK100-vuodesta",
type: "radiobutton",
options: {
enum: [
"Kyllä",
"Ei",
],
enumNames_fi: [
"Kyllä",
"Ei",
],
enumNames_en: [
"Yes",
"No",
],
},
required: true,
},
{
id: "xI_OlVAxM",
title_fi: "Terveisiä killalle",
title_en: "EN - Terveisiä killalle",
type: "text",
},
{
id: "04FkeTQZm",
title_fi: "Paras vuosijuhlamuisto",
title_en: "EN - Paras vuosijuhlamuisto",
type: "text",
},
],
schema: {
type: "object",
required: [
"WRflgsBe_",
"OF55WBbOx",
"ZY5UpArqx",
"0GMtDu46R",
"MMghazOPT",
"ofKH9GhFg",
"hA3b8X6P4",
"PnzuTUxZH",
"aM8Xjhsqs",
"13qShsW03",
],
properties: {
"04FkeTQZm": {
type: "string",
title: "Paras vuosijuhlamuisto",
},
"0GMtDu46R": {
enum: [
"Vegaaninen",
"Liha ja Kala",
],
type: "string",
title: "Pääjuhlan ruokatarjoilut",
pattern: "^Vegaaninen$|^Liha ja Kala$",
},
"0q74weKci": {
type: "string",
title: "Avec",
},
"13qShsW03": {
enum: [
"Kyllä",
"Ei",
],
type: "string",
title: "Haluan saada sähköpostiini lisää tietoa SIK100-vuodesta",
pattern: "^Kyllä$|^Ei$",
},
"1LaFnZ-Of": {
type: "string",
title: "Erikoisruokavaliot / Allergiat",
},
AsYHmSz2V: {
type: "string",
title: "Jos annat lahjan, mitä tahoa edustat?",
},
MMghazOPT: {
enum: [
"Alkoholillinen",
"Alkoholiton",
],
type: "string",
title: "Pääjuhlan juomatarjoilut ",
pattern: "^Alkoholillinen$|^Alkoholiton$",
},
OF55WBbOx: {
type: [
"string",
],
title: "Sähköposti",
format: "email",
default: null,
pattern: "^[a-zA-Z0-9.!#$%&\\u2019*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$",
},
PajprpSLa: {
type: "null",
title: "Liha ja kala menuvaihtoehto tarkoittaa sitä että menuun kuuluu molempia alku- tai pääruokana.",
description: "Huomioimme allergiat menuvalinnan lisäksi. Esimerkiksi jos on allerginen kalalle tämä otetaan huomioon jos on valinnut \"liha ja kala\" vaihtoehdon.",
},
PnzuTUxZH: {
enum: [
"Kyllä",
"Ei",
],
type: "string",
title: "Haluan osallistua sillikselle seuraavana päivänä (25€)",
pattern: "^Kyllä$|^Ei$",
},
WRflgsBe_: {
type: "string",
title: "Nimi",
},
ZY5UpArqx: {
enum: [
"Killan jäsen",
"Killan alumni",
"Jäsenen avec",
"Alumnin avec",
],
type: "string",
title: "Olen ",
pattern: "^Killan jäsen$|^Killan alumni$|^Jäsenen avec$|^Alumnin avec$",
},
aM8Xjhsqs: {
enum: [
"Kyllä",
"Ei",
],
type: "string",
title: "Haluan kuulla lisää SIK100-historiateoksesta ja mahdollisuudesta ostaa teoksen",
pattern: "^Kyllä$|^Ei$",
},
dUzh31kag: {
type: "string",
title: "Fuksivuosi (yyyy)",
},
fCYJxDSrL: {
type: "array",
items: {
enum: [
"Punaviini (42€)",
"Valkoviini (42€)",
"Kuohuviini (42€)",
"Shamppanja (68€)",
],
type: "string",
pattern: "^Punaviini \\(42€\\)$|^Valkoviini \\(42€\\)$|^Kuohuviini \\(42€\\)$|^Shamppanja \\(68€\\)$",
},
title: "Haluan tilata pöytään pullon viiniä tai kuohuvaa",
maxItems: 4,
uniqueItems: true,
},
hA3b8X6P4: {
enum: [
"Kyllä",
"Ei",
],
type: "string",
title: "Haluan osallistua jatkoille",
pattern: "^Kyllä$|^Ei$",
},
kqPI12VK_: {
type: "string",
title: "Pöytäseurue",
},
m2aKUikfI: {
type: "array",
items: {
enum: [
"15€",
"25€",
"50€",
],
type: "string",
pattern: "^15€$|^25€$|^50€$",
},
title: "Vapaaehtoinen kannatusmaksu",
maxItems: 3,
uniqueItems: true,
},
ofKH9GhFg: {
enum: [
"Kyllä",
"Ei",
],
type: "string",
title: "Annan lahjan lahjanantotilaisuudessa",
pattern: "^Kyllä$|^Ei$",
},
rf34jMWSe: {
type: "null",
title: "Sillikselle on rajattu määrä paikkoja, jolloin emme voi varmistaa kaikille pääsyä.",
description: "Ilmoitamme sähköpostilla siinä tapauksessa jos olet jonossa tai et maahtunut silliksen kiintiöön. ",
},
xI_OlVAxM: {
type: "string",
title: "Terveisiä killalle",
},
yigh6mhd4: {
type: "null",
title: "Ilmoittautuminen",
description: "Tämä ilmoittautuminen kustantaa 120€ opiskelijoille ja 180€ alumneille. Ilmoittautuminen on sitova.",
},
},
},
signups: [
"asd",
],
quota: 200,
};
const finnishQuestions = signupForm.questions.map((q) => 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(finnishQuestions, signupForm.title_fi)).toMatchSnapshot();
});
});
describe("buildUISchema", () => {
it("matches snapshot", () => {
expect(buildUISchema(finnishQuestions)).toMatchSnapshot();
});
});
describe("buildValidationSchema", () => {
it("matches snapshot", () => {
expect(buildValidationSchema(signupForm.questions)).toMatchSnapshot();
});
});
+3 -3
View File
@@ -64,10 +64,10 @@ const AdminPageWrapper: React.FC<PageProps> = ({ requiresAuthentication, childre
const router = useRouter();
const { completed, redirecting } = useShouldRedirect(requiresAuthentication);
const { pathname } = router;
const { asPath } = router;
if (redirecting) {
const loginURL = `/admin/login?next=${pathname}`;
const loginURL = `/admin/login?next=${asPath}`;
router.push(loginURL);
}
@@ -79,7 +79,7 @@ const AdminPageWrapper: React.FC<PageProps> = ({ requiresAuthentication, childre
<>
<AdminHeader />
<Main>
<AdminSidebar path={pathname} />
<AdminSidebar path={asPath} />
{children}
</Main>
</>
+24 -11
View File
@@ -38,45 +38,58 @@ test("Logged in user can create signup", async (t) => {
await t.click(newQuestionButton);
let question = lastQuestion();
let questionName = question.child("input");
let questionNameFi = question.child("input").nth(0);
let questionNameEn = question.child("input").nth(1);
let questionTypeSelect = question.child("select");
let requiredBox = question.child("label");
await t
.selectText(questionName)
.selectText(questionNameFi)
.pressKey("delete")
.typeText(questionName, "Nimi")
.typeText(questionNameFi, "Nimi")
.selectText(questionNameEn)
.pressKey("delete")
.typeText(questionNameEn, "Name")
.click(questionTypeSelect)
.click(questionTypeSelect.find("option").withExactText("name"))
.click(requiredBox);
await t.click(newQuestionButton);
question = lastQuestion();
questionName = question.child("input");
questionNameFi = question.child("input").nth(0);
questionNameEn = question.child("input").nth(1);
questionTypeSelect = question.child("select");
requiredBox = question.child("label");
await t
.selectText(questionName)
.selectText(questionNameFi)
.pressKey("delete")
.typeText(questionName, "S-Posti")
.typeText(questionNameFi, "S-Posti")
.selectText(questionNameEn)
.pressKey("delete")
.typeText(questionNameEn, "Email")
.click(questionTypeSelect)
.click(questionTypeSelect.find("option").withExactText("email"))
.click(requiredBox);
await t.click(newQuestionButton);
question = lastQuestion();
questionName = question.child("input");
questionNameFi = question.child("input");
questionTypeSelect = question.child("select");
const radioOptions = question.child("input").nth(-1);
const radioOptionsFi = question.child("input").nth(-2);
const radioOptionsEn = question.child("input").nth(-1);
await t
.selectText(questionName)
.selectText(questionNameFi)
.pressKey("delete")
.typeText(questionName, "Olen")
.typeText(questionNameFi, "Olen")
.selectText(questionNameEn)
.pressKey("delete")
.typeText(questionNameEn, "I am")
.click(questionTypeSelect)
.click(questionTypeSelect.find("option").withExactText("radiobutton"))
.typeText(radioOptions, "Nuori,Vanha,Testaaja");
.typeText(radioOptionsFi, "Nuori;Vanha;Testaaja")
.typeText(radioOptionsEn, "Yung;Old;Tester");
const submit = Selector("button[type=\"submit\"]");
+7 -7
View File
@@ -111,23 +111,23 @@ export const generateTestForm = async (jwt: string) => (
end_time: tomorrow,
email_content: "E2E Test",
questions: [{
id: "XS_Ox5Rry", name: "Nimi", type: "name", options: [], required: true,
id: "Kv0IRYUWE", type: "name", options: { enum: [], enumNames_en: [], enumNames_fi: [] }, required: true, title_en: "Name", title_fi: "Nimi",
}, {
id: "Ve02XSEEx", name: "S-Posti", type: "email", options: [], required: true,
id: "_9o78DbdZ", type: "email", options: { enum: [], enumNames_en: [], enumNames_fi: [] }, required: true, title_en: "Email", title_fi: "S-Posti",
}, {
id: "luMqnz5y9", name: "Olen", type: "radiobutton", options: ["Nuori", "Vanha", "Testaaja"],
id: "-Zk6tCy7U", type: "radiobutton", options: { enum: ["Nuori", "Vanha", "Testaaja"], enumNames_en: ["Yung", "Old", "Tester"], enumNames_fi: ["Nuori", "Vanha", "Testaaja"] }, title_en: "I am", title_fi: "Olen",
}],
id: 14,
isOpen: true,
schema: {
type: "object",
required: ["XS_Ox5Rry", "Ve02XSEEx"],
required: ["Kv0IRYUWE", "_9o78DbdZ"],
properties: {
XS_Ox5Rry: { type: "string", title: "Nimi" },
Ve02XSEEx: {
Kv0IRYUWE: { type: "string", title: "Nimi" },
_9o78DbdZ: {
type: ["string"], title: "S-Posti", format: "email", pattern: "^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$", default: null,
},
luMqnz5y9: {
"-Zk6tCy7U": {
type: "string", title: "Olen", pattern: "^Nuori$|^Vanha$|^Testaaja$", enum: ["Nuori", "Vanha", "Testaaja"],
},
},
+1
View File
@@ -60,6 +60,7 @@
"./tests/testcafe/**/*",
"next-sitemap.js",
"next.config.js",
"jest.config.js",
".eslintrc.js"
],
"exclude": [