feat: surface option ids (#6339)

This commit is contained in:
Dhruwang Jariwala
2025-08-05 09:33:12 +05:30
committed by GitHub
parent 3b07a6d013
commit 287c45f996
31 changed files with 1689 additions and 275 deletions
@@ -12,9 +12,10 @@ vi.mock("@/modules/ui/components/file-upload-response", () => ({
),
}));
vi.mock("@/modules/ui/components/picture-selection-response", () => ({
PictureSelectionResponse: ({ selected, isExpanded }: any) => (
<div data-testid="PictureSelectionResponse">
PictureSelection: {selected.join(",")} ({isExpanded ? "expanded" : "collapsed"})
PictureSelectionResponse: ({ selected, isExpanded, showId }: any) => (
<div data-testid="PictureSelectionResponse" data-show-id={showId}>
PictureSelection: {selected.join(",")} ({isExpanded ? "expanded" : "collapsed"}) showId:{" "}
{String(showId)}
</div>
),
}));
@@ -22,10 +23,28 @@ vi.mock("@/modules/ui/components/array-response", () => ({
ArrayResponse: ({ value }: any) => <div data-testid="ArrayResponse">{value.join(",")}</div>,
}));
vi.mock("@/modules/ui/components/response-badges", () => ({
ResponseBadges: ({ items }: any) => <div data-testid="ResponseBadges">{items.join(",")}</div>,
ResponseBadges: ({ items, showId }: any) => (
<div data-testid="ResponseBadges" data-show-id={showId}>
{Array.isArray(items)
? items
.map((item) => (typeof item === "object" ? `${item.value}:${item.id || "no-id"}` : item))
.join(",")
: items}{" "}
showId: {String(showId)}
</div>
),
}));
vi.mock("@/modules/ui/components/ranking-response", () => ({
RankingResponse: ({ value }: any) => <div data-testid="RankingResponse">{value.join(",")}</div>,
RankingResponse: ({ value, showId }: any) => (
<div data-testid="RankingResponse" data-show-id={showId}>
{Array.isArray(value)
? value
.map((item) => (typeof item === "object" ? `${item.value}:${item.id || "no-id"}` : item))
.join(",")
: value}{" "}
showId: {String(showId)}
</div>
),
}));
vi.mock("@/modules/analysis/utils", () => ({
renderHyperlinkedContent: vi.fn((text: string) => "hyper:" + text),
@@ -50,7 +69,14 @@ describe("RenderResponse", () => {
});
const defaultSurvey = { languages: [] } as any;
const defaultQuestion = { id: "q1", type: "Unknown" } as any;
const defaultQuestion = {
id: "q1",
type: "Unknown",
choices: [
{ id: "choice1", label: { default: "Option 1" } },
{ id: "choice2", label: { default: "Option 2" } },
],
} as any;
const dummyLanguage = "default";
test("returns '-' for empty responseData (string)", () => {
@@ -60,6 +86,7 @@ describe("RenderResponse", () => {
question={defaultQuestion}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(container.textContent).toBe("-");
@@ -72,6 +99,7 @@ describe("RenderResponse", () => {
question={defaultQuestion}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(container.textContent).toBe("-");
@@ -84,6 +112,7 @@ describe("RenderResponse", () => {
question={defaultQuestion}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(container.textContent).toBe("-");
@@ -92,7 +121,13 @@ describe("RenderResponse", () => {
test("renders RatingResponse for 'Rating' question with number", () => {
const question = { ...defaultQuestion, type: "rating", scale: 5, range: [1, 5] };
render(
<RenderResponse responseData={4} question={question} survey={defaultSurvey} language={dummyLanguage} />
<RenderResponse
responseData={4}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByTestId("RatingResponse")).toHaveTextContent("Rating: 4");
});
@@ -106,6 +141,7 @@ describe("RenderResponse", () => {
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByText(/formatted_/)).toBeInTheDocument();
@@ -119,6 +155,7 @@ describe("RenderResponse", () => {
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByTestId("PictureSelectionResponse")).toHaveTextContent(
@@ -134,6 +171,7 @@ describe("RenderResponse", () => {
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByTestId("FileUploadResponse")).toHaveTextContent("FileUpload: file1,file2");
@@ -149,6 +187,7 @@ describe("RenderResponse", () => {
question={question}
survey={{ languages: [] } as any}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByText("row1:processed:answer1")).toBeInTheDocument();
@@ -163,6 +202,7 @@ describe("RenderResponse", () => {
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByTestId("ArrayResponse")).toHaveTextContent("addr1,addr2");
@@ -176,6 +216,7 @@ describe("RenderResponse", () => {
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("Value");
@@ -184,7 +225,13 @@ describe("RenderResponse", () => {
test("renders ResponseBadges for 'Consent' question (number)", () => {
const question = { ...defaultQuestion, type: "consent" };
render(
<RenderResponse responseData={5} question={question} survey={defaultSurvey} language={dummyLanguage} />
<RenderResponse
responseData={5}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("5");
});
@@ -197,56 +244,67 @@ describe("RenderResponse", () => {
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("Click");
});
test("renders ResponseBadges for 'MultipleChoiceSingle' question (string)", () => {
const question = { ...defaultQuestion, type: "multipleChoiceSingle" };
const question = { ...defaultQuestion, type: "multipleChoiceSingle", choices: [] };
render(
<RenderResponse
responseData={"option1"}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("option1");
});
test("renders ResponseBadges for 'MultipleChoiceMulti' question (array)", () => {
const question = { ...defaultQuestion, type: "multipleChoiceMulti" };
const question = { ...defaultQuestion, type: "multipleChoiceMulti", choices: [] };
render(
<RenderResponse
responseData={["opt1", "opt2"]}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("opt1,opt2");
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("opt1:other,opt2:other");
});
test("renders ResponseBadges for 'NPS' question (number)", () => {
const question = { ...defaultQuestion, type: "nps" };
render(
<RenderResponse responseData={9} question={question} survey={defaultSurvey} language={dummyLanguage} />
<RenderResponse
responseData={9}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByTestId("ResponseBadges")).toHaveTextContent("9");
// NPS questions render as simple text, not ResponseBadges
expect(screen.getByText("9")).toBeInTheDocument();
});
test("renders RankingResponse for 'Ranking' question", () => {
const question = { ...defaultQuestion, type: "ranking" };
const question = { ...defaultQuestion, type: "ranking", choices: [] };
render(
<RenderResponse
responseData={["first", "second"]}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByTestId("RankingResponse")).toHaveTextContent("first,second");
expect(screen.getByTestId("RankingResponse")).toHaveTextContent("first:other,second:other showId: false");
});
test("renders default branch for unknown question type with string", () => {
@@ -257,6 +315,7 @@ describe("RenderResponse", () => {
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByText("hyper:some text")).toBeInTheDocument();
@@ -270,8 +329,178 @@ describe("RenderResponse", () => {
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
expect(screen.getByText("a, b")).toBeInTheDocument();
});
// New tests for showId functionality
test("passes showId prop to PictureSelectionResponse", () => {
const question = {
...defaultQuestion,
type: "pictureSelection",
choices: [{ id: "choice1", imageUrl: "url1" }],
};
render(
<RenderResponse
responseData={["choice1"]}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={true}
/>
);
const component = screen.getByTestId("PictureSelectionResponse");
expect(component).toHaveAttribute("data-show-id", "true");
expect(component).toHaveTextContent("showId: true");
});
test("passes showId prop to RankingResponse with choice ID extraction", () => {
const question = {
...defaultQuestion,
type: "ranking",
choices: [
{ id: "choice1", label: { default: "Option 1" } },
{ id: "choice2", label: { default: "Option 2" } },
],
};
render(
<RenderResponse
responseData={["Option 1", "Option 2"]}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={true}
/>
);
const component = screen.getByTestId("RankingResponse");
expect(component).toHaveAttribute("data-show-id", "true");
expect(component).toHaveTextContent("showId: true");
// Should extract choice IDs and pass them as value objects
expect(component).toHaveTextContent("Option 1:choice1,Option 2:choice2");
});
test("handles ranking response with missing choice IDs", () => {
const question = {
...defaultQuestion,
type: "ranking",
choices: [
{ id: "choice1", label: { default: "Option 1" } },
{ id: "choice2", label: { default: "Option 2" } },
],
};
render(
<RenderResponse
responseData={["Option 1", "Unknown Option"]}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={true}
/>
);
const component = screen.getByTestId("RankingResponse");
expect(component).toHaveTextContent("Option 1:choice1,Unknown Option:other");
});
test("passes showId prop to ResponseBadges for multiple choice single", () => {
const question = {
...defaultQuestion,
type: "multipleChoiceSingle",
choices: [{ id: "choice1", label: { default: "Option 1" } }],
};
render(
<RenderResponse
responseData={"Option 1"}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={true}
/>
);
const component = screen.getByTestId("ResponseBadges");
expect(component).toHaveAttribute("data-show-id", "true");
expect(component).toHaveTextContent("showId: true");
expect(component).toHaveTextContent("Option 1:choice1");
});
test("passes showId prop to ResponseBadges for multiple choice multi", () => {
const question = {
...defaultQuestion,
type: "multipleChoiceMulti",
choices: [
{ id: "choice1", label: { default: "Option 1" } },
{ id: "choice2", label: { default: "Option 2" } },
],
};
render(
<RenderResponse
responseData={["Option 1", "Option 2"]}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={true}
/>
);
const component = screen.getByTestId("ResponseBadges");
expect(component).toHaveAttribute("data-show-id", "true");
expect(component).toHaveTextContent("showId: true");
expect(component).toHaveTextContent("Option 1:choice1,Option 2:choice2");
});
test("handles multiple choice with missing choice IDs", () => {
const question = {
...defaultQuestion,
type: "multipleChoiceMulti",
choices: [{ id: "choice1", label: { default: "Option 1" } }],
};
render(
<RenderResponse
responseData={["Option 1", "Unknown Option"]}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={true}
/>
);
const component = screen.getByTestId("ResponseBadges");
expect(component).toHaveTextContent("Option 1:choice1,Unknown Option:other");
});
test("passes showId=false to components when showId is false", () => {
const question = {
...defaultQuestion,
type: "multipleChoiceMulti",
choices: [{ id: "choice1", label: { default: "Option 1" } }],
};
render(
<RenderResponse
responseData={["Option 1"]}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={false}
/>
);
const component = screen.getByTestId("ResponseBadges");
expect(component).toHaveAttribute("data-show-id", "false");
expect(component).toHaveTextContent("showId: false");
// Should still extract IDs but showId=false
expect(component).toHaveTextContent("Option 1:choice1");
});
test("handles questions without choices property", () => {
const question = { ...defaultQuestion, type: "multipleChoiceSingle" }; // No choices property
render(
<RenderResponse
responseData={"Option 1"}
question={question}
survey={defaultSurvey}
language={dummyLanguage}
showId={true}
/>
);
const component = screen.getByTestId("ResponseBadges");
expect(component).toHaveTextContent("Option 1:choice1");
});
});
@@ -1,5 +1,6 @@
import { cn } from "@/lib/cn";
import { getLanguageCode, getLocalizedValue } from "@/lib/i18n/utils";
import { getChoiceIdByValue } from "@/lib/response/utils";
import { processResponseData } from "@/lib/responses";
import { formatDateWithOrdinal } from "@/lib/utils/datetime";
import { capitalizeFirstLetter } from "@/lib/utils/strings";
@@ -27,6 +28,7 @@ interface RenderResponseProps {
survey: TSurvey;
language: string | null;
isExpanded?: boolean;
showId: boolean;
}
export const RenderResponse: React.FC<RenderResponseProps> = ({
@@ -35,6 +37,7 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
survey,
language,
isExpanded = true,
showId,
}) => {
if (
(typeof responseData === "string" && responseData === "") ||
@@ -81,6 +84,7 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
choices={(question as TSurveyPictureSelectionQuestion).choices}
selected={responseData}
isExpanded={isExpanded}
showId={showId}
/>
);
}
@@ -121,9 +125,10 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
if (typeof responseData === "string" || typeof responseData === "number") {
return (
<ResponseBadges
items={[capitalizeFirstLetter(responseData.toString())]}
items={[{ value: capitalizeFirstLetter(responseData.toString()) }]}
isExpanded={isExpanded}
icon={<PhoneIcon className="h-4 w-4 text-slate-500" />}
showId={showId}
/>
);
}
@@ -132,9 +137,10 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
if (typeof responseData === "string" || typeof responseData === "number") {
return (
<ResponseBadges
items={[capitalizeFirstLetter(responseData.toString())]}
items={[{ value: capitalizeFirstLetter(responseData.toString()) }]}
isExpanded={isExpanded}
icon={<CheckCheckIcon className="h-4 w-4 text-slate-500" />}
showId={showId}
/>
);
}
@@ -143,26 +149,43 @@ export const RenderResponse: React.FC<RenderResponseProps> = ({
if (typeof responseData === "string" || typeof responseData === "number") {
return (
<ResponseBadges
items={[capitalizeFirstLetter(responseData.toString())]}
items={[{ value: capitalizeFirstLetter(responseData.toString()) }]}
isExpanded={isExpanded}
icon={<MousePointerClickIcon className="h-4 w-4 text-slate-500" />}
showId={showId}
/>
);
}
break;
case TSurveyQuestionTypeEnum.MultipleChoiceMulti:
case TSurveyQuestionTypeEnum.MultipleChoiceSingle:
case TSurveyQuestionTypeEnum.NPS:
case TSurveyQuestionTypeEnum.Ranking:
if (typeof responseData === "string" || typeof responseData === "number") {
return <ResponseBadges items={[responseData.toString()]} isExpanded={isExpanded} />;
const choiceId = getChoiceIdByValue(responseData.toString(), question);
return (
<ResponseBadges
items={[{ value: responseData.toString(), id: choiceId }]}
isExpanded={isExpanded}
showId={showId}
/>
);
} else if (Array.isArray(responseData)) {
return <ResponseBadges items={responseData} isExpanded={isExpanded} />;
const itemsArray = responseData.map((choice) => {
const choiceId = getChoiceIdByValue(choice, question);
return { value: choice, id: choiceId };
});
return (
<>
{questionType === TSurveyQuestionTypeEnum.Ranking ? (
<RankingResponse value={itemsArray} isExpanded={isExpanded} showId={showId} />
) : (
<ResponseBadges items={itemsArray} isExpanded={isExpanded} showId={showId} />
)}
</>
);
}
break;
case TSurveyQuestionTypeEnum.Ranking:
if (Array.isArray(responseData)) {
return <RankingResponse value={responseData} isExpanded={isExpanded} />;
}
default:
if (
typeof responseData === "string" ||
@@ -76,7 +76,7 @@ export const SingleResponseCardBody = ({
<div key={`${question.id}`}>
{isValidValue(response.data[question.id]) ? (
<div>
<p className="text-sm text-slate-500">
<p className="mb-1 text-sm text-slate-500">
{formatTextWithSlashes(
parseRecallInfo(
getLocalizedValue(question.headline, "default"),
@@ -92,6 +92,7 @@ export const SingleResponseCardBody = ({
survey={survey}
responseData={response.data[question.id]}
language={response.language}
showId={true}
/>
</div>
</div>
@@ -3,6 +3,7 @@
import { timeSince } from "@/lib/time";
import { getContactIdentifier } from "@/lib/utils/contact";
import { PersonAvatar } from "@/modules/ui/components/avatars";
import { IdBadge } from "@/modules/ui/components/id-badge";
import { SurveyStatusIndicator } from "@/modules/ui/components/survey-status-indicator";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
import { useTranslate } from "@tolgee/react";
@@ -162,19 +163,21 @@ export const SingleResponseCardHeader = ({
{response.contact?.id ? (
user ? (
<Link
className="flex items-center"
className="flex items-center space-x-2"
href={`/environments/${environmentId}/contacts/${response.contact.id}`}>
<PersonAvatar personId={response.contact.id} />
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600 hover:underline">
{displayIdentifier}
</h3>
{response.contact.userId && <IdBadge id={response.contact.userId} />}
</Link>
) : (
<div className="flex items-center">
<div className="flex items-center space-x-2">
<PersonAvatar personId={response.contact.id} />
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600">
{displayIdentifier}
</h3>
{response.contact.userId && <IdBadge id={response.contact.userId} />}
</div>
)
) : (