mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-01 21:22:38 -05:00
Compare commits
1 Commits
fix/sentry
...
rip-out-ol
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8baeb7860 |
@@ -1,100 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TI18nString } from "@formbricks/types/i18n";
|
||||
import { TSurvey, TSurveyRecallItem } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { getEnabledLanguages } from "@/lib/i18n/utils";
|
||||
import { headlineToRecall, recallToHeadline } from "@/lib/utils/recall";
|
||||
import { LanguageIndicator } from "@/modules/survey/multi-language-surveys/components/language-indicator";
|
||||
|
||||
interface MultiLangWrapperRenderProps {
|
||||
value: TI18nString;
|
||||
onChange: (value: string, recallItems?: TSurveyRecallItem[], fallbacks?: { [key: string]: string }) => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
interface MultiLangWrapperProps {
|
||||
isTranslationIncomplete: boolean;
|
||||
value: TI18nString;
|
||||
onChange: (value: TI18nString) => void;
|
||||
localSurvey: TSurvey;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (code: string) => void;
|
||||
locale: TUserLocale;
|
||||
render: (props: MultiLangWrapperRenderProps) => ReactNode;
|
||||
}
|
||||
|
||||
export const MultiLangWrapper = ({
|
||||
isTranslationIncomplete,
|
||||
value,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
locale,
|
||||
render,
|
||||
onChange,
|
||||
}: MultiLangWrapperProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const defaultLanguageCode =
|
||||
localSurvey.languages.filter((lang) => lang.default)[0]?.language.code ?? "default";
|
||||
const usedLanguageCode = selectedLanguageCode === defaultLanguageCode ? "default" : selectedLanguageCode;
|
||||
|
||||
const enabledLanguages = useMemo(
|
||||
() => getEnabledLanguages(localSurvey.languages ?? []),
|
||||
[localSurvey.languages]
|
||||
);
|
||||
|
||||
const handleChange = (
|
||||
newValue: string,
|
||||
recallItems?: TSurveyRecallItem[],
|
||||
fallbacks?: { [key: string]: string }
|
||||
) => {
|
||||
const updatedValue = {
|
||||
...value,
|
||||
[usedLanguageCode]:
|
||||
recallItems && fallbacks ? headlineToRecall(newValue, recallItems, fallbacks) : newValue,
|
||||
};
|
||||
onChange(updatedValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div>
|
||||
{render({
|
||||
value,
|
||||
onChange: handleChange,
|
||||
children:
|
||||
enabledLanguages.length > 1 ? (
|
||||
<LanguageIndicator
|
||||
selectedLanguageCode={usedLanguageCode}
|
||||
surveyLanguages={localSurvey.languages}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
locale={locale}
|
||||
/>
|
||||
) : null,
|
||||
})}
|
||||
</div>
|
||||
|
||||
{enabledLanguages.length > 1 && (
|
||||
<>
|
||||
{usedLanguageCode !== "default" && value && typeof value["default"] !== "undefined" && (
|
||||
<div className="mt-1 text-xs text-slate-500">
|
||||
<strong>{t("environments.workspace.languages.translate")}:</strong>{" "}
|
||||
{getTextContent(recallToHeadline(value, localSurvey, false, "default")["default"] ?? "")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{usedLanguageCode === "default" && localSurvey.languages?.length > 1 && isTranslationIncomplete && (
|
||||
<div className="mt-1 text-xs text-red-400">
|
||||
{t("environments.workspace.languages.incomplete_translations")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -17,15 +17,16 @@ import {
|
||||
TSurveyRedirectUrlCard,
|
||||
TSurveyWelcomeCard,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { isValidHTML } from "@formbricks/types/surveys/validation";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { createI18nString, extractLanguageCodes } from "@/lib/i18n/utils";
|
||||
import { md } from "@/lib/markdownIt";
|
||||
import { useSyncScroll } from "@/lib/utils/hooks/useSyncScroll";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { MultiLangWrapper } from "@/modules/survey/components/element-form-input/components/multi-lang-wrapper";
|
||||
import { headlineToRecall, recallToHeadline } from "@/lib/utils/recall";
|
||||
import { RecallWrapper } from "@/modules/survey/components/element-form-input/components/recall-wrapper";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { LocalizedEditor } from "@/modules/survey/multi-language-surveys/components/localized-editor";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Editor } from "@/modules/ui/components/editor";
|
||||
import { FileInput } from "@/modules/ui/components/file-input";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
@@ -39,7 +40,6 @@ import {
|
||||
getMatrixLabel,
|
||||
getPlaceHolderById,
|
||||
getWelcomeCardText,
|
||||
isValueIncomplete,
|
||||
} from "./utils";
|
||||
|
||||
interface ElementFormInputProps {
|
||||
@@ -54,8 +54,8 @@ interface ElementFormInputProps {
|
||||
updateChoice?: (choiceIdx: number, data: Partial<TSurveyElementChoice>) => void;
|
||||
updateMatrixLabel?: (index: number, type: "row" | "column", matrixLabel: TI18nString) => void;
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
selectedLanguageCode?: string;
|
||||
setSelectedLanguageCode?: (languageCode: string) => void;
|
||||
label: string;
|
||||
maxLength?: number;
|
||||
placeholder?: string;
|
||||
@@ -80,14 +80,14 @@ export const ElementFormInput = ({
|
||||
updateChoice,
|
||||
updateMatrixLabel,
|
||||
isInvalid,
|
||||
selectedLanguageCode: _selectedLanguageCode,
|
||||
setSelectedLanguageCode: _setSelectedLanguageCode,
|
||||
label,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
maxLength,
|
||||
placeholder,
|
||||
onBlur,
|
||||
className,
|
||||
locale,
|
||||
locale: _locale,
|
||||
onKeyDown,
|
||||
isStorageConfigured = true,
|
||||
autoFocus,
|
||||
@@ -96,9 +96,7 @@ export const ElementFormInput = ({
|
||||
isExternalUrlsAllowed,
|
||||
}: ElementFormInputProps) => {
|
||||
const { t } = useTranslation();
|
||||
const defaultLanguageCode =
|
||||
localSurvey.languages.filter((lang) => lang.default)[0]?.language.code ?? "default";
|
||||
const usedLanguageCode = selectedLanguageCode === defaultLanguageCode ? "default" : selectedLanguageCode;
|
||||
const usedLanguageCode = "default";
|
||||
|
||||
const elements = useMemo(() => getElementsFromBlocks(localSurvey.blocks), [localSurvey.blocks]);
|
||||
|
||||
@@ -106,20 +104,21 @@ export const ElementFormInput = ({
|
||||
const isChoice = id.includes("choice");
|
||||
const isMatrixLabelRow = id.includes("row");
|
||||
const isMatrixLabelColumn = id.includes("column");
|
||||
const inputId = useMemo(() => {
|
||||
return isChoice || isMatrixLabelColumn || isMatrixLabelRow ? id.split("-")[0] : id;
|
||||
}, [id, isChoice, isMatrixLabelColumn, isMatrixLabelRow]);
|
||||
|
||||
const isEndingCard = elementIdx >= elements.length;
|
||||
const isWelcomeCard = elementIdx === -1;
|
||||
const index = getIndex(id, isChoice || isMatrixLabelColumn || isMatrixLabelRow);
|
||||
|
||||
const elementId = useMemo(() => {
|
||||
return isWelcomeCard
|
||||
? "start"
|
||||
: isEndingCard
|
||||
? localSurvey.endings[elementIdx - elements.length].id
|
||||
: currentElement.id;
|
||||
if (isWelcomeCard) {
|
||||
return "start";
|
||||
}
|
||||
|
||||
if (isEndingCard) {
|
||||
return localSurvey.endings[elementIdx - elements.length].id;
|
||||
}
|
||||
|
||||
return currentElement.id;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isWelcomeCard, isEndingCard, currentElement?.id]);
|
||||
const endingCard = localSurvey.endings.find((ending) => ending.id === elementId);
|
||||
@@ -128,11 +127,6 @@ export const ElementFormInput = ({
|
||||
() => extractLanguageCodes(localSurvey.languages),
|
||||
[localSurvey.languages]
|
||||
);
|
||||
const isTranslationIncomplete = useMemo(
|
||||
() => isValueIncomplete(inputId, isInvalid, surveyLanguageCodes, value),
|
||||
[value, inputId, isInvalid, surveyLanguageCodes]
|
||||
);
|
||||
|
||||
const elementText = useMemo((): TI18nString => {
|
||||
if (isChoice && typeof index === "number") {
|
||||
return getChoiceLabel(currentElement, index, surveyLanguageCodes);
|
||||
@@ -293,14 +287,14 @@ export const ElementFormInput = ({
|
||||
const getFileUrl = (): string | undefined => {
|
||||
if (isWelcomeCard) return localSurvey.welcomeCard.fileUrl;
|
||||
if (isEndingCard) {
|
||||
if (endingCard && endingCard.type === "endScreen") return endingCard.imageUrl;
|
||||
if (endingCard?.type === "endScreen") return endingCard.imageUrl;
|
||||
} else return currentElement.imageUrl;
|
||||
};
|
||||
|
||||
const getVideoUrl = (): string | undefined => {
|
||||
if (isWelcomeCard) return localSurvey.welcomeCard.videoUrl;
|
||||
if (isEndingCard) {
|
||||
if (endingCard && endingCard.type === "endScreen") return endingCard.videoUrl;
|
||||
if (endingCard?.type === "endScreen") return endingCard.videoUrl;
|
||||
} else return currentElement.videoUrl;
|
||||
};
|
||||
|
||||
@@ -446,24 +440,43 @@ export const ElementFormInput = ({
|
||||
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<div className="flex-1">
|
||||
<LocalizedEditor
|
||||
key={`${elementId}-${id}-${selectedLanguageCode}`}
|
||||
<Editor
|
||||
id={id}
|
||||
value={value}
|
||||
localSurvey={localSurvey}
|
||||
elementIdx={elementIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateElement={(isWelcomeCard || isEndingCard ? updateSurvey : updateElement)!}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
disableLists
|
||||
excludedToolbarItems={["blockType"]}
|
||||
firstRender={firstRender}
|
||||
setFirstRender={setFirstRender}
|
||||
locale={locale}
|
||||
elementId={elementId}
|
||||
isCard={isWelcomeCard || isEndingCard}
|
||||
autoFocus={autoFocus}
|
||||
getText={() => {
|
||||
const text = value ? (value.default ?? "") : "";
|
||||
let html = md.render(text);
|
||||
if (id === "headline" && text && !isValidHTML(text)) {
|
||||
html = html.replaceAll(/<p>([\s\S]*?)<\/p>/g, "<p><strong>$1</strong></p>");
|
||||
}
|
||||
return html;
|
||||
}}
|
||||
key={`${elementId}-${id}-default`}
|
||||
setFirstRender={setFirstRender}
|
||||
setText={(editorValue: string) => {
|
||||
if (suppressEditorUpdatesRef.current) return;
|
||||
const sanitizedContent = isExternalUrlsAllowed
|
||||
? editorValue
|
||||
: editorValue.replaceAll(/<a[^>]*>(.*?)<\/a>/gi, "$1");
|
||||
const translatedContent = {
|
||||
...value,
|
||||
default: sanitizedContent,
|
||||
};
|
||||
|
||||
if (isWelcomeCard || isEndingCard) {
|
||||
(updateSurvey as any)?.({ [id]: translatedContent });
|
||||
return;
|
||||
}
|
||||
|
||||
updateElement?.(elementIdx, { [id]: translatedContent });
|
||||
}}
|
||||
localSurvey={localSurvey}
|
||||
elementId={elementId}
|
||||
selectedLanguageCode="default"
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
suppressUpdates={() => suppressEditorUpdatesRef.current}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -532,98 +545,68 @@ export const ElementFormInput = ({
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
</div>
|
||||
)}
|
||||
<MultiLangWrapper
|
||||
isTranslationIncomplete={isTranslationIncomplete}
|
||||
value={text}
|
||||
<RecallWrapper
|
||||
localSurvey={localSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
locale={locale}
|
||||
key={selectedLanguageCode}
|
||||
onChange={(updatedText) => {
|
||||
elementId={elementId}
|
||||
value={text[usedLanguageCode]}
|
||||
onChange={(updatedValue, recallItems, fallbacks) => {
|
||||
const updatedText =
|
||||
recallItems && fallbacks
|
||||
? { ...text, [usedLanguageCode]: headlineToRecall(updatedValue, recallItems, fallbacks) }
|
||||
: { ...text, [usedLanguageCode]: updatedValue };
|
||||
setText(updatedText);
|
||||
debouncedHandleUpdate(updatedText[usedLanguageCode]);
|
||||
}}
|
||||
render={({ value, onChange, children: languageIndicator }) => {
|
||||
onAddFallback={() => {
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
isRecallAllowed={false}
|
||||
usedLanguageCode={usedLanguageCode}
|
||||
render={({ value, onChange, highlightedJSX, children: recallComponents, isRecallSelectVisible }) => {
|
||||
return (
|
||||
<RecallWrapper
|
||||
localSurvey={localSurvey}
|
||||
elementId={elementId}
|
||||
value={value[usedLanguageCode]}
|
||||
onChange={(value, recallItems, fallbacks) => {
|
||||
// Pass all values to MultiLangWrapper's onChange
|
||||
onChange(value, recallItems, fallbacks);
|
||||
}}
|
||||
onAddFallback={() => {
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
isRecallAllowed={false}
|
||||
usedLanguageCode={usedLanguageCode}
|
||||
render={({
|
||||
value,
|
||||
onChange,
|
||||
highlightedJSX,
|
||||
children: recallComponents,
|
||||
isRecallSelectVisible,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 bg-white" ref={animationParent}>
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
<div className="group relative w-full">
|
||||
{languageIndicator}
|
||||
{/* The highlight container is absolutely positioned behind the input */}
|
||||
<div className="h-10 w-full"></div>
|
||||
<div
|
||||
ref={highlightContainerRef}
|
||||
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent ${
|
||||
localSurvey.languages?.length > 1 ? "pr-24" : ""
|
||||
}`}
|
||||
dir="auto"
|
||||
key={highlightedJSX.toString()}>
|
||||
{highlightedJSX}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
key={`${elementId}-${id}-${usedLanguageCode}`}
|
||||
value={
|
||||
recallToHeadline(
|
||||
{
|
||||
[usedLanguageCode]: value,
|
||||
},
|
||||
localSurvey,
|
||||
false,
|
||||
usedLanguageCode
|
||||
)[usedLanguageCode]
|
||||
}
|
||||
dir="auto"
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
id={id}
|
||||
name={id}
|
||||
placeholder={placeholder ?? getPlaceHolderById(id, t)}
|
||||
aria-label={label}
|
||||
maxLength={maxLength}
|
||||
ref={inputRef}
|
||||
onBlur={onBlur}
|
||||
className={`absolute top-0 text-black caret-black ${
|
||||
localSurvey.languages?.length > 1 ? "pr-24" : ""
|
||||
} ${className}`}
|
||||
isInvalid={
|
||||
isInvalid &&
|
||||
text[usedLanguageCode]?.trim() === "" &&
|
||||
localSurvey.languages?.length > 1 &&
|
||||
isTranslationIncomplete
|
||||
}
|
||||
autoComplete={isRecallSelectVisible ? "off" : "on"}
|
||||
autoFocus={false}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
{recallComponents}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 bg-white" ref={animationParent}>
|
||||
<div className="flex w-full items-center space-x-2">
|
||||
<div className="group relative w-full">
|
||||
<div className="h-10 w-full"></div>
|
||||
<div
|
||||
ref={highlightContainerRef}
|
||||
className="no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent"
|
||||
dir="auto"
|
||||
key={`${elementId}-${id}-highlight`}>
|
||||
{highlightedJSX}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
key={`${elementId}-${id}-${usedLanguageCode}`}
|
||||
value={
|
||||
recallToHeadline(
|
||||
{
|
||||
[usedLanguageCode]: value,
|
||||
},
|
||||
localSurvey,
|
||||
false,
|
||||
usedLanguageCode
|
||||
)[usedLanguageCode]
|
||||
}
|
||||
dir="auto"
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
id={id}
|
||||
name={id}
|
||||
placeholder={placeholder ?? getPlaceHolderById(id, t)}
|
||||
aria-label={label}
|
||||
maxLength={maxLength}
|
||||
ref={inputRef}
|
||||
onBlur={onBlur}
|
||||
className={`absolute top-0 text-black caret-black ${className}`}
|
||||
isInvalid={isInvalid && text[usedLanguageCode]?.trim() === ""}
|
||||
autoComplete={isRecallSelectVisible ? "off" : "on"}
|
||||
autoFocus={false}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
{recallComponents}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -59,8 +59,6 @@ interface BlockCardProps {
|
||||
setActiveElementId: (elementId: string | null) => void;
|
||||
lastElement: boolean;
|
||||
lastElementIndex: number;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
invalidElements?: string[];
|
||||
addElement: (element: any, index?: number) => void;
|
||||
isFormbricksCloud: boolean;
|
||||
@@ -95,8 +93,6 @@ export const BlockCard = ({
|
||||
setActiveElementId,
|
||||
lastElement,
|
||||
lastElementIndex,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
invalidElements,
|
||||
addElement,
|
||||
isFormbricksCloud,
|
||||
@@ -136,11 +132,10 @@ export const BlockCard = ({
|
||||
const [elementsParent] = useAutoAnimate();
|
||||
|
||||
const getElementHeadline = (
|
||||
element: TSurveyElement,
|
||||
languageCode: string
|
||||
element: TSurveyElement
|
||||
): (string | React.ReactElement)[] | string | undefined => {
|
||||
const headlineData = recallToHeadline(element.headline, localSurvey, true, languageCode);
|
||||
const headlineText = headlineData[languageCode];
|
||||
const headlineData = recallToHeadline(element.headline, localSurvey, true, "default");
|
||||
const headlineText = headlineData.default;
|
||||
if (headlineText) {
|
||||
return formatTextWithSlashes(getTextContent(headlineText ?? ""));
|
||||
}
|
||||
@@ -168,8 +163,8 @@ export const BlockCard = ({
|
||||
element,
|
||||
elementIdx,
|
||||
updateElement,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
selectedLanguageCode: "default",
|
||||
setSelectedLanguageCode: () => {},
|
||||
isInvalid: invalidElements ? invalidElements.includes(element.id) : false,
|
||||
locale,
|
||||
isStorageConfigured,
|
||||
@@ -344,9 +339,7 @@ export const BlockCard = ({
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
<h3 className="text-sm font-semibold">
|
||||
{getElementHeadline(element, selectedLanguageCode)}
|
||||
</h3>
|
||||
<h3 className="text-sm font-semibold">{getElementHeadline(element)}</h3>
|
||||
{!isOpen && element.type !== TSurveyElementTypeEnum.CTA && (
|
||||
<p className="mt-1 truncate text-xs text-slate-500">
|
||||
{element?.required
|
||||
@@ -427,7 +420,7 @@ export const BlockCard = ({
|
||||
updateElement={updateElement}
|
||||
updateBlockLogic={updateBlockLogic}
|
||||
updateBlockLogicFallback={updateBlockLogicFallback}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
selectedLanguageCode="default"
|
||||
/>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
@@ -460,8 +453,7 @@ export const BlockCard = ({
|
||||
localSurvey={localSurvey}
|
||||
block={block}
|
||||
blockIndex={blockIdx}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
selectedLanguageCode="default"
|
||||
updateBlockButtonLabel={updateBlockButtonLabel}
|
||||
updateBlockLogic={updateBlockLogic}
|
||||
updateBlockLogicFallback={updateBlockLogicFallback}
|
||||
|
||||
@@ -17,7 +17,6 @@ interface BlockSettingsProps {
|
||||
block: TSurveyBlock;
|
||||
blockIndex: number;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
updateBlockButtonLabel: (
|
||||
blockIndex: number,
|
||||
labelKey: "buttonLabel" | "backButtonLabel",
|
||||
@@ -35,7 +34,6 @@ export const BlockSettings = ({
|
||||
block,
|
||||
blockIndex,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
updateBlockButtonLabel,
|
||||
updateBlockLogic,
|
||||
updateBlockLogicFallback,
|
||||
@@ -98,7 +96,6 @@ export const BlockSettings = ({
|
||||
}
|
||||
}}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
placeholder={t("common.back")}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
@@ -140,7 +137,6 @@ export const BlockSettings = ({
|
||||
}
|
||||
}}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
placeholder={t("common.next")}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
|
||||
@@ -25,8 +25,6 @@ interface BlocksDroppableProps {
|
||||
duplicateElement: (elementIdx: number) => void;
|
||||
activeElementId: string | null;
|
||||
setActiveElementId: (elementId: string | null) => void;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
invalidElements: string[] | null;
|
||||
addElement: (element: any, index?: number) => void;
|
||||
isFormbricksCloud: boolean;
|
||||
@@ -52,9 +50,7 @@ export const BlocksDroppable = ({
|
||||
setLocalSurvey,
|
||||
moveElement,
|
||||
project,
|
||||
selectedLanguageCode,
|
||||
setActiveElementId,
|
||||
setSelectedLanguageCode,
|
||||
updateElement,
|
||||
updateBlockLogic,
|
||||
updateBlockLogicFallback,
|
||||
@@ -97,8 +93,6 @@ export const BlocksDroppable = ({
|
||||
updateBlockLogicFallback={updateBlockLogicFallback}
|
||||
updateBlockButtonLabel={updateBlockButtonLabel}
|
||||
duplicateElement={duplicateElement}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
deleteElement={deleteElement}
|
||||
activeElementId={activeElementId}
|
||||
setActiveElementId={setActiveElementId}
|
||||
|
||||
@@ -33,8 +33,6 @@ interface EditEndingCardProps {
|
||||
setActiveElementId: (id: string | null) => void;
|
||||
activeElementId: string | null;
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
addEndingCard: (index: number) => void;
|
||||
isFormbricksCloud: boolean;
|
||||
locale: TUserLocale;
|
||||
@@ -50,8 +48,6 @@ export const EditEndingCard = ({
|
||||
setActiveElementId,
|
||||
activeElementId,
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
addEndingCard,
|
||||
isFormbricksCloud,
|
||||
locale,
|
||||
@@ -232,14 +228,10 @@ export const EditEndingCard = ({
|
||||
<p className="text-sm font-semibold">
|
||||
{endingCard.type === "endScreen" &&
|
||||
(endingCard.headline &&
|
||||
recallToHeadline(endingCard.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
]
|
||||
recallToHeadline(endingCard.headline, localSurvey, true, "default").default
|
||||
? formatTextWithSlashes(
|
||||
getTextContent(
|
||||
recallToHeadline(endingCard.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
]
|
||||
recallToHeadline(endingCard.headline, localSurvey, true, "default").default
|
||||
)
|
||||
)
|
||||
: t("environments.surveys.edit.ending_card"))}
|
||||
@@ -297,8 +289,6 @@ export const EditEndingCard = ({
|
||||
localSurvey={localSurvey}
|
||||
endingCardIndex={endingCardIndex}
|
||||
isInvalid={isInvalid}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
updateSurvey={updateSurvey}
|
||||
endingCard={endingCard}
|
||||
locale={locale}
|
||||
|
||||
@@ -23,8 +23,6 @@ interface EditWelcomeCardProps {
|
||||
setActiveElementId: (id: string | null) => void;
|
||||
activeElementId: string | null;
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
locale: TUserLocale;
|
||||
isStorageConfigured: boolean;
|
||||
isExternalUrlsAllowed?: boolean;
|
||||
@@ -36,8 +34,6 @@ export const EditWelcomeCard = ({
|
||||
setActiveElementId,
|
||||
activeElementId,
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
locale,
|
||||
isStorageConfigured = true,
|
||||
isExternalUrlsAllowed,
|
||||
@@ -155,8 +151,6 @@ export const EditWelcomeCard = ({
|
||||
elementIdx={-1}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
@@ -171,8 +165,6 @@ export const EditWelcomeCard = ({
|
||||
elementIdx={-1}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
@@ -191,8 +183,6 @@ export const EditWelcomeCard = ({
|
||||
placeholder={t("common.next")}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
label={t("environments.surveys.edit.next_button_label")}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { Language, Project } from "@prisma/client";
|
||||
import { Project } from "@prisma/client";
|
||||
import React, { SetStateAction, useEffect, useMemo } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -52,7 +52,6 @@ import {
|
||||
isUsedInRecall,
|
||||
} from "@/modules/survey/editor/lib/utils";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { MultiLanguageCard } from "@/modules/survey/multi-language-surveys/components/multi-language-card";
|
||||
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
|
||||
import { isEndingCardValid, isWelcomeCardValid, validateElement } from "../lib/validation";
|
||||
|
||||
@@ -62,11 +61,8 @@ interface ElementsViewProps {
|
||||
activeElementId: string | null;
|
||||
setActiveElementId: (elementId: string | null) => void;
|
||||
project: Project;
|
||||
projectLanguages: Language[];
|
||||
invalidElements: string[] | null;
|
||||
setInvalidElements: React.Dispatch<SetStateAction<string[] | null>>;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isFormbricksCloud: boolean;
|
||||
isCxMode: boolean;
|
||||
locale: TUserLocale;
|
||||
@@ -83,11 +79,8 @@ export const ElementsView = ({
|
||||
localSurvey,
|
||||
setLocalSurvey,
|
||||
project,
|
||||
projectLanguages,
|
||||
invalidElements,
|
||||
setInvalidElements,
|
||||
setSelectedLanguageCode,
|
||||
selectedLanguageCode,
|
||||
isFormbricksCloud,
|
||||
isCxMode,
|
||||
locale,
|
||||
@@ -173,13 +166,10 @@ export const ElementsView = ({
|
||||
const updatedElements = block.elements.map((element) => {
|
||||
let updatedElement = { ...element };
|
||||
|
||||
if (element.headline[selectedLanguageCode]?.includes(`recall:${compareId}`)) {
|
||||
if (element.headline.default?.includes(`recall:${compareId}`)) {
|
||||
updatedElement.headline = {
|
||||
...element.headline,
|
||||
[selectedLanguageCode]: element.headline[selectedLanguageCode].replaceAll(
|
||||
`recall:${compareId}`,
|
||||
`recall:${updatedId}`
|
||||
),
|
||||
default: element.headline.default.replaceAll(`recall:${compareId}`, `recall:${updatedId}`),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -785,7 +775,7 @@ export const ElementsView = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const elementWithEmptyFallback = checkForEmptyFallBackValue(localSurvey, selectedLanguageCode);
|
||||
const elementWithEmptyFallback = checkForEmptyFallBackValue(localSurvey, "default");
|
||||
if (elementWithEmptyFallback) {
|
||||
setActiveElementId(elementWithEmptyFallback.id);
|
||||
if (activeElementId === elementWithEmptyFallback.id) {
|
||||
@@ -793,7 +783,7 @@ export const ElementsView = ({
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeElementId, setActiveElementId, localSurvey, selectedLanguageCode]);
|
||||
}, [activeElementId, setActiveElementId, localSurvey]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
@@ -850,8 +840,6 @@ export const ElementsView = ({
|
||||
setActiveElementId={setActiveElementId}
|
||||
activeElementId={activeElementId}
|
||||
isInvalid={invalidElements ? invalidElements.includes("start") : false}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
@@ -874,8 +862,6 @@ export const ElementsView = ({
|
||||
updateBlockLogicFallback={updateBlockLogicFallback}
|
||||
updateBlockButtonLabel={updateBlockButtonLabel}
|
||||
duplicateElement={duplicateElement}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
deleteElement={deleteElement}
|
||||
activeElementId={activeElementId}
|
||||
setActiveElementId={setActiveElementId}
|
||||
@@ -915,8 +901,6 @@ export const ElementsView = ({
|
||||
setActiveElementId={setActiveElementId}
|
||||
activeElementId={activeElementId}
|
||||
isInvalid={invalidElements ? invalidElements.includes(ending.id) : false}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
addEndingCard={addEndingCard}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
locale={locale}
|
||||
@@ -949,16 +933,6 @@ export const ElementsView = ({
|
||||
setActiveElementId={setActiveElementId}
|
||||
quotas={quotas}
|
||||
/>
|
||||
|
||||
<MultiLanguageCard
|
||||
localSurvey={localSurvey}
|
||||
projectLanguages={projectLanguages}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
setActiveElementId={setActiveElementId}
|
||||
activeElementId={activeElementId}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
locale={locale}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -19,8 +19,6 @@ interface EndScreenFormProps {
|
||||
localSurvey: TSurvey;
|
||||
endingCardIndex: number;
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
updateSurvey: (
|
||||
input: Partial<TSurveyEndScreenCard & { _forceUpdate?: boolean }> | Partial<TSurveyRedirectUrlCard>
|
||||
) => void;
|
||||
@@ -34,8 +32,6 @@ export const EndScreenForm = ({
|
||||
localSurvey,
|
||||
endingCardIndex,
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
updateSurvey,
|
||||
endingCard,
|
||||
locale,
|
||||
@@ -44,12 +40,11 @@ export const EndScreenForm = ({
|
||||
}: EndScreenFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const selectedLanguageCode = "default";
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
|
||||
const questions = getElementsFromBlocks(localSurvey.blocks);
|
||||
|
||||
const defaultLanguageCode = localSurvey.languages.find((lang) => lang.default)?.language.code ?? "default";
|
||||
const usedLanguageCode = selectedLanguageCode === defaultLanguageCode ? "default" : selectedLanguageCode;
|
||||
const usedLanguageCode = "default";
|
||||
|
||||
const [showEndingCardCTA, setshowEndingCardCTA] = useState<boolean>(
|
||||
endingCard.type === "endScreen" &&
|
||||
@@ -66,8 +61,6 @@ export const EndScreenForm = ({
|
||||
elementIdx={questions.length + endingCardIndex}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
autoFocus={!endingCard.headline?.default || endingCard.headline.default.trim() === ""}
|
||||
@@ -85,8 +78,6 @@ export const EndScreenForm = ({
|
||||
elementIdx={questions.length + endingCardIndex}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
autoFocus={!endingCard.subheader?.default || endingCard.subheader.default.trim() === ""}
|
||||
@@ -155,8 +146,6 @@ export const EndScreenForm = ({
|
||||
elementIdx={questions.length + endingCardIndex}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
locale={locale}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurvey, TSurveyEditorTabs, TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { extractLanguageCodes, getEnabledLanguages } from "@/lib/i18n/utils";
|
||||
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||
import { useDocumentVisibility } from "@/lib/useDocumentVisibility";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||
@@ -55,7 +54,6 @@ interface SurveyEditorProps {
|
||||
export const SurveyEditor = ({
|
||||
survey,
|
||||
project,
|
||||
projectLanguages,
|
||||
environment,
|
||||
actionClasses,
|
||||
contactAttributeKeys,
|
||||
@@ -85,7 +83,6 @@ export const SurveyEditor = ({
|
||||
const [localSurvey, setLocalSurvey] = useState<TSurvey | null>(() => structuredClone(survey));
|
||||
const [invalidElements, setInvalidElements] = useState<string[] | null>([]);
|
||||
|
||||
const [selectedLanguageCode, setSelectedLanguageCode] = useState<string>("default");
|
||||
const surveyEditorRef = useRef(null);
|
||||
const [localProject, setLocalProject] = useState<Project>(project);
|
||||
|
||||
@@ -147,14 +144,6 @@ export const SurveyEditor = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSurvey?.type]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localSurvey?.languages) return;
|
||||
const enabledLanguageCodes = extractLanguageCodes(getEnabledLanguages(localSurvey.languages ?? []));
|
||||
if (!enabledLanguageCodes.includes(selectedLanguageCode)) {
|
||||
setSelectedLanguageCode("default");
|
||||
}
|
||||
}, [localSurvey?.languages, selectedLanguageCode]);
|
||||
|
||||
if (!localSurvey) {
|
||||
return <LoadingSkeleton />;
|
||||
}
|
||||
@@ -174,10 +163,7 @@ export const SurveyEditor = ({
|
||||
setInvalidElements={setInvalidElements}
|
||||
project={localProject}
|
||||
responseCount={responseCount}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isCxMode={isCxMode}
|
||||
locale={locale}
|
||||
setIsCautionDialogOpen={setIsCautionDialogOpen}
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
/>
|
||||
@@ -199,11 +185,8 @@ export const SurveyEditor = ({
|
||||
activeElementId={activeElementId}
|
||||
setActiveElementId={setActiveElementId}
|
||||
project={localProject}
|
||||
projectLanguages={projectLanguages}
|
||||
invalidElements={invalidElements}
|
||||
setInvalidElements={setInvalidElements}
|
||||
selectedLanguageCode={selectedLanguageCode || "default"}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isCxMode={isCxMode}
|
||||
locale={locale}
|
||||
@@ -255,7 +238,6 @@ export const SurveyEditor = ({
|
||||
<FollowUpsView
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurveyNonNull}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
mailFrom={mailFrom}
|
||||
isSurveyFollowUpsAllowed={isSurveyFollowUpsAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
@@ -273,7 +255,7 @@ export const SurveyEditor = ({
|
||||
project={localProject}
|
||||
environment={environment}
|
||||
previewType={localSurvey.type === "app" ? "modal" : "fullwidth"}
|
||||
languageCode={selectedLanguageCode}
|
||||
languageCode="default"
|
||||
isSpamProtectionAllowed={isSpamProtectionAllowed}
|
||||
publicDomain={publicDomain}
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
||||
import { TSegment } from "@formbricks/types/segment";
|
||||
import { TSurveyBlock } from "@formbricks/types/surveys/blocks";
|
||||
import {
|
||||
@@ -38,10 +37,7 @@ interface SurveyMenuBarProps {
|
||||
setInvalidElements: React.Dispatch<React.SetStateAction<string[] | null>>;
|
||||
project: Project;
|
||||
responseCount: number;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (selectedLanguage: string) => void;
|
||||
isCxMode: boolean;
|
||||
locale: string;
|
||||
setIsCautionDialogOpen: (open: boolean) => void;
|
||||
isStorageConfigured: boolean;
|
||||
}
|
||||
@@ -56,9 +52,7 @@ export const SurveyMenuBar = ({
|
||||
setInvalidElements,
|
||||
project,
|
||||
responseCount,
|
||||
selectedLanguageCode,
|
||||
isCxMode,
|
||||
locale,
|
||||
setIsCautionDialogOpen,
|
||||
isStorageConfigured = true,
|
||||
}: SurveyMenuBarProps) => {
|
||||
@@ -192,7 +186,17 @@ export const SurveyMenuBar = ({
|
||||
const validateSurveyWithZod = (): boolean => {
|
||||
const localSurveyValidation = ZSurvey.safeParse(localSurvey);
|
||||
if (!localSurveyValidation.success) {
|
||||
const issues = localSurveyValidation.error.issues;
|
||||
const issues = localSurveyValidation.error.issues.filter((issue) => {
|
||||
if (issue.code !== "custom") return true;
|
||||
const params = issue.params as { invalidLanguageCodes?: string[] } | undefined;
|
||||
if (params?.invalidLanguageCodes?.length) return false;
|
||||
return !issue.message.includes("-fLang-");
|
||||
});
|
||||
|
||||
if (issues.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const newInvalidIds: string[] = [];
|
||||
|
||||
for (const issue of issues) {
|
||||
@@ -236,21 +240,9 @@ export const SurveyMenuBar = ({
|
||||
|
||||
const firstError = issues[0];
|
||||
if (firstError.code === "custom") {
|
||||
const params = firstError.params ?? ({} as { invalidLanguageCodes: string[] });
|
||||
if (params.invalidLanguageCodes && params.invalidLanguageCodes.length) {
|
||||
const invalidLanguageLabels = params.invalidLanguageCodes.map(
|
||||
(invalidLanguage: string) => getLanguageLabel(invalidLanguage, locale) ?? invalidLanguage
|
||||
);
|
||||
|
||||
const messageSplit = firstError.message.split("-fLang-")[0];
|
||||
|
||||
toast.error(`${messageSplit} ${invalidLanguageLabels.join(", ")}`);
|
||||
} else {
|
||||
toast.error(firstError.message, {
|
||||
className: "w-fit !max-w-md",
|
||||
});
|
||||
}
|
||||
|
||||
toast.error(firstError.message, {
|
||||
className: "w-fit !max-w-md",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -359,7 +351,7 @@ export const SurveyMenuBar = ({
|
||||
}
|
||||
|
||||
try {
|
||||
const isSurveyValidResult = isSurveyValid(localSurvey, selectedLanguageCode, t, responseCount);
|
||||
const isSurveyValidResult = isSurveyValid(localSurvey, t, responseCount);
|
||||
if (!isSurveyValidResult) {
|
||||
setIsSurveySaving(false);
|
||||
return false;
|
||||
@@ -439,7 +431,7 @@ export const SurveyMenuBar = ({
|
||||
}
|
||||
|
||||
try {
|
||||
const isSurveyValidResult = isSurveyValid(localSurvey, selectedLanguageCode, t, responseCount);
|
||||
const isSurveyValidResult = isSurveyValid(localSurvey, t, responseCount);
|
||||
if (!isSurveyValidResult) {
|
||||
isSurveyPublishingRef.current = false;
|
||||
setIsSurveyPublishing(false);
|
||||
|
||||
@@ -949,7 +949,7 @@ describe("validation.isSurveyValid", () => {
|
||||
});
|
||||
|
||||
test("should return true for a completely valid survey", () => {
|
||||
expect(validation.isSurveyValid(baseSurvey, "en", mockT)).toBe(true);
|
||||
expect(validation.isSurveyValid(baseSurvey, mockT)).toBe(true);
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -966,7 +966,7 @@ describe("validation.isSurveyValid", () => {
|
||||
},
|
||||
required: false,
|
||||
});
|
||||
expect(validation.isSurveyValid(baseSurvey, "de", mockT)).toBe(false);
|
||||
expect(validation.isSurveyValid(baseSurvey, mockT)).toBe(false);
|
||||
expect(toast.error).toHaveBeenCalledWith("environments.surveys.edit.fallback_missing");
|
||||
});
|
||||
|
||||
@@ -975,7 +975,7 @@ describe("validation.isSurveyValid", () => {
|
||||
...baseSurvey,
|
||||
autoComplete: 0,
|
||||
};
|
||||
expect(validation.isSurveyValid(surveyWithZeroLimit, "en", mockT, 5)).toBe(false);
|
||||
expect(validation.isSurveyValid(surveyWithZeroLimit, mockT, 5)).toBe(false);
|
||||
expect(toast.error).toHaveBeenCalledWith("environments.surveys.edit.response_limit_can_t_be_set_to_0");
|
||||
});
|
||||
|
||||
@@ -984,7 +984,7 @@ describe("validation.isSurveyValid", () => {
|
||||
...baseSurvey,
|
||||
autoComplete: 5,
|
||||
};
|
||||
expect(validation.isSurveyValid(surveyWithLowLimit, "en", mockT, 5)).toBe(false);
|
||||
expect(validation.isSurveyValid(surveyWithLowLimit, mockT, 5)).toBe(false);
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
"environments.surveys.edit.response_limit_needs_to_exceed_number_of_received_responses",
|
||||
{
|
||||
@@ -998,7 +998,7 @@ describe("validation.isSurveyValid", () => {
|
||||
...baseSurvey,
|
||||
autoComplete: 3,
|
||||
};
|
||||
expect(validation.isSurveyValid(surveyWithLowLimit, "en", mockT, 5)).toBe(false);
|
||||
expect(validation.isSurveyValid(surveyWithLowLimit, mockT, 5)).toBe(false);
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
"environments.surveys.edit.response_limit_needs_to_exceed_number_of_received_responses",
|
||||
{
|
||||
@@ -1012,7 +1012,7 @@ describe("validation.isSurveyValid", () => {
|
||||
...baseSurvey,
|
||||
autoComplete: 10,
|
||||
};
|
||||
expect(validation.isSurveyValid(surveyWithValidLimit, "en", mockT, 5)).toBe(true);
|
||||
expect(validation.isSurveyValid(surveyWithValidLimit, mockT, 5)).toBe(true);
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1021,7 +1021,7 @@ describe("validation.isSurveyValid", () => {
|
||||
...baseSurvey,
|
||||
autoComplete: null,
|
||||
};
|
||||
expect(validation.isSurveyValid(surveyWithNoLimit, "en", mockT, 5)).toBe(true);
|
||||
expect(validation.isSurveyValid(surveyWithNoLimit, mockT, 5)).toBe(true);
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1035,7 +1035,7 @@ describe("validation.isSurveyValid", () => {
|
||||
},
|
||||
} as unknown as TSurvey;
|
||||
|
||||
expect(validation.isSurveyValid(surveyWithEmptyClosedMessageHeading, "en", mockT)).toBe(false);
|
||||
expect(validation.isSurveyValid(surveyWithEmptyClosedMessageHeading, mockT)).toBe(false);
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
"environments.surveys.edit.survey_closed_message_heading_required"
|
||||
);
|
||||
@@ -1051,7 +1051,7 @@ describe("validation.isSurveyValid", () => {
|
||||
},
|
||||
} as unknown as TSurvey;
|
||||
|
||||
expect(validation.isSurveyValid(surveyWithWhitespaceClosedMessageHeading, "en", mockT)).toBe(false);
|
||||
expect(validation.isSurveyValid(surveyWithWhitespaceClosedMessageHeading, mockT)).toBe(false);
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
"environments.surveys.edit.survey_closed_message_heading_required"
|
||||
);
|
||||
@@ -1067,7 +1067,7 @@ describe("validation.isSurveyValid", () => {
|
||||
},
|
||||
} as unknown as TSurvey;
|
||||
|
||||
expect(validation.isSurveyValid(surveyWithHeadingOnlyClosedMessage, "en", mockT)).toBe(true);
|
||||
expect(validation.isSurveyValid(surveyWithHeadingOnlyClosedMessage, mockT)).toBe(true);
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1081,7 +1081,7 @@ describe("validation.isSurveyValid", () => {
|
||||
},
|
||||
} as unknown as TSurvey;
|
||||
|
||||
expect(validation.isSurveyValid(surveyWithClosedMessageContent, "en", mockT)).toBe(true);
|
||||
expect(validation.isSurveyValid(surveyWithClosedMessageContent, mockT)).toBe(true);
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1101,7 +1101,7 @@ describe("validation.isSurveyValid", () => {
|
||||
},
|
||||
} as unknown as TSurvey;
|
||||
|
||||
expect(validation.isSurveyValid(surveyWithInvalidSegment, "en", mockT)).toBe(false); // Zod parse will fail
|
||||
expect(validation.isSurveyValid(surveyWithInvalidSegment, mockT)).toBe(false); // Zod parse will fail
|
||||
expect(toast.error).toHaveBeenCalledWith("environments.surveys.edit.invalid_targeting");
|
||||
});
|
||||
|
||||
@@ -1128,7 +1128,7 @@ describe("validation.isSurveyValid", () => {
|
||||
const mockSafeParse = vi.spyOn(ZSegmentFilters, "safeParse");
|
||||
mockSafeParse.mockReturnValue({ success: true, data: surveyWithValidSegment.segment!.filters } as any);
|
||||
|
||||
expect(validation.isSurveyValid(surveyWithValidSegment, "en", mockT)).toBe(true);
|
||||
expect(validation.isSurveyValid(surveyWithValidSegment, mockT)).toBe(true);
|
||||
expect(toast.error).not.toHaveBeenCalled();
|
||||
mockSafeParse.mockRestore();
|
||||
});
|
||||
|
||||
@@ -23,65 +23,56 @@ import {
|
||||
TSurveyRedirectUrlCard,
|
||||
TSurveyWelcomeCard,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { findLanguageCodesForDuplicateLabels, getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { extractLanguageCodes, getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { checkForEmptyFallBackValue } from "@/lib/utils/recall";
|
||||
|
||||
// Utility function to check if label is valid for all required languages
|
||||
const getDefaultText = (label?: TI18nString): string => {
|
||||
return getTextContent(label?.default ?? "");
|
||||
};
|
||||
|
||||
export const isLabelValidForAllLanguages = (
|
||||
label: TI18nString,
|
||||
surveyLanguages: TSurveyLanguage[]
|
||||
_surveyLanguages: TSurveyLanguage[]
|
||||
): boolean => {
|
||||
const filteredLanguages = surveyLanguages.filter((surveyLanguages) => {
|
||||
return surveyLanguages.enabled;
|
||||
});
|
||||
const languageCodes = extractLanguageCodes(filteredLanguages);
|
||||
const languages = languageCodes.length === 0 ? ["default"] : languageCodes;
|
||||
return languages.every((language) => label?.[language] && getTextContent(label[language]).length > 0);
|
||||
return getDefaultText(label).trim().length > 0;
|
||||
};
|
||||
|
||||
// Validation logic for multiple choice elements
|
||||
const handleI18nCheckForMultipleChoice = (
|
||||
element: TSurveyMultipleChoiceElement,
|
||||
languages: TSurveyLanguage[]
|
||||
_languages: TSurveyLanguage[]
|
||||
): boolean => {
|
||||
const invalidLangCodes = findLanguageCodesForDuplicateLabels(
|
||||
element.choices.map((choice) => choice.label),
|
||||
languages
|
||||
);
|
||||
|
||||
if (invalidLangCodes.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return element.choices.every((choice) => isLabelValidForAllLanguages(choice.label, languages));
|
||||
const normalizedLabels = element.choices
|
||||
.map((choice) => getDefaultText(choice.label).trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
const hasDuplicates = new Set(normalizedLabels).size !== normalizedLabels.length;
|
||||
return !hasDuplicates && element.choices.every((choice) => getDefaultText(choice.label).trim().length > 0);
|
||||
};
|
||||
|
||||
const handleI18nCheckForMatrixLabels = (
|
||||
element: TSurveyMatrixElement,
|
||||
languages: TSurveyLanguage[]
|
||||
_languages: TSurveyLanguage[]
|
||||
): boolean => {
|
||||
const rowsAndColumns = [...element.rows, ...element.columns];
|
||||
const rowLabels = element.rows.map((row) => getDefaultText(row.label).trim().toLowerCase()).filter(Boolean);
|
||||
const colLabels = element.columns
|
||||
.map((column) => getDefaultText(column.label).trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
|
||||
const invalidRowsLangCodes = findLanguageCodesForDuplicateLabels(
|
||||
element.rows.map((row) => row.label),
|
||||
languages
|
||||
);
|
||||
const invalidColumnsLangCodes = findLanguageCodesForDuplicateLabels(
|
||||
element.columns.map((column) => column.label),
|
||||
languages
|
||||
);
|
||||
const hasDuplicateRows = new Set(rowLabels).size !== rowLabels.length;
|
||||
const hasDuplicateColumns = new Set(colLabels).size !== colLabels.length;
|
||||
|
||||
if (invalidRowsLangCodes.length > 0 || invalidColumnsLangCodes.length > 0) {
|
||||
if (hasDuplicateRows || hasDuplicateColumns) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return rowsAndColumns.every((choice) => isLabelValidForAllLanguages(choice.label, languages));
|
||||
return rowsAndColumns.every((choice) => getDefaultText(choice.label).trim().length > 0);
|
||||
};
|
||||
|
||||
const handleI18nCheckForContactAndAddressFields = (
|
||||
element: TSurveyContactInfoElement | TSurveyAddressElement,
|
||||
languages: TSurveyLanguage[]
|
||||
_languages: TSurveyLanguage[]
|
||||
): boolean => {
|
||||
let fields: TInputFieldConfig[] = [];
|
||||
if (element.type === "contactInfo") {
|
||||
@@ -93,7 +84,7 @@ const handleI18nCheckForContactAndAddressFields = (
|
||||
}
|
||||
return fields.every((field) => {
|
||||
if (field.show) {
|
||||
return isLabelValidForAllLanguages(field.placeholder, languages);
|
||||
return getDefaultText(field.placeholder).trim().length > 0;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -101,11 +92,11 @@ const handleI18nCheckForContactAndAddressFields = (
|
||||
|
||||
// Validation rules
|
||||
export const validationRules = {
|
||||
openText: (element: TSurveyOpenTextElement, languages: TSurveyLanguage[]) => {
|
||||
openText: (element: TSurveyOpenTextElement, _languages: TSurveyLanguage[]) => {
|
||||
return element.placeholder &&
|
||||
getLocalizedValue(element.placeholder, "default").trim() !== "" &&
|
||||
languages.length > 1
|
||||
? isLabelValidForAllLanguages(element.placeholder, languages)
|
||||
getLocalizedValue(element.placeholder, "default").trim() !== ""
|
||||
? getDefaultText(element.placeholder).trim().length > 0
|
||||
: true;
|
||||
},
|
||||
multipleChoiceMulti: (element: TSurveyMultipleChoiceElement, languages: TSurveyLanguage[]) => {
|
||||
@@ -114,15 +105,15 @@ export const validationRules = {
|
||||
multipleChoiceSingle: (element: TSurveyMultipleChoiceElement, languages: TSurveyLanguage[]) => {
|
||||
return handleI18nCheckForMultipleChoice(element, languages);
|
||||
},
|
||||
consent: (element: TSurveyConsentElement, languages: TSurveyLanguage[]) => {
|
||||
return isLabelValidForAllLanguages(element.label, languages);
|
||||
consent: (element: TSurveyConsentElement, _languages: TSurveyLanguage[]) => {
|
||||
return getDefaultText(element.label).trim().length > 0;
|
||||
},
|
||||
pictureSelection: (element: TSurveyPictureSelectionElement) => {
|
||||
return element.choices.length >= 2;
|
||||
},
|
||||
cta: (element: TSurveyCTAElement, languages: TSurveyLanguage[]) => {
|
||||
cta: (element: TSurveyCTAElement, _languages: TSurveyLanguage[]) => {
|
||||
return element.buttonExternal && element.ctaButtonLabel
|
||||
? isLabelValidForAllLanguages(element.ctaButtonLabel, languages)
|
||||
? getDefaultText(element.ctaButtonLabel).trim().length > 0
|
||||
: true;
|
||||
},
|
||||
matrix: (element: TSurveyMatrixElement, languages: TSurveyLanguage[]) => {
|
||||
@@ -135,14 +126,14 @@ export const validationRules = {
|
||||
return handleI18nCheckForContactAndAddressFields(element, languages);
|
||||
},
|
||||
// Assuming headline is of type TI18nString
|
||||
defaultValidation: (element: TSurveyElement, languages: TSurveyLanguage[]) => {
|
||||
defaultValidation: (element: TSurveyElement, _languages: TSurveyLanguage[]) => {
|
||||
// headline and subheader are default for every element
|
||||
const isHeadlineValid = isLabelValidForAllLanguages(element.headline, languages);
|
||||
const isHeadlineValid = getDefaultText(element.headline).trim().length > 0;
|
||||
const isSubheaderValid =
|
||||
element.subheader &&
|
||||
getLocalizedValue(element.subheader, "default").trim() !== "" &&
|
||||
languages.length > 1
|
||||
? isLabelValidForAllLanguages(element.subheader, languages)
|
||||
getLocalizedValue(element.subheader, "default").trim() !== ""
|
||||
? getDefaultText(element.subheader).trim().length > 0
|
||||
: true;
|
||||
let isValid = isHeadlineValid && isSubheaderValid;
|
||||
const defaultLanguageCode = "default";
|
||||
@@ -152,7 +143,7 @@ export const validationRules = {
|
||||
for (const field of fieldsToValidate) {
|
||||
const fieldValue = (element as unknown as Record<string, Record<string, string> | undefined>)[field];
|
||||
if (fieldValue?.[defaultLanguageCode] !== undefined && fieldValue[defaultLanguageCode].trim() !== "") {
|
||||
isValid = isValid && isLabelValidForAllLanguages(fieldValue, languages);
|
||||
isValid = isValid && getDefaultText(fieldValue).trim().length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,8 +186,8 @@ export const validateSurveyElementsInBatch = (
|
||||
return invalidElements;
|
||||
};
|
||||
|
||||
const isContentValid = (content: Record<string, string> | undefined, surveyLanguages: TSurveyLanguage[]) => {
|
||||
return !content || isLabelValidForAllLanguages(content, surveyLanguages);
|
||||
const isContentValid = (content: Record<string, string> | undefined, _surveyLanguages: TSurveyLanguage[]) => {
|
||||
return !content || getDefaultText(content).trim().length > 0;
|
||||
};
|
||||
|
||||
const hasValidSurveyClosedMessageHeading = (survey: TSurvey): boolean => {
|
||||
@@ -247,13 +238,8 @@ export const isEndingCardValid = (
|
||||
}
|
||||
};
|
||||
|
||||
export const isSurveyValid = (
|
||||
survey: TSurvey,
|
||||
selectedLanguageCode: string,
|
||||
t: TFunction,
|
||||
responseCount?: number
|
||||
) => {
|
||||
const questionWithEmptyFallback = checkForEmptyFallBackValue(survey, selectedLanguageCode);
|
||||
export const isSurveyValid = (survey: TSurvey, t: TFunction, responseCount?: number) => {
|
||||
const questionWithEmptyFallback = checkForEmptyFallBackValue(survey, "default");
|
||||
if (questionWithEmptyFallback) {
|
||||
toast.error(t("environments.surveys.edit.fallback_missing"));
|
||||
return false;
|
||||
|
||||
@@ -19,7 +19,6 @@ import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
interface FollowUpItemProps {
|
||||
followUp: TSurveyFollowUp;
|
||||
localSurvey: TSurvey;
|
||||
selectedLanguageCode: string;
|
||||
mailFrom: string;
|
||||
userEmail: string;
|
||||
teamMemberDetails: TFollowUpEmailToUser[];
|
||||
@@ -31,7 +30,6 @@ export const FollowUpItem = ({
|
||||
followUp,
|
||||
localSurvey,
|
||||
mailFrom,
|
||||
selectedLanguageCode,
|
||||
userEmail,
|
||||
teamMemberDetails,
|
||||
setLocalSurvey,
|
||||
@@ -190,7 +188,6 @@ export const FollowUpItem = ({
|
||||
open={editFollowUpModalOpen}
|
||||
setOpen={setEditFollowUpModalOpen}
|
||||
mailFrom={mailFrom}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
defaultValues={{
|
||||
surveyFollowUpId: followUp.id,
|
||||
followUpName: followUp.name,
|
||||
|
||||
@@ -68,7 +68,6 @@ interface AddFollowUpModalProps {
|
||||
localSurvey: TSurvey;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
selectedLanguageCode: string;
|
||||
mailFrom: string;
|
||||
defaultValues?: Partial<TCreateSurveyFollowUpForm & { surveyFollowUpId: string }>;
|
||||
mode?: "create" | "edit";
|
||||
@@ -88,7 +87,6 @@ export const FollowUpModal = ({
|
||||
localSurvey,
|
||||
open,
|
||||
setOpen,
|
||||
selectedLanguageCode,
|
||||
mailFrom,
|
||||
defaultValues,
|
||||
mode = "create",
|
||||
@@ -98,6 +96,7 @@ export const FollowUpModal = ({
|
||||
locale,
|
||||
}: AddFollowUpModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const selectedLanguageCode = "default";
|
||||
const ELEMENTS_ICON_MAP = getElementIconMap(t);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
|
||||
@@ -15,7 +15,6 @@ import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
|
||||
interface FollowUpsViewProps {
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
|
||||
selectedLanguageCode: string;
|
||||
mailFrom: string;
|
||||
isSurveyFollowUpsAllowed: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
@@ -27,7 +26,6 @@ interface FollowUpsViewProps {
|
||||
export const FollowUpsView = ({
|
||||
localSurvey,
|
||||
setLocalSurvey,
|
||||
selectedLanguageCode,
|
||||
mailFrom,
|
||||
isSurveyFollowUpsAllowed,
|
||||
isFormbricksCloud,
|
||||
@@ -105,7 +103,6 @@ export const FollowUpsView = ({
|
||||
followUp={followUp}
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
mailFrom={mailFrom}
|
||||
userEmail={userEmail}
|
||||
teamMemberDetails={teamMemberDetails}
|
||||
@@ -120,7 +117,6 @@ export const FollowUpsView = ({
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
open={addFollowUpModalOpen}
|
||||
setOpen={setAddFollowUpModalOpen}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
mailFrom={mailFrom}
|
||||
userEmail={userEmail}
|
||||
teamMemberDetails={teamMemberDetails}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Language } from "@prisma/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
||||
import { DefaultTag } from "@/modules/ui/components/default-tag";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/modules/ui/components/select";
|
||||
import type { ConfirmationModalProps } from "./multi-language-card";
|
||||
|
||||
interface DefaultLanguageSelectProps {
|
||||
defaultLanguage?: Language;
|
||||
handleDefaultLanguageChange: (languageCode: string) => void;
|
||||
projectLanguages: Language[];
|
||||
setConfirmationModalInfo: (confirmationModal: ConfirmationModalProps) => void;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export function DefaultLanguageSelect({
|
||||
defaultLanguage,
|
||||
handleDefaultLanguageChange,
|
||||
projectLanguages,
|
||||
setConfirmationModalInfo,
|
||||
locale,
|
||||
}: DefaultLanguageSelectProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label>{t("environments.surveys.edit.1_choose_the_default_language_for_this_survey")}</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-48">
|
||||
<Select
|
||||
defaultValue={`${defaultLanguage?.code}`}
|
||||
disabled={Boolean(defaultLanguage)}
|
||||
onValueChange={(languageCode) => {
|
||||
setConfirmationModalInfo({
|
||||
open: true,
|
||||
title:
|
||||
t("environments.surveys.edit.confirm_default_language") +
|
||||
": " +
|
||||
getLanguageLabel(languageCode, locale),
|
||||
body: t(
|
||||
"environments.surveys.edit.once_set_the_default_language_for_this_survey_can_only_be_changed_by_disabling_the_multi_language_option_and_deleting_all_translations"
|
||||
),
|
||||
buttonText: t("common.confirm"),
|
||||
onConfirm: () => {
|
||||
handleDefaultLanguageChange(languageCode);
|
||||
},
|
||||
buttonVariant: "default",
|
||||
});
|
||||
}}
|
||||
value={`${defaultLanguage?.code}`}>
|
||||
<SelectTrigger className="w-full max-w-full truncate px-4 text-xs text-slate-800">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{projectLanguages.map((language) => (
|
||||
<SelectItem
|
||||
className="px-0.5 py-1 text-sm text-slate-800"
|
||||
key={language.id}
|
||||
value={language.code}>
|
||||
{`${getLanguageLabel(language.code, locale)} (${language.code})`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<DefaultTag />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
||||
import type { TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { useClickOutside } from "@/lib/utils/hooks/useClickOutside";
|
||||
|
||||
interface LanguageIndicatorProps {
|
||||
selectedLanguageCode: string;
|
||||
surveyLanguages: TSurveyLanguage[];
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
setFirstRender?: (firstRender: boolean) => void;
|
||||
locale: string;
|
||||
}
|
||||
export function LanguageIndicator({
|
||||
surveyLanguages,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
setFirstRender,
|
||||
locale,
|
||||
}: LanguageIndicatorProps) {
|
||||
const [showLanguageDropdown, setShowLanguageDropdown] = useState(false);
|
||||
const toggleDropdown = () => {
|
||||
setShowLanguageDropdown((prev) => !prev);
|
||||
};
|
||||
const languageDropdownRef = useRef(null);
|
||||
|
||||
const changeLanguage = (language: TSurveyLanguage) => {
|
||||
setSelectedLanguageCode(language.default ? "default" : language.language.code);
|
||||
if (setFirstRender) {
|
||||
//for lexical editor
|
||||
setFirstRender(true);
|
||||
}
|
||||
setShowLanguageDropdown(false);
|
||||
};
|
||||
|
||||
const languageToBeDisplayed = surveyLanguages.find((language) => {
|
||||
return selectedLanguageCode === "default"
|
||||
? language.default
|
||||
: language.language.code === selectedLanguageCode;
|
||||
});
|
||||
|
||||
useClickOutside(languageDropdownRef, () => {
|
||||
setShowLanguageDropdown(false);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="absolute right-2 top-2">
|
||||
<button
|
||||
aria-expanded={showLanguageDropdown}
|
||||
aria-haspopup="true"
|
||||
className="relative z-20 flex max-w-[120px] items-center justify-center rounded-md bg-slate-900 p-1 px-2 text-xs text-white hover:bg-slate-700"
|
||||
onClick={toggleDropdown}
|
||||
tabIndex={-1}
|
||||
type="button">
|
||||
<span className="max-w-full truncate">
|
||||
{languageToBeDisplayed ? getLanguageLabel(languageToBeDisplayed.language.code, locale) : ""}
|
||||
</span>
|
||||
<ChevronDown className="ml-1 h-4 w-4 flex-shrink-0" />
|
||||
</button>
|
||||
{showLanguageDropdown ? (
|
||||
<div
|
||||
className="absolute right-0 z-30 mt-1 max-h-64 w-48 space-y-2 overflow-auto rounded-md bg-slate-900 p-1 text-xs text-white"
|
||||
ref={languageDropdownRef}>
|
||||
{surveyLanguages.map(
|
||||
(language) =>
|
||||
language.language.code !== languageToBeDisplayed?.language.code &&
|
||||
language.enabled && (
|
||||
<button
|
||||
className="flex w-full rounded-sm p-1 text-left hover:bg-slate-700"
|
||||
key={language.language.id}
|
||||
onClick={() => {
|
||||
changeLanguage(language);
|
||||
}}
|
||||
type="button">
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{getLanguageLabel(language.language.code, locale)}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Language } from "@prisma/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getLanguageLabel } from "@formbricks/i18n-utils/src/utils";
|
||||
import type { TUserLocale } from "@formbricks/types/user";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { Switch } from "@/modules/ui/components/switch";
|
||||
|
||||
interface LanguageToggleProps {
|
||||
language: Language;
|
||||
isChecked: boolean;
|
||||
onToggle: () => void;
|
||||
onEdit: () => void;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export function LanguageToggle({ language, isChecked, onToggle, onEdit, locale }: LanguageToggleProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex max-w-96 items-center space-x-4">
|
||||
<Switch
|
||||
checked={isChecked}
|
||||
id={`${language.code}-toggle`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
/>
|
||||
<Label className="truncate font-medium text-slate-800" htmlFor={`${language.code}-toggle`}>
|
||||
{getLanguageLabel(language.code, locale)}
|
||||
</Label>
|
||||
{isChecked ? (
|
||||
<button
|
||||
className="truncate text-xs text-slate-600 underline hover:text-slate-800"
|
||||
onClick={onEdit}
|
||||
type="button">
|
||||
{t("environments.surveys.edit.edit_translations", {
|
||||
lang: getLanguageLabel(language.code, locale),
|
||||
})}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useTransition } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TI18nString } from "@formbricks/types/i18n";
|
||||
import type { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent, isValidHTML } from "@formbricks/types/surveys/validation";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { md } from "@/lib/markdownIt";
|
||||
import { recallToHeadline } from "@/lib/utils/recall";
|
||||
import { isLabelValidForAllLanguages } from "@/modules/survey/editor/lib/validation";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { Editor } from "@/modules/ui/components/editor";
|
||||
import { LanguageIndicator } from "./language-indicator";
|
||||
|
||||
interface LocalizedEditorProps {
|
||||
id: string;
|
||||
value: TI18nString | undefined;
|
||||
localSurvey: TSurvey;
|
||||
isInvalid: boolean;
|
||||
updateElement: any;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
elementIdx: number;
|
||||
firstRender: boolean;
|
||||
setFirstRender?: Dispatch<SetStateAction<boolean>>;
|
||||
locale: TUserLocale;
|
||||
elementId: string;
|
||||
isCard?: boolean; // Flag to indicate if this is a welcome/ending card
|
||||
autoFocus?: boolean;
|
||||
isExternalUrlsAllowed?: boolean;
|
||||
suppressUpdates?: () => boolean; // Function to check if updates should be suppressed (e.g., during deletion)
|
||||
}
|
||||
|
||||
const checkIfValueIsIncomplete = (
|
||||
id: string,
|
||||
isInvalid: boolean,
|
||||
surveyLanguageCodes: TSurveyLanguage[],
|
||||
value?: TI18nString
|
||||
) => {
|
||||
const labelIds = ["subheader", "headline", "html"];
|
||||
if (value === undefined) return false;
|
||||
const isDefaultIncomplete = labelIds.includes(id)
|
||||
? getTextContent(value.default ?? "").trim() !== ""
|
||||
: false;
|
||||
return isInvalid && !isLabelValidForAllLanguages(value, surveyLanguageCodes) && isDefaultIncomplete;
|
||||
};
|
||||
|
||||
export function LocalizedEditor({
|
||||
id,
|
||||
value,
|
||||
localSurvey,
|
||||
isInvalid,
|
||||
updateElement,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
elementIdx,
|
||||
firstRender,
|
||||
setFirstRender,
|
||||
locale,
|
||||
elementId,
|
||||
isCard,
|
||||
autoFocus,
|
||||
isExternalUrlsAllowed,
|
||||
suppressUpdates,
|
||||
}: Readonly<LocalizedEditorProps>) {
|
||||
// Derive elements from blocks for migrated surveys
|
||||
const elements = useMemo(() => getElementsFromBlocks(localSurvey.blocks), [localSurvey.blocks]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isInComplete = useMemo(
|
||||
() => checkIfValueIsIncomplete(id, isInvalid, localSurvey.languages, value),
|
||||
[id, isInvalid, localSurvey.languages, value]
|
||||
);
|
||||
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
return (
|
||||
<div className="relative w-full">
|
||||
<Editor
|
||||
id={id}
|
||||
disableLists
|
||||
excludedToolbarItems={["blockType"]}
|
||||
firstRender={firstRender}
|
||||
autoFocus={autoFocus}
|
||||
getText={() => {
|
||||
const text = value ? (value[selectedLanguageCode] ?? "") : "";
|
||||
let html = md.render(text);
|
||||
|
||||
// For backwards compatibility: wrap plain text headlines in <strong> tags
|
||||
// This ensures old surveys maintain semibold styling when converted to HTML
|
||||
if (id === "headline" && text && !isValidHTML(text)) {
|
||||
// Use [\s\S]*? to match any character including newlines
|
||||
html = html.replaceAll(/<p>([\s\S]*?)<\/p>/g, "<p><strong>$1</strong></p>");
|
||||
}
|
||||
|
||||
return html;
|
||||
}}
|
||||
key={`${elementId}-${id}-${selectedLanguageCode}`}
|
||||
setFirstRender={setFirstRender}
|
||||
setText={(v: string) => {
|
||||
// Early exit if updates are suppressed (e.g., during deletion)
|
||||
// This prevents race conditions where setText fires with stale props before React updates state
|
||||
if (suppressUpdates?.()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let sanitizedContent = v;
|
||||
if (!isExternalUrlsAllowed) {
|
||||
sanitizedContent = v.replaceAll(/<a[^>]*>(.*?)<\/a>/gi, "$1");
|
||||
}
|
||||
|
||||
const currentElement = elements[elementIdx];
|
||||
|
||||
startTransition(() => {
|
||||
// if this is a card, we wanna check if the card exists in the localSurvey
|
||||
if (isCard) {
|
||||
const isWelcomeCard = elementIdx === -1;
|
||||
const isEndingCard = elementIdx >= elements.length;
|
||||
|
||||
// For ending cards, check if the field exists before updating
|
||||
if (isEndingCard) {
|
||||
const ending = localSurvey.endings.find((ending) => ending.id === elementId);
|
||||
// If the field doesn't exist on the ending card, don't create it
|
||||
if ((ending as Record<string, unknown>)?.[id] === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// For welcome cards, check if it exists
|
||||
if (isWelcomeCard && !localSurvey.welcomeCard) {
|
||||
return;
|
||||
}
|
||||
|
||||
const translatedContent = {
|
||||
...value,
|
||||
[selectedLanguageCode]: sanitizedContent,
|
||||
};
|
||||
updateElement({ [id]: translatedContent });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the field exists on the element (not just if it's not undefined)
|
||||
if (
|
||||
currentElement &&
|
||||
id in currentElement &&
|
||||
(currentElement as Record<string, unknown>)[id] !== undefined
|
||||
) {
|
||||
const translatedContent = {
|
||||
...value,
|
||||
[selectedLanguageCode]: sanitizedContent,
|
||||
};
|
||||
updateElement(elementIdx, { [id]: translatedContent });
|
||||
}
|
||||
});
|
||||
}}
|
||||
localSurvey={localSurvey}
|
||||
elementId={elementId}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
/>
|
||||
{localSurvey.languages.length > 1 && (
|
||||
<div>
|
||||
<LanguageIndicator
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setFirstRender={setFirstRender}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
surveyLanguages={localSurvey.languages}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
{value && selectedLanguageCode !== "default" && value.default ? (
|
||||
<div className="mt-1 flex text-xs text-gray-500">
|
||||
<strong>{t("environments.workspace.languages.translate")}:</strong>
|
||||
<span className="ml-1">
|
||||
{getTextContent(recallToHeadline(value, localSurvey, false, "default").default ?? "")}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isInComplete ? (
|
||||
<div className="mt-1 text-xs text-red-400">
|
||||
{t("environments.workspace.languages.incomplete_translations")}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { Language } from "@prisma/client";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { ArrowUpRight, Languages } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { addMultiLanguageLabels, extractLanguageCodes, getEnabledLanguages } from "@/lib/i18n/utils";
|
||||
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { ConfirmationModal } from "@/modules/ui/components/confirmation-modal";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { Switch } from "@/modules/ui/components/switch";
|
||||
import { DefaultLanguageSelect } from "./default-language-select";
|
||||
import { SecondaryLanguageSelect } from "./secondary-language-select";
|
||||
|
||||
interface MultiLanguageCardProps {
|
||||
localSurvey: TSurvey;
|
||||
projectLanguages: Language[];
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
activeElementId: string | null;
|
||||
setActiveElementId: (elementId: string | null) => void;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export interface ConfirmationModalProps {
|
||||
body: string;
|
||||
open: boolean;
|
||||
title: string;
|
||||
buttonText: string;
|
||||
buttonVariant?: "default" | "destructive";
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
|
||||
activeElementId,
|
||||
localSurvey,
|
||||
setActiveElementId,
|
||||
setLocalSurvey,
|
||||
projectLanguages,
|
||||
setSelectedLanguageCode,
|
||||
locale,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const environmentId = localSurvey.environmentId;
|
||||
const open = activeElementId === "multiLanguage";
|
||||
const [isMultiLanguageActivated, setIsMultiLanguageActivated] = useState(localSurvey.languages.length > 0);
|
||||
const [confirmationModalInfo, setConfirmationModalInfo] = useState<ConfirmationModalProps>({
|
||||
title: "",
|
||||
open: false,
|
||||
body: "",
|
||||
buttonText: "",
|
||||
onConfirm: () => {},
|
||||
});
|
||||
|
||||
const defaultLanguage = useMemo(
|
||||
() => localSurvey.languages.find((language) => language.default)?.language,
|
||||
[localSurvey.languages]
|
||||
);
|
||||
|
||||
const setOpen = (open: boolean) => {
|
||||
if (open) {
|
||||
setActiveElementId("multiLanguage");
|
||||
} else {
|
||||
setActiveElementId(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (localSurvey.languages.length === 0) {
|
||||
setIsMultiLanguageActivated(false);
|
||||
}
|
||||
}, [localSurvey.languages]);
|
||||
|
||||
const updateSurveyTranslations = (survey: TSurvey, updatedLanguages: TSurveyLanguage[]) => {
|
||||
const translatedSurveyResult = addMultiLanguageLabels(survey, extractLanguageCodes(updatedLanguages));
|
||||
|
||||
const updatedSurvey = { ...translatedSurveyResult, languages: updatedLanguages };
|
||||
setLocalSurvey(updatedSurvey as TSurvey);
|
||||
};
|
||||
|
||||
const updateSurveyLanguages = (language: Language) => {
|
||||
let updatedLanguages = localSurvey.languages;
|
||||
const languageIndex = localSurvey.languages.findIndex(
|
||||
(surveyLanguage) => surveyLanguage.language.code === language.code
|
||||
);
|
||||
if (languageIndex >= 0) {
|
||||
// Toggle the 'enabled' property of the existing language
|
||||
updatedLanguages = updatedLanguages.map((surveyLanguage, index) =>
|
||||
index === languageIndex ? { ...surveyLanguage, enabled: !surveyLanguage.enabled } : surveyLanguage
|
||||
);
|
||||
} else {
|
||||
// Add the new language
|
||||
updatedLanguages = [
|
||||
...updatedLanguages,
|
||||
{
|
||||
enabled: true,
|
||||
default: false,
|
||||
language,
|
||||
},
|
||||
];
|
||||
}
|
||||
updateSurveyTranslations(localSurvey, updatedLanguages);
|
||||
};
|
||||
|
||||
const updateSurvey = (data: { languages: TSurveyLanguage[] }) => {
|
||||
setLocalSurvey({ ...localSurvey, ...data });
|
||||
};
|
||||
|
||||
const handleDefaultLanguageChange = (languageCode: string) => {
|
||||
const language = projectLanguages.find((lang) => lang.code === languageCode);
|
||||
if (language) {
|
||||
let languageExists = false;
|
||||
|
||||
// Update all languages and check if the new default language already exists
|
||||
const newLanguages =
|
||||
localSurvey.languages.map((lang) => {
|
||||
if (lang.language.code === language.code) {
|
||||
languageExists = true;
|
||||
return { ...lang, default: true };
|
||||
}
|
||||
return { ...lang, default: false };
|
||||
}) ?? [];
|
||||
|
||||
if (!languageExists) {
|
||||
// If the language doesn't exist, add it as the default
|
||||
newLanguages.push({
|
||||
enabled: true,
|
||||
default: true,
|
||||
language,
|
||||
});
|
||||
}
|
||||
|
||||
setConfirmationModalInfo({ ...confirmationModalInfo, open: false });
|
||||
updateSurvey({ languages: newLanguages });
|
||||
}
|
||||
};
|
||||
|
||||
const handleActivationSwitchLogic = () => {
|
||||
if (isMultiLanguageActivated) {
|
||||
if (localSurvey.languages.length > 0) {
|
||||
setConfirmationModalInfo({
|
||||
open: true,
|
||||
title: t("environments.surveys.edit.remove_translations"),
|
||||
body: t("environments.surveys.edit.this_action_will_remove_all_the_translations_from_this_survey"),
|
||||
buttonText: t("environments.surveys.edit.remove_translations"),
|
||||
buttonVariant: "destructive",
|
||||
onConfirm: () => {
|
||||
updateSurveyTranslations(localSurvey, []);
|
||||
setIsMultiLanguageActivated(false);
|
||||
setConfirmationModalInfo({ ...confirmationModalInfo, open: false });
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setIsMultiLanguageActivated(false);
|
||||
}
|
||||
} else {
|
||||
setIsMultiLanguageActivated(true);
|
||||
if (!open) {
|
||||
setOpen(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleLanguageSwitchToggle = () => {
|
||||
setLocalSurvey({ ...localSurvey, showLanguageSwitch: !localSurvey.showLanguageSwitch });
|
||||
};
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const enabledLanguages = getEnabledLanguages(localSurvey.languages);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
open ? "shadow-lg" : "shadow-md",
|
||||
"group z-10 flex flex-row rounded-lg bg-white text-slate-900"
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
open ? "bg-slate-50" : "bg-white group-hover:bg-slate-50",
|
||||
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none"
|
||||
)}>
|
||||
<p>
|
||||
<Languages className="h-6 w-6 rounded-full bg-indigo-500 p-1 text-white" />
|
||||
</p>
|
||||
</div>
|
||||
<Collapsible.Root
|
||||
className="flex-1 rounded-r-lg border border-slate-200 transition-all duration-300 ease-in-out"
|
||||
onOpenChange={setOpen}
|
||||
open={open}>
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
className="flex cursor-pointer justify-between rounded-r-lg p-4 hover:bg-slate-50">
|
||||
<div>
|
||||
<div className="inline-flex">
|
||||
<div>
|
||||
<p className="text-sm font-semibold">{t("common.multiple_languages")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="multi-lang-toggle">
|
||||
{isMultiLanguageActivated ? t("common.on") : t("common.off")}
|
||||
</Label>
|
||||
|
||||
<Switch
|
||||
checked={isMultiLanguageActivated}
|
||||
disabled={projectLanguages.length === 0}
|
||||
id="multi-lang-toggle"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleActivationSwitchLogic();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-6"}`} ref={parent}>
|
||||
<div className="space-y-6 pt-3">
|
||||
{projectLanguages.length === 0 && (
|
||||
<div className="mb-4 text-sm italic text-slate-500">
|
||||
{t("environments.surveys.edit.no_languages_found_add_first_one_to_get_started")}
|
||||
</div>
|
||||
)}
|
||||
{projectLanguages.length > 0 && (
|
||||
<div className="space-y-6">
|
||||
{isMultiLanguageActivated ? (
|
||||
<div className="space-y-6">
|
||||
<DefaultLanguageSelect
|
||||
defaultLanguage={defaultLanguage}
|
||||
handleDefaultLanguageChange={handleDefaultLanguageChange}
|
||||
projectLanguages={projectLanguages}
|
||||
setConfirmationModalInfo={setConfirmationModalInfo}
|
||||
locale={locale}
|
||||
/>
|
||||
{defaultLanguage && projectLanguages.length > 1 ? (
|
||||
<SecondaryLanguageSelect
|
||||
defaultLanguage={defaultLanguage}
|
||||
localSurvey={localSurvey}
|
||||
projectLanguages={projectLanguages}
|
||||
setActiveElementId={setActiveElementId}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
updateSurveyLanguages={updateSurveyLanguages}
|
||||
locale={locale}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm italic text-slate-500">
|
||||
{t("environments.surveys.edit.switch_multi_language_on_to_get_started")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button asChild size="sm" variant="secondary">
|
||||
<Link href={`/environments/${environmentId}/workspace/languages`} target="_blank">
|
||||
{t("environments.surveys.edit.manage_languages")}
|
||||
<ArrowUpRight />
|
||||
</Link>
|
||||
</Button>
|
||||
{isMultiLanguageActivated && (
|
||||
<AdvancedOptionToggle
|
||||
customContainerClass="px-0 pt-0"
|
||||
htmlId="languageSwitch"
|
||||
disabled={enabledLanguages.length <= 1}
|
||||
isChecked={!!localSurvey.showLanguageSwitch}
|
||||
onToggle={handleLanguageSwitchToggle}
|
||||
title={t("environments.surveys.edit.show_language_switch")}
|
||||
description={t(
|
||||
"environments.surveys.edit.enable_participants_to_switch_the_survey_language_at_any_point_during_the_survey"
|
||||
)}
|
||||
childBorder={true}></AdvancedOptionToggle>
|
||||
)}
|
||||
|
||||
<ConfirmationModal
|
||||
buttonText={confirmationModalInfo.buttonText}
|
||||
buttonVariant={confirmationModalInfo.buttonVariant}
|
||||
onConfirm={confirmationModalInfo.onConfirm}
|
||||
open={confirmationModalInfo.open}
|
||||
setOpen={() => {
|
||||
setConfirmationModalInfo((prev) => ({ ...prev, open: !prev.open }));
|
||||
}}
|
||||
body={confirmationModalInfo.body}
|
||||
title={confirmationModalInfo.title}
|
||||
/>
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Language } from "@prisma/client";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
import { LanguageToggle } from "./language-toggle";
|
||||
|
||||
interface SecondaryLanguageSelectProps {
|
||||
projectLanguages: Language[];
|
||||
defaultLanguage: Language;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
setActiveElementId: (elementId: string) => void;
|
||||
localSurvey: TSurvey;
|
||||
updateSurveyLanguages: (language: Language) => void;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export function SecondaryLanguageSelect({
|
||||
projectLanguages,
|
||||
defaultLanguage,
|
||||
setSelectedLanguageCode,
|
||||
setActiveElementId,
|
||||
localSurvey,
|
||||
updateSurveyLanguages,
|
||||
locale,
|
||||
}: SecondaryLanguageSelectProps) {
|
||||
const { t } = useTranslation();
|
||||
const isLanguageToggled = (language: Language) => {
|
||||
return localSurvey.languages.some(
|
||||
(surveyLanguage) => surveyLanguage.language.code === language.code && surveyLanguage.enabled
|
||||
);
|
||||
};
|
||||
|
||||
const elements = getElementsFromBlocks(localSurvey.blocks);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-slate-800">
|
||||
{t("environments.surveys.edit.2_activate_translation_for_specific_languages")}
|
||||
</p>{" "}
|
||||
{projectLanguages
|
||||
.filter((lang) => lang.id !== defaultLanguage.id)
|
||||
.map((language) => (
|
||||
<LanguageToggle
|
||||
isChecked={isLanguageToggled(language)}
|
||||
key={language.id}
|
||||
language={language}
|
||||
onEdit={() => {
|
||||
setSelectedLanguageCode(language.code);
|
||||
setActiveElementId(elements[0]?.id);
|
||||
}}
|
||||
onToggle={() => {
|
||||
updateSurveyLanguages(language);
|
||||
}}
|
||||
locale={locale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { surveys } from "@/playwright/utils/mock";
|
||||
import { test } from "./lib/fixtures";
|
||||
import * as helper from "./utils/helper";
|
||||
import { createSurvey, createSurveyWithLogic, uploadFileForFileUploadQuestion } from "./utils/helper";
|
||||
import { createSurvey, createSurveyWithLogic } from "./utils/helper";
|
||||
|
||||
test.use({
|
||||
launchOptions: {
|
||||
@@ -237,498 +236,6 @@ test.describe("Survey Create & Submit Response without logic", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Multi Language Survey Create", async () => {
|
||||
// 5 minutes
|
||||
test.setTimeout(1000 * 60 * 5);
|
||||
|
||||
test("Create Survey", async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
|
||||
|
||||
//add a new language
|
||||
await page.getByRole("link", { name: "Configuration" }).click();
|
||||
await page.getByRole("link", { name: "Survey Languages" }).click();
|
||||
await page.getByRole("button", { name: "Edit languages" }).click();
|
||||
await page.getByRole("button", { name: "Add language" }).click();
|
||||
await page.getByRole("button", { name: "Select" }).click();
|
||||
await page.getByPlaceholder("Search items").click();
|
||||
await page.getByPlaceholder("Search items").fill("Eng");
|
||||
await page.getByText("English", { exact: true }).click();
|
||||
await page.getByRole("button", { name: "Save changes" }).click();
|
||||
await page.getByRole("button", { name: "Edit languages" }).click();
|
||||
await page.getByRole("button", { name: "Add language" }).click();
|
||||
await page.getByRole("button", { name: "Select" }).click();
|
||||
await page.getByRole("textbox", { name: "Search items" }).click();
|
||||
await page.getByRole("textbox", { name: "Search items" }).fill("German");
|
||||
await page.getByText("German", { exact: true }).nth(1).click();
|
||||
await page.getByRole("button", { name: "Save changes" }).click();
|
||||
await page.waitForTimeout(2000);
|
||||
await page.getByRole("link", { name: "Surveys" }).click();
|
||||
await page.getByText("Start from scratch").click();
|
||||
await page.getByRole("button", { name: "Create survey", exact: true }).click();
|
||||
await page.locator("#multi-lang-toggle").click();
|
||||
await page.getByRole("combobox").click();
|
||||
await page.getByLabel("English (en)").click();
|
||||
await page.getByRole("button", { name: "Confirm" }).click();
|
||||
await page.getByLabel("German").click();
|
||||
await page.locator("#welcome-toggle").click();
|
||||
|
||||
// Add questions in default language
|
||||
await page.getByText("Add Block").click();
|
||||
await page.getByRole("button", { name: "Single-Select" }).click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.createAndSubmit.singleSelectQuestion.question);
|
||||
await page.getByPlaceholder("Option 1").fill(surveys.createAndSubmit.singleSelectQuestion.options[0]);
|
||||
await page.getByPlaceholder("Option 2").fill(surveys.createAndSubmit.singleSelectQuestion.options[1]);
|
||||
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Add BlockChoose the first question on your Block$/ })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Multi-Select Ask respondents" }).click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.createAndSubmit.multiSelectQuestion.question);
|
||||
await page.getByPlaceholder("Option 1").fill(surveys.createAndSubmit.multiSelectQuestion.options[0]);
|
||||
await page.getByPlaceholder("Option 2").fill(surveys.createAndSubmit.multiSelectQuestion.options[1]);
|
||||
await page.getByPlaceholder("Option 3").fill(surveys.createAndSubmit.multiSelectQuestion.options[2]);
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Add BlockChoose the first question on your Block$/ })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Picture Selection" }).click();
|
||||
await helper.fillRichTextEditor(
|
||||
page,
|
||||
"Question*",
|
||||
surveys.createAndSubmit.pictureSelectQuestion.question
|
||||
);
|
||||
|
||||
// Handle file uploads
|
||||
await uploadFileForFileUploadQuestion(page);
|
||||
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Add BlockChoose the first question on your Block$/ })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Rating" }).click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.createAndSubmit.ratingQuestion.question);
|
||||
await page.getByPlaceholder("Not good").fill(surveys.createAndSubmit.ratingQuestion.lowLabel);
|
||||
await page.getByPlaceholder("Very satisfied").fill(surveys.createAndSubmit.ratingQuestion.highLabel);
|
||||
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Add BlockChoose the first question on your Block$/ })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Net Promoter Score (NPS)" }).click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.createAndSubmit.npsQuestion.question);
|
||||
await page.getByLabel("Lower label").fill(surveys.createAndSubmit.npsQuestion.lowLabel);
|
||||
await page.getByLabel("Upper label").fill(surveys.createAndSubmit.npsQuestion.highLabel);
|
||||
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Add BlockChoose the first question on your Block$/ })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Date" }).click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.createAndSubmit.dateQuestion.question);
|
||||
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Add BlockChoose the first question on your Block$/ })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByRole("button", { name: "File Upload" }).click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.createAndSubmit.fileUploadQuestion.question);
|
||||
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Add BlockChoose the first question on your Block$/ })
|
||||
.nth(1)
|
||||
.click();
|
||||
|
||||
await page.getByRole("button", { name: "Matrix" }).scrollIntoViewIfNeeded();
|
||||
await page.getByRole("button", { name: "Matrix" }).click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.createAndSubmit.matrix.question);
|
||||
await page.locator("#row-0").click();
|
||||
await page.locator("#row-0").fill(surveys.createAndSubmit.matrix.rows[0]);
|
||||
await page.locator("#row-1").click();
|
||||
await page.locator("#row-1").fill(surveys.createAndSubmit.matrix.rows[1]);
|
||||
await page.getByRole("button", { name: "Add row" }).click();
|
||||
await page.locator("#row-2").click();
|
||||
await page.locator("#row-2").fill(surveys.createAndSubmit.matrix.rows[2]);
|
||||
await page.locator("#column-0").click();
|
||||
await page.locator("#column-0").fill(surveys.createAndSubmit.matrix.columns[0]);
|
||||
await page.locator("#column-1").click();
|
||||
await page.locator("#column-1").fill(surveys.createAndSubmit.matrix.columns[1]);
|
||||
await page.getByRole("button", { name: "Add column" }).click();
|
||||
await page.locator("#column-2").click();
|
||||
await page.locator("#column-2").fill(surveys.createAndSubmit.matrix.columns[2]);
|
||||
await page.getByRole("button", { name: "Add column" }).click();
|
||||
await page.locator("#column-3").click();
|
||||
await page.locator("#column-3").fill(surveys.createAndSubmit.matrix.columns[3]);
|
||||
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Add BlockChoose the first question on your Block$/ })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Address" }).click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.createAndSubmit.address.question);
|
||||
await page.getByRole("row", { name: "Address Line 2" }).getByRole("switch").nth(1).click();
|
||||
await page.getByRole("row", { name: "City" }).getByRole("cell").nth(2).click();
|
||||
await page.getByRole("row", { name: "State" }).getByRole("switch").nth(1).click();
|
||||
await page.getByRole("row", { name: "Zip" }).getByRole("cell").nth(2).click();
|
||||
await page.getByRole("row", { name: "Country" }).getByRole("switch").nth(1).click();
|
||||
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Add BlockChoose the first question on your Block$/ })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByRole("button", { name: "Ranking" }).click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.createAndSubmit.ranking.question);
|
||||
await page.getByPlaceholder("Option 1").click();
|
||||
await page.getByPlaceholder("Option 1").fill(surveys.createAndSubmit.ranking.choices[0]);
|
||||
await page.getByPlaceholder("Option 2").click();
|
||||
await page.getByPlaceholder("Option 2").fill(surveys.createAndSubmit.ranking.choices[1]);
|
||||
await page.getByRole("button", { name: "Add option" }).click();
|
||||
await page.getByPlaceholder("Option 3").click();
|
||||
await page.getByPlaceholder("Option 3").fill(surveys.createAndSubmit.ranking.choices[2]);
|
||||
await page.getByRole("button", { name: "Add option" }).click();
|
||||
await page.getByPlaceholder("Option 4").click();
|
||||
await page.getByPlaceholder("Option 4").fill(surveys.createAndSubmit.ranking.choices[3]);
|
||||
await page.getByRole("button", { name: "Add option" }).click();
|
||||
await page.getByPlaceholder("Option 5").click();
|
||||
await page.getByPlaceholder("Option 5").fill(surveys.createAndSubmit.ranking.choices[4]);
|
||||
|
||||
// Enable translation in german
|
||||
await page.getByText("Welcome CardShownOn").click();
|
||||
await page.getByRole("button", { name: "English" }).nth(1).click();
|
||||
await page.getByRole("button", { name: "German" }).click();
|
||||
|
||||
// Fill welcome card in german using rich text editor helper
|
||||
await helper.fillRichTextEditor(page, "Note*", surveys.germanCreate.welcomeCard.headline);
|
||||
await helper.fillRichTextEditor(page, "Welcome message", surveys.germanCreate.welcomeCard.description);
|
||||
await page.getByPlaceholder("Next").click();
|
||||
await page.getByPlaceholder("Next").fill(surveys.germanCreate.welcomeCard.buttonLabel);
|
||||
|
||||
// Fill Open text question in german
|
||||
await page.getByRole("main").getByText("Free text").click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.germanCreate.openTextQuestion.question);
|
||||
await page.getByLabel("Placeholder").click();
|
||||
await page.getByLabel("Placeholder").fill(surveys.germanCreate.openTextQuestion.placeholder);
|
||||
await page.getByText("Show Block settings").first().click();
|
||||
await page.getByRole("textbox", { name: "Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.next);
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Block 11 question$/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// Fill Single select question in german
|
||||
await page.getByRole("main").getByText("Single-Select").click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.germanCreate.singleSelectQuestion.question);
|
||||
await page.getByPlaceholder("Option 1").click();
|
||||
await page.getByPlaceholder("Option 1").fill(surveys.germanCreate.singleSelectQuestion.options[0]);
|
||||
await page.getByPlaceholder("Option 2").click();
|
||||
await page.getByPlaceholder("Option 2").fill(surveys.germanCreate.singleSelectQuestion.options[1]);
|
||||
await page.getByText("Show Block settings").first().click();
|
||||
await page.getByRole("textbox", { name: "Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.next);
|
||||
await page.getByRole("textbox", { name: "“Back” Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "“Back” Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.back);
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Block 21 question$/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// Fill Multi select question in german
|
||||
await page.getByRole("main").getByRole("heading", { name: "Multi-Select" }).click();
|
||||
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.germanCreate.multiSelectQuestion.question);
|
||||
await page.getByPlaceholder("Option 1").click();
|
||||
await page.getByPlaceholder("Option 1").fill(surveys.germanCreate.multiSelectQuestion.options[0]);
|
||||
await page.getByPlaceholder("Option 2").click();
|
||||
await page.getByPlaceholder("Option 2").fill(surveys.germanCreate.multiSelectQuestion.options[1]);
|
||||
await page.getByPlaceholder("Option 3").click();
|
||||
await page.getByPlaceholder("Option 3").fill(surveys.germanCreate.multiSelectQuestion.options[2]);
|
||||
await page.getByText("Show Block settings").first().click();
|
||||
await page.getByRole("textbox", { name: "Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.next);
|
||||
await page.getByRole("textbox", { name: "“Back” Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "“Back” Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.back);
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Block 31 question$/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// Fill Picture select question in german
|
||||
await page.getByRole("main").getByText("Picture Selection").click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.germanCreate.pictureSelectQuestion.question);
|
||||
await page.getByText("Show Block settings").first().click();
|
||||
await page.getByRole("textbox", { name: "Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.next);
|
||||
await page.getByRole("textbox", { name: "“Back” Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "“Back” Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.back);
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Block 41 question$/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// Fill Rating question in german
|
||||
await page.getByRole("main").getByText("Rating").click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.germanCreate.ratingQuestion.question);
|
||||
await page.getByPlaceholder("Not good").click();
|
||||
await page.getByPlaceholder("Not good").fill(surveys.germanCreate.ratingQuestion.lowLabel);
|
||||
await page.getByPlaceholder("Very satisfied").click();
|
||||
await page.getByPlaceholder("Very satisfied").fill(surveys.germanCreate.ratingQuestion.highLabel);
|
||||
await page.getByText("Show Block settings").first().click();
|
||||
await page.getByRole("textbox", { name: "Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.next);
|
||||
await page.getByRole("textbox", { name: "“Back” Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "“Back” Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.back);
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Block 51 question$/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// Fill NPS question in german
|
||||
await page.getByRole("main").getByText("Net Promoter Score (NPS)").click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.germanCreate.npsQuestion.question);
|
||||
await page.getByLabel("Lower Label").click();
|
||||
await page.getByLabel("Lower Label").fill(surveys.germanCreate.npsQuestion.lowLabel);
|
||||
await page.getByLabel("Upper Label").click();
|
||||
await page.getByLabel("Upper Label").fill(surveys.germanCreate.npsQuestion.highLabel);
|
||||
await page.getByText("Show Block settings").first().click();
|
||||
await page.getByRole("textbox", { name: "Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.next);
|
||||
await page.getByRole("textbox", { name: "“Back” Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "“Back” Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.back);
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Block 61 question$/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// Fill Date question in german
|
||||
await page.getByRole("main").getByText("Date").click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.germanCreate.dateQuestion.question);
|
||||
await page.getByText("Show Block settings").first().click();
|
||||
await page.getByRole("textbox", { name: "Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.next);
|
||||
await page.getByRole("textbox", { name: "“Back” Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "“Back” Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.back);
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Block 71 question$/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// Fill File upload question in german
|
||||
await page.getByRole("main").getByText("File Upload").click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.germanCreate.fileUploadQuestion.question);
|
||||
await page.getByText("Show Block settings").first().click();
|
||||
await page.getByRole("textbox", { name: "Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.next);
|
||||
await page.getByRole("textbox", { name: "“Back” Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "“Back” Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.back);
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Block 81 question$/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// Fill Matrix question in german
|
||||
await page.getByRole("main").getByText("Matrix").click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.germanCreate.matrix.question);
|
||||
await page.locator("#row-0").click();
|
||||
await page.locator("#row-0").fill(surveys.germanCreate.matrix.rows[0]);
|
||||
await page.locator("#row-1").click();
|
||||
await page.locator("#row-1").fill(surveys.germanCreate.matrix.rows[1]);
|
||||
await page.locator("#row-2").click();
|
||||
await page.locator("#row-2").fill(surveys.germanCreate.matrix.rows[2]);
|
||||
await page.locator("#column-0").click();
|
||||
await page.locator("#column-0").fill(surveys.germanCreate.matrix.columns[0]);
|
||||
await page.locator("#column-1").click();
|
||||
await page.locator("#column-1").fill(surveys.germanCreate.matrix.columns[1]);
|
||||
await page.locator("#column-2").click();
|
||||
await page.locator("#column-2").fill(surveys.germanCreate.matrix.columns[2]);
|
||||
await page.locator("#column-3").click();
|
||||
await page.locator("#column-3").fill(surveys.germanCreate.matrix.columns[3]);
|
||||
await page.getByText("Show Block settings").first().click();
|
||||
await page.getByRole("textbox", { name: "Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.next);
|
||||
await page.getByRole("textbox", { name: "“Back” Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "“Back” Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.back);
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Block 91 question$/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// Fill Address question in german
|
||||
await page.getByRole("main").getByText("Address").click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.germanCreate.addressQuestion.question);
|
||||
await page.locator('[id="addressLine1\\.placeholder"]').click();
|
||||
await page
|
||||
.locator('[id="addressLine1\\.placeholder"]')
|
||||
.fill(surveys.germanCreate.addressQuestion.placeholder.addressLine1);
|
||||
await page.locator('[id="addressLine2\\.placeholder"]').click();
|
||||
await page
|
||||
.locator('[id="addressLine2\\.placeholder"]')
|
||||
.fill(surveys.germanCreate.addressQuestion.placeholder.addressLine2);
|
||||
await page.locator('[id="city\\.placeholder"]').click();
|
||||
await page
|
||||
.locator('[id="city\\.placeholder"]')
|
||||
.fill(surveys.germanCreate.addressQuestion.placeholder.city);
|
||||
await page.locator('[id="state\\.placeholder"]').click();
|
||||
await page
|
||||
.locator('[id="state\\.placeholder"]')
|
||||
.fill(surveys.germanCreate.addressQuestion.placeholder.state);
|
||||
await page.locator('[id="zip\\.placeholder"]').click();
|
||||
await page.locator('[id="zip\\.placeholder"]').fill(surveys.germanCreate.addressQuestion.placeholder.zip);
|
||||
await page.locator('[id="country\\.placeholder"]').click();
|
||||
await page
|
||||
.locator('[id="country\\.placeholder"]')
|
||||
.fill(surveys.germanCreate.addressQuestion.placeholder.country);
|
||||
await page.getByText("Show Block settings").first().click();
|
||||
await page.getByRole("textbox", { name: "Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.next);
|
||||
await page.getByRole("textbox", { name: "“Back” Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "“Back” Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.back);
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Block 101 question$/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// Fill Ranking question in german
|
||||
await page.getByRole("main").getByText("Ranking").click();
|
||||
await helper.fillRichTextEditor(page, "Question*", surveys.germanCreate.ranking.question);
|
||||
await page.getByPlaceholder("Option 1").click();
|
||||
await page.getByPlaceholder("Option 1").fill(surveys.germanCreate.ranking.choices[0]);
|
||||
await page.getByPlaceholder("Option 2").click();
|
||||
await page.getByPlaceholder("Option 2").fill(surveys.germanCreate.ranking.choices[1]);
|
||||
await page.getByPlaceholder("Option 3").click();
|
||||
await page.getByPlaceholder("Option 3").fill(surveys.germanCreate.ranking.choices[2]);
|
||||
await page.getByPlaceholder("Option 4").click();
|
||||
await page.getByPlaceholder("Option 4").fill(surveys.germanCreate.ranking.choices[3]);
|
||||
await page.getByPlaceholder("Option 5").click();
|
||||
await page.getByPlaceholder("Option 5").fill(surveys.germanCreate.ranking.choices[4]);
|
||||
await page.getByText("Show Block settings").first().click();
|
||||
await page.getByRole("textbox", { name: "Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.next);
|
||||
await page.getByRole("textbox", { name: "“Back” Button Label", exact: true }).first().click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "“Back” Button Label", exact: true })
|
||||
.first()
|
||||
.fill(surveys.germanCreate.back);
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: /^Block 111 question$/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// Fill Thank you card in german
|
||||
await page.getByText("Ending card").first().click();
|
||||
await helper.fillRichTextEditor(page, "Note*", surveys.germanCreate.endingCard.headline);
|
||||
await helper.fillRichTextEditor(page, "Description", surveys.germanCreate.endingCard.description);
|
||||
|
||||
await page.locator("#showButton").check();
|
||||
|
||||
await page.getByPlaceholder("Create your own Survey").click();
|
||||
await page.getByPlaceholder("Create your own Survey").fill(surveys.germanCreate.endingCard.buttonLabel);
|
||||
|
||||
// TODO: @pandeymangg - figure out if this is required
|
||||
await page.getByRole("button", { name: "Settings", exact: true }).click();
|
||||
|
||||
await page.locator("#howToSendCardTrigger").click();
|
||||
await expect(page.locator("#howToSendCardOption-link")).toBeVisible();
|
||||
await page.locator("#howToSendCardOption-link").click();
|
||||
|
||||
// Wait for any auto-save to complete before publishing
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.getByRole("button", { name: "Publish" }).click();
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await page.waitForURL(/\/environments\/[^/]+\/surveys\/[^/]+\/summary(\?.*)?$/, { timeout: 60000 });
|
||||
await page.getByLabel("Select Language").click();
|
||||
await page.getByText("German").click();
|
||||
await page.getByLabel("Copy survey link to clipboard").click();
|
||||
const germanSurveyUrl = await page.evaluate("navigator.clipboard.readText()");
|
||||
expect(germanSurveyUrl).toContain("lang=de");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Testing Survey with advanced logic", async () => {
|
||||
// 8 minutes
|
||||
test.setTimeout(1000 * 60 * 8);
|
||||
|
||||
@@ -45,28 +45,13 @@ How to deliver a specific language depends on the survey type (app or link surve
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Enable Multi-language Support">
|
||||
In the survey editor, scroll down to the **Multiple Languages** section at the bottom and enable the toggle next to it:
|
||||
|
||||

|
||||
|
||||
Choose a **Default Language** for your survey.
|
||||
|
||||
<Note>Changing the default language will reset all the translations you have made for the survey.</Note>
|
||||
</Step>
|
||||
|
||||
<Step title="Add Supported Languages">
|
||||
Add the languages from the dropdown that you want to support in your survey:
|
||||
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Preview and Translate Content">
|
||||
|
||||
You can now see the survey in the selected language by clicking on the language dropdown in any of the questions.
|
||||
|
||||
Now you can translate all survey content, including questions, options, and button placeholders, into the selected language.
|
||||
<Step title="Current editor behavior">
|
||||
Survey language definitions still come from **Configuration → Survey Languages**.
|
||||
|
||||
<Note>
|
||||
The old translation controls inside the Questions tab have been removed temporarily. In-editor translation
|
||||
editing will return with the upcoming dedicated **Language** tab.
|
||||
</Note>
|
||||
</Step>
|
||||
|
||||
<Step title="Publish Your Survey">
|
||||
|
||||
Reference in New Issue
Block a user