feat: Placeholder input for contact and address question (#4549)

Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Dhruwang Jariwala
2025-01-09 15:15:24 +05:30
committed by GitHub
parent 6e1ee6df12
commit d197c91995
15 changed files with 515 additions and 115 deletions
@@ -83,9 +83,7 @@ export const AddressQuestionForm = ({
.filter((field) => field.show)
.every((field) => !field.required);
if (allFieldsAreOptional) {
updateQuestion(questionIdx, { required: false });
}
updateQuestion(questionIdx, { required: !allFieldsAreOptional });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
question.addressLine1,
@@ -152,25 +150,13 @@ export const AddressQuestionForm = ({
<QuestionToggleTable
type="address"
fields={fields}
onShowToggle={(field, show) => {
updateQuestion(questionIdx, {
[field.id]: {
show,
required: field.required,
},
// when show changes, and the field is required, the question should be required
...(show && field.required && { required: true }),
});
}}
onRequiredToggle={(field, required) => {
updateQuestion(questionIdx, {
[field.id]: {
show: field.show,
required,
},
required: true,
});
}}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
/>
</div>
</form>
@@ -78,9 +78,8 @@ export const ContactInfoQuestionForm = ({
.filter((field) => field.show)
.every((field) => !field.required);
if (allFieldsAreOptional) {
updateQuestion(questionIdx, { required: false });
}
updateQuestion(questionIdx, { required: !allFieldsAreOptional });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [question.firstName, question.lastName, question.email, question.phone, question.company]);
@@ -142,25 +141,13 @@ export const ContactInfoQuestionForm = ({
<QuestionToggleTable
type="contact"
fields={fields}
onShowToggle={(field, show) => {
updateQuestion(questionIdx, {
[field.id]: {
show,
required: field.required,
},
// when show changes, and the field is required, the question should be required
...(show && field.required && { required: true }),
});
}}
onRequiredToggle={(field, required) => {
updateQuestion(questionIdx, {
[field.id]: {
show: field.show,
required,
},
required: true,
});
}}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
/>
</div>
</form>
@@ -108,6 +108,21 @@ export const QuestionCard = ({
const getIsRequiredToggleDisabled = (): boolean => {
if (question.type === TSurveyQuestionTypeEnum.Address) {
const allFieldsAreOptional = [
question.addressLine1,
question.addressLine2,
question.city,
question.state,
question.zip,
question.country,
]
.filter((field) => field.show)
.every((field) => !field.required);
if (allFieldsAreOptional) {
return true;
}
return [
question.addressLine1,
question.addressLine2,
@@ -121,6 +136,20 @@ export const QuestionCard = ({
}
if (question.type === TSurveyQuestionTypeEnum.ContactInfo) {
const allFieldsAreOptional = [
question.firstName,
question.lastName,
question.email,
question.phone,
question.company,
]
.filter((field) => field.show)
.every((field) => !field.required);
if (allFieldsAreOptional) {
return true;
}
return [question.firstName, question.lastName, question.email, question.phone, question.company]
.filter((field) => field.show)
.some((condition) => condition.required === true);
@@ -6,9 +6,12 @@ import { checkForEmptyFallBackValue } from "@formbricks/lib/utils/recall";
import { ZSegmentFilters } from "@formbricks/types/segment";
import {
TI18nString,
TInputFieldConfig,
TSurvey,
TSurveyAddressQuestion,
TSurveyCTAQuestion,
TSurveyConsentQuestion,
TSurveyContactInfoQuestion,
TSurveyEndScreenCard,
TSurveyLanguage,
TSurveyMatrixQuestion,
@@ -67,6 +70,26 @@ const handleI18nCheckForMatrixLabels = (
return rowsAndColumns.every((label) => isLabelValidForAllLanguages(label, languages));
};
const handleI18nCheckForContactAndAddressFields = (
question: TSurveyContactInfoQuestion | TSurveyAddressQuestion,
languages: TSurveyLanguage[]
): boolean => {
let fields: TInputFieldConfig[] = [];
if (question.type === "contactInfo") {
const { firstName, lastName, phone, email, company } = question;
fields = [firstName, lastName, phone, email, company];
} else if (question.type === "address") {
const { addressLine1, addressLine2, city, state, zip, country } = question;
fields = [addressLine1, addressLine2, city, state, zip, country];
}
return fields.every((field) => {
if (field.show) {
return isLabelValidForAllLanguages(field.placeholder, languages);
}
return true;
});
};
// Validation rules
export const validationRules = {
openText: (question: TSurveyOpenTextQuestion, languages: TSurveyLanguage[]) => {
@@ -96,6 +119,12 @@ export const validationRules = {
matrix: (question: TSurveyMatrixQuestion, languages: TSurveyLanguage[]) => {
return handleI18nCheckForMatrixLabels(question, languages);
},
contactInfo: (question: TSurveyContactInfoQuestion, languages: TSurveyLanguage[]) => {
return handleI18nCheckForContactAndAddressFields(question, languages);
},
address: (question: TSurveyAddressQuestion, languages: TSurveyLanguage[]) => {
return handleI18nCheckForContactAndAddressFields(question, languages);
},
// Assuming headline is of type TI18nString
defaultValidation: (question: TSurveyQuestion, languages: TSurveyLanguage[], isFirstQuestion: boolean) => {
// headline and subheader are default for every question
@@ -25,7 +25,7 @@ export function LanguageIndicator({
const languageDropdownRef = useRef(null);
const changeLanguage = (language: TSurveyLanguage) => {
setSelectedLanguageCode(language.language.code);
setSelectedLanguageCode(language.default ? "default" : language.language.code);
if (setFirstRender) {
//for lexical editor
setFirstRender(true);
@@ -148,7 +148,12 @@ export const QuestionFormInput = ({
}
return (
(question && (question[id as keyof TSurveyQuestion] as TI18nString)) ||
(question &&
(id.includes(".")
? // Handle nested properties
(question[id.split(".")[0] as keyof TSurveyQuestion] as any)?.[id.split(".")[1]]
: // Original behavior
(question[id as keyof TSurveyQuestion] as TI18nString))) ||
createI18nString("", surveyLanguageCodes)
);
}, [
@@ -340,10 +345,22 @@ export const QuestionFormInput = ({
const updateQuestionDetails = useCallback(
(translatedText: TI18nString) => {
if (updateQuestion) {
updateQuestion(questionIdx, { [id]: translatedText });
// Handle nested properties if id contains a dot
if (id.includes(".")) {
const [parent, child] = id.split(".");
updateQuestion(questionIdx, {
[parent]: {
...question[parent],
[child]: translatedText,
},
});
} else {
// Original behavior for non-nested properties
updateQuestion(questionIdx, { [id]: translatedText });
}
}
},
[id, questionIdx, updateQuestion]
[id, questionIdx, updateQuestion, question]
);
const handleUpdate = useCallback(
@@ -1,5 +1,13 @@
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { Switch } from "@/modules/ui/components/switch";
import { useTranslations } from "next-intl";
import {
TI18nString,
TSurvey,
TSurveyAddressQuestion,
TSurveyContactInfoQuestion,
} from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
interface QuestionToggleTableProps {
type: "address" | "contact";
@@ -8,48 +16,75 @@ interface QuestionToggleTableProps {
show: boolean;
id: string;
label: string;
placeholder: TI18nString;
}[];
onShowToggle: (
field: {
id: string;
required: boolean;
show: boolean;
},
show: boolean
) => void;
onRequiredToggle: (
field: {
id: string;
show: boolean;
required: boolean;
},
required: boolean
localSurvey: TSurvey;
questionIdx: number;
isInvalid: boolean;
updateQuestion: (
questionIdx: number,
updatedAttributes: Partial<TSurveyContactInfoQuestion | TSurveyAddressQuestion>
) => void;
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
locale: TUserLocale;
}
export const QuestionToggleTable = ({
type,
fields,
onShowToggle,
onRequiredToggle,
localSurvey,
questionIdx,
isInvalid,
updateQuestion,
selectedLanguageCode,
setSelectedLanguageCode,
locale,
}: QuestionToggleTableProps) => {
const onShowToggle = (
field: { id: string; show: boolean; required: boolean; placeholder: TI18nString },
show: boolean
) => {
updateQuestion(questionIdx, {
[field.id]: {
show,
required: field.required,
placeholder: field.placeholder,
},
});
};
const onRequiredToggle = (
field: { id: string; show: boolean; required: boolean; placeholder: TI18nString },
required: boolean
) => {
updateQuestion(questionIdx, {
[field.id]: {
show: field.show,
required,
placeholder: field.placeholder,
},
});
};
const t = useTranslations();
return (
<table className="mt-4 w-1/2 table-fixed">
<table className="mt-4 w-full table-fixed">
<thead>
<tr className="text-left text-slate-800">
<th className="w-1/2 text-sm font-semibold">
<th className="w-1/4 text-sm font-semibold">
{type === "address"
? t("environments.surveys.edit.address_fields")
: t("environments.surveys.edit.contact_fields")}
</th>
<th className="w-1/4 text-sm font-semibold">{t("common.show")}</th>
<th className="w-1/4 text-sm font-semibold">{t("environments.surveys.edit.required")}</th>
<th className="w-1/6 text-sm font-semibold">{t("common.show")}</th>
<th className="w-1/6 text-sm font-semibold">{t("environments.surveys.edit.required")}</th>
<th className="text-sm font-semibold">{t("common.placeholder")}</th>
</tr>
</thead>
<tbody>
{fields.map((field) => (
<tr className="text-slate-900">
<tr className="text-slate-900" key={field.id}>
<td className="py-2 text-sm">{field.label}</td>
<td className="py-">
<Switch
@@ -72,6 +107,21 @@ export const QuestionToggleTable = ({
disabled={!field.show}
/>
</td>
<td className="py-2">
<QuestionFormInput
id={`${field.id}.placeholder`}
label={""}
value={field.placeholder}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
contactAttributeKeys={[]}
locale={locale}
/>
</td>
</tr>
))}
</tbody>
+46 -4
View File
@@ -184,8 +184,16 @@ test.describe("Survey Create & Submit Response without logic", async () => {
// Address Question
await expect(page.getByText(surveys.createAndSubmit.address.question)).toBeVisible();
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder)).toBeVisible();
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder).fill("This is my Address");
await expect(
page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.addressLine1)
).toBeVisible();
await page
.getByPlaceholder(surveys.createAndSubmit.address.placeholder.addressLine1)
.fill("This is my Address");
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.city)).toBeVisible();
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.city).fill("This is my city");
await expect(page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.zip)).toBeVisible();
await page.getByPlaceholder(surveys.createAndSubmit.address.placeholder.zip).fill("12345");
await page.locator("#questionCard-10").getByRole("button", { name: "Next" }).click();
// Contact Info Question
@@ -523,6 +531,28 @@ test.describe("Multi Language Survey Create", async () => {
await page
.getByPlaceholder("Your question here. Recall")
.fill(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 Advanced settings").first().click();
await page.getByPlaceholder("Next").click();
await page.getByPlaceholder("Next").fill(surveys.germanCreate.next);
@@ -810,10 +840,22 @@ test.describe("Testing Survey with advanced logic", async () => {
// Address Question
await expect(page.getByText(surveys.createWithLogicAndSubmit.address.question)).toBeVisible();
await expect(page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder)).toBeVisible();
await expect(
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
).toBeVisible();
await page
.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder)
.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.addressLine1)
.fill("This is my Address");
await expect(
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.city)
).toBeVisible();
await page
.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.city)
.fill("This is my city");
await expect(
page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.zip)
).toBeVisible();
await page.getByPlaceholder(surveys.createWithLogicAndSubmit.address.placeholder.zip).fill("12345");
await page.locator("#questionCard-13").getByRole("button", { name: "Next" }).click();
// loading spinner -> wait for it to disappear
+18 -2
View File
@@ -158,7 +158,11 @@ export const surveys = {
},
address: {
question: "Where do you live?",
placeholder: "Address Line 1",
placeholder: {
addressLine1: "Address Line 1",
city: "City",
zip: "Zip",
},
},
contactInfo: {
question: "Contact Info",
@@ -233,7 +237,11 @@ export const surveys = {
},
address: {
question: "Where do you live?",
placeholder: "Address Line 1",
placeholder: {
addressLine1: "Address Line 1",
city: "City",
zip: "Zip",
},
},
ranking: {
question: "This is my Ranking Question",
@@ -307,6 +315,14 @@ export const surveys = {
},
addressQuestion: {
question: "Wo wohnst du ?",
placeholder: {
addressLine1: "Adresse",
addressLine2: "Adresse",
city: "Adresse",
state: "Adresse",
zip: "Adresse",
country: "Adresse",
},
},
ranking: {
question: "Was ist für Sie im Leben am wichtigsten?",