mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-22 14:10:45 -06:00
Compare commits
14 Commits
harsh/ios-
...
randomize-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5301fbd8f3 | ||
|
|
ad0ad9008c | ||
|
|
f813db65e9 | ||
|
|
d97e90a515 | ||
|
|
16170a73a6 | ||
|
|
be7de68d4f | ||
|
|
c4e4f0c272 | ||
|
|
14cfb6fdf8 | ||
|
|
d208e99f06 | ||
|
|
409fc8bb01 | ||
|
|
b078655f82 | ||
|
|
31eae02a78 | ||
|
|
b3ad45cce1 | ||
|
|
f7108b1d1e |
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
62
packages/ui/components/ShuffleOptionSelect/index.tsx
Normal file
62
packages/ui/components/ShuffleOptionSelect/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user