clean up block & question toggles for clarity

This commit is contained in:
Johannes
2025-11-18 12:16:16 +01:00
parent ce0a0573be
commit c50b46f715
15 changed files with 135 additions and 114 deletions
+1
View File
@@ -1373,6 +1373,7 @@ checksums:
environments/surveys/edit/logic_error_warning: 542fbb918ffdb29e6f9a4a6196ffb558
environments/surveys/edit/logic_error_warning_text: f2afad8852a95ed169a39959efbf592c
environments/surveys/edit/long_answer: 3a97f8d2e90aba6e679917a0c5670c53
environments/surveys/edit/long_answer_toggle_description: 86bcdfeb74d9825c2f2d5a215e92d111
environments/surveys/edit/lower_label: 45985bca022d4370bd6e013af75d5160
environments/surveys/edit/manage_languages: 9c56d5afee8a73dfc283a452470f3a10
environments/surveys/edit/matrix_all_fields: 187240509163b2f52a400a565e57c67f
+1
View File
@@ -1458,6 +1458,7 @@
"logic_error_warning": "Änderungen werden zu Logikfehlern führen",
"logic_error_warning_text": "Das Ändern des Fragetypen entfernt die Logikbedingungen von dieser Frage",
"long_answer": "Lange Antwort",
"long_answer_toggle_description": "Ermöglichen Sie den Befragten, längere Antworten über mehrere Zeilen zu schreiben.",
"lower_label": "Unteres Label",
"manage_languages": "Sprachen verwalten",
"matrix_all_fields": "Alle Felder",
+1
View File
@@ -1458,6 +1458,7 @@
"logic_error_warning": "Changing will cause logic errors",
"logic_error_warning_text": "Changing the question type will remove the logic conditions from this question",
"long_answer": "Long answer",
"long_answer_toggle_description": "Allow respondents to write longer, multi-line answers.",
"lower_label": "Lower Label",
"manage_languages": "Manage Languages",
"matrix_all_fields": "All fields",
+1
View File
@@ -1458,6 +1458,7 @@
"logic_error_warning": "Changer causera des erreurs logiques",
"logic_error_warning_text": "Changer le type de question supprimera les conditions logiques de cette question.",
"long_answer": "Longue réponse",
"long_answer_toggle_description": "Permettre aux répondants d'écrire des réponses plus longues et sur plusieurs lignes.",
"lower_label": "Étiquette inférieure",
"manage_languages": "Gérer les langues",
"matrix_all_fields": "Tous les champs",
+1
View File
@@ -1458,6 +1458,7 @@
"logic_error_warning": "変更するとロジックエラーが発生します",
"logic_error_warning_text": "質問の種類を変更すると、この質問のロジック条件が削除されます",
"long_answer": "長文回答",
"long_answer_toggle_description": "回答者が長文の複数行の回答を書けるようにします。",
"lower_label": "下限ラベル",
"manage_languages": "言語を管理",
"matrix_all_fields": "すべてのフィールド",
+1
View File
@@ -1458,6 +1458,7 @@
"logic_error_warning": "Mudar vai causar erros de lógica",
"logic_error_warning_text": "Mudar o tipo de pergunta vai remover as condições lógicas dessa pergunta",
"long_answer": "resposta longa",
"long_answer_toggle_description": "Permitir que os respondentes escrevam respostas mais longas e com várias linhas.",
"lower_label": "Etiqueta Inferior",
"manage_languages": "Gerenciar Idiomas",
"matrix_all_fields": "Todos os campos",
+1
View File
@@ -1458,6 +1458,7 @@
"logic_error_warning": "A alteração causará erros de lógica",
"logic_error_warning_text": "Alterar o tipo de pergunta irá remover as condições lógicas desta pergunta",
"long_answer": "Resposta longa",
"long_answer_toggle_description": "Permitir que os inquiridos escrevam respostas mais longas e com várias linhas.",
"lower_label": "Etiqueta Inferior",
"manage_languages": "Gerir Idiomas",
"matrix_all_fields": "Todos os campos",
+1
View File
@@ -1458,6 +1458,7 @@
"logic_error_warning": "Schimbarea va provoca erori de logică",
"logic_error_warning_text": "Schimbarea tipului de întrebare va elimina condițiile de logică din această întrebare",
"long_answer": "Răspuns lung",
"long_answer_toggle_description": "Permite respondenților să scrie răspunsuri mai lungi, pe mai multe rânduri.",
"lower_label": "Etichetă inferioară",
"manage_languages": "Gestionați limbile",
"matrix_all_fields": "Toate câmpurile",
+1
View File
@@ -1458,6 +1458,7 @@
"logic_error_warning": "更改 将 导致 逻辑 错误",
"logic_error_warning_text": "更改问题类型 会 移除 此问题 的 逻辑条件",
"long_answer": "长答案",
"long_answer_toggle_description": "允许受访者填写较长的多行答案。",
"lower_label": "下限标签",
"manage_languages": "管理 语言",
"matrix_all_fields": "所有字段",
+1
View File
@@ -1458,6 +1458,7 @@
"logic_error_warning": "變更將導致邏輯錯誤",
"logic_error_warning_text": "變更問題類型將會從此問題中移除邏輯條件",
"long_answer": "長回答",
"long_answer_toggle_description": "允許受訪者撰寫較長的多行回答。",
"lower_label": "下標籤",
"manage_languages": "管理語言",
"matrix_all_fields": "所有欄位",
@@ -25,6 +25,7 @@ import { Button } from "@/modules/ui/components/button";
import { FileInput } from "@/modules/ui/components/file-input";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch";
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
import {
determineImageUploaderVisibility,
@@ -310,6 +311,60 @@ export const QuestionFormInput = ({
return false;
};
const getIsRequiredToggleDisabled = (): boolean => {
if (!question) return false;
if (question.type === TSurveyElementTypeEnum.Address) {
const allFieldsAreOptional = [
question.addressLine1,
question.addressLine2,
question.city,
question.state,
question.zip,
question.country,
]
.filter((field) => field.show)
.every((field) => !field.required);
if (allFieldsAreOptional) {
return true;
}
return [
question.addressLine1,
question.addressLine2,
question.city,
question.state,
question.zip,
question.country,
]
.filter((field) => field.show)
.some((condition) => condition.required === true);
}
if (question.type === TSurveyElementTypeEnum.ContactInfo) {
const allFieldsAreOptional = [
question.firstName,
question.lastName,
question.email,
question.phone,
question.company,
]
.filter((field) => field.show)
.every((field) => !field.required);
if (allFieldsAreOptional) {
return true;
}
return [question.firstName, question.lastName, question.email, question.phone, question.company]
.filter((field) => field.show)
.some((condition) => condition.required === true);
}
return false;
};
const useRichTextEditor = id === "headline" || id === "subheader" || id === "html";
// For rich text editor fields, we need either updateQuestion or updateSurvey
@@ -321,8 +376,23 @@ export const QuestionFormInput = ({
return (
<div className="w-full">
{label && (
<div className="mb-2 mt-3">
<div className="mb-2 mt-3 flex items-center justify-between">
<Label htmlFor={id}>{label}</Label>
{id === "headline" && question && updateQuestion && (
<div className="flex items-center space-x-2">
<Label htmlFor="required-toggle" className="text-sm">
{t("environments.surveys.edit.required")}
</Label>
<Switch
id="required-toggle"
checked={question.required}
disabled={getIsRequiredToggleDisabled()}
onCheckedChange={(checked) => {
updateQuestion(questionIdx, { required: checked });
}}
/>
</div>
)}
</div>
)}
<div className="flex flex-col gap-4" ref={animationParent}>
@@ -376,7 +446,9 @@ export const QuestionFormInput = ({
</div>
{id === "headline" && !isWelcomeCard && (
<TooltipRenderer tooltipContent={t("environments.surveys.edit.add_photo_or_video")}>
<TooltipRenderer
tooltipContent={t("environments.surveys.edit.add_photo_or_video")}
delayDuration={100}>
<Button
variant="secondary"
size="icon"
@@ -434,8 +506,23 @@ export const QuestionFormInput = ({
return (
<div className="w-full">
{label && (
<div className="mb-2 mt-3">
<div className="mb-2 mt-3 flex items-center justify-between">
<Label htmlFor={id}>{label}</Label>
{id === "headline" && question && updateQuestion && (
<div className="flex items-center space-x-2">
<Label htmlFor="required-toggle" className="text-sm">
{t("environments.surveys.edit.required")}
</Label>
<Switch
id="required-toggle"
checked={question.required}
disabled={getIsRequiredToggleDisabled()}
onCheckedChange={(checked) => {
updateQuestion(questionIdx, { required: checked });
}}
/>
</div>
)}
</div>
)}
<MultiLangWrapper
@@ -38,8 +38,6 @@ import { RatingQuestionForm } from "@/modules/survey/editor/components/rating-qu
import { formatTextWithSlashes } from "@/modules/survey/editor/lib/utils";
import { getQuestionIconMap, getTSurveyQuestionTypeEnumName } from "@/modules/survey/lib/questions";
import { Alert, AlertButton, AlertTitle } from "@/modules/ui/components/alert";
import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch";
interface BlockCardProps {
localSurvey: TSurvey;
@@ -214,62 +212,6 @@ export const BlockCard = ({
const isInvalid = invalidQuestions ? invalidQuestions.includes(element.id) : false;
const open = activeQuestionId === element.id;
const getIsRequiredToggleDisabled = (): boolean => {
if (element.type === TSurveyElementTypeEnum.Address) {
const allFieldsAreOptional = [
element.addressLine1,
element.addressLine2,
element.city,
element.state,
element.zip,
element.country,
]
.filter((field) => field.show)
.every((field) => !field.required);
if (allFieldsAreOptional) {
return true;
}
return [
element.addressLine1,
element.addressLine2,
element.city,
element.state,
element.zip,
element.country,
]
.filter((field) => field.show)
.some((condition) => condition.required === true);
}
if (element.type === TSurveyElementTypeEnum.ContactInfo) {
const allFieldsAreOptional = [
element.firstName,
element.lastName,
element.email,
element.phone,
element.company,
]
.filter((field) => field.show)
.every((field) => !field.required);
if (allFieldsAreOptional) {
return true;
}
return [element.firstName, element.lastName, element.email, element.phone, element.company]
.filter((field) => field.show)
.some((condition) => condition.required === true);
}
return false;
};
const handleRequiredToggle = () => {
updateQuestion(questionIdx, { required: !element.required });
};
return (
<div key={element.id} className={cn(elementIndex > 0 && "border-t border-slate-200")}>
<Collapsible.Root
@@ -608,46 +550,24 @@ export const BlockCard = ({
</Collapsible.Root>
</div>
</Collapsible.CollapsibleContent>
{open && (
<div className="mx-4 flex justify-end space-x-6 border-t border-slate-200">
{element.type === "openText" && (
<div className="my-4 flex items-center justify-end space-x-2">
<Label htmlFor="longAnswer">{t("environments.surveys.edit.long_answer")}</Label>
<Switch
id="longAnswer"
disabled={element.inputType !== "text"}
checked={element.longAnswer !== false}
onClick={(e) => {
e.stopPropagation();
updateQuestion(questionIdx, {
longAnswer:
typeof element.longAnswer === "undefined" ? false : !element.longAnswer,
});
}}
/>
</div>
)}
{
<div className="my-4 flex items-center justify-end space-x-2">
<Label htmlFor="required-toggle">{t("environments.surveys.edit.required")}</Label>
<Switch
id="required-toggle"
checked={element.required}
disabled={getIsRequiredToggleDisabled()}
onClick={(e) => {
e.stopPropagation();
handleRequiredToggle();
}}
/>
</div>
}
</div>
)}
</Collapsible.Root>
</div>
);
})}
<hr className="mb-4 border-dashed border-slate-200" />
{/* Add Question to Block button */}
<div className="p-4 pt-0">
<AddQuestionToBlockButton
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
block={block}
project={project}
isCxMode={isCxMode}
/>
</div>
<hr className="border-dashed border-slate-200" />
{/* Block Settings */}
<div className="p-4">
@@ -665,18 +585,6 @@ export const BlockCard = ({
isLastBlock={blockIdx === totalBlocks - 1}
/>
</div>
{/* Add Question to Block button */}
<div className="p-4 pt-0">
<AddQuestionToBlockButton
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
block={block}
project={project}
isCxMode={isCxMode}
/>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.Root>
</div>
@@ -189,7 +189,7 @@ export const FileUploadQuestionForm = ({
</Button>
)}
</div>
<div className="mb-8 mt-6 space-y-6">
<div className="mt-6 space-y-6">
<AdvancedOptionToggle
isChecked={question.allowMultipleFiles}
onToggle={() => updateQuestion(questionIdx, { allowMultipleFiles: !question.allowMultipleFiles })}
@@ -167,7 +167,7 @@ export const OpenQuestionForm = ({
/>
</div>
</div>
<div className="mt-3">
<div className="mt-6 space-y-6">
{showCharLimits && (
<AdvancedOptionToggle
isChecked={isCharLimitEnabled}
@@ -230,6 +230,21 @@ export const OpenQuestionForm = ({
</div>
</AdvancedOptionToggle>
)}
<div className="mt-4">
<AdvancedOptionToggle
isChecked={question.longAnswer !== false}
onToggle={(checked: boolean) => {
updateQuestion(questionIdx, {
longAnswer: checked,
});
}}
htmlId="longAnswer"
title={t("environments.surveys.edit.long_answer")}
description={t("environments.surveys.edit.long_answer_toggle_description")}
disabled={question.inputType !== "text"}
customContainerClass="p-0"
/>
</div>
</div>
</form>
);
@@ -32,7 +32,7 @@ const TooltipContent: React.ComponentType<TooltipPrimitive.TooltipContentProps>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
interface TooltipRendererProps {
tooltipContent: ReactNode;
@@ -40,12 +40,13 @@ interface TooltipRendererProps {
className?: string;
triggerClass?: string;
shouldRender?: boolean;
delayDuration?: number;
}
export const TooltipRenderer = (props: TooltipRendererProps) => {
const { children, shouldRender = true, tooltipContent, className, triggerClass } = props;
const { children, shouldRender = true, tooltipContent, className, triggerClass, delayDuration = 0 } = props;
if (shouldRender) {
return (
<TooltipProvider delayDuration={0}>
<TooltipProvider delayDuration={delayDuration}>
<Tooltip>
<TooltipTrigger asChild>
<span className={triggerClass}>{children}</span>