diff --git a/web/frontend/src/components/utils/FillFormInfo.tsx b/web/frontend/src/components/utils/FillFormInfo.tsx index c397ea614..31700f2f8 100644 --- a/web/frontend/src/components/utils/FillFormInfo.tsx +++ b/web/frontend/src/components/utils/FillFormInfo.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { ID } from 'types/configuration'; -import { FormInfo, LightFormInfo, Results, Status } from 'types/form'; +import { FormInfo, LightFormInfo, Results, Status, Title } from 'types/form'; const useFillFormInfo = (formData: FormInfo) => { const [id, setId] = useState(''); @@ -52,9 +52,7 @@ const useFillFormInfo = (formData: FormInfo) => { const useFillLightFormInfo = (formData: LightFormInfo) => { const [id, setId] = useState(''); - const [title, setTitle] = useState(''); - const [titleFr, setTitleFr] = useState(''); - const [titleDe, setTitleDe] = useState(''); + const [title, setTitle] = useState({ en: '', fr: '', de: '' }); const [status, setStatus] = useState<Status>(null); const [pubKey, setPubKey] = useState<string>(''); @@ -64,16 +62,12 @@ const useFillLightFormInfo = (formData: LightFormInfo) => { setTitle(formData.Title); setStatus(formData.Status); setPubKey(formData.Pubkey); - setTitleFr(formData.TitleFr); - setTitleDe(formData.TitleDe); } }, [formData]); return { id, title, - titleFr, - titleDe, status, setStatus, pubKey, diff --git a/web/frontend/src/mocks/mockData.ts b/web/frontend/src/mocks/mockData.ts index 5c7680f4c..847a8add6 100644 --- a/web/frontend/src/mocks/mockData.ts +++ b/web/frontend/src/mocks/mockData.ts @@ -23,8 +23,8 @@ const mockRoster: string[] = [ ]; const mockForm1: any = { - MainTitle: - '{ "en" : "Life on the campus", "fr" : "Vie sur le campus", "de" : "Life on the campus"}', + Title: + { "en" : "Life on the campus", "fr" : "Vie sur le campus", "de" : "Life on the campus"}, Scaffold: [ { ID: (0xa2ab).toString(), @@ -33,7 +33,7 @@ const mockForm1: any = { Subjects: [ { Title: - '{ "en" : "Let s talk about the food", "fr" : "Parlons de la nourriture", "de" : "Let s talk about food"}', + { "en" : "Let s talk about the food", "fr" : "Parlons de la nourriture", "de" : "Let s talk about food"}, ID: (0xff31).toString(), Order: [(0xa319).toString(), (0x19c7).toString()], Subjects: [], @@ -41,7 +41,7 @@ const mockForm1: any = { Selects: [ { Title: - '{ "en" : "Select your ingredients", "fr" : "Choisi tes ingrédients", "de" : "Select your ingredients"}', + { "en" : "Select your ingredients", "fr" : "Choisi tes ingrédients", "de" : "Select your ingredients"}, ID: (0xa319).toString(), MaxN: 2, MinN: 1, @@ -56,7 +56,7 @@ const mockForm1: any = { Ranks: [ { Title: - '{ "en" : "Rank the cafeteria", "fr" : "Ordonne les cafet", "de" : "Rank the cafeteria"}', + { "en" : "Rank the cafeteria", "fr" : "Ordonne les cafet", "de" : "Rank the cafeteria"}, ID: (0x19c7).toString(), MaxN: 3, MinN: 3, @@ -74,7 +74,7 @@ const mockForm1: any = { Selects: [ { Title: - '{"en" : "How did you find the provided material, from 1 (bad) to 5 (excellent) ?", "fr" : "Comment trouves-tu le matériel fourni, de 1 (mauvais) à 5 (excellent) ?", "de" : "How did you find the provided material, from 1 (bad) to 5 (excellent) ?"}', + {"en" : "How did you find the provided material, from 1 (bad) to 5 (excellent) ?", "fr" : "Comment trouves-tu le matériel fourni, de 1 (mauvais) à 5 (excellent) ?", "de" : "How did you find the provided material, from 1 (bad) to 5 (excellent) ?"}, ID: (0x3fb2).toString(), MaxN: 1, MinN: 1, @@ -89,7 +89,7 @@ const mockForm1: any = { }, { Title: - '{"en": "How did you find the teaching ?","fr": "Comment trouves-tu l enseignement ?","de": "How did you find the teaching ?"}', + {"en": "How did you find the teaching ?","fr": "Comment trouves-tu l enseignement ?","de": "How did you find the teaching ?"}, ID: (0x41e2).toString(), MaxN: 1, MinN: 1, @@ -104,7 +104,7 @@ const mockForm1: any = { Texts: [ { Title: - '{ "en" : "Who were the two best TAs ?", "fr" : "Quels sont les deux meilleurs TA ?", "de" : "Who were the two best TAs ?"} ', + { "en" : "Who were the two best TAs ?", "fr" : "Quels sont les deux meilleurs TA ?", "de" : "Who were the two best TAs ?"} , ID: (0xcd13).toString(), MaxLength: 20, MaxN: 2, @@ -153,13 +153,13 @@ const mockForm2: any = { Scaffold: [ { ID: (0xa2ab).toString(), - Title: '{"en": "Rate the course", "fr": "Note le cours", "de": "Rate the course"}', + Title: {"en": "Rate the course", "fr": "Note le cours", "de": "Rate the course"}, Order: [(0x3fb2).toString(), (0xcd13).toString()], Selects: [ { Title: - '{"en": "How did you find the provided material, from 1 (bad) to 5 (excellent) ?", "fr" : "Comment trouves-tu le matériel fourni, de 1 (mauvais) à 5 (excellent) ?", "de" : "How did you find the provided material, from 1 (bad) to 5 (excellent) ?"}', + {"en": "How did you find the provided material, from 1 (bad) to 5 (excellent) ?", "fr" : "Comment trouves-tu le matériel fourni, de 1 (mauvais) à 5 (excellent) ?", "de" : "How did you find the provided material, from 1 (bad) to 5 (excellent) ?"}, ID: (0x3fb2).toString(), MaxN: 1, MinN: 1, @@ -176,7 +176,7 @@ const mockForm2: any = { Texts: [ { Title: - '{"en" : "Who were the two best TAs ?", "fr" : "Quels sont les deux meilleurs TA ?", "de" : "Who were the two best TAs ?"}', + {"en" : "Who were the two best TAs ?", "fr" : "Quels sont les deux meilleurs TA ?", "de" : "Who were the two best TAs ?"}, ID: (0xcd13).toString(), MaxLength: 40, MaxN: 2, @@ -195,12 +195,12 @@ const mockForm2: any = { }, { ID: (0x1234).toString(), - Title: '{"en": "Tough choices", "fr": "Choix difficiles", "de": "Tough choices"}', + Title: {"en": "Tough choices", "fr": "Choix difficiles", "de": "Tough choices"}, Order: [(0xa319).toString(), (0xcafe).toString(), (0xbeef).toString()], Selects: [ { Title: - '{"en": "Select your ingredients", "fr": "Choisis tes ingrédients", "de": "Select your ingredients"}', + {"en": "Select your ingredients", "fr": "Choisis tes ingrédients", "de": "Select your ingredients"}, ID: (0xa319).toString(), MaxN: 3, MinN: 0, @@ -217,7 +217,7 @@ const mockForm2: any = { Ranks: [ { Title: - '{"en": "Which cafeteria serves the best coffee ?", "fr": "Quelle cafétéria sert le meilleur café ?", "de": "Which cafeteria serves the best coffee ?"}', + {"en": "Which cafeteria serves the best coffee ?", "fr": "Quelle cafétéria sert le meilleur café ?", "de": "Which cafeteria serves the best coffee ?"}, ID: (0xcafe).toString(), MaxN: 4, MinN: 1, @@ -230,7 +230,7 @@ const mockForm2: any = { Hint: '{"en": "", "fr": "", "de": ""}', }, { - Title: '{"en": "IN or SC ?", "fr": "IN ou SC ?", "de": "IN or SC ?"}', + Title: {"en": "IN or SC ?", "fr": "IN ou SC ?", "de": "IN or SC ?"}, ID: (0xbeef).toString(), MaxN: 2, MinN: 1, @@ -249,7 +249,7 @@ const mockForm3: any = { Scaffold: [ { ID: '3cVHIxpx', - Title: '{"en": "Choose your lunch", "fr": "Choisis ton déjeuner", "de": "Choose your lunch"}', + Title: {"en": "Choose your lunch", "fr": "Choisis ton déjeuner", "de": "Choose your lunch"}, Order: ['PGP'], Ranks: [], Selects: [], @@ -257,7 +257,7 @@ const mockForm3: any = { { ID: 'PGP', Title: - '{"en": "Select what you want", "fr": "Choisis ce que tu veux", "de": "Select what you want"}', + {"en": "Select what you want", "fr": "Choisis ce que tu veux", "de": "Select what you want"}, MaxN: 4, MinN: 0, MaxLength: 50, diff --git a/web/frontend/src/mocks/setupMockForms.ts b/web/frontend/src/mocks/setupMockForms.ts index 44e79444b..2ce31cc0a 100644 --- a/web/frontend/src/mocks/setupMockForms.ts +++ b/web/frontend/src/mocks/setupMockForms.ts @@ -99,9 +99,7 @@ const toLightFormInfo = (mockForms: Map<ID, FormInfo>, formID: ID): LightFormInf return { FormID: formID, - Title: form.Configuration.MainTitle, - TitleFr: form.Configuration.TitleFr, - TitleDe: form.Configuration.TitleDe, + Title: form.Configuration.Title, Status: form.Status, Pubkey: form.Pubkey, }; diff --git a/web/frontend/src/pages/ballot/components/BallotDisplay.tsx b/web/frontend/src/pages/ballot/components/BallotDisplay.tsx index bf62f5044..ee7b3ac9e 100644 --- a/web/frontend/src/pages/ballot/components/BallotDisplay.tsx +++ b/web/frontend/src/pages/ballot/components/BallotDisplay.tsx @@ -5,7 +5,6 @@ import Rank, { handleOnDragEnd } from './Rank'; import Select from './Select'; import Text from './Text'; import { DragDropContext } from 'react-beautiful-dnd'; -import { isJson } from 'types/JSONparser'; type BallotDisplayProps = { configuration: Configuration; @@ -24,18 +23,8 @@ const BallotDisplay: FC<BallotDisplayProps> = ({ }) => { const [titles, setTitles] = useState<any>({}); useEffect(() => { - if (configuration.MainTitle === '') return; - if (isJson(configuration.MainTitle)) { - const ts = JSON.parse(configuration.MainTitle); - setTitles(ts); - } else { - const t = { - en: configuration.MainTitle, - fr: configuration.TitleFr, - de: configuration.TitleDe, - }; - setTitles(t); - } + if (configuration.Title === undefined) return; + setTitles(configuration.Title); }, [configuration]); const SubjectElementDisplay = (element: types.SubjectElement) => { @@ -65,17 +54,12 @@ const BallotDisplay: FC<BallotDisplayProps> = ({ }; const SubjectTree = (subject: types.Subject) => { - let sbj; - if (isJson(subject.Title)) { - sbj = JSON.parse(subject.Title); - } - if (sbj === undefined) sbj = { en: subject.Title, fr: subject.TitleFr, de: subject.TitleDe }; return ( <div key={subject.ID}> <h3 className="text-xl break-all pt-1 pb-1 sm:pt-2 sm:pb-2 border-t font-bold text-gray-600"> - {language === 'en' && sbj.en} - {language === 'fr' && sbj.fr} - {language === 'de' && sbj.de} + {language === 'en' && subject.Title.en} + {language === 'fr' && subject.Title.fr} + {language === 'de' && subject.Title.de} </h3> {subject.Order.map((id: ID) => ( <div key={id}> diff --git a/web/frontend/src/pages/ballot/components/Rank.tsx b/web/frontend/src/pages/ballot/components/Rank.tsx index 8918b6ccd..1f451c579 100644 --- a/web/frontend/src/pages/ballot/components/Rank.tsx +++ b/web/frontend/src/pages/ballot/components/Rank.tsx @@ -3,7 +3,6 @@ import { Draggable, DropResult, Droppable } from 'react-beautiful-dnd'; import { Answers, ID, RankQuestion } from 'types/configuration'; import { answersFrom } from 'types/getObjectType'; import HintButton from 'components/buttons/HintButton'; -import { isJson } from 'types/JSONparser'; export const handleOnDragEnd = ( result: DropResult, @@ -59,12 +58,7 @@ const Rank: FC<RankProps> = ({ rank, answers, language }) => { }; const [titles, setTitles] = useState<any>({}); useEffect(() => { - if (isJson(rank.Title)) { - const ts = JSON.parse(rank.Title); - setTitles(ts); - } else { - setTitles({ en: rank.Title, fr: rank.TitleFr, de: rank.TitleDe }); - } + setTitles(rank.Title); }, [rank]); const choiceDisplay = (choice: string, rankIndex: number) => { return ( diff --git a/web/frontend/src/pages/ballot/components/Select.tsx b/web/frontend/src/pages/ballot/components/Select.tsx index 32ba57c16..f4006bd36 100644 --- a/web/frontend/src/pages/ballot/components/Select.tsx +++ b/web/frontend/src/pages/ballot/components/Select.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { Answers, SelectQuestion } from 'types/configuration'; import { answersFrom } from 'types/getObjectType'; import HintButton from 'components/buttons/HintButton'; -import { isJson } from 'types/JSONparser'; type SelectProps = { select: SelectQuestion; answers: Answers; @@ -57,12 +56,7 @@ const Select: FC<SelectProps> = ({ select, answers, setAnswers, language }) => { }; const [titles, setTitles] = useState<any>({}); useEffect(() => { - if (isJson(select.Title)) { - const ts = JSON.parse(select.Title); - setTitles(ts); - } else { - setTitles({ en: select.Title, fr: select.TitleFr, de: select.TitleDe }); - } + setTitles(select.Title); }, [select]); const choiceDisplay = (isChecked: boolean, choice: string, choiceIndex: number) => { diff --git a/web/frontend/src/pages/ballot/components/Text.tsx b/web/frontend/src/pages/ballot/components/Text.tsx index 4837cad4e..089e2e7f4 100644 --- a/web/frontend/src/pages/ballot/components/Text.tsx +++ b/web/frontend/src/pages/ballot/components/Text.tsx @@ -101,9 +101,9 @@ const Text: FC<TextProps> = ({ text, answers, setAnswers, language }) => { <div className="grid grid-rows-1 grid-flow-col"> <div> <h3 className="text-lg break-words text-gray-600 w-96"> - {language === 'en' && text.Title} - {language === 'fr' && text.TitleFr} - {language === 'de' && text.TitleDe} + {language === 'en' && text.Title.en} + {language === 'fr' && text.Title.fr} + {language === 'de' && text.Title.de} </h3> </div> <div className="text-right"> diff --git a/web/frontend/src/pages/form/GroupedResult.tsx b/web/frontend/src/pages/form/GroupedResult.tsx index cb3015027..ad5f13c43 100644 --- a/web/frontend/src/pages/form/GroupedResult.tsx +++ b/web/frontend/src/pages/form/GroupedResult.tsx @@ -29,7 +29,6 @@ import { import { default as i18n } from 'i18next'; import SelectResult from './components/SelectResult'; import TextResult from './components/TextResult'; -import { isJson } from 'types/JSONparser'; type GroupedResultProps = { rankResult: RankResults; @@ -53,12 +52,7 @@ const GroupedResult: FC<GroupedResultProps> = ({ rankResult, selectResult, textR const SubjectElementResultDisplay = (element: SubjectElement) => { let titles; - if (isJson(element.Title)) { - titles = JSON.parse(element.Title); - } - if (titles === undefined) { - titles = { en: element.Title, fr: element.TitleFr, de: element.TitleDe }; - } + titles = element.Title; return ( <div className="pl-4 pb-4 sm:pl-6 sm:pb-6"> <div className="flex flex-row"> @@ -88,19 +82,12 @@ const GroupedResult: FC<GroupedResultProps> = ({ rankResult, selectResult, textR }; const displayResults = (subject: Subject) => { - let sbj; - if (isJson(subject.Title)) { - sbj = JSON.parse(subject.Title); - } - if (sbj === undefined) { - sbj = { en: subject.Title, fr: subject.TitleFr, de: subject.TitleDe }; - } return ( <div key={subject.ID}> <h2 className="text-xl pt-1 pb-1 sm:pt-2 sm:pb-2 border-t font-bold text-gray-600"> - {i18n.language === 'en' && sbj.en} - {i18n.language === 'fr' && sbj.fr} - {i18n.language === 'de' && sbj.de} + {i18n.language === 'en' && subject.Title.en} + {i18n.language === 'fr' && subject.Title.fr} + {i18n.language === 'de' && subject.Title.de} </h2> {subject.Order.map((id: ID) => ( <div key={id}> @@ -118,7 +105,7 @@ const GroupedResult: FC<GroupedResultProps> = ({ rankResult, selectResult, textR }; const getResultData = (subject: Subject, dataToDownload: DownloadedResults[]) => { - dataToDownload.push({ Title: subject.Title }); + dataToDownload.push({ Title: subject.Title.en }); subject.Order.forEach((id: ID) => { const element = subject.Elements.get(id); @@ -134,7 +121,7 @@ const GroupedResult: FC<GroupedResultProps> = ({ rankResult, selectResult, textR return { Candidate: rank.Choices[index], Percentage: `${percent}%` }; } ); - dataToDownload.push({ Title: element.Title, Results: res }); + dataToDownload.push({ Title: element.Title.en, Results: res }); } break; @@ -145,7 +132,7 @@ const GroupedResult: FC<GroupedResultProps> = ({ rankResult, selectResult, textR res = countSelectResult(selectResult.get(id)).resultsInPercent.map((percent, index) => { return { Candidate: select.Choices[index], Percentage: `${percent}%` }; }); - dataToDownload.push({ Title: element.Title, Results: res }); + dataToDownload.push({ Title: element.Title.en, Results: res }); } break; @@ -158,7 +145,7 @@ const GroupedResult: FC<GroupedResultProps> = ({ rankResult, selectResult, textR res = Array.from(countTextResult(textResult.get(id)).resultsInPercent).map((r) => { return { Candidate: r[0], Percentage: `${r[1]}%` }; }); - dataToDownload.push({ Title: element.Title, Results: res }); + dataToDownload.push({ Title: element.Title.en, Results: res }); } break; } @@ -166,10 +153,9 @@ const GroupedResult: FC<GroupedResultProps> = ({ rankResult, selectResult, textR }; const exportJSONData = () => { - const fileName = `result_${configuration.MainTitle.replace(/[^a-zA-Z0-9]/g, '_').slice( - 0, - 99 - )}__grouped`; // replace spaces with underscores; + const fileName = `result_${configuration.Title.en + .replace(/[^a-zA-Z0-9]/g, '_') + .slice(0, 99)}__grouped`; // replace spaces with underscores; const dataToDownload: DownloadedResults[] = []; @@ -178,9 +164,7 @@ const GroupedResult: FC<GroupedResultProps> = ({ rankResult, selectResult, textR }); const data = { - MainTitle: configuration.MainTitle, - TitleFr: configuration.TitleFr, - TitleDe: configuration.TitleDe, + Title: configuration.Title, NumberOfVotes: result.length, Results: dataToDownload, }; diff --git a/web/frontend/src/pages/form/IndividualResult.tsx b/web/frontend/src/pages/form/IndividualResult.tsx index c130e1ca7..157c228d6 100644 --- a/web/frontend/src/pages/form/IndividualResult.tsx +++ b/web/frontend/src/pages/form/IndividualResult.tsx @@ -31,7 +31,6 @@ import Loading from 'pages/Loading'; import saveAs from 'file-saver'; import { useNavigate } from 'react-router'; import { default as i18n } from 'i18next'; -import { isJson } from 'types/JSONparser'; type IndividualResultProps = { rankResult: RankResults; @@ -70,13 +69,6 @@ const IndividualResult: FC<IndividualResultProps> = ({ [TEXT]: <MenuAlt1Icon />, }; - let titles; - if (isJson(element.Title)) { - titles = JSON.parse(element.Title); - } - if (titles === undefined) { - titles = { en: element.Title, fr: element.TitleFr, de: element.TitleDe }; - } return ( <div className="pl-4 pb-4 sm:pl-6 sm:pb-6"> <div className="flex flex-row"> @@ -84,9 +76,9 @@ const IndividualResult: FC<IndividualResultProps> = ({ {questionIcons[element.Type]} </div> <h2 className="flex align-text-middle text-lg pb-2"> - {i18n.language === 'en' && titles.en} - {i18n.language === 'fr' && titles.fr} - {i18n.language === 'de' && titles.de} + {i18n.language === 'en' && element.Title.en} + {i18n.language === 'fr' && element.Title.fr} + {i18n.language === 'de' && element.Title.de} </h2> </div> {element.Type === RANK && rankResult.has(element.ID) && ( @@ -115,19 +107,12 @@ const IndividualResult: FC<IndividualResultProps> = ({ const displayResults = useCallback( (subject: Subject) => { - let sbj; - if (isJson(subject.Title)) { - sbj = JSON.parse(subject.Title); - } - if (sbj === undefined) { - sbj = { en: subject.Title, fr: subject.TitleFr, de: subject.TitleDe }; - } return ( <div key={subject.ID}> <h2 className="text-xl pt-1 pb-1 sm:pt-2 sm:pb-2 border-t font-bold text-gray-600"> - {i18n.language === 'en' && sbj.en} - {i18n.language === 'fr' && sbj.fr} - {i18n.language === 'de' && sbj.de} + {i18n.language === 'en' && subject.Title.en} + {i18n.language === 'fr' && subject.Title.fr} + {i18n.language === 'de' && subject.Title.de} </h2> {subject.Order.map((id: ID) => ( <div key={id}> @@ -151,7 +136,7 @@ const IndividualResult: FC<IndividualResultProps> = ({ dataToDownload: DownloadedResults[], BallotID: number ) => { - dataToDownload.push({ Title: subject.Title }); + dataToDownload.push({ Title: subject.Title.en }); subject.Order.forEach((id: ID) => { const element = subject.Elements.get(id); @@ -168,7 +153,7 @@ const IndividualResult: FC<IndividualResultProps> = ({ Choice: rankQues.Choices[rankResult.get(id)[BallotID].indexOf(index)], }; }); - dataToDownload.push({ Title: element.Title, Results: res }); + dataToDownload.push({ Title: element.Title.en, Results: res }); } break; @@ -180,7 +165,7 @@ const IndividualResult: FC<IndividualResultProps> = ({ const checked = select ? 'True' : 'False'; return { Candidate: selectQues.Choices[index], Checked: checked }; }); - dataToDownload.push({ Title: element.Title, Results: res }); + dataToDownload.push({ Title: element.Title.en, Results: res }); } break; @@ -195,7 +180,7 @@ const IndividualResult: FC<IndividualResultProps> = ({ res = textResult.get(id)[BallotID].map((text, index) => { return { Field: textQues.Choices[index], Answer: text }; }); - dataToDownload.push({ Title: element.Title, Results: res }); + dataToDownload.push({ Title: element.Title.en, Results: res }); } break; } @@ -203,10 +188,9 @@ const IndividualResult: FC<IndividualResultProps> = ({ }; const exportJSONData = () => { - const fileName = `result_${configuration.MainTitle.replace(/[^a-zA-Z0-9]/g, '_').slice( - 0, - 99 - )}__individual`; + const fileName = `result_${configuration.Title.en + .replace(/[^a-zA-Z0-9]/g, '_') + .slice(0, 99)}__individual`; const ballotsToDownload: BallotResults[] = []; const indices: number[] = [...Array(ballotNumber).keys()]; @@ -219,9 +203,7 @@ const IndividualResult: FC<IndividualResultProps> = ({ }); const data = { - MainTitle: configuration.MainTitle, - TitleFr: configuration.TitleFr, - TitleDe: configuration.TitleDe, + Title: configuration.Title, NumberOfVotes: ballotNumber, Ballots: ballotsToDownload, }; diff --git a/web/frontend/src/pages/form/Result.tsx b/web/frontend/src/pages/form/Result.tsx index 07a6f40cd..c9d88b46f 100644 --- a/web/frontend/src/pages/form/Result.tsx +++ b/web/frontend/src/pages/form/Result.tsx @@ -97,9 +97,9 @@ const FormResult: FC = () => { {t('totalNumberOfVotes', { votes: result.length })} </h2> <h3 className="py-6 border-t text-2xl text-center text-gray-700"> - {i18n.language === 'en' && configuration.MainTitle} - {i18n.language === 'fr' && configuration.TitleFr} - {i18n.language === 'de' && configuration.TitleDe} + {i18n.language === 'en' && configuration.Title.en} + {i18n.language === 'fr' && configuration.Title.fr} + {i18n.language === 'de' && configuration.Title.de} </h3> <div> diff --git a/web/frontend/src/pages/form/components/AddQuestionModal.tsx b/web/frontend/src/pages/form/components/AddQuestionModal.tsx index 50ec75bd4..b782670c9 100644 --- a/web/frontend/src/pages/form/components/AddQuestionModal.tsx +++ b/web/frontend/src/pages/form/components/AddQuestionModal.tsx @@ -45,7 +45,7 @@ const AddQuestionModal: FC<AddQuestionModalProps> = ({ updateChoice, } = useQuestionForm(question); const [language, setLanguage] = useState('en'); - const { Title, TitleDe, TitleFr, MaxN, MinN, ChoicesMap, Hint, HintFr, HintDe } = values; + const { Title, MaxN, MinN, ChoicesMap, Hint, HintFr, HintDe } = values; const [errors, setErrors] = useState([]); const handleSave = async () => { try { @@ -189,9 +189,9 @@ const AddQuestionModal: FC<AddQuestionModalProps> = ({ </label> {language === 'en' && ( <input - value={Title} - onChange={handleChange()} - name="Title" + value={Title.en} + onChange={(e) => handleChange('Title')(e)} + name="en" type="text" placeholder={t('enterTitleLg')} className="my-1 px-1 w-60 ml-1 border rounded-md" @@ -199,9 +199,9 @@ const AddQuestionModal: FC<AddQuestionModalProps> = ({ )} {language === 'fr' && ( <input - value={TitleFr} - onChange={handleChange()} - name="TitleFr" + value={Title.fr} + onChange={(e) => handleChange('Title')(e)} + name="fr" type="text" placeholder={t('enterTitleLg1')} className="my-1 px-1 w-60 ml-1 border rounded-md" @@ -209,9 +209,9 @@ const AddQuestionModal: FC<AddQuestionModalProps> = ({ )} {language === 'de' && ( <input - value={TitleDe} - onChange={handleChange()} - name="TitleDe" + value={Title.de} + onChange={(e) => handleChange('Title')(e)} + name="de" type="text" placeholder={t('enterTitleLg2')} className="my-1 px-1 w-60 ml-1 border rounded-md" diff --git a/web/frontend/src/pages/form/components/FormForm.tsx b/web/frontend/src/pages/form/components/FormForm.tsx index 7a21a5ae6..5b961b2e9 100644 --- a/web/frontend/src/pages/form/components/FormForm.tsx +++ b/web/frontend/src/pages/form/components/FormForm.tsx @@ -38,7 +38,7 @@ import { default as i18n } from 'i18next'; type FormFormProps = {}; const FormForm: FC<FormFormProps> = () => { - // conf is the configuration object containing MainTitle and Scaffold which + // conf is the configuration object containing Title and Scaffold which // contains an array of subject. const { t } = useTranslation(); const emptyConf: Configuration = emptyConfiguration(); @@ -54,7 +54,7 @@ const FormForm: FC<FormFormProps> = () => { const [marshalledConf, setMarshalledConf] = useState<any>(marshalConfig(conf)); const { configuration: previewConf, answers, setAnswers } = useConfiguration(marshalledConf); - const { MainTitle, Scaffold, TitleFr, TitleDe } = conf; + const { Title, Scaffold } = conf; const [language, setLanguage] = useState(i18n.language); const regexPattern = /[^a-zA-Z0-9]/g; @@ -152,7 +152,7 @@ const FormForm: FC<FormFormProps> = () => { const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(JSON.stringify(data))}`; const link = document.createElement('a'); link.href = jsonString; - const title = MainTitle.replace(regexPattern, '_').slice(0, 99); // replace spaces with underscores + const title = Title.en.replace(regexPattern, '_').slice(0, 99); // replace spaces with underscores link.download = title + '.json'; link.click(); }; @@ -192,9 +192,9 @@ const FormForm: FC<FormFormProps> = () => { {language === 'en' && ( <input - value={MainTitle} - onChange={(e) => setConf({ ...conf, MainTitle: e.target.value })} - name="MainTitle" + value={Title.en} + onChange={(e) => setConf({ ...conf, Title: { ...Title, en: e.target.value } })} + name="Title" type="text" placeholder={t('enterMainTitleLg')} className="m-3 px-1 w-100 text-lg border rounded-md" @@ -202,8 +202,8 @@ const FormForm: FC<FormFormProps> = () => { )} {language === 'fr' && ( <input - value={TitleFr} - onChange={(e) => setConf({ ...conf, TitleFr: e.target.value })} + value={Title.fr} + onChange={(e) => setConf({ ...conf, Title: { ...Title, fr: e.target.value } })} name="MainTitle1" type="text" placeholder={t('enterMainTitleLg1')} @@ -212,8 +212,8 @@ const FormForm: FC<FormFormProps> = () => { )} {language === 'de' && ( <input - value={TitleDe} - onChange={(e) => setConf({ ...conf, TitleDe: e.target.value })} + value={Title.de} + onChange={(e) => setConf({ ...conf, Title: { ...Title, de: e.target.value } })} name="MainTitle2" type="text" placeholder={t('enterMainTitleLg2')} @@ -223,9 +223,9 @@ const FormForm: FC<FormFormProps> = () => { <div className="ml-1"> <button className={`border p-1 rounded-md ${ - MainTitle.length === 0 ? 'bg-gray-100' : ' ' + Title.en.length === 0 ? 'bg-gray-100' : ' ' }`} - disabled={MainTitle.length === 0} + disabled={Title.en.length === 0} onClick={() => setTitleChanging(false)}> <CheckIcon className="h-5 w-5" aria-hidden="true" /> </button> @@ -236,9 +236,9 @@ const FormForm: FC<FormFormProps> = () => { <div className="mt-1 ml-3 w-[90%] break-words" onClick={() => setTitleChanging(true)}> - {language === 'en' && MainTitle} - {language === 'fr' && TitleFr} - {language === 'de' && TitleDe} + {language === 'en' && Title.en} + {language === 'fr' && Title.fr} + {language === 'de' && Title.de} </div> <div className="ml-1"> <button diff --git a/web/frontend/src/pages/form/components/FormRow.tsx b/web/frontend/src/pages/form/components/FormRow.tsx index 7c0da7ae9..f09ad8c1a 100644 --- a/web/frontend/src/pages/form/components/FormRow.tsx +++ b/web/frontend/src/pages/form/components/FormRow.tsx @@ -4,7 +4,6 @@ import { Link } from 'react-router-dom'; import FormStatus from './FormStatus'; import QuickAction from './QuickAction'; import { default as i18n } from 'i18next'; -import { isJson } from 'types/JSONparser'; type FormRowProps = { form: LightFormInfo; @@ -13,12 +12,8 @@ type FormRowProps = { const FormRow: FC<FormRowProps> = ({ form }) => { const [titles, setTitles] = useState<any>({}); useEffect(() => { - if (form.Title === '') return; - if (isJson(form.Title)) { - setTitles(JSON.parse(form.Title)); - } else { - setTitles({ en: form.Title, fr: form.TitleFr, de: form.TitleDe }); - } + if (form.Title === undefined) return; + setTitles({ en: form.Title.en, fr: form.Title.fr, de: form.Title.de }); }, [form]); // let i18next handle choosing the appropriate language const formRowI18n = i18n.createInstance(); diff --git a/web/frontend/src/pages/form/components/Question.tsx b/web/frontend/src/pages/form/components/Question.tsx index 646d98dd7..d56162077 100644 --- a/web/frontend/src/pages/form/components/Question.tsx +++ b/web/frontend/src/pages/form/components/Question.tsx @@ -15,7 +15,7 @@ type QuestionProps = { }; const Question: FC<QuestionProps> = ({ question, notifyParent, removeQuestion, language }) => { - const { Title, Type, TitleFr, TitleDe } = question; + const { Title, Type } = question; const [openModal, setOpenModal] = useState<boolean>(false); const dropdownContent: { @@ -53,9 +53,9 @@ const Question: FC<QuestionProps> = ({ question, notifyParent, removeQuestion, l <DisplayTypeIcon Type={Type} /> </div> <div className="pt-1.5 max-w-md pr-8 truncate"> - {language === 'en' && (Title.length ? Title : `Enter ${Type} title`)} - {language === 'fr' && (TitleFr.length ? TitleFr : Title)} - {language === 'de' && (TitleDe.length ? TitleDe : Title)} + {language === 'en' && (Title.en.length ? Title.en : `Enter ${Type} title`)} + {language === 'fr' && (Title.fr.length ? Title.fr : Title.en)} + {language === 'de' && (Title.de.length ? Title.de : Title.en)} </div> </div> diff --git a/web/frontend/src/pages/form/components/SubjectComponent.tsx b/web/frontend/src/pages/form/components/SubjectComponent.tsx index 6defdf28e..6298e9b70 100644 --- a/web/frontend/src/pages/form/components/SubjectComponent.tsx +++ b/web/frontend/src/pages/form/components/SubjectComponent.tsx @@ -46,7 +46,7 @@ const SubjectComponent: FC<SubjectComponentProps> = ({ const isSubjectMounted = useRef<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false); const [titleChanging, setTitleChanging] = useState<boolean>( - subjectObject.Title.length ? false : true + subjectObject.Title.en.length ? false : true ); const [openModal, setOpenModal] = useState<boolean>(false); const [showRemoveElementModal, setShowRemoveElementModal] = useState<boolean>(false); @@ -54,7 +54,7 @@ const SubjectComponent: FC<SubjectComponentProps> = ({ const [elementToRemove, setElementToRemove] = useState(emptyElementToRemove); const [components, setComponents] = useState<ReactElement[]>([]); - const { Title, Order, Elements, TitleFr, TitleDe } = subject; + const { Title, Order, Elements } = subject; // When a property changes, we notify the parent with the new subject object useEffect(() => { // We only notify the parent when the subject is mounted @@ -308,8 +308,10 @@ const SubjectComponent: FC<SubjectComponentProps> = ({ <div className="flex flex-col mt-3 mb-2"> {language === 'en' && ( <input - value={Title} - onChange={(e) => setSubject({ ...subject, Title: e.target.value })} + value={Title.en} + onChange={(e) => + setSubject({ ...subject, Title: { ...Title, en: e.target.value } }) + } name="Title" type="text" placeholder={t('enterSubjectTitleLg')} @@ -320,8 +322,10 @@ const SubjectComponent: FC<SubjectComponentProps> = ({ )} {language === 'fr' && ( <input - value={TitleFr} - onChange={(e) => setSubject({ ...subject, TitleFr: e.target.value })} + value={Title.fr} + onChange={(e) => + setSubject({ ...subject, Title: { ...Title, fr: e.target.value } }) + } name="Title" type="text" placeholder={t('enterSubjectTitleLg1')} @@ -332,8 +336,10 @@ const SubjectComponent: FC<SubjectComponentProps> = ({ )} {language === 'de' && ( <input - value={TitleDe} - onChange={(e) => setSubject({ ...subject, TitleDe: e.target.value })} + value={Title.de} + onChange={(e) => + setSubject({ ...subject, Title: { ...Title, de: e.target.value } }) + } name="Title" type="text" placeholder={t('enterSubjectTitleLg2')} @@ -344,8 +350,8 @@ const SubjectComponent: FC<SubjectComponentProps> = ({ )} <div className="ml-1"> <button - className={`border p-1 rounded-md ${Title.length === 0 && 'bg-gray-100'}`} - disabled={Title.length === 0} + className={`border p-1 rounded-md ${Title.en.length === 0 && 'bg-gray-100'}`} + disabled={Title.en.length === 0} onClick={() => setTitleChanging(false)}> <CheckIcon className="h-5 w-5" aria-hidden="true" /> </button> @@ -354,9 +360,9 @@ const SubjectComponent: FC<SubjectComponentProps> = ({ ) : ( <div className="flex mb-2 max-w-md truncate"> <div className="pt-1.5 truncate" onClick={() => setTitleChanging(true)}> - {language === 'en' && Title} - {language === 'fr' && TitleFr} - {language === 'de' && TitleDe} + {language === 'en' && Title.en} + {language === 'fr' && Title.fr} + {language === 'de' && Title.de} </div> <div className="ml-1 pr-10"> <button diff --git a/web/frontend/src/pages/form/components/utils/useQuestionForm.ts b/web/frontend/src/pages/form/components/utils/useQuestionForm.ts index 913082fee..2fbcc7971 100644 --- a/web/frontend/src/pages/form/components/utils/useQuestionForm.ts +++ b/web/frontend/src/pages/form/components/utils/useQuestionForm.ts @@ -20,6 +20,9 @@ const useQuestionForm = (initState: RankQuestion | SelectQuestion | TextQuestion newChoicesMap.set('fr', [...newChoicesMap.get('fr'), '']); newChoicesMap.set('de', [...newChoicesMap.get('de'), '']); switch (Exception) { + case 'Title': + setState({ ...state, Title: { ...state.Title, [name]: value } }); + break; case 'RankMinMax': setState({ ...state, MinN: Number(value), MaxN: Number(value) }); break; diff --git a/web/frontend/src/schema/configurationValidation.ts b/web/frontend/src/schema/configurationValidation.ts index 07b8f64ea..e2881a599 100644 --- a/web/frontend/src/schema/configurationValidation.ts +++ b/web/frontend/src/schema/configurationValidation.ts @@ -1,8 +1,7 @@ import * as yup from 'yup'; const idSchema = yup.string().min(1).required(); -const titleSchema = yup.string().required(); -const formTitleSchema = yup.object({ +const titleSchema = yup.object({ en: yup.string().required(), fr: yup.string(), de: yup.string(), @@ -408,7 +407,7 @@ const subjectSchema = yup.object({ }); const configurationSchema = yup.object({ - MainTitle: yup.lazy(() => formTitleSchema), + MainTitle: yup.lazy(() => titleSchema), Scaffold: yup.array().of(subjectSchema).required(), }); diff --git a/web/frontend/src/schema/form_conf.json b/web/frontend/src/schema/form_conf.json index 0569bc2d7..43f7ba510 100644 --- a/web/frontend/src/schema/form_conf.json +++ b/web/frontend/src/schema/form_conf.json @@ -2,14 +2,14 @@ "$id": "configurationSchema", "type": "object", "properties": { - "MainTitle": { "type": "string" }, + "Title": { "type": "object" }, "Scaffold": { "type": "array", "items": { "type": "object", "properties": { "ID": { "type": "string" }, - "Title": { "type": "string" }, + "Title": { "type": "object" }, "Order": { "type": "array", "items": { "type": "string" } @@ -24,7 +24,7 @@ "type": "object", "properties": { "ID": { "type": "string" }, - "Title": { "type": "string" }, + "Title": { "type": "object" }, "MaxN": { "type": "number" }, "MinN": { "type": "number" }, "Choices": { @@ -43,7 +43,7 @@ "type": "object", "properties": { "ID": { "type": "string" }, - "Title": { "type": "string" }, + "Title": { "type": "object" }, "MaxN": { "type": "number" }, "MinN": { "type": "number" }, "Choices": { @@ -62,7 +62,7 @@ "type": "object", "properties": { "ID": { "type": "string" }, - "Title": { "type": "string" }, + "Title": { "type": "object" }, "MaxN": { "type": "number" }, "MinN": { "type": "number" }, "Regex": { "type": "string" }, diff --git a/web/frontend/src/types/JSONparser.ts b/web/frontend/src/types/JSONparser.ts index b40daea98..d9fa36d96 100644 --- a/web/frontend/src/types/JSONparser.ts +++ b/web/frontend/src/types/JSONparser.ts @@ -15,7 +15,6 @@ const isJson = (str: string) => { }; const unmarshalText = (text: any): types.TextQuestion => { const t = text as types.TextQuestion; - const titles = JSON.parse(t.Title); if (t.Hint === undefined) { t.Hint = JSON.stringify({ en: '', @@ -26,9 +25,7 @@ const unmarshalText = (text: any): types.TextQuestion => { const hint = JSON.parse(t.Hint); return { ...text, - Title: titles.en, - TitleFr: titles.fr, - TitleDe: titles.de, + Title: t.Title, Hint: hint.en, HintFr: hint.fr, HintDe: hint.de, @@ -39,7 +36,6 @@ const unmarshalText = (text: any): types.TextQuestion => { const unmarshalRank = (rank: any): types.RankQuestion => { const r = rank as types.RankQuestion; - const titles = JSON.parse(r.Title); if (r.Hint === undefined) { r.Hint = JSON.stringify({ en: '', @@ -50,8 +46,7 @@ const unmarshalRank = (rank: any): types.RankQuestion => { const hint = JSON.parse(r.Hint); return { ...rank, - TitleFr: titles.fr, - TitleDe: titles.de, + Title: r.Title, Hint: hint.en, HintFr: hint.fr, HintDe: hint.de, @@ -62,7 +57,6 @@ const unmarshalRank = (rank: any): types.RankQuestion => { const unmarshalSelect = (select: any): types.SelectQuestion => { const s = select as types.SelectQuestion; - const titles = JSON.parse(s.Title); if (s.Hint === undefined) { s.Hint = JSON.stringify({ en: '', @@ -73,8 +67,7 @@ const unmarshalSelect = (select: any): types.SelectQuestion => { const hint = JSON.parse(s.Hint); return { ...select, - TitleFr: titles.fr, - TitleDe: titles.de, + Title: s.Title, Hint: hint.en, HintFr: hint.fr, HintDe: hint.de, @@ -168,20 +161,8 @@ const unmarshalSubjectAndCreateAnswers = ( }; const unmarshalConfig = (json: any): types.Configuration => { - let titles; - if (isJson(json.MainTitle)) titles = JSON.parse(json.MainTitle); - else { - titles = { - en: json.MainTitle, - fr: json.TitleFr, - de: json.TitleDe, - }; - } - const conf = { - MainTitle: titles.en, - TitleFr: titles.fr, - TitleDe: titles.de, + Title: json.Title, Scaffold: [], }; for (const subject of json.Scaffold) { @@ -247,12 +228,7 @@ const marshalSubject = (subject: types.Subject): any => { }; const marshalConfig = (configuration: types.Configuration): any => { - const title = { - en: configuration.MainTitle, - fr: configuration.TitleFr, - de: configuration.TitleDe, - }; - const conf = { MainTitle: JSON.stringify(title), Scaffold: [] }; + const conf = { MainTitle: JSON.stringify(configuration.Title), Scaffold: [] }; for (const subject of configuration.Scaffold) { conf.Scaffold.push(marshalSubject(subject)); } diff --git a/web/frontend/src/types/configuration.ts b/web/frontend/src/types/configuration.ts index b28fa6e14..d2ff7db6d 100644 --- a/web/frontend/src/types/configuration.ts +++ b/web/frontend/src/types/configuration.ts @@ -5,12 +5,17 @@ export const SELECT: string = 'select'; export const SUBJECT: string = 'subject'; export const TEXT: string = 'text'; +// Title +interface Title { + en: string; + fr: string; + de: string; +} + interface SubjectElement { ID: ID; Type: string; - Title: string; - TitleFr: string; - TitleDe: string; + Title: Title; } // Rank describes a "rank" question, which requires the user to rank choices. @@ -55,10 +60,8 @@ interface Subject extends SubjectElement { // Configuration contains the configuration of a new poll. interface Configuration { - MainTitle: string; + Title: Title; Scaffold: Subject[]; - TitleFr: string; - TitleDe: string; } // Answers describes the current answers for each type of question diff --git a/web/frontend/src/types/form.ts b/web/frontend/src/types/form.ts index 1aa99b7d6..4f04cadc7 100644 --- a/web/frontend/src/types/form.ts +++ b/web/frontend/src/types/form.ts @@ -44,6 +44,12 @@ export const enum OngoingAction { Canceling, } +interface Title { + en: string; + fr: string; + de: string; +} + interface FormInfo { FormID: ID; Status: Status; @@ -58,9 +64,7 @@ interface FormInfo { interface LightFormInfo { FormID: ID; - Title: string; - TitleFr: string; - TitleDe: string; + Title: Title; Status: Status; Pubkey: string; } @@ -98,4 +102,5 @@ export type { SelectResults, DownloadedResults, BallotResults, + Title, }; diff --git a/web/frontend/src/types/getObjectType.ts b/web/frontend/src/types/getObjectType.ts index b244e4753..c65c59ec1 100644 --- a/web/frontend/src/types/getObjectType.ts +++ b/web/frontend/src/types/getObjectType.ts @@ -6,19 +6,23 @@ const uid: Function = new ShortUniqueId({ length: 8 }); const emptyConfiguration = (): types.Configuration => { return { - MainTitle: '', + Title: { + en: '', + fr: '', + de: '', + }, Scaffold: [], - TitleFr: '', - TitleDe: '', }; }; const newSubject = (): types.Subject => { return { ID: uid(), - Title: '', - TitleFr: '', - TitleDe: '', + Title: { + en: '', + fr: '', + de: '', + }, Order: [], Type: SUBJECT, Elements: new Map(), @@ -28,9 +32,11 @@ const obj = { en: [''], fr: [''], de: [''] }; const newRank = (): types.RankQuestion => { return { ID: uid(), - Title: '', - TitleFr: '', - TitleDe: '', + Title: { + en: '', + fr: '', + de: '', + }, MaxN: 2, MinN: 2, Choices: [], @@ -45,9 +51,11 @@ const newRank = (): types.RankQuestion => { const newSelect = (): types.SelectQuestion => { return { ID: uid(), - Title: '', - TitleDe: '', - TitleFr: '', + Title: { + en: '', + fr: '', + de: '', + }, MaxN: 1, MinN: 1, Choices: [], @@ -62,9 +70,11 @@ const newSelect = (): types.SelectQuestion => { const newText = (): types.TextQuestion => { return { ID: uid(), - Title: '', - TitleFr: '', - TitleDe: '', + Title: { + en: '', + fr: '', + de: '', + }, MaxN: 1, MinN: 0, MaxLength: 50, @@ -142,16 +152,10 @@ const toArraysOfSubjectElement = ( const selectQuestion: types.SelectQuestion[] = []; const textQuestion: types.TextQuestion[] = []; const subjects: types.Subject[] = []; - let title = ''; let hint = ''; elements.forEach((element) => { switch (element.Type) { case RANK: - title = JSON.stringify({ - en: element.Title, - fr: element.TitleFr, - de: element.TitleDe, - }); hint = JSON.stringify({ en: (element as types.RankQuestion).Hint, fr: (element as types.RankQuestion).HintFr, @@ -159,7 +163,7 @@ const toArraysOfSubjectElement = ( }); rankQuestion.push({ ...(element as types.RankQuestion), - Title: title, + Title: element.Title, Choices: choicesMapToChoices( (element as types.RankQuestion).ChoicesMap ), @@ -167,11 +171,6 @@ const toArraysOfSubjectElement = ( }); break; case SELECT: - title = JSON.stringify({ - en: element.Title, - fr: element.TitleFr, - de: element.TitleDe, - }); hint = JSON.stringify({ en: (element as types.SelectQuestion).Hint, fr: (element as types.SelectQuestion).HintFr, @@ -179,7 +178,7 @@ const toArraysOfSubjectElement = ( }); selectQuestion.push({ ...(element as types.SelectQuestion), - Title: title, + Title: element.Title, Choices: choicesMapToChoices( (element as types.SelectQuestion).ChoicesMap ), @@ -187,11 +186,6 @@ const toArraysOfSubjectElement = ( }); break; case TEXT: - title = JSON.stringify({ - en: element.Title, - fr: element.TitleFr, - de: element.TitleDe, - }); hint = JSON.stringify({ en: (element as types.TextQuestion).Hint, fr: (element as types.TextQuestion).HintFr, @@ -199,7 +193,7 @@ const toArraysOfSubjectElement = ( }); textQuestion.push({ ...(element as types.TextQuestion), - Title: title, + Title: element.Title, Choices: choicesMapToChoices( (element as types.TextQuestion).ChoicesMap ), @@ -207,14 +201,9 @@ const toArraysOfSubjectElement = ( }); break; case SUBJECT: - title = JSON.stringify({ - en: element.Title, - fr: element.TitleFr, - de: element.TitleDe, - }); subjects.push({ ...(element as types.Subject), - Title: title, + Title: element.Title, }); break; }