diff --git a/src/framework/processing/py/port/api/props.py b/src/framework/processing/py/port/api/props.py index 888d96a9..823c8750 100644 --- a/src/framework/processing/py/port/api/props.py +++ b/src/framework/processing/py/port/api/props.py @@ -211,6 +211,74 @@ def toDict(self): return dict +@dataclass +class PropsUIQuestionOpen: + """ + NO DOCS YET + """ + id: int + question: Translatable + + def toDict(self): + dict = {} + dict["__type__"] = "PropsUIQuestionOpen" + dict["id"] = self.id + dict["question"] = self.question.toDict() + return dict + + +@dataclass +class PropsUIQuestionMultipleChoiceCheckbox: + """ + NO DOCS YET + """ + id: int + question: Translatable + choices: list[Translatable] + + def toDict(self): + dict = {} + dict["__type__"] = "PropsUIQuestionMultipleChoiceCheckbox" + dict["id"] = self.id + dict["question"] = self.question.toDict() + dict["choices"] = [c.toDict() for c in self.choices] + return dict + + +@dataclass +class PropsUIQuestionMultipleChoice: + """ + NO DOCS YET + """ + id: int + question: Translatable + choices: list[Translatable] + + def toDict(self): + dict = {} + dict["__type__"] = "PropsUIQuestionMultipleChoice" + dict["id"] = self.id + dict["question"] = self.question.toDict() + dict["choices"] = [c.toDict() for c in self.choices] + return dict + + +@dataclass +class PropsUIPromptQuestionnaire: + """ + NO DOCS YET + """ + description: Translatable + questions: list[PropsUIQuestionMultipleChoice | PropsUIQuestionMultipleChoiceCheckbox | PropsUIQuestionOpen] + + def toDict(self): + dict = {} + dict["__type__"] = "PropsUIPromptQuestionnaire" + dict["description"] = self.description.toDict() + dict["questions"] = [q.toDict() for q in self.questions] + return dict + + @dataclass class PropsUIPageDonation: """A multi-purpose page that gets shown to the user @@ -228,6 +296,7 @@ class PropsUIPageDonation: | PropsUIPromptConsentForm | PropsUIPromptFileInput | PropsUIPromptConfirm + | PropsUIPromptQuestionnaire ) footer: PropsUIFooter diff --git a/src/framework/types/elements.ts b/src/framework/types/elements.ts index 76ed9c92..20b2a4d1 100644 --- a/src/framework/types/elements.ts +++ b/src/framework/types/elements.ts @@ -425,3 +425,35 @@ export interface Translatable { export function isTranslatable(arg: any): arg is Translatable { return isLike(arg, ["translations"]) } + + +// QUESTION ITEMS + +export interface PropsUIQuestionMultipleChoice { + __type__: 'PropsUIQuestionMultipleChoice' + id: number + question: Text + choices: Text[] +} +export function isPropsUIQuestionMultipleChoice (arg: any): arg is PropsUIQuestionMultipleChoice { + return isInstanceOf(arg, 'PropsUIQuestionMultipleChoice', ['id', 'question', 'choices']) +} + +export interface PropsUIQuestionMultipleChoiceCheckbox { + __type__: 'PropsUIQuestionMultipleChoiceCheckbox' + id: number + question: Text + choices: Text[] +} +export function isPropsUIQuestionMultipleChoiceCheckbox (arg: any): arg is PropsUIQuestionMultipleChoiceCheckbox { + return isInstanceOf(arg, 'PropsUIQuestionMultipleChoiceCheckbox', ['id', 'question', 'choices']) +} + +export interface PropsUIQuestionOpen { + __type__: 'PropsUIQuestionOpen' + id: number + question: Text +} +export function isPropsUIQuestionOpen (arg: any): arg is PropsUIQuestionOpen { + return isInstanceOf(arg, 'PropsUIQuestionOpen', ['id', 'question']) +} diff --git a/src/framework/types/pages.ts b/src/framework/types/pages.ts index c91136c7..e192ef6f 100644 --- a/src/framework/types/pages.ts +++ b/src/framework/types/pages.ts @@ -1,6 +1,12 @@ import { isInstanceOf } from '../helpers' import { PropsUIHeader } from './elements' -import { PropsUIPromptFileInput, PropsUIPromptConfirm, PropsUIPromptConsentForm, PropsUIPromptRadioInput } from './prompts' +import { + PropsUIPromptFileInput, + PropsUIPromptConfirm, + PropsUIPromptConsentForm, + PropsUIPromptRadioInput, + PropsUIPromptQuestionnaire +} from './prompts' export type PropsUIPage = PropsUIPageSplashScreen | @@ -22,7 +28,7 @@ export interface PropsUIPageDonation { __type__: 'PropsUIPageDonation' platform: string header: PropsUIHeader - body: PropsUIPromptFileInput | PropsUIPromptConfirm | PropsUIPromptConsentForm | PropsUIPromptRadioInput + body: PropsUIPromptFileInput | PropsUIPromptConfirm | PropsUIPromptConsentForm | PropsUIPromptRadioInput | PropsUIPromptQuestionnaire } export function isPropsUIPageDonation (arg: any): arg is PropsUIPageDonation { return isInstanceOf(arg, 'PropsUIPageDonation', ['platform', 'header', 'body']) diff --git a/src/framework/types/prompts.ts b/src/framework/types/prompts.ts index 791caf95..2c239f03 100644 --- a/src/framework/types/prompts.ts +++ b/src/framework/types/prompts.ts @@ -1,5 +1,9 @@ import { isInstanceOf } from "../helpers" -import { PropsUIRadioItem, Text } from "./elements" +import { + PropsUIRadioItem, + PropsUIQuestionMultipleChoice, + Text +} from './elements' export type PropsUIPrompt = | PropsUIPromptFileInput @@ -8,7 +12,12 @@ export type PropsUIPrompt = | PropsUIPromptConfirm export function isPropsUIPrompt(arg: any): arg is PropsUIPrompt { - return isPropsUIPromptFileInput(arg) || isPropsUIPromptRadioInput(arg) || isPropsUIPromptConsentForm(arg) + return ( + isPropsUIPromptFileInput(arg) || + isPropsUIPromptRadioInput(arg) || + isPropsUIPromptConsentForm(arg) || + isPropsUIPromptQuestionnaire(arg) + ) } export interface PropsUIPromptConfirm { @@ -68,3 +77,12 @@ export function isPropsUIPromptConsentFormTable(arg: any): arg is PropsUIPromptC "data_frame", ]) } + + export interface PropsUIPromptQuestionnaire { + __type__: 'PropsUIPromptQuestionnaire' + questions: PropsUIQuestionMultipleChoice[] + description: Text + } + export function isPropsUIPromptQuestionnaire (arg: any): arg is PropsUIPromptQuestionnaire { + return isInstanceOf(arg, 'PropsUIPromptQuestionnaire', ['questions', 'description']) + } diff --git a/src/framework/visualisation/react/ui/elements/question_multiple_choice.tsx b/src/framework/visualisation/react/ui/elements/question_multiple_choice.tsx new file mode 100644 index 00000000..6e3b339a --- /dev/null +++ b/src/framework/visualisation/react/ui/elements/question_multiple_choice.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import { PropsUIQuestionMultipleChoice } from '../../../../types/elements' +import { Translator } from '../../../../translator' +import { ReactFactoryContext } from '../../factory' +import { Title3 } from './text' + +interface parentSetter { + parentSetter: (arg: any) => any +} + +type Props = PropsUIQuestionMultipleChoice & parentSetter & ReactFactoryContext + +export const MultipleChoiceQuestion = (props: Props): JSX.Element => { + const { question, choices, id, parentSetter, locale } = props + const [selectedChoice, setSelectedChoice] = React.useState(""); + const [checkedArray, setCheckedArray] = React.useState(Array(choices.length).fill(false)); + + const copy = prepareCopy(locale) + + const handleChoiceSelect = (choice: string, index: number) => { + setSelectedChoice(choice) + setCheckedArray(Array.from({ length: choices.length }, (_, i) => i === index)) + }; + + const setParentState = () => { + parentSetter((prevState: any) => { + prevState[id] = selectedChoice + return prevState + }) + } + + React.useEffect(() => { + setParentState() + }) + + return ( +
+ +
    + {copy.choices.map((choice, index) => ( +
  • + + {choice} +
  • + ))} +
+
+ ); + + function prepareCopy (locale: string): Copy { + return { + choices: choices.map((choice) => Translator.translate(choice, locale)), + question: Translator.translate(question, locale) + } + } +} + +interface Copy { + choices: string[] + question: string +} diff --git a/src/framework/visualisation/react/ui/elements/question_multiple_choice_checkbox.tsx b/src/framework/visualisation/react/ui/elements/question_multiple_choice_checkbox.tsx new file mode 100644 index 00000000..ad8e1251 --- /dev/null +++ b/src/framework/visualisation/react/ui/elements/question_multiple_choice_checkbox.tsx @@ -0,0 +1,79 @@ +import React from 'react' +import { PropsUIQuestionMultipleChoiceCheckbox } from '../../../../types/elements' +import { Translator } from '../../../../translator' +import { ReactFactoryContext } from '../../factory' +import {Title3 } from './text' + +interface parentSetter { + parentSetter: (arg: any) => any +} + +type Props = PropsUIQuestionMultipleChoiceCheckbox & parentSetter & ReactFactoryContext + +export const MultipleChoiceQuestionCheckbox = (props: Props): JSX.Element => { + const { question, choices, id, parentSetter, locale } = props + const [selectedChoices, setSelectedChoices] = React.useState([]); + + const copy = prepareCopy(locale) + + const setParentState = () => { + parentSetter((prevState: any) => { + prevState[id] = selectedChoices + return prevState + }) + } + + React.useEffect(() => { + setParentState() + }) + + + const handleChoiceSelect = (event: React.ChangeEvent) => { + const { value, checked } = event.target; + if (checked) { + setSelectedChoices((prevSelectedChoices) => [ + ...prevSelectedChoices, + value, + ]); + } else { + setSelectedChoices((prevSelectedChoices) => + prevSelectedChoices.filter((choice) => choice !== value) + ); + } + }; + + return ( +
+ +
    + {copy.choices.map((choice, index) => ( +
  • + +
  • + ))} +
+
+ ); + + function prepareCopy (locale: string): Copy { + return { + choices: choices.map((choice) => Translator.translate(choice, locale)), + question: Translator.translate(question, locale) + } + } +} + +interface Copy { + choices: string[] + question: string +} diff --git a/src/framework/visualisation/react/ui/elements/question_open.tsx b/src/framework/visualisation/react/ui/elements/question_open.tsx new file mode 100644 index 00000000..a874201c --- /dev/null +++ b/src/framework/visualisation/react/ui/elements/question_open.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import { PropsUIQuestionOpen } from '../../../../types/elements' +import { Translator } from '../../../../translator' +import { ReactFactoryContext } from '../../factory' + +import { Title3 } from './text' + +interface parentSetter { + parentSetter: (arg: any) => any +} + +type Props = PropsUIQuestionOpen & parentSetter & ReactFactoryContext + +export const OpenQuestion = (props: Props): JSX.Element => { + + const { question, id, parentSetter, locale } = props + const [userAnswer, setUserAnswer] = React.useState(""); + const copy = prepareCopy(locale) + + const handleInputChange = (event: React.ChangeEvent) => { + setUserAnswer(event.target.value); + }; + + const setParentState = () => { + parentSetter((prevState: any) => { + prevState[id] = userAnswer + return prevState + }) + } + + React.useEffect(() => { + setParentState() + }) + + return ( +
+ + +
+ ); + + function prepareCopy (locale: string): Copy { + return { + question: Translator.translate(question, locale) + } + } +} + +interface Copy { + question: string +} diff --git a/src/framework/visualisation/react/ui/pages/donation_page.tsx b/src/framework/visualisation/react/ui/pages/donation_page.tsx index becee83f..ddabd4f6 100644 --- a/src/framework/visualisation/react/ui/pages/donation_page.tsx +++ b/src/framework/visualisation/react/ui/pages/donation_page.tsx @@ -1,15 +1,21 @@ -import React from 'react' import { Weak } from '../../../../helpers' import TextBundle from '../../../../text_bundle' import { Translator } from '../../../../translator' import { Translatable } from '../../../../types/elements' import { PropsUIPageDonation } from '../../../../types/pages' -import { isPropsUIPromptConfirm, isPropsUIPromptConsentForm, isPropsUIPromptFileInput, isPropsUIPromptRadioInput } from '../../../../types/prompts' +import { + isPropsUIPromptConfirm, + isPropsUIPromptConsentForm, + isPropsUIPromptFileInput, + isPropsUIPromptRadioInput, + isPropsUIPromptQuestionnaire +} from '../../../../types/prompts' import { ReactFactoryContext } from '../../factory' import { Title1 } from '../elements/text' import { Confirm } from '../prompts/confirm' import { ConsentForm } from '../prompts/consent_form' import { FileInput } from '../prompts/file_input' +import { Questionnaire } from '../prompts/questionnaire' import { RadioInput } from '../prompts/radio_input' import { Page } from './templates/page' @@ -34,6 +40,9 @@ export const DonationPage = (props: Props): JSX.Element => { if (isPropsUIPromptRadioInput(body)) { return } + if (isPropsUIPromptQuestionnaire(body)) { + return + } throw new TypeError('Unknown body type') } diff --git a/src/framework/visualisation/react/ui/prompts/questionnaire.tsx b/src/framework/visualisation/react/ui/prompts/questionnaire.tsx new file mode 100644 index 00000000..a68727d9 --- /dev/null +++ b/src/framework/visualisation/react/ui/prompts/questionnaire.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { ReactFactoryContext } from '../../factory' +import { Weak } from '../../../../helpers' +import { LabelButton, PrimaryButton } from '../elements/button' +import { PropsUIPromptQuestionnaire } from '../../../../types/prompts' +import { Translator } from '../../../../translator' +import { isPropsUIQuestionMultipleChoice } from '../../../../types/elements' +import { isPropsUIQuestionMultipleChoiceCheckbox } from '../../../../types/elements' +import { isPropsUIQuestionOpen } from '../../../../types/elements' +import { MultipleChoiceQuestion } from '../../ui/elements/question_multiple_choice' +import { MultipleChoiceQuestionCheckbox } from '../../ui/elements/question_multiple_choice_checkbox' +import { OpenQuestion } from '../../ui/elements/question_open' + +type Props = Weak & ReactFactoryContext + +export const Questionnaire = (props: Props): JSX.Element => { + const { questions, description, resolve, locale } = props + const [answers, setAnswers] = React.useState<{}>({}); + const copy = prepareCopy(locale) + + + React.useEffect(() => { + // check if running in an iframe + if (window.frameElement) { + window.parent.scrollTo(0,0) + } else { + window.scrollTo(0,0) + } + }, []) + + function handleDonate (): void { + const value = JSON.stringify(answers) + resolve?.({ __type__: 'PayloadJSON', value }) + } + + function handleCancel (): void { + resolve?.({ __type__: 'PayloadFalse', value: false }) + } + + const renderQuestion = (item: any) => { + if (isPropsUIQuestionMultipleChoice(item)) { + return ( +
+ +
+ ) + } + if (isPropsUIQuestionMultipleChoiceCheckbox(item)) { + return ( +
+ +
+ ) + } + if (isPropsUIQuestionOpen(item)) { + return ( +
+ +
+ ) + } + } + + const renderQuestions = () => { + return questions.map((item) => renderQuestion(item)) + } + + return ( +
+
+ {copy.description} +
+
+ {renderQuestions()} +
+
+ + +
+
+ ); + + function prepareCopy (locale: string): Copy { + return { + description: Translator.translate(description, locale) + } + } +}; + + +interface Copy { + description: string +}