Compare commits

...

14 Commits

Author SHA1 Message Date
Dhruwang
5301fbd8f3 fix shuffle in ranking question 2024-10-04 17:20:05 +05:30
Dhruwang
ad0ad9008c refactors 2024-10-04 15:31:43 +05:30
Dhruwang
f813db65e9 Merge branch 'main' of https://github.com/formbricks/formbricks into randomize-row-order-matrix-questions 2024-10-04 14:40:42 +05:30
SaiSawant1
d97e90a515 ShuffleOptionType 2024-10-04 02:29:52 +05:30
SaiSawant1
16170a73a6 Merge branch 'main' into randomize-row-order-matrix-questions 2024-10-04 02:15:20 +05:30
SaiSawant1
be7de68d4f create shuffle option select component in
packages/ui/components/ShuffleOptionSelect
which is imported in MatrixQuestionForm, MultipleChoiceQuestionForm,
RankingQuestionForm
2024-10-04 01:20:32 +05:30
SaiSawant1
c4e4f0c272 Merge branch 'main' into randomize-row-order-matrix-questions 2024-10-03 20:12:46 +05:30
SaiSawant1
14cfb6fdf8 Preview error fix.
The preview matrix question was not being updated when questions was
being updated.
2024-10-03 20:10:10 +05:30
SaiSawant1
d208e99f06 Merge branch 'main' into randomize-row-order-matrix-questions 2024-10-03 14:33:32 +05:30
SaiSawant1
409fc8bb01 random error fix 2024-10-02 02:04:33 +05:30
SaiSawant1
b078655f82 set default shuffle to none 2024-10-01 08:08:30 +05:30
SaiSawant1
31eae02a78 create copy of row and shuffle the copy instead of original row 2024-10-01 01:36:28 +05:30
SaiSawant1
b3ad45cce1 build error fix 2024-10-01 00:37:33 +05:30
SaiSawant1
f7108b1d1e feature allow user to randomize rows. 2024-10-01 00:30:41 +05:30
8 changed files with 162 additions and 72 deletions

View File

