From 20c9ab0f65588175e2c9abb40dd57efb6de18594 Mon Sep 17 00:00:00 2001 From: hom3mad3 <8156337+hom3mad3@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:43:26 +0100 Subject: [PATCH] polls: refactor PollDetail components --- .../polls/static/PollDetail/CharCounter.jsx | 13 +- .../polls/static/PollDetail/ChoiceRow.jsx | 97 +++ .../polls/static/PollDetail/PollChoice.jsx | 96 +++ .../static/PollDetail/PollOpenQuestion.jsx | 61 +- .../polls/static/PollDetail/PollQuestion.jsx | 170 ----- .../polls/static/PollDetail/PollQuestions.jsx | 73 +- .../static/PollDetail/TextareaWithCounter.jsx | 54 ++ .../static/__tests__/CharCounter.jest.jsx | 2 +- .../polls/static/__tests__/ChoiceRow.jest.jsx | 97 +++ .../__tests__/EditPollOpenQuestion.jest.jsx | 4 +- .../__tests__/EditPollQuestion.jest.jsx | 8 +- .../static/__tests__/PollQuestion.jest.jsx | 130 ---- .../__snapshots__/CharCounter.jest.jsx.snap | 14 +- .../__snapshots__/PollQuestion.jest.jsx.snap | 649 ------------------ .../__tests__/__testdata__/QUESTION_OBJECT.js | 37 +- changelog/8462.md | 9 + 16 files changed, 476 insertions(+), 1038 deletions(-) create mode 100644 adhocracy4/polls/static/PollDetail/ChoiceRow.jsx create mode 100644 adhocracy4/polls/static/PollDetail/PollChoice.jsx delete mode 100644 adhocracy4/polls/static/PollDetail/PollQuestion.jsx create mode 100644 adhocracy4/polls/static/PollDetail/TextareaWithCounter.jsx create mode 100644 adhocracy4/polls/static/__tests__/ChoiceRow.jest.jsx delete mode 100644 adhocracy4/polls/static/__tests__/PollQuestion.jest.jsx delete mode 100644 adhocracy4/polls/static/__tests__/__snapshots__/PollQuestion.jest.jsx.snap create mode 100644 changelog/8462.md diff --git a/adhocracy4/polls/static/PollDetail/CharCounter.jsx b/adhocracy4/polls/static/PollDetail/CharCounter.jsx index b2434f295..e287b2a02 100644 --- a/adhocracy4/polls/static/PollDetail/CharCounter.jsx +++ b/adhocracy4/polls/static/PollDetail/CharCounter.jsx @@ -1,9 +1,16 @@ import React from 'react' +import django from 'django' -export const CharCounter = (props) => { - const current = props.value.length +const translated = { + characters: django.gettext('characters') +} + +export const CharCounter = ({ value, max, id }) => { + const current = value.length return ( - {current}/{props.max} + + {current}/{max} {translated.characters} + ) } diff --git a/adhocracy4/polls/static/PollDetail/ChoiceRow.jsx b/adhocracy4/polls/static/PollDetail/ChoiceRow.jsx new file mode 100644 index 000000000..c3d65582a --- /dev/null +++ b/adhocracy4/polls/static/PollDetail/ChoiceRow.jsx @@ -0,0 +1,97 @@ +import React, { useState, useEffect } from 'react' +import django from 'django' +import { TextareaWithCounter } from './TextareaWithCounter' + +const translated = { + other: django.gettext('other') +} + +const ChoiceInput = ({ + type, + choice, + checked, + onInputChange, + disabled +}) => ( + onInputChange(event, choice.is_other_choice)} + disabled={disabled} + aria-describedby={'textarea-with-counter-' + choice.id} + /> +) + +// eslint-disable-next-line react/display-name +export const ChoiceRow = React.memo(({ + choice, + checked, + onInputChange, + type, + disabled, + otherChoiceAnswer, + onOtherChange, + isAuthenticated, + isReadOnly, + errors +}) => { + const [textareaValue, setTextareaValue] = useState(otherChoiceAnswer) + const [showTextarea, setShowTextarea] = useState(false) + + // When the choice is selected or changed, update the textarea visibility + useEffect(() => { + if (checked && choice.is_other_choice) { + setShowTextarea(true) + } else { + setShowTextarea(false) + } + }, [checked, choice.is_other_choice]) + + const handleChange = (event, isOtherChoice) => { + // Update the checkbox/radio button state + onInputChange(event, isOtherChoice) + + // If the "Other" option is selected, show the textarea + if (isOtherChoice && event.target.checked) { + setShowTextarea(true) + } else { + setShowTextarea(false) + } + } + + const handleTextareaChange = (event) => { + // Preserve the value of the textarea even if options are changed + setTextareaValue(event.target.value) + onOtherChange(event) + } + + return ( + + ) +}) diff --git a/adhocracy4/polls/static/PollDetail/PollChoice.jsx b/adhocracy4/polls/static/PollDetail/PollChoice.jsx new file mode 100644 index 000000000..e79bc3669 --- /dev/null +++ b/adhocracy4/polls/static/PollDetail/PollChoice.jsx @@ -0,0 +1,96 @@ +import React, { useEffect, useState } from 'react' +import django from 'django' +import { ChoiceRow } from './ChoiceRow' + +const translated = { + multiple: django.gettext('Multiple answers are possible.') +} + +export const PollChoice = (props) => { + const getUserAnswer = () => { + const userAnswerId = props.question.other_choice_user_answer + const userAnswer = props.question.other_choice_answers.find(oc => oc.vote_id === userAnswerId) + return props.question.other_choice_answer + ? props.question.other_choice_answer + : ((userAnswerId && userAnswer) + ? userAnswer.answer + : '' + ) + } + + const [userChoices, setUserChoices] = useState([]) + const [otherChoiceAnswer, setOtherChoiceAnswer] = useState(getUserAnswer()) + const [errors, setErrors] = useState() + + const multiHelpText = props.question.multiple_choice ?
{translated.multiple}
: null + const questionHelpText = props.question.help_text ?
{props.question.help_text}
: null + const userAllowedVote = props.question.authenticated || props.allowUnregisteredUsers + + useEffect(() => { + setUserChoices(props.question.userChoices || []) + setErrors(props.errors) + }, [props.question.userChoices, props.errors]) + + const findOtherChoice = () => { + return props.question.choices.find(c => c.is_other_choice) + } + + const handleSingleChange = (event, isOther) => { + const choiceId = parseInt(event.target.value) + setUserChoices([choiceId]) + props.onSingleChange(props.question.id, choiceId) + if (!isOther) { + setOtherChoiceAnswer('') + props.onOtherChange(props.question.id, '', findOtherChoice()) + } + } + + const handleMultiChange = (event, isOther) => { + const choiceId = parseInt(event.target.value) + const newChoices = userChoices.includes(choiceId) + ? userChoices.filter(id => id !== choiceId) + : [...userChoices, choiceId] + + setUserChoices(newChoices) + props.onMultiChange(props.question.id, choiceId) + + if (!newChoices.includes(findOtherChoice()?.id)) { + setOtherChoiceAnswer('') + props.onOtherChange(props.question.id, '', findOtherChoice()) + } + } + + const handleOtherChange = (event) => { + const otherAnswer = event.target.value + setOtherChoiceAnswer(otherAnswer) + props.onOtherChange(props.question.id, otherAnswer) + } + + return ( +
+

{props.question.label}

+ {questionHelpText} + {multiHelpText} +
+ {props.question.choices.map((choice) => { + const checked = userChoices.indexOf(choice.id) !== -1 + return ( + + ) + })} +
+
+ ) +} diff --git a/adhocracy4/polls/static/PollDetail/PollOpenQuestion.jsx b/adhocracy4/polls/static/PollDetail/PollOpenQuestion.jsx index dc7d40e73..188a89ba4 100644 --- a/adhocracy4/polls/static/PollDetail/PollOpenQuestion.jsx +++ b/adhocracy4/polls/static/PollDetail/PollOpenQuestion.jsx @@ -1,48 +1,47 @@ import React, { useState } from 'react' -import { CharCounter } from './CharCounter' - -export const PollOpenQuestion = (props) => { - // | Function to define state +import { TextareaWithCounter } from './TextareaWithCounter' +export const PollOpenQuestion = ({ + question, + allowUnregisteredUsers, + onOpenChange, + errors +}) => { const getUserOpenAnswer = () => { - const userAnswerId = props.question.userAnswer - const userAnswer = props.question.answers.find(oa => oa.id === userAnswerId) - return props.question.open_answer - ? props.question.open_answer - : ((userAnswerId && userAnswer) - ? userAnswer.answer - : '' - ) + const userAnswerId = question.userAnswer + const userAnswer = question.answers.find((oa) => oa.id === userAnswerId) + return question.open_answer + ? question.open_answer + : userAnswerId && userAnswer + ? userAnswer.answer + : '' } const [userAnswer, setUserAnswer] = useState(getUserOpenAnswer()) - const questionHelpText = props.question.help_text ?
{props.question.help_text}
: null - const maxlength = 750 - const userAllowedVote = props.question.authenticated || props.allowUnregisteredUsers + const questionHelpText = question.help_text + ? ( +
{question.help_text}
+ ) + : null + const userAllowedVote = question.authenticated || allowUnregisteredUsers const handleOpenChange = (event) => { setUserAnswer(event.target.value) - props.onOpenChange(props.question.id, event.target.value) + onOpenChange(question.id, event.target.value) } return (
-

{props.question.label}

+

{question.label}

{questionHelpText} -
-