@@ -7,6 +7,7 @@ import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/s
import { Button } from "@formbricks/ui/components/Button"; import { Button } from "@formbricks/ui/components/Button";
import { Label } from "@formbricks/ui/components/Label"; import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput"; import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { ShuffleOptionSelect } from "@formbricks/ui/components/ShuffleOptionSelect";
import { isLabelValidForAllLanguages } from "../lib/validation"; import { isLabelValidForAllLanguages } from "../lib/validation";
interface MatrixQuestionFormProps { interface MatrixQuestionFormProps {
@@ -78,6 +79,24 @@ export const MatrixQuestionForm = ({
} }
}; };
const shuffleOptionsTypes = {
none: {
id: "none",
label: "Keep current order",
show: true,
},
all: {
id: "all",
label: "Randomize all",
show: true,
},
exceptLast: {
id: "exceptLast",
label: "Randomize all except last option",
show: true,
},
};
return ( return (
<form> <form>
<QuestionFormInput <QuestionFormInput
@@ -211,6 +230,14 @@ export const MatrixQuestionForm = ({
<span>Add column</span> <span>Add column</span>
</Button> </Button>
</div> </div>
<div className="mt-3 flex flex-1 items-center justify-end gap-2">
<ShuffleOptionSelect
shuffleOptionsTypes={shuffleOptionsTypes}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
shuffleOption={question.shuffleOption}
/>
</div>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -19,13 +19,7 @@ import {
import { Button } from "@formbricks/ui/components/Button"; import { Button } from "@formbricks/ui/components/Button";
import { Label } from "@formbricks/ui/components/Label"; import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput"; import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { import { ShuffleOptionSelect } from "@formbricks/ui/components/ShuffleOptionSelect";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@formbricks/ui/components/Select";
import { QuestionOptionChoice } from "./QuestionOptionChoice"; import { QuestionOptionChoice } from "./QuestionOptionChoice";
interface OpenQuestionFormProps { interface OpenQuestionFormProps {
@@ -290,29 +284,12 @@ export const MultipleChoiceQuestionForm = ({
</Button> </Button>
<div className="flex flex-1 items-center justify-end gap-2"> <div className="flex flex-1 items-center justify-end gap-2">
<Select <ShuffleOptionSelect
defaultValue={question.shuffleOption} questionIdx={questionIdx}
value={question.shuffleOption} shuffleOption={question.shuffleOption}
onValueChange={(e: TShuffleOption) => { updateQuestion={updateQuestion}
updateQuestion(questionIdx, { shuffleOption: e }); shuffleOptionsTypes={shuffleOptionsTypes}
}}> />
<SelectTrigger className="w-fit space-x-2 overflow-hidden border-0 font-medium text-slate-600">
<SelectValue placeholder="Select ordering" />
</SelectTrigger>
<SelectContent>
{Object.values(shuffleOptionsTypes).map(
(shuffleOptionsType) =>
shuffleOptionsType.show && (
<SelectItem
key={shuffleOptionsType.id}
value={shuffleOptionsType.id}
title={shuffleOptionsType.label}>
{shuffleOptionsType.label}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,22 +7,11 @@ import { PlusIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils"; import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes"; import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { import { TI18nString, TSurvey, TSurveyRankingQuestion } from "@formbricks/types/surveys/types";
TI18nString,
TShuffleOption,
TSurvey,
TSurveyRankingQuestion,
} from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/components/Button"; import { Button } from "@formbricks/ui/components/Button";
import { Label } from "@formbricks/ui/components/Label"; import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput"; import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { import { ShuffleOptionSelect } from "@formbricks/ui/components/ShuffleOptionSelect";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@formbricks/ui/components/Select";
import { QuestionOptionChoice } from "./QuestionOptionChoice"; import { QuestionOptionChoice } from "./QuestionOptionChoice";
interface RankingQuestionFormProps { interface RankingQuestionFormProps {
@@ -227,29 +216,12 @@ export const RankingQuestionForm = ({
onClick={() => addOption()}> onClick={() => addOption()}>
Add option Add option
</Button> </Button>
<Select <ShuffleOptionSelect
defaultValue={question.shuffleOption} shuffleOptionsTypes={shuffleOptionsTypes}
value={question.shuffleOption} updateQuestion={updateQuestion}
onValueChange={(option: TShuffleOption) => { shuffleOption={question.shuffleOption}
updateQuestion(questionIdx, { shuffleOption: option }); questionIdx={questionIdx}
}}> />
<SelectTrigger className="w-fit space-x-2 overflow-hidden border-0 font-medium text-slate-600">
<SelectValue placeholder="Select ordering" />
</SelectTrigger>
<SelectContent>
{Object.values(shuffleOptionsTypes).map(
(shuffleOptionsType) =>
shuffleOptionsType.show && (
<SelectItem
key={shuffleOptionsType.id}
value={shuffleOptionsType.id}
title={shuffleOptionsType.label}>
{shuffleOptionsType.label}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,6 +5,7 @@ import { QuestionMedia } from "@/components/general/QuestionMedia";
import { Subheader } from "@/components/general/Subheader"; import { Subheader } from "@/components/general/Subheader";
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer"; import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { getUpdatedTtc, useTtc } from "@/lib/ttc";
import { getShuffledRowIndices } from "@/lib/utils";
import { JSX } from "preact"; import { JSX } from "preact";
import { useCallback, useMemo, useState } from "preact/hooks"; import { useCallback, useMemo, useState } from "preact/hooks";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
@@ -42,6 +43,27 @@ export const MatrixQuestion = ({
const isMediaAvailable = question.imageUrl || question.videoUrl; const isMediaAvailable = question.imageUrl || question.videoUrl;
useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId); useTtc(question.id, ttc, setTtc, startTime, setStartTime, question.id === currentQuestionId);
const rowShuffleIdx = useMemo(() => {
if (question.shuffleOption) {
return getShuffledRowIndices(question.rows.length, question.shuffleOption);
} else {
return question.rows.map((_, id) => id);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [question.shuffleOption, question.rows.length]);
const questionRows = useMemo(() => {
if (!question.rows) {
return [];
}
if (question.shuffleOption === "none" || question.shuffleOption === undefined) {
return question.rows;
}
return rowShuffleIdx.map((shuffledIdx) => {
return question.rows[shuffledIdx];
});
}, [question.shuffleOption, question.rows, rowShuffleIdx]);
const handleSelect = useCallback( const handleSelect = useCallback(
(column: string, row: string) => { (column: string, row: string) => {
let responseValue = let responseValue =
@@ -116,7 +138,7 @@ export const MatrixQuestion = ({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{question.rows.map((row, rowIndex) => ( {questionRows.map((row, rowIndex) => (
// Table rows // Table rows
<tr className={`${rowIndex % 2 === 0 ? "bg-input-bg" : ""}`}> <tr className={`${rowIndex % 2 === 0 ? "bg-input-bg" : ""}`}>
<td <td

View File

@@ -5,7 +5,7 @@ import { QuestionMedia } from "@/components/general/QuestionMedia";
import { Subheader } from "@/components/general/Subheader"; import { Subheader } from "@/components/general/Subheader";
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer"; import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
import { getUpdatedTtc, useTtc } from "@/lib/ttc"; import { getUpdatedTtc, useTtc } from "@/lib/ttc";
import { cn } from "@/lib/utils"; import { cn, getShuffledChoicesIds } from "@/lib/utils";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useCallback, useMemo, useState } from "preact/hooks"; import { useCallback, useMemo, useState } from "preact/hooks";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
@@ -48,9 +48,18 @@ export const RankingQuestion = ({
.filter((item): item is TSurveyQuestionChoice => item !== undefined) .filter((item): item is TSurveyQuestionChoice => item !== undefined)
); );
const [unsortedItems, setUnsortedItems] = useState<TSurveyQuestionChoice[]>( const [unsortedItems, setUnsortedItems] = useState<TSurveyQuestionChoice[]>(() => {
question.choices.filter((c) => !value.includes(c.id)) if (!question.shuffleOption || question.shuffleOption === "none" || sortedItems.length > 1) {
); // Return unshuffled items
return question.choices.filter((c) => !value.includes(c.id));
} else {
// Shuffle options
const shuffledChoiceIds = getShuffledChoicesIds(question.choices, question.shuffleOption);
return shuffledChoiceIds
.map((choiceId) => question.choices.find((choice) => choice.id === choiceId))
.filter((choice) => choice !== undefined);
}
});
const [parent] = useAutoAnimate(); const [parent] = useAutoAnimate();

View File

@@ -1,4 +1,5 @@
import { import {
TShuffleOption,
TSurvey, TSurvey,
TSurveyLogic, TSurveyLogic,
TSurveyLogicAction, TSurveyLogicAction,
@@ -17,7 +18,26 @@ const shuffle = (array: any[]) => {
} }
}; };
export const getShuffledChoicesIds = (choices: TSurveyQuestionChoice[], shuffleOption: string): string[] => { export const getShuffledRowIndices = (n: number, shuffleOption: TShuffleOption): number[] => {
// Create an array with numbers from 0 to n-1
let array = Array.from(Array(n).keys());
if (shuffleOption === "all") {
shuffle(array);
} else if (shuffleOption === "exceptLast") {
const lastElement = array.pop();
if (lastElement) {
shuffle(array);
array.push(lastElement);
}
}
return array;
};
export const getShuffledChoicesIds = (
choices: TSurveyQuestionChoice[],
shuffleOption: TShuffleOption
): string[] => {
const otherOption = choices.find((choice) => { const otherOption = choices.find((choice) => {
return choice.id === "other"; return choice.id === "other";
}); });

View File

@@ -573,6 +573,7 @@ export const ZSurveyMatrixQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.Matrix), type: z.literal(TSurveyQuestionTypeEnum.Matrix),
rows: z.array(ZI18nString), rows: z.array(ZI18nString),
columns: z.array(ZI18nString), columns: z.array(ZI18nString),
shuffleOption: ZShuffleOption.optional().default("none"),
}); });
export type TSurveyMatrixQuestion = z.infer<typeof ZSurveyMatrixQuestion>; export type TSurveyMatrixQuestion = z.infer<typeof ZSurveyMatrixQuestion>;

View File

@@ -0,0 +1,62 @@
import {
TShuffleOption,
TSurveyMatrixQuestion,
TSurveyMultipleChoiceQuestion,
TSurveyRankingQuestion,
} from "@formbricks/types/surveys/types";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../Select";
interface ShuffleOptionType {
id: string;
label: string;
show: boolean;
}
interface ShuffleOptionsTypes {
none?: ShuffleOptionType;
all?: ShuffleOptionType;
exceptLast?: ShuffleOptionType;
}
interface ShuffleOptionSelectProps {
shuffleOption: TShuffleOption | undefined;
updateQuestion: (
questionIdx: number,
updatedAttributes: Partial<TSurveyMatrixQuestion | TSurveyMultipleChoiceQuestion | TSurveyRankingQuestion>
) => void;
questionIdx: number;
shuffleOptionsTypes: ShuffleOptionsTypes;
}
export const ShuffleOptionSelect: React.FC<ShuffleOptionSelectProps> = ({
questionIdx,
shuffleOption,
updateQuestion,
shuffleOptionsTypes,
}) => {
return (
<Select
defaultValue={shuffleOption}
value={shuffleOption}
onValueChange={(e: TShuffleOption) => {
updateQuestion(questionIdx, { shuffleOption: e });
}}>
<SelectTrigger className="w-fit space-x-2 overflow-hidden border-0 font-medium text-slate-600">
<SelectValue placeholder="Select ordering" />
</SelectTrigger>
<SelectContent>
{Object.values(shuffleOptionsTypes).map(
(shuffleOptionsType) =>
shuffleOptionsType.show && (
<SelectItem
key={shuffleOptionsType.id}
value={shuffleOptionsType.id}
title={shuffleOptionsType.label}>
{shuffleOptionsType.label}
</SelectItem>
)
)}
</SelectContent>
</Select>
);
};