mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 05:40:02 -06:00
feat: recall from hidden fields and attributes (#2601)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com> Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
f917d2171e
commit
5b78487b94
@@ -4,6 +4,7 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys";
|
||||
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
@@ -18,6 +19,7 @@ interface AddressQuestionFormProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const AddressQuestionForm = ({
|
||||
@@ -28,6 +30,7 @@ export const AddressQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: AddressQuestionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
|
||||
@@ -43,6 +46,7 @@ export const AddressQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -58,6 +62,7 @@ export const AddressQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import { LogicEditor } from "./LogicEditor";
|
||||
@@ -8,6 +9,7 @@ interface AdvancedSettingsProps {
|
||||
questionIdx: number;
|
||||
localSurvey: TSurvey;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const AdvancedSettings = ({
|
||||
@@ -15,6 +17,7 @@ export const AdvancedSettings = ({
|
||||
questionIdx,
|
||||
localSurvey,
|
||||
updateQuestion,
|
||||
attributeClasses,
|
||||
}: AdvancedSettingsProps) => {
|
||||
return (
|
||||
<div>
|
||||
@@ -24,6 +27,7 @@ export const AdvancedSettings = ({
|
||||
updateQuestion={updateQuestion}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -18,6 +19,7 @@ interface CTAQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const CTAQuestionForm = ({
|
||||
@@ -29,6 +31,7 @@ export const CTAQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: CTAQuestionFormProps): JSX.Element => {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
|
||||
@@ -43,6 +46,7 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div className="mt-3">
|
||||
@@ -95,6 +99,7 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
{questionIdx !== 0 && (
|
||||
@@ -109,6 +114,7 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -143,6 +149,7 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
@@ -17,6 +18,7 @@ interface CalQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const CalQuestionForm = ({
|
||||
@@ -27,6 +29,7 @@ export const CalQuestionForm = ({
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
attributeClasses,
|
||||
}: CalQuestionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -42,6 +45,7 @@ export const CalQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
@@ -56,6 +60,7 @@ export const CalQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
@@ -15,6 +16,7 @@ interface ConsentQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const ConsentQuestionForm = ({
|
||||
@@ -25,6 +27,7 @@ export const ConsentQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: ConsentQuestionFormProps): JSX.Element => {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
|
||||
@@ -39,6 +42,7 @@ export const ConsentQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div className="mt-3">
|
||||
@@ -70,6 +74,7 @@ export const ConsentQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -17,6 +18,7 @@ interface IDateQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
const dateOptions = [
|
||||
@@ -42,6 +44,7 @@ export const DateQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: IDateQuestionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -57,6 +60,7 @@ export const DateQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
@@ -71,6 +75,7 @@ export const DateQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -19,6 +20,7 @@ interface EditThankYouCardProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const EditThankYouCard = ({
|
||||
@@ -29,6 +31,7 @@ export const EditThankYouCard = ({
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: EditThankYouCardProps) => {
|
||||
// const [open, setOpen] = useState(false);
|
||||
let open = activeQuestionId == "end";
|
||||
@@ -117,6 +120,7 @@ export const EditThankYouCard = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<QuestionFormInput
|
||||
@@ -128,6 +132,7 @@ export const EditThankYouCard = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
@@ -170,6 +175,7 @@ export const EditThankYouCard = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useState } from "react";
|
||||
|
||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { FileInput } from "@formbricks/ui/FileInput";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -20,6 +21,7 @@ interface EditWelcomeCardProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const EditWelcomeCard = ({
|
||||
@@ -30,6 +32,7 @@ export const EditWelcomeCard = ({
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: EditWelcomeCardProps) => {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const path = usePathname();
|
||||
@@ -130,6 +133,7 @@ export const EditWelcomeCard = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
@@ -164,6 +168,7 @@ export const EditWelcomeCard = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { toast } from "react-hot-toast";
|
||||
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { createI18nString } from "@formbricks/lib/i18n/utils";
|
||||
import { useGetBillingInfo } from "@formbricks/lib/organization/hooks/useGetBillingInfo";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
|
||||
@@ -25,6 +26,7 @@ interface FileUploadFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const FileUploadQuestionForm = ({
|
||||
@@ -36,6 +38,7 @@ export const FileUploadQuestionForm = ({
|
||||
product,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: FileUploadFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const [extension, setExtension] = useState("");
|
||||
@@ -121,6 +124,7 @@ export const FileUploadQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
@@ -135,6 +139,7 @@ export const FileUploadQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -94,18 +94,18 @@ export const HiddenFieldsCard = ({
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||
<div className="flex gap-2">
|
||||
{localSurvey.hiddenFields?.fieldIds && localSurvey.hiddenFields?.fieldIds?.length > 0 ? (
|
||||
localSurvey.hiddenFields?.fieldIds?.map((question) => {
|
||||
localSurvey.hiddenFields?.fieldIds?.map((fieldId) => {
|
||||
return (
|
||||
<Tag
|
||||
key={question}
|
||||
key={fieldId}
|
||||
onDelete={() => {
|
||||
updateSurvey({
|
||||
enabled: true,
|
||||
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== question),
|
||||
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== fieldId),
|
||||
});
|
||||
}}
|
||||
tagId={question}
|
||||
tagName={question}
|
||||
tagId={fieldId}
|
||||
tagName={fieldId}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -11,7 +11,8 @@ import { toast } from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyLogic,
|
||||
@@ -36,6 +37,7 @@ interface LogicEditorProps {
|
||||
questionIdx: number;
|
||||
question: TSurveyQuestion;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
type LogicConditions = {
|
||||
@@ -47,11 +49,17 @@ type LogicConditions = {
|
||||
};
|
||||
};
|
||||
|
||||
export const LogicEditor = ({ localSurvey, question, questionIdx, updateQuestion }: LogicEditorProps) => {
|
||||
export const LogicEditor = ({
|
||||
localSurvey,
|
||||
question,
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
attributeClasses,
|
||||
}: LogicEditorProps) => {
|
||||
const [searchValue, setSearchValue] = useState<string>("");
|
||||
localSurvey = useMemo(() => {
|
||||
return checkForRecallInHeadline(localSurvey, "default");
|
||||
}, [localSurvey]);
|
||||
return replaceHeadlineRecall(localSurvey, "default", attributeClasses);
|
||||
}, [localSurvey, attributeClasses]);
|
||||
|
||||
const questionValues = useMemo(() => {
|
||||
if ("choices" in question) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { createI18nString, extractLanguageCodes, getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -21,6 +22,7 @@ interface MatrixQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const MatrixQuestionForm = ({
|
||||
@@ -31,6 +33,7 @@ export const MatrixQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: MatrixQuestionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const languageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -110,6 +113,7 @@ export const MatrixQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
@@ -124,6 +128,7 @@ export const MatrixQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -168,6 +173,7 @@ export const MatrixQuestionForm = ({
|
||||
isInvalid={
|
||||
isInvalid && !isLabelValidForAllLanguages(question.rows[index], localSurvey.languages)
|
||||
}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
{question.rows.length > 2 && (
|
||||
<TrashIcon
|
||||
@@ -209,6 +215,7 @@ export const MatrixQuestionForm = ({
|
||||
isInvalid={
|
||||
isInvalid && !isLabelValidForAllLanguages(question.columns[index], localSurvey.languages)
|
||||
}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
{question.columns.length > 2 && (
|
||||
<TrashIcon
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import {
|
||||
TI18nString,
|
||||
TShuffleOption,
|
||||
@@ -31,6 +32,7 @@ interface OpenQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const MultipleChoiceQuestionForm = ({
|
||||
@@ -41,6 +43,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: OpenQuestionFormProps): JSX.Element => {
|
||||
const lastChoiceRef = useRef<HTMLInputElement>(null);
|
||||
const [isNew, setIsNew] = useState(true);
|
||||
@@ -190,6 +193,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -205,6 +209,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -283,6 +288,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
surveyLanguageCodes={surveyLanguageCodes}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
@@ -17,6 +18,7 @@ interface NPSQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const NPSQuestionForm = ({
|
||||
@@ -28,6 +30,7 @@ export const NPSQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: NPSQuestionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -42,6 +45,7 @@ export const NPSQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -57,6 +61,7 @@ export const NPSQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -99,6 +104,7 @@ export const NPSQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
@@ -111,6 +117,7 @@ export const NPSQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,6 +135,7 @@ export const NPSQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyOpenTextQuestion,
|
||||
@@ -39,6 +40,7 @@ interface OpenQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const OpenQuestionForm = ({
|
||||
@@ -49,6 +51,7 @@ export const OpenQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: OpenQuestionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const defaultPlaceholder = getPlaceholderByInputType(question.inputType ?? "text");
|
||||
@@ -73,6 +76,7 @@ export const OpenQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -88,6 +92,7 @@ export const OpenQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -131,6 +136,7 @@ export const OpenQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyPictureSelectionQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { FileInput } from "@formbricks/ui/FileInput";
|
||||
@@ -20,6 +21,7 @@ interface PictureSelectionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const PictureSelectionForm = ({
|
||||
@@ -30,6 +32,7 @@ export const PictureSelectionForm = ({
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
attributeClasses,
|
||||
}: PictureSelectionFormProps): JSX.Element => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const environmentId = localSurvey.environmentId;
|
||||
@@ -46,6 +49,7 @@ export const PictureSelectionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
@@ -60,6 +64,7 @@ export const PictureSelectionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TI18nString, TSurvey, TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -62,6 +63,7 @@ interface QuestionCardProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const QuestionCard = ({
|
||||
@@ -79,6 +81,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
attributeClasses,
|
||||
}: QuestionCardProps) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: question.id,
|
||||
@@ -213,13 +216,21 @@ export const QuestionCard = ({
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold">
|
||||
{recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
]
|
||||
{recallToHeadline(
|
||||
question.headline,
|
||||
localSurvey,
|
||||
true,
|
||||
selectedLanguageCode,
|
||||
attributeClasses
|
||||
)[selectedLanguageCode]
|
||||
? formatTextWithSlashes(
|
||||
recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
] ?? ""
|
||||
recallToHeadline(
|
||||
question.headline,
|
||||
localSurvey,
|
||||
true,
|
||||
selectedLanguageCode,
|
||||
attributeClasses
|
||||
)[selectedLanguageCode] ?? ""
|
||||
)
|
||||
: getTSurveyQuestionTypeName(question.type)}
|
||||
</p>
|
||||
@@ -251,6 +262,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
|
||||
<MultipleChoiceQuestionForm
|
||||
@@ -262,6 +274,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
|
||||
<MultipleChoiceQuestionForm
|
||||
@@ -273,6 +286,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.NPS ? (
|
||||
<NPSQuestionForm
|
||||
@@ -284,6 +298,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.CTA ? (
|
||||
<CTAQuestionForm
|
||||
@@ -295,6 +310,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Rating ? (
|
||||
<RatingQuestionForm
|
||||
@@ -306,6 +322,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Consent ? (
|
||||
<ConsentQuestionForm
|
||||
@@ -316,6 +333,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Date ? (
|
||||
<DateQuestionForm
|
||||
@@ -327,6 +345,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.PictureSelection ? (
|
||||
<PictureSelectionForm
|
||||
@@ -338,6 +357,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.FileUpload ? (
|
||||
<FileUploadQuestionForm
|
||||
@@ -350,6 +370,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Cal ? (
|
||||
<CalQuestionForm
|
||||
@@ -361,6 +382,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Matrix ? (
|
||||
<MatrixQuestionForm
|
||||
@@ -372,6 +394,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Address ? (
|
||||
<AddressQuestionForm
|
||||
@@ -383,6 +406,7 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : null}
|
||||
<div className="mt-4">
|
||||
@@ -423,6 +447,7 @@ export const QuestionCard = ({
|
||||
if (questionIdx === localSurvey.questions.length - 1) return;
|
||||
updateEmptyNextButtonLabels(translatedNextButtonLabel);
|
||||
}}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
{questionIdx !== 0 && (
|
||||
@@ -437,6 +462,7 @@ export const QuestionCard = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -456,6 +482,7 @@ export const QuestionCard = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -465,6 +492,7 @@ export const QuestionCard = ({
|
||||
questionIdx={questionIdx}
|
||||
localSurvey={localSurvey}
|
||||
updateQuestion={updateQuestion}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
@@ -18,6 +19,7 @@ interface QuestionsDraggableProps {
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
invalidQuestions: string[] | null;
|
||||
internalQuestionIdMap: Record<string, string>;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const QuestionsDroppable = ({
|
||||
@@ -33,6 +35,7 @@ export const QuestionsDroppable = ({
|
||||
setSelectedLanguageCode,
|
||||
updateQuestion,
|
||||
internalQuestionIdMap,
|
||||
attributeClasses,
|
||||
}: QuestionsDraggableProps) => {
|
||||
return (
|
||||
<div className="group mb-5 grid w-full gap-5">
|
||||
@@ -54,6 +57,7 @@ export const QuestionsDroppable = ({
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
lastQuestion={questionIdx === localSurvey.questions.length - 1}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(question.id) : false}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { MultiLanguageCard } from "@formbricks/ee/multiLanguage/components/Multi
|
||||
import { extractLanguageCodes, getLocalizedValue, translateQuestion } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
@@ -43,6 +44,7 @@ interface QuestionsViewProps {
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isMultiLanguageAllowed?: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const QuestionsView = ({
|
||||
@@ -57,6 +59,7 @@ export const QuestionsView = ({
|
||||
selectedLanguageCode,
|
||||
isMultiLanguageAllowed,
|
||||
isFormbricksCloud,
|
||||
attributeClasses,
|
||||
}: QuestionsViewProps) => {
|
||||
const internalQuestionIdMap = useMemo(() => {
|
||||
return localSurvey.questions.reduce((acc, question) => {
|
||||
@@ -339,6 +342,7 @@ export const QuestionsView = ({
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes("start") : false}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -356,6 +360,7 @@ export const QuestionsView = ({
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
invalidQuestions={invalidQuestions}
|
||||
internalQuestionIdMap={internalQuestionIdMap}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</DndContext>
|
||||
|
||||
@@ -369,6 +374,7 @@ export const QuestionsView = ({
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes("end") : false}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
{localSurvey.type === "link" ? (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { HashIcon, PlusIcon, SmileIcon, StarIcon, TrashIcon } from "lucide-react
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyRatingQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
@@ -18,6 +19,7 @@ interface RatingQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const RatingQuestionForm = ({
|
||||
@@ -28,6 +30,7 @@ export const RatingQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: RatingQuestionFormProps) => {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -43,6 +46,7 @@ export const RatingQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -58,6 +62,7 @@ export const RatingQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -135,6 +140,7 @@ export const RatingQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
@@ -148,6 +154,7 @@ export const RatingQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,6 +172,7 @@ export const RatingQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import toast from "react-hot-toast";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { createI18nString } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
@@ -35,6 +36,7 @@ interface ChoiceProps {
|
||||
question: TSurveyMultipleChoiceQuestion;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyMultipleChoiceQuestion>) => void;
|
||||
surveyLanguageCodes: string[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const SelectQuestionChoice = ({
|
||||
@@ -54,6 +56,7 @@ export const SelectQuestionChoice = ({
|
||||
question,
|
||||
surveyLanguageCodes,
|
||||
updateQuestion,
|
||||
attributeClasses,
|
||||
}: ChoiceProps) => {
|
||||
const isDragDisabled = choice.id === "other";
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
||||
@@ -100,6 +103,7 @@ export const SelectQuestionChoice = ({
|
||||
isInvalid && !isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
|
||||
}
|
||||
className={`${choice.id === "other" ? "border border-dashed" : ""} mt-0`}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
{choice.id === "other" && (
|
||||
<QuestionFormInput
|
||||
@@ -119,6 +123,7 @@ export const SelectQuestionChoice = ({
|
||||
isInvalid && !isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
|
||||
}
|
||||
className="border border-dashed"
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -163,6 +163,7 @@ export const SurveyEditor = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isMultiLanguageAllowed={isMultiLanguageAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
@@ -12,9 +13,15 @@ interface ResponseSectionProps {
|
||||
environment: TEnvironment;
|
||||
personId: string;
|
||||
environmentTags: TTag[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const ResponseSection = async ({ environment, personId, environmentTags }: ResponseSectionProps) => {
|
||||
export const ResponseSection = async ({
|
||||
environment,
|
||||
personId,
|
||||
environmentTags,
|
||||
attributeClasses,
|
||||
}: ResponseSectionProps) => {
|
||||
const responses = await getResponsesByPersonId(personId);
|
||||
const surveyIds = responses?.map((response) => response.surveyId) || [];
|
||||
const surveys: TSurvey[] = surveyIds.length === 0 ? [] : (await getSurveys(environment.id)) ?? [];
|
||||
@@ -34,6 +41,7 @@ export const ResponseSection = async ({ environment, personId, environmentTags }
|
||||
responses={responses}
|
||||
environment={environment}
|
||||
environmentTags={environmentTags}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ResponseFeed } from "@/app/(app)/environments/[environmentId]/(people)/
|
||||
import { ArrowDownUpIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
@@ -16,6 +17,7 @@ interface ResponseTimelineProps {
|
||||
responses: TResponse[];
|
||||
environment: TEnvironment;
|
||||
environmentTags: TTag[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const ResponseTimeline = ({
|
||||
@@ -24,6 +26,7 @@ export const ResponseTimeline = ({
|
||||
environment,
|
||||
responses,
|
||||
environmentTags,
|
||||
attributeClasses,
|
||||
}: ResponseTimelineProps) => {
|
||||
const [sortedResponses, setSortedResponses] = useState(responses);
|
||||
const toggleSortResponses = () => {
|
||||
@@ -53,6 +56,7 @@ export const ResponseTimeline = ({
|
||||
surveys={surveys}
|
||||
user={user}
|
||||
environmentTags={environmentTags}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import { useMembershipRole } from "@formbricks/lib/membership/hooks/useMembershipRole";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
@@ -19,6 +20,7 @@ interface ResponseTimelineProps {
|
||||
responses: TResponse[];
|
||||
environment: TEnvironment;
|
||||
environmentTags: TTag[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const ResponseFeed = ({
|
||||
@@ -27,6 +29,7 @@ export const ResponseFeed = ({
|
||||
surveys,
|
||||
user,
|
||||
environmentTags,
|
||||
attributeClasses,
|
||||
}: ResponseTimelineProps) => {
|
||||
const [fetchedResponses, setFetchedResponses] = useState(responses);
|
||||
|
||||
@@ -59,6 +62,7 @@ export const ResponseFeed = ({
|
||||
environment={environment}
|
||||
deleteResponse={deleteResponse}
|
||||
updateResponse={updateResponse}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
@@ -74,6 +78,7 @@ const ResponseSurveyCard = ({
|
||||
environment,
|
||||
deleteResponse,
|
||||
updateResponse,
|
||||
attributeClasses,
|
||||
}: {
|
||||
response: TResponse;
|
||||
surveys: TSurvey[];
|
||||
@@ -82,6 +87,7 @@ const ResponseSurveyCard = ({
|
||||
environment: TEnvironment;
|
||||
deleteResponse: (responseId: string) => void;
|
||||
updateResponse: (responseId: string, response: TResponse) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}) => {
|
||||
const survey = surveys.find((survey) => {
|
||||
return survey.id === response.surveyId;
|
||||
@@ -95,7 +101,7 @@ const ResponseSurveyCard = ({
|
||||
{survey && (
|
||||
<SingleResponseCard
|
||||
response={response}
|
||||
survey={checkForRecallInHeadline(survey, "default")}
|
||||
survey={replaceHeadlineRecall(survey, "default", attributeClasses)}
|
||||
user={user}
|
||||
pageType="people"
|
||||
environmentTags={environmentTags}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ResponseSection } from "@/app/(app)/environments/[environmentId]/(peopl
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { getAttributes } from "@formbricks/lib/attribute/service";
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
@@ -18,7 +19,7 @@ import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const Page = async ({ params }) => {
|
||||
const [environment, environmentTags, product, session, organization, person, attributes] =
|
||||
const [environment, environmentTags, product, session, organization, person, attributes, attributeClasses] =
|
||||
await Promise.all([
|
||||
getEnvironment(params.environmentId),
|
||||
getTagsByEnvironmentId(params.environmentId),
|
||||
@@ -27,6 +28,7 @@ const Page = async ({ params }) => {
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
getPerson(params.personId),
|
||||
getAttributes(params.personId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
@@ -68,6 +70,7 @@ const Page = async ({ params }) => {
|
||||
environment={environment}
|
||||
personId={params.personId}
|
||||
environmentTags={environmentTags}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<ActivitySection environmentId={params.environmentId} personId={params.personId} />
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,8 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationAirtable,
|
||||
@@ -38,6 +39,7 @@ type AddIntegrationModalProps = {
|
||||
airtableArray: TIntegrationItem[];
|
||||
surveys: TSurvey[];
|
||||
airtableIntegration: TIntegrationAirtable;
|
||||
attributeClasses: TAttributeClass[];
|
||||
} & EditModeProps;
|
||||
|
||||
export type IntegrationModalInputs = {
|
||||
@@ -65,6 +67,7 @@ export const AddIntegrationModal = ({
|
||||
airtableIntegration,
|
||||
isEditMode,
|
||||
defaultData,
|
||||
attributeClasses,
|
||||
}: AddIntegrationModalProps) => {
|
||||
const router = useRouter();
|
||||
const [tables, setTables] = useState<TIntegrationAirtableTables["tables"]>([]);
|
||||
@@ -282,32 +285,36 @@ export const AddIntegrationModal = ({
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{checkForRecallInHeadline(selectedSurvey, "default")?.questions.map((question) => (
|
||||
<Controller
|
||||
key={question.id}
|
||||
control={control}
|
||||
name={"questions"}
|
||||
render={({ field }) => (
|
||||
<div className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={field.value?.includes(question.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, question.id])
|
||||
: field.onChange(field.value?.filter((value) => value !== question.id));
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">{getLocalizedValue(question.headline, "default")}</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map(
|
||||
(question) => (
|
||||
<Controller
|
||||
key={question.id}
|
||||
control={control}
|
||||
name={"questions"}
|
||||
render={({ field }) => (
|
||||
<div className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={field.value?.includes(question.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, question.id])
|
||||
: field.onChange(field.value?.filter((value) => value !== question.id));
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">
|
||||
{getLocalizedValue(question.headline, "default")}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { authorize } from "@/app/(app)/environments/[environmentId]/integrations
|
||||
import airtableLogo from "@/images/airtableLogo.svg";
|
||||
import { useState } from "react";
|
||||
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
@@ -19,6 +20,7 @@ interface AirtableWrapperProps {
|
||||
environment: TEnvironment;
|
||||
isEnabled: boolean;
|
||||
webAppUrl: string;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const AirtableWrapper = ({
|
||||
@@ -29,6 +31,7 @@ export const AirtableWrapper = ({
|
||||
environment,
|
||||
isEnabled,
|
||||
webAppUrl,
|
||||
attributeClasses,
|
||||
}: AirtableWrapperProps) => {
|
||||
const [isConnected, setIsConnected] = useState(
|
||||
airtableIntegration ? airtableIntegration.config?.key : false
|
||||
@@ -50,6 +53,7 @@ export const AirtableWrapper = ({
|
||||
airtableIntegration={airtableIntegration}
|
||||
setIsConnected={setIsConnected}
|
||||
surveys={surveys}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
) : (
|
||||
<ConnectIntegration
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
@@ -24,12 +25,21 @@ interface ManageIntegrationProps {
|
||||
setIsConnected: (data: boolean) => void;
|
||||
surveys: TSurvey[];
|
||||
airtableArray: TIntegrationItem[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
const tableHeaders = ["Survey", "Table Name", "Questions", "Updated At"];
|
||||
|
||||
export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
const { airtableIntegration, environment, environmentId, setIsConnected, surveys, airtableArray } = props;
|
||||
const {
|
||||
airtableIntegration,
|
||||
environment,
|
||||
environmentId,
|
||||
setIsConnected,
|
||||
surveys,
|
||||
airtableArray,
|
||||
attributeClasses,
|
||||
} = props;
|
||||
const [isDeleting, setisDeleting] = useState(false);
|
||||
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
|
||||
const [defaultValues, setDefaultValues] = useState<(IntegrationModalInputs & { index: number }) | null>(
|
||||
@@ -142,6 +152,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
environmentId={environmentId}
|
||||
surveys={surveys}
|
||||
airtableIntegration={airtableIntegration}
|
||||
attributeClasses={attributeClasses}
|
||||
{...data}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
||||
|
||||
import { getAirtableTables } from "@formbricks/lib/airtable/service";
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
@@ -14,10 +15,11 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const Page = async ({ params }) => {
|
||||
const isEnabled = !!AIRTABLE_CLIENT_ID;
|
||||
const [surveys, integrations, environment] = await Promise.all([
|
||||
const [surveys, integrations, environment, attributeClasses] = await Promise.all([
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrations(params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
]);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
@@ -49,6 +51,7 @@ const Page = async ({ params }) => {
|
||||
surveys={surveys}
|
||||
environment={environment}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -12,7 +12,8 @@ import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
TIntegrationGoogleSheetsConfigData,
|
||||
@@ -33,6 +34,7 @@ interface AddIntegrationModalProps {
|
||||
setOpen: (v: boolean) => void;
|
||||
googleSheetIntegration: TIntegrationGoogleSheets;
|
||||
selectedIntegration?: (TIntegrationGoogleSheetsConfigData & { index: number }) | null;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const AddIntegrationModal = ({
|
||||
@@ -42,6 +44,7 @@ export const AddIntegrationModal = ({
|
||||
setOpen,
|
||||
googleSheetIntegration,
|
||||
selectedIntegration,
|
||||
attributeClasses,
|
||||
}: AddIntegrationModalProps) => {
|
||||
const integrationData = {
|
||||
spreadsheetId: "",
|
||||
@@ -216,25 +219,27 @@ export const AddIntegrationModal = ({
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{checkForRecallInHeadline(selectedSurvey, "default")?.questions.map((question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={selectedQuestions.includes(question.id)}
|
||||
onCheckedChange={() => {
|
||||
handleCheckboxChange(question.id);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2 w-[30rem] truncate">
|
||||
{getLocalizedValue(question.headline, "default")}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map(
|
||||
(question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={selectedQuestions.includes(question.id)}
|
||||
onCheckedChange={() => {
|
||||
handleCheckboxChange(question.id);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2 w-[30rem] truncate">
|
||||
{getLocalizedValue(question.headline, "default")}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { authorize } from "@/app/(app)/environments/[environmentId]/integrations
|
||||
import googleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||
import { useState } from "react";
|
||||
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
@@ -21,6 +22,7 @@ interface GoogleSheetWrapperProps {
|
||||
surveys: TSurvey[];
|
||||
googleSheetIntegration?: TIntegrationGoogleSheets;
|
||||
webAppUrl: string;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const GoogleSheetWrapper = ({
|
||||
@@ -29,6 +31,7 @@ export const GoogleSheetWrapper = ({
|
||||
surveys,
|
||||
googleSheetIntegration,
|
||||
webAppUrl,
|
||||
attributeClasses,
|
||||
}: GoogleSheetWrapperProps) => {
|
||||
const [isConnected, setIsConnected] = useState(
|
||||
googleSheetIntegration ? googleSheetIntegration.config?.key : false
|
||||
@@ -57,6 +60,7 @@ export const GoogleSheetWrapper = ({
|
||||
setOpen={setModalOpen}
|
||||
googleSheetIntegration={googleSheetIntegration}
|
||||
selectedIntegration={selectedIntegration}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import {
|
||||
GOOGLE_SHEETS_CLIENT_ID,
|
||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||
@@ -17,10 +18,11 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
|
||||
const Page = async ({ params }) => {
|
||||
const isEnabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
|
||||
const [surveys, integrations, environment] = await Promise.all([
|
||||
const [surveys, integrations, environment, attributeClasses] = await Promise.all([
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrations(params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
]);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
@@ -45,6 +47,7 @@ const Page = async ({ params }) => {
|
||||
surveys={surveys}
|
||||
googleSheetIntegration={googleSheetIntegration}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -14,7 +14,8 @@ import toast from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TIntegrationInput } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationNotion,
|
||||
@@ -35,6 +36,7 @@ interface AddIntegrationModalProps {
|
||||
notionIntegration: TIntegrationNotion;
|
||||
databases: TIntegrationNotionDatabase[];
|
||||
selectedIntegration: (TIntegrationNotionConfigData & { index: number }) | null;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const AddIntegrationModal = ({
|
||||
@@ -45,6 +47,7 @@ export const AddIntegrationModal = ({
|
||||
notionIntegration,
|
||||
databases,
|
||||
selectedIntegration,
|
||||
attributeClasses,
|
||||
}: AddIntegrationModalProps) => {
|
||||
const { handleSubmit } = useForm();
|
||||
const [selectedDatabase, setSelectedDatabase] = useState<TIntegrationNotionDatabase | null>();
|
||||
@@ -109,7 +112,7 @@ export const AddIntegrationModal = ({
|
||||
|
||||
const questionItems = useMemo(() => {
|
||||
const questions = selectedSurvey
|
||||
? checkForRecallInHeadline(selectedSurvey, "default")?.questions.map((q) => ({
|
||||
? replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map((q) => ({
|
||||
id: q.id,
|
||||
name: getLocalizedValue(q.headline, "default"),
|
||||
type: q.type,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/inte
|
||||
import notionLogo from "@/images/notion.png";
|
||||
import { useState } from "react";
|
||||
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationNotion,
|
||||
@@ -23,6 +24,7 @@ interface NotionWrapperProps {
|
||||
webAppUrl: string;
|
||||
surveys: TSurvey[];
|
||||
databasesArray: TIntegrationNotionDatabase[];
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const NotionWrapper = ({
|
||||
@@ -32,6 +34,7 @@ export const NotionWrapper = ({
|
||||
webAppUrl,
|
||||
surveys,
|
||||
databasesArray,
|
||||
attributeClasses,
|
||||
}: NotionWrapperProps) => {
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(
|
||||
@@ -61,6 +64,7 @@ export const NotionWrapper = ({
|
||||
notionIntegration={notionIntegration}
|
||||
databases={databasesArray}
|
||||
selectedIntegration={selectedIntegration}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
|
||||
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import {
|
||||
NOTION_AUTH_URL,
|
||||
NOTION_OAUTH_CLIENT_ID,
|
||||
@@ -23,10 +24,11 @@ const Page = async ({ params }) => {
|
||||
NOTION_AUTH_URL &&
|
||||
NOTION_REDIRECT_URI
|
||||
);
|
||||
const [surveys, notionIntegration, environment] = await Promise.all([
|
||||
const [surveys, notionIntegration, environment, attributeClasses] = await Promise.all([
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrationByType(params.environmentId, "notion"),
|
||||
getEnvironment(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!environment) {
|
||||
@@ -49,6 +51,7 @@ const Page = async ({ params }) => {
|
||||
notionIntegration={notionIntegration as TIntegrationNotion}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
databasesArray={databasesArray}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,8 @@ import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationSlack,
|
||||
@@ -28,6 +29,7 @@ interface AddChannelMappingModalProps {
|
||||
slackIntegration: TIntegrationSlack;
|
||||
channels: TIntegrationItem[];
|
||||
selectedIntegration?: (TIntegrationSlackConfigData & { index: number }) | null;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const AddChannelMappingModal = ({
|
||||
@@ -38,6 +40,7 @@ export const AddChannelMappingModal = ({
|
||||
channels,
|
||||
slackIntegration,
|
||||
selectedIntegration,
|
||||
attributeClasses,
|
||||
}: AddChannelMappingModalProps) => {
|
||||
const { handleSubmit } = useForm();
|
||||
|
||||
@@ -223,23 +226,25 @@ export const AddChannelMappingModal = ({
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{checkForRecallInHeadline(selectedSurvey, "default")?.questions?.map((question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={selectedQuestions.includes(question.id)}
|
||||
onCheckedChange={() => {
|
||||
handleCheckboxChange(question.id);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">{getLocalizedValue(question.headline, "default")}</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions?.map(
|
||||
(question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={selectedQuestions.includes(question.id)}
|
||||
onCheckedChange={() => {
|
||||
handleCheckboxChange(question.id);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">{getLocalizedValue(question.headline, "default")}</span>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { authorize } from "@/app/(app)/environments/[environmentId]/integrations
|
||||
import slackLogo from "@/images/slacklogo.png";
|
||||
import { useState } from "react";
|
||||
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
||||
@@ -20,6 +21,7 @@ interface SlackWrapperProps {
|
||||
channelsArray: TIntegrationItem[];
|
||||
slackIntegration?: TIntegrationSlack;
|
||||
webAppUrl: string;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const SlackWrapper = ({
|
||||
@@ -29,6 +31,7 @@ export const SlackWrapper = ({
|
||||
channelsArray,
|
||||
slackIntegration,
|
||||
webAppUrl,
|
||||
attributeClasses,
|
||||
}: SlackWrapperProps) => {
|
||||
const [isConnected, setIsConnected] = useState(slackIntegration ? slackIntegration.config?.key : false);
|
||||
const [slackChannels, setSlackChannels] = useState(channelsArray);
|
||||
@@ -60,6 +63,7 @@ export const SlackWrapper = ({
|
||||
channels={slackChannels}
|
||||
slackIntegration={slackIntegration}
|
||||
selectedIntegration={selectedIntegration}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
||||
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrationByType } from "@formbricks/lib/integration/service";
|
||||
@@ -14,10 +15,11 @@ import { PageHeader } from "@formbricks/ui/PageHeader";
|
||||
const Page = async ({ params }) => {
|
||||
const isEnabled = !!(SLACK_CLIENT_ID && SLACK_CLIENT_SECRET);
|
||||
|
||||
const [surveys, slackIntegration, environment] = await Promise.all([
|
||||
const [surveys, slackIntegration, environment, attributeClasses] = await Promise.all([
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrationByType(params.environmentId, "slack"),
|
||||
getEnvironment(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!environment) {
|
||||
@@ -41,6 +43,7 @@ const Page = async ({ params }) => {
|
||||
surveys={surveys}
|
||||
slackIntegration={slackIntegration as TIntegrationSlack}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
@@ -65,10 +64,6 @@ export const ResponsePage = ({
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
survey = useMemo(() => {
|
||||
return checkForRecallInHeadline(survey, "default");
|
||||
}, [survey]);
|
||||
|
||||
const fetchNextPage = useCallback(async () => {
|
||||
const newPage = page + 1;
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
@@ -46,6 +47,7 @@ interface SummaryPageProps {
|
||||
webAppUrl: string;
|
||||
user?: TUser;
|
||||
totalResponseCount: number;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const SummaryPage = ({
|
||||
@@ -55,6 +57,7 @@ export const SummaryPage = ({
|
||||
webAppUrl,
|
||||
user,
|
||||
totalResponseCount,
|
||||
attributeClasses,
|
||||
}: SummaryPageProps) => {
|
||||
const params = useParams();
|
||||
const sharingKey = params.sharingKey as string;
|
||||
@@ -104,9 +107,9 @@ export const SummaryPage = ({
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
survey = useMemo(() => {
|
||||
return checkForRecallInHeadline(survey, "default");
|
||||
}, [survey]);
|
||||
const surveyMemoized = useMemo(() => {
|
||||
return replaceHeadlineRecall(survey, "default", attributeClasses);
|
||||
}, [survey, attributeClasses]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchParams?.get("referer")) {
|
||||
@@ -123,13 +126,13 @@ export const SummaryPage = ({
|
||||
/>
|
||||
{showDropOffs && <SummaryDropOffs dropOff={surveySummary.dropOff} />}
|
||||
<div className="flex gap-1.5">
|
||||
<CustomFilter survey={survey} />
|
||||
{!isSharingPage && <ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />}
|
||||
<CustomFilter survey={surveyMemoized} />
|
||||
{!isSharingPage && <ResultsShareButton survey={surveyMemoized} webAppUrl={webAppUrl} user={user} />}
|
||||
</div>
|
||||
<SummaryList
|
||||
summary={surveySummary.summary}
|
||||
responseCount={responseCount}
|
||||
survey={survey}
|
||||
survey={surveyMemoized}
|
||||
environment={environment}
|
||||
fetchingSummary={isFetchingSummary}
|
||||
totalResponseCount={totalResponseCount}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surv
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
@@ -29,16 +30,17 @@ const Page = async ({ params }) => {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
|
||||
if (!survey) {
|
||||
throw new Error("Survey not found");
|
||||
}
|
||||
const environment = await getEnvironment(survey.environmentId);
|
||||
|
||||
const [survey, environment, attributeClasses] = await Promise.all([
|
||||
getSurvey(params.surveyId),
|
||||
getEnvironment(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
]);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
if (!survey) {
|
||||
throw new Error("Survey not found");
|
||||
}
|
||||
|
||||
const product = await getProductByEnvironmentId(environment.id);
|
||||
if (!product) {
|
||||
@@ -87,6 +89,7 @@ const Page = async ({ params }) => {
|
||||
webAppUrl={WEBAPP_URL}
|
||||
user={user}
|
||||
totalResponseCount={totalResponseCount}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { sendNoLiveSurveyNotificationEmail, sendWeeklySummaryNotificationEmail }
|
||||
import { CRON_SECRET } from "@formbricks/lib/constants";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { convertResponseValue } from "@formbricks/lib/responses";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import {
|
||||
TWeeklySummaryEnvironmentData,
|
||||
TWeeklySummaryNotificationDataSurvey,
|
||||
@@ -150,6 +150,19 @@ const getProductsByOrganizationId = async (organizationId: string): Promise<TWee
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
hiddenFields: true,
|
||||
},
|
||||
},
|
||||
attributeClasses: {
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
name: true,
|
||||
description: true,
|
||||
type: true,
|
||||
environmentId: true,
|
||||
archived: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -187,7 +200,7 @@ const getNotificationResponse = (
|
||||
const surveys: TWeeklySummaryNotificationDataSurvey[] = [];
|
||||
// iterate through the surveys and calculate the overall insights
|
||||
for (const survey of environment.surveys) {
|
||||
const parsedSurvey = checkForRecallInHeadline(survey, "default");
|
||||
const parsedSurvey = replaceHeadlineRecall(survey, "default", environment.attributeClasses);
|
||||
const surveyData: TWeeklySummaryNotificationDataSurvey = {
|
||||
id: parsedSurvey.id,
|
||||
name: parsedSurvey.name,
|
||||
|
||||
@@ -4,13 +4,14 @@ import { headers } from "next/headers";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { sendResponseFinishedEmail } from "@formbricks/email";
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { INTERNAL_SECRET } from "@formbricks/lib/constants";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { convertDatesInObject } from "@formbricks/lib/time";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { ZPipelineInput } from "@formbricks/types/pipelines";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
|
||||
@@ -38,6 +39,7 @@ export const POST = async (request: Request) => {
|
||||
|
||||
const { environmentId, surveyId, event, response } = inputValidation.data;
|
||||
const product = await getProductByEnvironmentId(environmentId);
|
||||
const attributeClasses = await getAttributeClasses(environmentId);
|
||||
if (!product) return;
|
||||
|
||||
// get all webhooks of this environment where event in triggers
|
||||
@@ -106,7 +108,7 @@ export const POST = async (request: Request) => {
|
||||
getIntegrations(environmentId),
|
||||
getSurvey(surveyId),
|
||||
]);
|
||||
const survey = surveyData ? checkForRecallInHeadline(surveyData, "default") : undefined;
|
||||
const survey = surveyData ? replaceHeadlineRecall(surveyData, "default", attributeClasses) : undefined;
|
||||
|
||||
if (integrations.length > 0 && survey) {
|
||||
handleIntegrations(integrations, inputValidation.data, survey);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { sendFreeLimitReachedEventToPosthogBiWeekly } from "@/app/api/v1/client/[environmentId]/app/sync/lib/posthog";
|
||||
import {
|
||||
replaceAttributeRecall,
|
||||
replaceAttributeRecallInLegacySurveys,
|
||||
} from "@/app/api/v1/client/[environmentId]/app/sync/lib/utils";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { NextRequest, userAgent } from "next/server";
|
||||
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { getAttribute } from "@formbricks/lib/attribute/service";
|
||||
import { getAttributes } from "@formbricks/lib/attribute/service";
|
||||
import {
|
||||
IS_FORMBRICKS_CLOUD,
|
||||
PRICING_APPSURVEYS_FREE_RESPONSES,
|
||||
@@ -154,8 +158,8 @@ export const GET = async (
|
||||
highlightBorderColor: product.styling.highlightBorderColor.light,
|
||||
}),
|
||||
};
|
||||
|
||||
const language = await getAttribute("language", person.id);
|
||||
const attributes = await getAttributes(person.id);
|
||||
const language = attributes["language"];
|
||||
const noCodeActionClasses = actionClasses.filter((actionClass) => actionClass.type === "noCode");
|
||||
|
||||
// Scenario 1: Multi language and updated trigger action classes supported.
|
||||
@@ -164,7 +168,9 @@ export const GET = async (
|
||||
|
||||
// creating state object
|
||||
let state: TJsAppStateSync | TJsAppLegacyStateSync = {
|
||||
surveys: !isInAppSurveyLimitReached ? transformedSurveys : [],
|
||||
surveys: !isInAppSurveyLimitReached
|
||||
? transformedSurveys.map((survey) => replaceAttributeRecall(survey, attributes))
|
||||
: [],
|
||||
actionClasses,
|
||||
language,
|
||||
product: updatedProduct,
|
||||
@@ -183,7 +189,9 @@ export const GET = async (
|
||||
);
|
||||
|
||||
state = {
|
||||
surveys: !isInAppSurveyLimitReached ? transformedSurveys : [],
|
||||
surveys: !isInAppSurveyLimitReached
|
||||
? transformedSurveys.map((survey) => replaceAttributeRecallInLegacySurveys(survey, attributes))
|
||||
: [],
|
||||
person,
|
||||
noCodeActionClasses,
|
||||
language,
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TLegacySurvey } from "@formbricks/types/LegacySurvey";
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
export const replaceAttributeRecall = (survey: TSurvey, attributes: TAttributes): TSurvey => {
|
||||
const surveyTemp = structuredClone(survey);
|
||||
const languages = Object.keys(survey.questions[0].headline);
|
||||
surveyTemp.questions.forEach((question) => {
|
||||
languages.forEach((language) => {
|
||||
if (question.headline[language].includes("recall:")) {
|
||||
question.headline[language] = parseRecallInfo(question.headline[language], attributes);
|
||||
}
|
||||
if (question.subheader && question.subheader[language].includes("recall:")) {
|
||||
question.subheader[language] = parseRecallInfo(question.subheader[language], attributes);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (surveyTemp.welcomeCard.enabled && surveyTemp.welcomeCard.headline) {
|
||||
languages.forEach((language) => {
|
||||
if (surveyTemp.welcomeCard.headline && surveyTemp.welcomeCard.headline[language].includes("recall:")) {
|
||||
surveyTemp.welcomeCard.headline[language] = parseRecallInfo(
|
||||
surveyTemp.welcomeCard.headline[language],
|
||||
attributes
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (surveyTemp.thankYouCard.enabled) {
|
||||
languages.forEach((language) => {
|
||||
if (
|
||||
surveyTemp.thankYouCard.headline &&
|
||||
surveyTemp.thankYouCard.headline[language].includes("recall:")
|
||||
) {
|
||||
surveyTemp.thankYouCard.headline[language] = parseRecallInfo(
|
||||
surveyTemp.thankYouCard.headline[language],
|
||||
attributes
|
||||
);
|
||||
if (
|
||||
surveyTemp.thankYouCard.subheader &&
|
||||
surveyTemp.thankYouCard.subheader[language].includes("recall:")
|
||||
) {
|
||||
surveyTemp.thankYouCard.subheader[language] = parseRecallInfo(
|
||||
surveyTemp.thankYouCard.subheader[language],
|
||||
attributes
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return surveyTemp;
|
||||
};
|
||||
|
||||
export const replaceAttributeRecallInLegacySurveys = (
|
||||
survey: TLegacySurvey,
|
||||
attributes: TAttributes
|
||||
): TLegacySurvey => {
|
||||
const surveyTemp = structuredClone(survey);
|
||||
surveyTemp.questions.forEach((question) => {
|
||||
if (question.headline.includes("recall:")) {
|
||||
question.headline = parseRecallInfo(question.headline, attributes);
|
||||
}
|
||||
if (question.subheader && question.subheader.includes("recall:")) {
|
||||
question.subheader = parseRecallInfo(question.subheader, attributes);
|
||||
}
|
||||
});
|
||||
if (surveyTemp.welcomeCard.enabled && surveyTemp.welcomeCard.headline) {
|
||||
if (surveyTemp.welcomeCard.headline && surveyTemp.welcomeCard.headline.includes("recall:")) {
|
||||
surveyTemp.welcomeCard.headline = parseRecallInfo(surveyTemp.welcomeCard.headline, attributes);
|
||||
}
|
||||
}
|
||||
if (surveyTemp.thankYouCard.enabled && surveyTemp.thankYouCard.headline) {
|
||||
if (surveyTemp.thankYouCard.headline && surveyTemp.thankYouCard.headline.includes("recall:")) {
|
||||
surveyTemp.thankYouCard.headline = parseRecallInfo(surveyTemp.thankYouCard.headline, attributes);
|
||||
if (surveyTemp.thankYouCard.subheader && surveyTemp.thankYouCard.subheader.includes("recall:")) {
|
||||
surveyTemp.thankYouCard.subheader = parseRecallInfo(surveyTemp.thankYouCard.subheader, attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
return surveyTemp;
|
||||
};
|
||||
@@ -9,8 +9,9 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { FormbricksAPI } from "@formbricks/api";
|
||||
import { ResponseQueue } from "@formbricks/lib/responseQueue";
|
||||
import { SurveyState } from "@formbricks/lib/surveyState";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TResponse, TResponseUpdate } from "@formbricks/types/responses";
|
||||
import { TResponse, TResponseData, TResponseUpdate } from "@formbricks/types/responses";
|
||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { ClientLogo } from "@formbricks/ui/ClientLogo";
|
||||
@@ -32,6 +33,7 @@ interface LinkSurveyProps {
|
||||
responseCount?: number;
|
||||
verifiedEmail?: string;
|
||||
languageCode: string;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const LinkSurvey = ({
|
||||
@@ -45,6 +47,7 @@ export const LinkSurvey = ({
|
||||
responseCount,
|
||||
verifiedEmail,
|
||||
languageCode,
|
||||
attributeClasses,
|
||||
}: LinkSurveyProps) => {
|
||||
const responseId = singleUseResponse?.id;
|
||||
const searchParams = useSearchParams();
|
||||
@@ -120,8 +123,8 @@ export const LinkSurvey = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const hiddenFieldsRecord = useMemo<Record<string, string | number | string[]> | null>(() => {
|
||||
const fieldsRecord: Record<string, string | number | string[]> = {};
|
||||
const hiddenFieldsRecord = useMemo<TResponseData | undefined>(() => {
|
||||
const fieldsRecord: TResponseData = {};
|
||||
let fieldsSet = false;
|
||||
|
||||
survey.hiddenFields?.fieldIds?.forEach((field) => {
|
||||
@@ -133,7 +136,7 @@ export const LinkSurvey = ({
|
||||
});
|
||||
|
||||
// Only return the record if at least one field was set.
|
||||
return fieldsSet ? fieldsRecord : null;
|
||||
return fieldsSet ? fieldsRecord : undefined;
|
||||
}, [searchParams, survey.hiddenFields?.fieldIds]);
|
||||
|
||||
const getVerifiedEmail = useMemo<Record<string, string> | null>(() => {
|
||||
@@ -160,6 +163,7 @@ export const LinkSurvey = ({
|
||||
isErrorComponent={true}
|
||||
languageCode={languageCode}
|
||||
styling={product.styling}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -170,6 +174,7 @@ export const LinkSurvey = ({
|
||||
survey={survey}
|
||||
languageCode={languageCode}
|
||||
styling={product.styling}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -280,6 +285,7 @@ export const LinkSurvey = ({
|
||||
setQuestionId = f;
|
||||
}}
|
||||
startAtQuestionId={startAt && isStartAtValid ? startAt : undefined}
|
||||
hiddenFieldsRecord={hiddenFieldsRecord}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
@@ -26,6 +27,7 @@ interface PinScreenProps {
|
||||
IS_FORMBRICKS_CLOUD: boolean;
|
||||
verifiedEmail?: string;
|
||||
languageCode: string;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const PinScreen = (props: PinScreenProps) => {
|
||||
@@ -42,6 +44,7 @@ export const PinScreen = (props: PinScreenProps) => {
|
||||
IS_FORMBRICKS_CLOUD,
|
||||
verifiedEmail,
|
||||
languageCode,
|
||||
attributeClasses,
|
||||
} = props;
|
||||
|
||||
const [localPinEntry, setLocalPinEntry] = useState<string>("");
|
||||
@@ -122,6 +125,7 @@ export const PinScreen = (props: PinScreenProps) => {
|
||||
webAppUrl={webAppUrl}
|
||||
verifiedEmail={verifiedEmail}
|
||||
languageCode={languageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</MediaBackground>
|
||||
<LegalFooter
|
||||
|
||||
@@ -7,7 +7,8 @@ import { useMemo, useState } from "react";
|
||||
import { Toaster, toast } from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TProductStyling } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
@@ -20,6 +21,7 @@ interface VerifyEmailProps {
|
||||
singleUseId?: string;
|
||||
languageCode: string;
|
||||
styling: TProductStyling;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const VerifyEmail = ({
|
||||
@@ -28,10 +30,11 @@ export const VerifyEmail = ({
|
||||
singleUseId,
|
||||
languageCode,
|
||||
styling,
|
||||
attributeClasses,
|
||||
}: VerifyEmailProps) => {
|
||||
survey = useMemo(() => {
|
||||
return checkForRecallInHeadline(survey, "default");
|
||||
}, [survey]);
|
||||
return replaceHeadlineRecall(survey, "default", attributeClasses);
|
||||
}, [survey, attributeClasses]);
|
||||
|
||||
const [showPreviewQuestions, setShowPreviewQuestions] = useState(false);
|
||||
const [email, setEmail] = useState<string | null>(null);
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { IMPRINT_URL, IS_FORMBRICKS_CLOUD, PRIVACY_URL, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { createPerson, getPersonByUserId } from "@formbricks/lib/person/service";
|
||||
@@ -125,6 +126,8 @@ const Page = async ({ params, searchParams }: LinkSurveyPageProps) => {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const attributeClasses = await getAttributeClasses(survey.environmentId);
|
||||
|
||||
const getLanguageCode = (): string => {
|
||||
if (!langParam || !isMultiLanguageAllowed) return "default";
|
||||
else {
|
||||
@@ -170,6 +173,7 @@ const Page = async ({ params, searchParams }: LinkSurveyPageProps) => {
|
||||
IS_FORMBRICKS_CLOUD={IS_FORMBRICKS_CLOUD}
|
||||
verifiedEmail={verifiedEmail}
|
||||
languageCode={languageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -188,6 +192,7 @@ const Page = async ({ params, searchParams }: LinkSurveyPageProps) => {
|
||||
responseCount={survey.welcomeCard.showResponseCount ? responseCount : undefined}
|
||||
verifiedEmail={verifiedEmail}
|
||||
languageCode={languageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<LegalFooter
|
||||
IMPRINT_URL={IMPRINT_URL}
|
||||
|
||||
@@ -18,23 +18,24 @@ const Page = async ({ params }) => {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
const [survey, environment, product, tags] = await Promise.all([
|
||||
getSurvey(params.surveyId),
|
||||
getEnvironment(params.environmentId),
|
||||
getProductByEnvironmentId(params.environmentId),
|
||||
getTagsByEnvironmentId(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!survey) {
|
||||
throw new Error("Survey not found");
|
||||
}
|
||||
|
||||
const environment = await getEnvironment(survey.environmentId);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
const product = await getProductByEnvironmentId(environment.id);
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const tags = await getTagsByEnvironmentId(environment.id);
|
||||
const totalResponseCount = await getResponseCountBySurveyId(surveyId);
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentI
|
||||
import { SummaryPage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
@@ -17,18 +18,21 @@ const Page = async ({ params }) => {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const survey = await getSurvey(surveyId);
|
||||
const [survey, environment, attributeClasses, product] = await Promise.all([
|
||||
getSurvey(params.surveyId),
|
||||
getEnvironment(params.environmentId),
|
||||
getAttributeClasses(params.environmentId),
|
||||
getProductByEnvironmentId(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!survey) {
|
||||
throw new Error("Survey not found");
|
||||
}
|
||||
const environment = await getEnvironment(survey.environmentId);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
const product = await getProductByEnvironmentId(environment.id);
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
@@ -52,6 +56,7 @@ const Page = async ({ params }) => {
|
||||
surveyId={survey.id}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
totalResponseCount={totalResponseCount}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</PageContentWrapper>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const LanguageIndicator = ({
|
||||
setShowLanguageDropdown(false);
|
||||
};
|
||||
|
||||
const langaugeToBeDisplayed = surveyLanguages.find((language) => {
|
||||
const languageToBeDisplayed = surveyLanguages.find((language) => {
|
||||
return selectedLanguageCode === "default"
|
||||
? language.default === true
|
||||
: language.language.code === selectedLanguageCode;
|
||||
@@ -48,7 +48,7 @@ export const LanguageIndicator = ({
|
||||
tabIndex={-1}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={showLanguageDropdown}>
|
||||
{langaugeToBeDisplayed ? getLanguageLabel(langaugeToBeDisplayed?.language.code) : ""}
|
||||
{languageToBeDisplayed ? getLanguageLabel(languageToBeDisplayed?.language.code) : ""}
|
||||
<ChevronDown className="ml-1 h-4 w-4" />
|
||||
</button>
|
||||
{showLanguageDropdown && (
|
||||
@@ -57,7 +57,7 @@ export const LanguageIndicator = ({
|
||||
ref={languageDropdownRef}>
|
||||
{surveyLanguages.map(
|
||||
(language) =>
|
||||
language.language.code !== langaugeToBeDisplayed?.language.code && (
|
||||
language.language.code !== languageToBeDisplayed?.language.code && (
|
||||
<button
|
||||
key={language.language.id}
|
||||
type="button"
|
||||
|
||||
@@ -95,7 +95,7 @@ export const LocalizedEditor = ({
|
||||
className="fb-htmlbody ml-1" // styles are in global.css
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(
|
||||
recallToHeadline(value, localSurvey, false, "default")["default"] ?? ""
|
||||
recallToHeadline(value, localSurvey, false, "default", [])["default"] ?? ""
|
||||
),
|
||||
}}></label>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@ import { TSurveySummary } from "@formbricks/types/surveys";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
|
||||
import { getAttributes } from "../attribute/service";
|
||||
import { getAttributeClasses } from "../attributeClass/service";
|
||||
import { cache } from "../cache";
|
||||
import { ITEMS_PER_PAGE, WEBAPP_URL } from "../constants";
|
||||
import { displayCache } from "../display/cache";
|
||||
@@ -36,7 +37,7 @@ import { putFile } from "../storage/service";
|
||||
import { getSurvey } from "../survey/service";
|
||||
import { captureTelemetry } from "../telemetry";
|
||||
import { convertToCsv, convertToXlsxBuffer } from "../utils/fileConversion";
|
||||
import { checkForRecallInHeadline } from "../utils/recall";
|
||||
import { replaceHeadlineRecall } from "../utils/recall";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { responseCache } from "./cache";
|
||||
import {
|
||||
@@ -559,10 +560,10 @@ export const getSurveySummary = (
|
||||
|
||||
try {
|
||||
const survey = await getSurvey(surveyId);
|
||||
|
||||
if (!survey) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
const attributeClasses = await getAttributeClasses(survey.environmentId);
|
||||
|
||||
const batchSize = 3000;
|
||||
const responseCount = await getResponseCountBySurveyId(surveyId, filterCriteria);
|
||||
@@ -582,7 +583,7 @@ export const getSurveySummary = (
|
||||
const dropOff = getSurveySummaryDropOff(survey, responses, displayCount);
|
||||
const meta = getSurveySummaryMeta(responses, displayCount);
|
||||
const questionWiseSummary = getQuestionWiseSummary(
|
||||
checkForRecallInHeadline(survey, "default"),
|
||||
replaceHeadlineRecall(survey, "default", attributeClasses),
|
||||
responses,
|
||||
dropOff
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
|
||||
import { selectPerson } from "../../person/service";
|
||||
import { mockSurveyOutput } from "../../survey/tests/__mock__/survey.mock";
|
||||
import { mockAttributeClass, mockSurveyOutput } from "../../survey/tests/__mock__/survey.mock";
|
||||
import {
|
||||
createResponse,
|
||||
createResponseLegacy,
|
||||
@@ -472,6 +472,7 @@ describe("Tests for getSurveySummary service", () => {
|
||||
it("Returns a summary of the survey responses", async () => {
|
||||
prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput);
|
||||
prisma.response.findMany.mockResolvedValue([mockResponse]);
|
||||
prisma.attributeClass.findMany.mockResolvedValueOnce([mockAttributeClass]);
|
||||
|
||||
const summary = await getSurveySummary(mockSurveyId);
|
||||
expect(summary).toEqual(mockSurveySummaryOutput);
|
||||
@@ -490,6 +491,7 @@ describe("Tests for getSurveySummary service", () => {
|
||||
|
||||
prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput);
|
||||
prisma.response.findMany.mockRejectedValue(errToThrow);
|
||||
prisma.attributeClass.findMany.mockResolvedValueOnce([mockAttributeClass]);
|
||||
|
||||
await expect(getSurveySummary(mockSurveyId)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
@@ -499,6 +501,7 @@ describe("Tests for getSurveySummary service", () => {
|
||||
|
||||
prisma.survey.findUnique.mockResolvedValue(mockSurveyOutput);
|
||||
prisma.response.findMany.mockRejectedValue(new Error(mockErrorMessage));
|
||||
prisma.attributeClass.findMany.mockResolvedValueOnce([mockAttributeClass]);
|
||||
|
||||
await expect(getSurveySummary(mockSurveyId)).rejects.toThrow(Error);
|
||||
});
|
||||
|
||||
@@ -374,7 +374,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
|
||||
: [];
|
||||
const updatedLanguageIds =
|
||||
languages.length > 1 ? updatedSurvey.languages.map((l) => l.language.id) : [];
|
||||
const enabledLangaugeIds = languages.map((language) => {
|
||||
const enabledLanguageIds = languages.map((language) => {
|
||||
if (language.enabled) return language.language.id;
|
||||
});
|
||||
|
||||
@@ -392,7 +392,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
|
||||
where: { languageId: surveyLanguage.language.id },
|
||||
data: {
|
||||
default: surveyLanguage.language.id === defaultLanguageId,
|
||||
enabled: enabledLangaugeIds.includes(surveyLanguage.language.id),
|
||||
enabled: enabledLanguageIds.includes(surveyLanguage.language.id),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -401,7 +401,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
|
||||
data.languages.create = languagesToAdd.map((languageId) => ({
|
||||
languageId: languageId,
|
||||
default: languageId === defaultLanguageId,
|
||||
enabled: enabledLangaugeIds.includes(languageId),
|
||||
enabled: enabledLanguageIds.includes(languageId),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -409,7 +409,7 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
|
||||
if (languagesToRemove.length > 0) {
|
||||
data.languages.deleteMany = languagesToRemove.map((languageId) => ({
|
||||
languageId: languageId,
|
||||
enabled: enabledLangaugeIds.includes(languageId),
|
||||
enabled: enabledLanguageIds.includes(languageId),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { TI18nString, TSurvey, TSurveyQuestion, TSurveyQuestionsObject } from "@formbricks/types/surveys";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionsObject,
|
||||
TSurveyRecallItem,
|
||||
} from "@formbricks/types/surveys";
|
||||
|
||||
import { getLocalizedValue } from "../i18n/utils";
|
||||
import { structuredClone } from "../pollyfills/structuredClone";
|
||||
import { formatDateWithOrdinal, isValidDateString } from "./datetime";
|
||||
|
||||
export interface fallbacks {
|
||||
[id: string]: string;
|
||||
@@ -37,8 +47,9 @@ export const extractFallbackValue = (text: string): string => {
|
||||
};
|
||||
|
||||
// Extracts the complete recall information (ID and fallback) from a headline string.
|
||||
export const extractRecallInfo = (headline: string): string | null => {
|
||||
const pattern = /#recall:([A-Za-z0-9_-]+)\/fallback:(\S*)#/;
|
||||
export const extractRecallInfo = (headline: string, id?: string): string | null => {
|
||||
const idPattern = id ? id : "[A-Za-z0-9_-]+";
|
||||
const pattern = new RegExp(`#recall:(${idPattern})\\/fallback:(\\S*)#`);
|
||||
const match = headline.match(pattern);
|
||||
return match ? match[0] : null;
|
||||
};
|
||||
@@ -50,67 +61,87 @@ export const findRecallInfoById = (text: string, id: string): string | null => {
|
||||
return match ? match[0] : null;
|
||||
};
|
||||
|
||||
const getRecallItemLabel = <T extends TSurveyQuestionsObject>(
|
||||
recallItemId: string,
|
||||
survey: T,
|
||||
languageCode: string,
|
||||
attributeClasses: TAttributeClass[]
|
||||
): string | undefined => {
|
||||
const isHiddenField = survey.hiddenFields.fieldIds?.includes(recallItemId);
|
||||
if (isHiddenField) return recallItemId;
|
||||
|
||||
const surveyQuestion = survey.questions.find((question) => question.id === recallItemId);
|
||||
if (surveyQuestion) return surveyQuestion.headline[languageCode];
|
||||
|
||||
const attributeClass = attributeClasses.find(
|
||||
(attributeClass) => attributeClass.name.replaceAll(" ", "nbsp") === recallItemId
|
||||
);
|
||||
return attributeClass?.name;
|
||||
};
|
||||
|
||||
// Converts recall information in a headline to a corresponding recall question headline, with or without a slash.
|
||||
export const recallToHeadline = <T extends TSurveyQuestionsObject>(
|
||||
headline: TI18nString,
|
||||
survey: T,
|
||||
withSlash: boolean,
|
||||
language: string
|
||||
languageCode: string,
|
||||
attributeClasses: TAttributeClass[]
|
||||
): TI18nString => {
|
||||
let newHeadline = structuredClone(headline);
|
||||
if (!newHeadline[language]?.includes("#recall:")) return headline;
|
||||
const localizedHeadline = newHeadline[languageCode];
|
||||
|
||||
while (newHeadline[language].includes("#recall:")) {
|
||||
const recallInfo = extractRecallInfo(getLocalizedValue(newHeadline, language));
|
||||
if (recallInfo) {
|
||||
const questionId = extractId(recallInfo);
|
||||
let questionHeadline = getLocalizedValue(
|
||||
survey.questions.find((question) => question.id === questionId)?.headline,
|
||||
language
|
||||
);
|
||||
while (questionHeadline?.includes("#recall:")) {
|
||||
const recallInfo = extractRecallInfo(questionHeadline);
|
||||
if (recallInfo) {
|
||||
questionHeadline = questionHeadline.replaceAll(recallInfo, "___");
|
||||
if (!localizedHeadline?.includes("#recall:")) return headline;
|
||||
|
||||
const replaceNestedRecalls = (text: string): string => {
|
||||
while (text.includes("#recall:")) {
|
||||
const recallInfo = extractRecallInfo(text);
|
||||
if (!recallInfo) break;
|
||||
|
||||
const recallItemId = extractId(recallInfo);
|
||||
if (!recallItemId) break;
|
||||
|
||||
let recallItemLabel =
|
||||
getRecallItemLabel(recallItemId, survey, languageCode, attributeClasses) || recallItemId;
|
||||
|
||||
while (recallItemLabel.includes("#recall:")) {
|
||||
const nestedRecallInfo = extractRecallInfo(recallItemLabel);
|
||||
if (nestedRecallInfo) {
|
||||
recallItemLabel = recallItemLabel.replace(nestedRecallInfo, "___");
|
||||
}
|
||||
}
|
||||
if (withSlash) {
|
||||
newHeadline[language] = newHeadline[language].replace(recallInfo, `/${questionHeadline}\\`);
|
||||
} else {
|
||||
newHeadline[language] = newHeadline[language].replace(recallInfo, `@${questionHeadline}`);
|
||||
}
|
||||
|
||||
const replacement = withSlash ? `/${recallItemLabel}\\` : `@${recallItemLabel}`;
|
||||
text = text.replace(recallInfo, replacement);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
newHeadline[languageCode] = replaceNestedRecalls(localizedHeadline);
|
||||
return newHeadline;
|
||||
};
|
||||
|
||||
// Replaces recall information in a survey question's headline with an ___.
|
||||
export const replaceRecallInfoWithUnderline = (
|
||||
recallQuestion: TSurveyQuestion,
|
||||
language: string
|
||||
): TSurveyQuestion => {
|
||||
while (getLocalizedValue(recallQuestion.headline, language).includes("#recall:")) {
|
||||
const recallInfo = extractRecallInfo(getLocalizedValue(recallQuestion.headline, language));
|
||||
export const replaceRecallInfoWithUnderline = (label: string): string => {
|
||||
let newLabel = label;
|
||||
while (newLabel.includes("#recall:")) {
|
||||
const recallInfo = extractRecallInfo(newLabel);
|
||||
if (recallInfo) {
|
||||
recallQuestion.headline[language] = getLocalizedValue(recallQuestion.headline, language).replace(
|
||||
recallInfo,
|
||||
"___"
|
||||
);
|
||||
newLabel = newLabel.replace(recallInfo, "___");
|
||||
}
|
||||
}
|
||||
return recallQuestion;
|
||||
return newLabel;
|
||||
};
|
||||
|
||||
// Checks for survey questions with a "recall" pattern but no fallback value.
|
||||
export const checkForEmptyFallBackValue = (survey: TSurvey, langauge: string): TSurveyQuestion | null => {
|
||||
export const checkForEmptyFallBackValue = (survey: TSurvey, language: string): TSurveyQuestion | null => {
|
||||
const findRecalls = (text: string) => {
|
||||
const recalls = text.match(/#recall:[^ ]+/g);
|
||||
return recalls && recalls.some((recall) => !extractFallbackValue(recall));
|
||||
};
|
||||
for (const question of survey.questions) {
|
||||
if (
|
||||
findRecalls(getLocalizedValue(question.headline, langauge)) ||
|
||||
(question.subheader && findRecalls(getLocalizedValue(question.subheader, langauge)))
|
||||
findRecalls(getLocalizedValue(question.headline, language)) ||
|
||||
(question.subheader && findRecalls(getLocalizedValue(question.subheader, language)))
|
||||
) {
|
||||
return question;
|
||||
}
|
||||
@@ -119,32 +150,51 @@ export const checkForEmptyFallBackValue = (survey: TSurvey, langauge: string): T
|
||||
};
|
||||
|
||||
// Processes each question in a survey to ensure headlines are formatted correctly for recall and return the modified survey.
|
||||
export const checkForRecallInHeadline = <T extends TSurveyQuestionsObject>(
|
||||
export const replaceHeadlineRecall = <T extends TSurveyQuestionsObject>(
|
||||
survey: T,
|
||||
langauge: string
|
||||
language: string,
|
||||
attributeClasses: TAttributeClass[]
|
||||
): T => {
|
||||
const modifiedSurvey: T = structuredClone(survey);
|
||||
const modifiedSurvey = structuredClone(survey);
|
||||
modifiedSurvey.questions.forEach((question) => {
|
||||
question.headline = recallToHeadline(question.headline, modifiedSurvey, false, langauge);
|
||||
question.headline = recallToHeadline(
|
||||
question.headline,
|
||||
modifiedSurvey,
|
||||
false,
|
||||
language,
|
||||
attributeClasses
|
||||
);
|
||||
});
|
||||
return modifiedSurvey;
|
||||
};
|
||||
|
||||
// Retrieves an array of survey questions referenced in a text containing recall information.
|
||||
export const getRecallQuestions = (text: string, survey: TSurvey, langauge: string): TSurveyQuestion[] => {
|
||||
export const getRecallItems = (
|
||||
text: string,
|
||||
survey: TSurvey,
|
||||
languageCode: string,
|
||||
attributeClasses: TAttributeClass[]
|
||||
): TSurveyRecallItem[] => {
|
||||
if (!text.includes("#recall:")) return [];
|
||||
|
||||
const ids = extractIds(text);
|
||||
let recallQuestionArray: TSurveyQuestion[] = [];
|
||||
ids.forEach((questionId) => {
|
||||
let recallQuestion = survey.questions.find((question) => question.id === questionId);
|
||||
if (recallQuestion) {
|
||||
let recallQuestionTemp = structuredClone(recallQuestion);
|
||||
recallQuestionTemp = replaceRecallInfoWithUnderline(recallQuestionTemp, langauge);
|
||||
recallQuestionArray.push(recallQuestionTemp);
|
||||
let recallItems: TSurveyRecallItem[] = [];
|
||||
ids.forEach((recallItemId) => {
|
||||
const isHiddenField = survey.hiddenFields.fieldIds?.includes(recallItemId);
|
||||
const isSurveyQuestion = survey.questions.find((question) => question.id === recallItemId);
|
||||
|
||||
const recallItemLabel = getRecallItemLabel(recallItemId, survey, languageCode, attributeClasses);
|
||||
if (recallItemLabel) {
|
||||
let recallItemLabelTemp = recallItemLabel;
|
||||
recallItemLabelTemp = replaceRecallInfoWithUnderline(recallItemLabelTemp);
|
||||
recallItems.push({
|
||||
id: recallItemId,
|
||||
label: recallItemLabelTemp,
|
||||
type: isHiddenField ? "hiddenField" : isSurveyQuestion ? "question" : "attributeClass",
|
||||
});
|
||||
}
|
||||
});
|
||||
return recallQuestionArray;
|
||||
return recallItems;
|
||||
};
|
||||
|
||||
// Constructs a fallbacks object from a text containing multiple recall and fallback patterns.
|
||||
@@ -165,13 +215,76 @@ export const getFallbackValues = (text: string): fallbacks => {
|
||||
// Transforms headlines in a text to their corresponding recall information.
|
||||
export const headlineToRecall = (
|
||||
text: string,
|
||||
recallQuestions: TSurveyQuestion[],
|
||||
fallbacks: fallbacks,
|
||||
langauge: string
|
||||
recallItems: TSurveyRecallItem[],
|
||||
fallbacks: fallbacks
|
||||
): string => {
|
||||
recallQuestions.forEach((recallQuestion) => {
|
||||
const recallInfo = `#recall:${recallQuestion.id}/fallback:${fallbacks[recallQuestion.id]}#`;
|
||||
text = text.replace(`@${recallQuestion.headline[langauge]}`, recallInfo);
|
||||
recallItems.forEach((recallItem) => {
|
||||
const recallInfo = `#recall:${recallItem.id}/fallback:${fallbacks[recallItem.id]}#`;
|
||||
text = text.replace(`@${recallItem.label}`, recallInfo);
|
||||
});
|
||||
return text;
|
||||
};
|
||||
|
||||
export const parseRecallInfo = (
|
||||
text: string,
|
||||
attributes?: TAttributes,
|
||||
responseData?: TResponseData,
|
||||
withSlash: boolean = false
|
||||
) => {
|
||||
let modifiedText = text;
|
||||
const attributeKeys = attributes ? Object.keys(attributes) : [];
|
||||
const questionIds = responseData ? Object.keys(responseData) : [];
|
||||
if (attributes && attributeKeys.length > 0) {
|
||||
attributeKeys.forEach((attributeKey) => {
|
||||
const recallPattern = `#recall:${attributeKey}`;
|
||||
while (modifiedText.includes(recallPattern)) {
|
||||
const recallInfo = extractRecallInfo(modifiedText, attributeKey);
|
||||
if (!recallInfo) break; // Exit the loop if no recall info is found
|
||||
|
||||
const recallItemId = extractId(recallInfo);
|
||||
if (!recallItemId) continue; // Skip to the next iteration if no ID could be extracted
|
||||
|
||||
const fallback = extractFallbackValue(recallInfo).replaceAll("nbsp", " ");
|
||||
let value = attributes[recallItemId.replace("nbsp", " ")] || fallback;
|
||||
if (withSlash) {
|
||||
modifiedText = modifiedText.replace(recallInfo, "#/" + value + "\\#");
|
||||
} else {
|
||||
modifiedText = modifiedText.replace(recallInfo, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (responseData && questionIds.length > 0) {
|
||||
while (modifiedText.includes("recall:")) {
|
||||
const recallInfo = extractRecallInfo(modifiedText);
|
||||
if (!recallInfo) break; // Exit the loop if no recall info is found
|
||||
|
||||
const recallItemId = extractId(recallInfo);
|
||||
if (!recallItemId) return modifiedText; // Return the text if no ID could be extracted
|
||||
|
||||
const fallback = extractFallbackValue(recallInfo).replaceAll("nbsp", " ");
|
||||
let value;
|
||||
|
||||
// Fetching value from responseData or attributes based on recallItemId
|
||||
if (responseData[recallItemId]) {
|
||||
value = (responseData[recallItemId] as string) ?? fallback;
|
||||
}
|
||||
// Additional value formatting if it exists
|
||||
if (value) {
|
||||
if (isValidDateString(value)) {
|
||||
value = formatDateWithOrdinal(new Date(value));
|
||||
} else if (Array.isArray(value)) {
|
||||
value = value.filter((item) => item).join(", "); // Filters out empty values and joins with a comma
|
||||
}
|
||||
}
|
||||
|
||||
if (withSlash) {
|
||||
modifiedText = modifiedText.replace(recallInfo, "#/" + (value ?? fallback) + "\\#");
|
||||
} else {
|
||||
modifiedText = modifiedText.replace(recallInfo, value ?? fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedText;
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ export const Survey = ({
|
||||
onFileUpload,
|
||||
responseCount,
|
||||
startAtQuestionId,
|
||||
hiddenFieldsRecord,
|
||||
clickOutside,
|
||||
shouldResetQuestionId,
|
||||
}: SurveyBaseProps) => {
|
||||
@@ -58,7 +59,7 @@ export const Survey = ({
|
||||
|
||||
const [loadingElement, setLoadingElement] = useState(false);
|
||||
const [history, setHistory] = useState<string[]>([]);
|
||||
const [responseData, setResponseData] = useState<TResponseData>({});
|
||||
const [responseData, setResponseData] = useState<TResponseData>(hiddenFieldsRecord ?? {});
|
||||
const [ttc, setTtc] = useState<TResponseTtc>({});
|
||||
const cardArrangement = useMemo(() => {
|
||||
if (survey.type === "link") {
|
||||
@@ -253,6 +254,7 @@ export const Survey = ({
|
||||
languageCode={languageCode}
|
||||
responseCount={responseCount}
|
||||
isInIframe={isInIframe}
|
||||
replaceRecallInfo={replaceRecallInfo}
|
||||
/>
|
||||
);
|
||||
} else if (questionIdx === survey.questions.length) {
|
||||
|
||||
@@ -19,6 +19,7 @@ interface WelcomeCardProps {
|
||||
languageCode: string;
|
||||
responseCount?: number;
|
||||
isInIframe: boolean;
|
||||
replaceRecallInfo: (text: string, responseData: TResponseData) => string;
|
||||
}
|
||||
|
||||
const TimerIcon = () => {
|
||||
@@ -68,6 +69,7 @@ export const WelcomeCard = ({
|
||||
survey,
|
||||
responseCount,
|
||||
isInIframe,
|
||||
replaceRecallInfo,
|
||||
}: WelcomeCardProps) => {
|
||||
const calculateTimeToComplete = () => {
|
||||
let idx = calculateElementIdx(survey, 0);
|
||||
@@ -109,8 +111,14 @@ export const WelcomeCard = ({
|
||||
<img src={fileUrl} className="mb-8 max-h-96 w-1/3 rounded-lg object-contain" alt="Company Logo" />
|
||||
)}
|
||||
|
||||
<Headline headline={getLocalizedValue(headline, languageCode)} questionId="welcomeCard" />
|
||||
<HtmlBody htmlString={getLocalizedValue(html, languageCode)} questionId="welcomeCard" />
|
||||
<Headline
|
||||
headline={replaceRecallInfo(getLocalizedValue(headline, languageCode), {})}
|
||||
questionId="welcomeCard"
|
||||
/>
|
||||
<HtmlBody
|
||||
htmlString={replaceRecallInfo(getLocalizedValue(html, languageCode), {})}
|
||||
questionId="welcomeCard"
|
||||
/>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
|
||||
|
||||
@@ -6,23 +6,37 @@ import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
export const replaceRecallInfo = (text: string, responseData: TResponseData): string => {
|
||||
while (text.includes("recall:")) {
|
||||
const recallInfo = extractRecallInfo(text);
|
||||
if (recallInfo) {
|
||||
const questionId = extractId(recallInfo);
|
||||
const fallback = extractFallbackValue(recallInfo).replaceAll("nbsp", " ");
|
||||
let value = questionId && responseData[questionId] ? (responseData[questionId] as string) : fallback;
|
||||
let modifiedText = text;
|
||||
|
||||
while (modifiedText.includes("recall:")) {
|
||||
const recallInfo = extractRecallInfo(modifiedText);
|
||||
if (!recallInfo) break; // Exit the loop if no recall info is found
|
||||
|
||||
const recallItemId = extractId(recallInfo);
|
||||
if (!recallItemId) return modifiedText; // Return the text if no ID could be extracted
|
||||
|
||||
const fallback = extractFallbackValue(recallInfo).replaceAll("nbsp", " ");
|
||||
let value = null;
|
||||
|
||||
// Fetching value from responseData or attributes based on recallItemId
|
||||
if (responseData[recallItemId]) {
|
||||
value = (responseData[recallItemId] as string) ?? fallback;
|
||||
}
|
||||
|
||||
// Additional value formatting if it exists
|
||||
if (value) {
|
||||
if (isValidDateString(value)) {
|
||||
value = formatDateWithOrdinal(new Date(value));
|
||||
} else if (Array.isArray(value)) {
|
||||
value = value.filter((item) => item).join(", "); // Filters out empty values and joins with a comma
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
value = value.filter((item) => item !== null && item !== undefined && item !== "").join(", ");
|
||||
}
|
||||
text = text.replace(recallInfo, value);
|
||||
}
|
||||
|
||||
// Replace the recallInfo in the text with the obtained or fallback value
|
||||
modifiedText = modifiedText.replace(recallInfo, value || fallback);
|
||||
}
|
||||
return text;
|
||||
|
||||
return modifiedText;
|
||||
};
|
||||
|
||||
export const parseRecallInformation = (
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface SurveyBaseProps {
|
||||
responseCount?: number;
|
||||
isCardBorderVisible?: boolean;
|
||||
startAtQuestionId?: string;
|
||||
hiddenFieldsRecord?: TResponseData;
|
||||
clickOutside?: boolean;
|
||||
shouldResetQuestionId?: boolean;
|
||||
}
|
||||
|
||||
@@ -438,7 +438,10 @@ export const ZSurveyQuestions = z.array(ZSurveyQuestion);
|
||||
|
||||
export type TSurveyQuestions = z.infer<typeof ZSurveyQuestions>;
|
||||
|
||||
export const ZSurveyQuestionsObject = z.object({ questions: ZSurveyQuestions });
|
||||
export const ZSurveyQuestionsObject = z.object({
|
||||
questions: ZSurveyQuestions,
|
||||
hiddenFields: ZSurveyHiddenFields,
|
||||
});
|
||||
|
||||
export type TSurveyQuestionsObject = z.infer<typeof ZSurveyQuestionsObject>;
|
||||
|
||||
@@ -877,3 +880,11 @@ const ZSortOption = z.object({
|
||||
|
||||
export type TSortOption = z.infer<typeof ZSortOption>;
|
||||
export type TSurveySummary = z.infer<typeof ZSurveySummary>;
|
||||
|
||||
export const ZSurveyRecallItem = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
type: z.enum(["question", "hiddenField", "attributeClass"]),
|
||||
});
|
||||
|
||||
export type TSurveyRecallItem = z.infer<typeof ZSurveyRecallItem>;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ZAttributeClass } from "./attributeClasses";
|
||||
import { ZResponseData } from "./responses";
|
||||
import { ZSurveyQuestion, ZSurveyStatus } from "./surveys";
|
||||
import { ZSurveyHiddenFields, ZSurveyQuestion, ZSurveyStatus } from "./surveys";
|
||||
import { ZUserNotificationSettings } from "./user";
|
||||
|
||||
const ZWeeklySummaryInsights = z.object({
|
||||
@@ -60,6 +61,7 @@ export const ZWeeklySummarySurveyData = z.object({
|
||||
status: ZSurveyStatus,
|
||||
responses: z.array(ZWeeklyEmailResponseData),
|
||||
displays: z.array(z.object({ id: z.string() })),
|
||||
hiddenFields: ZSurveyHiddenFields,
|
||||
});
|
||||
|
||||
export type TWeeklySummarySurveyData = z.infer<typeof ZWeeklySummarySurveyData>;
|
||||
@@ -67,6 +69,7 @@ export type TWeeklySummarySurveyData = z.infer<typeof ZWeeklySummarySurveyData>;
|
||||
export const ZWeeklySummaryEnvironmentData = z.object({
|
||||
id: z.string(),
|
||||
surveys: z.array(ZWeeklySummarySurveyData),
|
||||
attributeClasses: z.array(ZAttributeClass),
|
||||
});
|
||||
|
||||
export type TWeeklySummaryEnvironmentData = z.infer<typeof ZWeeklySummaryEnvironmentData>;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { RefObject } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
import { TSurveyRecallItem } from "@formbricks/types/surveys";
|
||||
|
||||
import { Button } from "../../Button";
|
||||
import { Input } from "../../Input";
|
||||
|
||||
interface FallbackInputProps {
|
||||
filteredRecallQuestions: (TSurveyQuestion | undefined)[];
|
||||
filteredRecallItems: (TSurveyRecallItem | undefined)[];
|
||||
fallbacks: { [type: string]: string };
|
||||
setFallbacks: (fallbacks: { [type: string]: string }) => void;
|
||||
fallbackInputRef: RefObject<HTMLInputElement>;
|
||||
@@ -15,7 +15,7 @@ interface FallbackInputProps {
|
||||
}
|
||||
|
||||
export const FallbackInput = ({
|
||||
filteredRecallQuestions,
|
||||
filteredRecallItems,
|
||||
fallbacks,
|
||||
setFallbacks,
|
||||
fallbackInputRef,
|
||||
@@ -31,16 +31,17 @@ export const FallbackInput = ({
|
||||
return (
|
||||
<div className="fixed z-30 mt-1 rounded-md border border-slate-300 bg-slate-50 p-3 text-xs">
|
||||
<p className="font-medium">Add a placeholder to show if the question gets skipped:</p>
|
||||
{filteredRecallQuestions.map((recallQuestion) => {
|
||||
if (!recallQuestion) return;
|
||||
{filteredRecallItems.map((recallItem) => {
|
||||
if (!recallItem) return;
|
||||
return (
|
||||
<div className="mt-2 flex flex-col" key={recallQuestion.id}>
|
||||
<div className="mt-2 flex flex-col" key={recallItem.id}>
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
className="placeholder:text-md h-full bg-white"
|
||||
ref={fallbackInputRef}
|
||||
id="fallback"
|
||||
value={fallbacks[recallQuestion.id]?.replaceAll("nbsp", " ")}
|
||||
value={fallbacks[recallItem.id]?.replaceAll("nbsp", " ")}
|
||||
placeholder={"Fallback for " + recallItem.label}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key == "Enter") {
|
||||
e.preventDefault();
|
||||
@@ -53,7 +54,7 @@ export const FallbackInput = ({
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const newFallbacks = { ...fallbacks };
|
||||
newFallbacks[recallQuestion.id] = e.target.value;
|
||||
newFallbacks[recallItem.id] = e.target.value;
|
||||
setFallbacks(newFallbacks);
|
||||
}}
|
||||
/>
|
||||
|
||||
205
packages/ui/QuestionFormInput/components/RecallItemSelect.tsx
Normal file
205
packages/ui/QuestionFormInput/components/RecallItemSelect.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import { DropdownMenuItem } from "@radix-ui/react-dropdown-menu";
|
||||
import {
|
||||
CalendarDaysIcon,
|
||||
EyeOffIcon,
|
||||
HomeIcon,
|
||||
ListIcon,
|
||||
MessageSquareTextIcon,
|
||||
PhoneIcon,
|
||||
PresentationIcon,
|
||||
Rows3Icon,
|
||||
StarIcon,
|
||||
TagIcon,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { replaceRecallInfoWithUnderline } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TSurvey, TSurveyHiddenFields, TSurveyQuestion, TSurveyRecallItem } from "@formbricks/types/surveys";
|
||||
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "../../DropdownMenu";
|
||||
import { Input } from "../../Input";
|
||||
|
||||
const questionIconMapping = {
|
||||
openText: MessageSquareTextIcon,
|
||||
multipleChoiceSingle: Rows3Icon,
|
||||
multipleChoiceMulti: ListIcon,
|
||||
rating: StarIcon,
|
||||
nps: PresentationIcon,
|
||||
date: CalendarDaysIcon,
|
||||
cal: PhoneIcon,
|
||||
address: HomeIcon,
|
||||
};
|
||||
|
||||
interface RecallItemSelectProps {
|
||||
localSurvey: TSurvey;
|
||||
questionId: string;
|
||||
addRecallItem: (question: TSurveyRecallItem) => void;
|
||||
setShowRecallItemSelect: (show: boolean) => void;
|
||||
recallItems: TSurveyRecallItem[];
|
||||
selectedLanguageCode: string;
|
||||
hiddenFields: TSurveyHiddenFields;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const RecallItemSelect = ({
|
||||
localSurvey,
|
||||
questionId,
|
||||
addRecallItem,
|
||||
setShowRecallItemSelect,
|
||||
recallItems,
|
||||
selectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: RecallItemSelectProps) => {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const isNotAllowedQuestionType = (question: TSurveyQuestion): boolean => {
|
||||
return (
|
||||
question.type === "fileUpload" ||
|
||||
question.type === "cta" ||
|
||||
question.type === "consent" ||
|
||||
question.type === "pictureSelection" ||
|
||||
question.type === "cal" ||
|
||||
question.type === "matrix"
|
||||
);
|
||||
};
|
||||
|
||||
const recallItemIds = useMemo(() => {
|
||||
return recallItems.map((recallItem) => recallItem.id);
|
||||
}, [recallItems]);
|
||||
|
||||
const hiddenFieldRecallItems = useMemo(() => {
|
||||
if (localSurvey.type !== "link") return [];
|
||||
if (localSurvey.hiddenFields.fieldIds) {
|
||||
return localSurvey.hiddenFields.fieldIds
|
||||
.filter((hiddenFieldId) => {
|
||||
return !recallItemIds.includes(hiddenFieldId);
|
||||
})
|
||||
.map((hiddenFieldId) => ({
|
||||
id: hiddenFieldId,
|
||||
label: hiddenFieldId,
|
||||
type: "hiddenField" as const,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}, [localSurvey.hiddenFields]);
|
||||
|
||||
const attributeClassRecallItems = useMemo(() => {
|
||||
if (localSurvey.type !== "app") return [];
|
||||
return attributeClasses
|
||||
.filter((attributeClass) => !recallItemIds.includes(attributeClass.name.replaceAll(" ", "nbsp")))
|
||||
.map((attributeClass) => {
|
||||
return {
|
||||
id: attributeClass.name.replaceAll(" ", "nbsp"),
|
||||
label: attributeClass.name,
|
||||
type: "attributeClass" as const,
|
||||
};
|
||||
});
|
||||
}, [attributeClasses]);
|
||||
|
||||
const surveyQuestionRecallItems = useMemo(() => {
|
||||
const idx =
|
||||
questionId === "end"
|
||||
? localSurvey.questions.length
|
||||
: localSurvey.questions.findIndex((recallQuestion) => recallQuestion.id === questionId);
|
||||
const filteredQuestions = localSurvey.questions
|
||||
.filter((question, index) => {
|
||||
const notAllowed = isNotAllowedQuestionType(question);
|
||||
return (
|
||||
!recallItemIds.includes(question.id) && !notAllowed && question.id !== questionId && idx > index
|
||||
);
|
||||
})
|
||||
.map((question) => {
|
||||
return { id: question.id, label: question.headline[selectedLanguageCode], type: "question" as const };
|
||||
});
|
||||
|
||||
return filteredQuestions;
|
||||
}, [localSurvey.questions, questionId, recallItemIds]);
|
||||
|
||||
const filteredRecallItems: TSurveyRecallItem[] = useMemo(() => {
|
||||
return [...surveyQuestionRecallItems, ...hiddenFieldRecallItems, ...attributeClassRecallItems].filter(
|
||||
(recallItems) => {
|
||||
if (searchValue.trim() === "") return true;
|
||||
else {
|
||||
return recallItems.label.toLowerCase().startsWith(searchValue.toLowerCase());
|
||||
}
|
||||
}
|
||||
);
|
||||
}, [surveyQuestionRecallItems, hiddenFieldRecallItems, attributeClassRecallItems, searchValue]);
|
||||
|
||||
// function to modify headline (recallInfo to corresponding headline)
|
||||
const getRecallLabel = (label: string): string => {
|
||||
return replaceRecallInfoWithUnderline(label);
|
||||
};
|
||||
|
||||
const getQuestionIcon = (recallItem: TSurveyRecallItem) => {
|
||||
switch (recallItem.type) {
|
||||
case "question":
|
||||
const question = localSurvey.questions.find((question) => question.id === questionId);
|
||||
if (question) {
|
||||
return questionIconMapping[question?.type as keyof typeof questionIconMapping];
|
||||
}
|
||||
case "hiddenField":
|
||||
return EyeOffIcon;
|
||||
case "attributeClass":
|
||||
return TagIcon;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu defaultOpen={true} modal={false}>
|
||||
<DropdownMenuTrigger className="z-10 cursor-pointer" asChild>
|
||||
<div className="flex h-0 w-full items-center justify-between overflow-hidden" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-96 bg-slate-50 text-slate-700" align="start" side="bottom">
|
||||
<p className="m-2 text-sm font-medium">Recall Information from...</p>
|
||||
<Input
|
||||
id="recallItemSearchInput"
|
||||
placeholder="Search options"
|
||||
className="mb-1 w-full bg-white"
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
autoFocus={true}
|
||||
value={searchValue}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowDown") {
|
||||
document.getElementById("recallItem-0")?.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="max-h-72 overflow-y-auto overflow-x-hidden">
|
||||
{filteredRecallItems.map((recallItem, index) => {
|
||||
const IconComponent = getQuestionIcon(recallItem);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
id={"recallItem-" + index}
|
||||
key={recallItem.id}
|
||||
title={recallItem.label}
|
||||
onSelect={() => {
|
||||
addRecallItem({ id: recallItem.id, label: recallItem.label, type: recallItem.type });
|
||||
setShowRecallItemSelect(false);
|
||||
}}
|
||||
autoFocus={false}
|
||||
className="flex w-full cursor-pointer rounded-md p-2 focus:bg-slate-200 focus:outline-none"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "ArrowUp" && index === 0) {
|
||||
document.getElementById("recallItemSearchInput")?.focus();
|
||||
} else if (e.key === "ArrowDown" && index === filteredRecallItems.length - 1) {
|
||||
document.getElementById("recallItemSearchInput")?.focus();
|
||||
}
|
||||
}}>
|
||||
<div>{IconComponent && <IconComponent className="mr-2 w-4" />}</div>
|
||||
<div className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
||||
{getRecallLabel(recallItem.label)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
{filteredRecallItems.length === 0 && (
|
||||
<p className="p-2 text-sm font-medium text-slate-700">No recall items found 🤷</p>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,150 +0,0 @@
|
||||
import {
|
||||
CalendarDaysIcon,
|
||||
HomeIcon,
|
||||
ListIcon,
|
||||
MessageSquareTextIcon,
|
||||
PhoneIcon,
|
||||
PresentationIcon,
|
||||
Rows3Icon,
|
||||
StarIcon,
|
||||
} from "lucide-react";
|
||||
import { RefObject, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { replaceRecallInfoWithUnderline } from "@formbricks/lib/utils/recall";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
const questionIconMapping = {
|
||||
openText: MessageSquareTextIcon,
|
||||
multipleChoiceSingle: Rows3Icon,
|
||||
multipleChoiceMulti: ListIcon,
|
||||
rating: StarIcon,
|
||||
nps: PresentationIcon,
|
||||
date: CalendarDaysIcon,
|
||||
cal: PhoneIcon,
|
||||
address: HomeIcon,
|
||||
};
|
||||
|
||||
interface RecallQuestionSelectProps {
|
||||
localSurvey: TSurvey;
|
||||
questionId: string;
|
||||
addRecallQuestion: (question: TSurveyQuestion) => void;
|
||||
setShowQuestionSelect: (show: boolean) => void;
|
||||
showQuestionSelect: boolean;
|
||||
inputRef: RefObject<HTMLInputElement>;
|
||||
recallQuestions: TSurveyQuestion[];
|
||||
selectedLanguageCode: string;
|
||||
}
|
||||
|
||||
export const RecallQuestionSelect = ({
|
||||
localSurvey,
|
||||
questionId,
|
||||
addRecallQuestion,
|
||||
setShowQuestionSelect,
|
||||
showQuestionSelect,
|
||||
inputRef,
|
||||
recallQuestions,
|
||||
selectedLanguageCode,
|
||||
}: RecallQuestionSelectProps) => {
|
||||
const [focusedQuestionIdx, setFocusedQuestionIdx] = useState(0); // New state for managing focus
|
||||
const isNotAllowedQuestionType = (question: TSurveyQuestion) => {
|
||||
return (
|
||||
question.type === "fileUpload" ||
|
||||
question.type === "cta" ||
|
||||
question.type === "consent" ||
|
||||
question.type === "pictureSelection" ||
|
||||
question.type === "cal" ||
|
||||
question.type === "matrix"
|
||||
);
|
||||
};
|
||||
|
||||
const recallQuestionIds = useMemo(() => {
|
||||
return recallQuestions.map((recallQuestion) => recallQuestion.id);
|
||||
}, [recallQuestions]);
|
||||
|
||||
// function to remove some specific type of questions (fileUpload, imageSelect etc) from the list of questions to recall from and few other checks
|
||||
const filteredRecallQuestions = useMemo(() => {
|
||||
const idx =
|
||||
questionId === "end"
|
||||
? localSurvey.questions.length
|
||||
: localSurvey.questions.findIndex((recallQuestion) => recallQuestion.id === questionId);
|
||||
const filteredQuestions = localSurvey.questions.filter((question, index) => {
|
||||
const notAllowed = isNotAllowedQuestionType(question);
|
||||
return (
|
||||
!recallQuestionIds.includes(question.id) && !notAllowed && question.id !== questionId && idx > index
|
||||
);
|
||||
});
|
||||
return filteredQuestions;
|
||||
}, [localSurvey.questions, questionId, recallQuestionIds]);
|
||||
|
||||
// function to modify headline (recallInfo to corresponding headline)
|
||||
const getRecallHeadline = (question: TSurveyQuestion): TSurveyQuestion => {
|
||||
let questionTemp = structuredClone(question);
|
||||
questionTemp = replaceRecallInfoWithUnderline(questionTemp, selectedLanguageCode);
|
||||
return questionTemp;
|
||||
};
|
||||
|
||||
// function to handle key press
|
||||
useEffect(() => {
|
||||
const handleKeyPress = (event: KeyboardEvent) => {
|
||||
if (showQuestionSelect) {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
setFocusedQuestionIdx((prevIdx) => (prevIdx + 1) % filteredRecallQuestions.length);
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
setFocusedQuestionIdx((prevIdx) =>
|
||||
prevIdx === 0 ? filteredRecallQuestions.length - 1 : prevIdx - 1
|
||||
);
|
||||
} else if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const selectedQuestion = filteredRecallQuestions[focusedQuestionIdx];
|
||||
setShowQuestionSelect(false);
|
||||
if (!selectedQuestion) return;
|
||||
addRecallQuestion(selectedQuestion);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const inputElement = inputRef.current;
|
||||
inputElement?.addEventListener("keydown", handleKeyPress);
|
||||
|
||||
return () => {
|
||||
inputElement?.removeEventListener("keydown", handleKeyPress);
|
||||
};
|
||||
}, [showQuestionSelect, localSurvey.questions, focusedQuestionIdx]);
|
||||
|
||||
return (
|
||||
<div className="absolute z-30 mt-1 flex max-h-40 max-w-[85%] flex-col overflow-y-auto rounded-md border border-slate-300 bg-slate-50 p-3 text-xs ">
|
||||
{filteredRecallQuestions.length === 0 ? (
|
||||
<p className="font-medium text-slate-900">There is no information to recall yet 🤷</p>
|
||||
) : (
|
||||
<p className="mb-2 font-medium">Recall Information from...</p>
|
||||
)}
|
||||
<div>
|
||||
{filteredRecallQuestions.map((q, idx) => {
|
||||
const isFocused = idx === focusedQuestionIdx;
|
||||
const IconComponent = questionIconMapping[q.type as keyof typeof questionIconMapping];
|
||||
return (
|
||||
<div
|
||||
key={q.id}
|
||||
className={`flex max-w-full cursor-pointer items-center rounded-md px-3 py-2 ${
|
||||
isFocused ? "bg-slate-200" : "hover:bg-slate-200 "
|
||||
}`}
|
||||
onClick={() => {
|
||||
addRecallQuestion(q);
|
||||
setShowQuestionSelect(false);
|
||||
}}>
|
||||
<div>{IconComponent && <IconComponent className="mr-2 w-4" />}</div>
|
||||
<div className="max-w-full overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{getLocalizedValue(getRecallHeadline(q).headline, selectedLanguageCode)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -13,12 +13,19 @@ import {
|
||||
extractRecallInfo,
|
||||
findRecallInfoById,
|
||||
getFallbackValues,
|
||||
getRecallQuestions,
|
||||
getRecallItems,
|
||||
headlineToRecall,
|
||||
recallToHeadline,
|
||||
replaceRecallInfoWithUnderline,
|
||||
} from "@formbricks/lib/utils/recall";
|
||||
import { TI18nString, TSurvey, TSurveyChoice, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyChoice,
|
||||
TSurveyQuestion,
|
||||
TSurveyRecallItem,
|
||||
} from "@formbricks/types/surveys";
|
||||
|
||||
import { LanguageIndicator } from "../../ee/multiLanguage/components/LanguageIndicator";
|
||||
import { createI18nString } from "../../lib/i18n/utils";
|
||||
@@ -26,8 +33,7 @@ import { FileInput } from "../FileInput";
|
||||
import { Input } from "../Input";
|
||||
import { Label } from "../Label";
|
||||
import { FallbackInput } from "./components/FallbackInput";
|
||||
import { RecallQuestionSelect } from "./components/RecallQuestionSelect";
|
||||
import { isValueIncomplete } from "./lib/utils";
|
||||
import { RecallItemSelect } from "./components/RecallItemSelect";
|
||||
import {
|
||||
determineImageUploaderVisibility,
|
||||
getCardText,
|
||||
@@ -36,6 +42,7 @@ import {
|
||||
getLabelById,
|
||||
getMatrixLabel,
|
||||
getPlaceHolderById,
|
||||
isValueIncomplete,
|
||||
} from "./utils";
|
||||
|
||||
interface QuestionFormInputProps {
|
||||
@@ -56,6 +63,7 @@ interface QuestionFormInputProps {
|
||||
ref?: RefObject<HTMLInputElement>;
|
||||
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
||||
className?: string;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const QuestionFormInput = ({
|
||||
@@ -75,6 +83,7 @@ export const QuestionFormInput = ({
|
||||
placeholder,
|
||||
onBlur,
|
||||
className,
|
||||
attributeClasses,
|
||||
}: QuestionFormInputProps) => {
|
||||
const question: TSurveyQuestion = localSurvey.questions[questionIdx];
|
||||
const isChoice = id.includes("choice");
|
||||
@@ -126,11 +135,16 @@ export const QuestionFormInput = ({
|
||||
const [showImageUploader, setShowImageUploader] = useState<boolean>(
|
||||
determineImageUploaderVisibility(questionIdx, localSurvey)
|
||||
);
|
||||
const [showQuestionSelect, setShowQuestionSelect] = useState(false);
|
||||
const [showRecallItemSelect, setShowRecallItemSelect] = useState(false);
|
||||
const [showFallbackInput, setShowFallbackInput] = useState(false);
|
||||
const [recallQuestions, setRecallQuestions] = useState<TSurveyQuestion[]>(
|
||||
const [recallItems, setRecallItems] = useState<TSurveyRecallItem[]>(
|
||||
getLocalizedValue(text, selectedLanguageCode).includes("#recall:")
|
||||
? getRecallQuestions(getLocalizedValue(text, selectedLanguageCode), localSurvey, selectedLanguageCode)
|
||||
? getRecallItems(
|
||||
getLocalizedValue(text, selectedLanguageCode),
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
attributeClasses
|
||||
)
|
||||
: []
|
||||
);
|
||||
const [fallbacks, setFallbacks] = useState<{ [type: string]: string }>(
|
||||
@@ -143,31 +157,31 @@ export const QuestionFormInput = ({
|
||||
const fallbackInputRef = useRef<HTMLInputElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const filteredRecallQuestions = Array.from(new Set(recallQuestions.map((q) => q.id))).map((id) => {
|
||||
return recallQuestions.find((q) => q.id === id);
|
||||
const filteredRecallItems = Array.from(new Set(recallItems.map((q) => q.id))).map((id) => {
|
||||
return recallItems.find((q) => q.id === id);
|
||||
});
|
||||
|
||||
// Hook to synchronize the horizontal scroll position of highlightContainerRef and inputRef.
|
||||
useSyncScroll(highlightContainerRef, inputRef);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWelcomeCard && (id === "headline" || id === "subheader")) {
|
||||
if (id === "headline" || id === "subheader") {
|
||||
checkForRecallSymbol();
|
||||
}
|
||||
// Generates an array of headlines from recallQuestions, replacing nested recall questions with '___' .
|
||||
const recallQuestionHeadlines = recallQuestions.flatMap((recallQuestion) => {
|
||||
if (!getLocalizedValue(recallQuestion.headline, selectedLanguageCode).includes("#recall:")) {
|
||||
return [(recallQuestion.headline as TI18nString)[selectedLanguageCode]];
|
||||
// Generates an array of headlines from recallItems, replacing nested recall questions with '___' .
|
||||
const recallItemLabels = recallItems.flatMap((recallItem) => {
|
||||
if (!recallItem.label.includes("#recall:")) {
|
||||
return [recallItem.label];
|
||||
}
|
||||
const recallQuestionText = (recallQuestion[id as keyof typeof recallQuestion] as string) || "";
|
||||
const recallInfo = extractRecallInfo(recallQuestionText);
|
||||
const recallItemLabel = recallItem.label;
|
||||
const recallInfo = extractRecallInfo(recallItemLabel);
|
||||
|
||||
if (recallInfo) {
|
||||
const recallQuestionId = extractId(recallInfo);
|
||||
const recallQuestion = localSurvey.questions.find((question) => question.id === recallQuestionId);
|
||||
const recallItemId = extractId(recallInfo);
|
||||
const recallQuestion = localSurvey.questions.find((question) => question.id === recallItemId);
|
||||
|
||||
if (recallQuestion) {
|
||||
return [recallQuestionText.replace(recallInfo, `___`)];
|
||||
return [recallItemLabel.replace(recallInfo, `___`)];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
@@ -176,12 +190,12 @@ export const QuestionFormInput = ({
|
||||
// Constructs an array of JSX elements representing segmented parts of text, interspersed with special formatted spans for recall headlines.
|
||||
const processInput = (): JSX.Element[] => {
|
||||
const parts: JSX.Element[] = [];
|
||||
let remainingText = recallToHeadline(text, localSurvey, false, selectedLanguageCode)[
|
||||
let remainingText = recallToHeadline(text, localSurvey, false, selectedLanguageCode, attributeClasses)[
|
||||
selectedLanguageCode
|
||||
];
|
||||
filterRecallQuestions(remainingText);
|
||||
recallQuestionHeadlines.forEach((headline) => {
|
||||
const index = remainingText.indexOf("@" + headline);
|
||||
filterRecallItems(remainingText);
|
||||
recallItemLabels.forEach((label) => {
|
||||
const index = remainingText.indexOf("@" + label);
|
||||
if (index !== -1) {
|
||||
if (index > 0) {
|
||||
parts.push(
|
||||
@@ -194,10 +208,10 @@ export const QuestionFormInput = ({
|
||||
<span
|
||||
className="z-30 flex cursor-pointer items-center justify-center whitespace-pre rounded-md bg-slate-100 text-sm text-transparent"
|
||||
key={parts.length}>
|
||||
{"@" + headline}
|
||||
{"@" + label}
|
||||
</span>
|
||||
);
|
||||
remainingText = remainingText.substring(index + headline.length + 1);
|
||||
remainingText = remainingText.substring(index + label.length + 1);
|
||||
}
|
||||
});
|
||||
if (remainingText?.length) {
|
||||
@@ -225,73 +239,71 @@ export const QuestionFormInput = ({
|
||||
const checkForRecallSymbol = () => {
|
||||
const pattern = /(^|\s)@(\s|$)/;
|
||||
if (pattern.test(getLocalizedValue(text, selectedLanguageCode))) {
|
||||
setShowQuestionSelect(true);
|
||||
setShowRecallItemSelect(true);
|
||||
} else {
|
||||
setShowQuestionSelect(false);
|
||||
setShowRecallItemSelect(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Adds a new recall question to the recallQuestions array, updates fallbacks, modifies the text with recall details.
|
||||
const addRecallQuestion = (recallQuestion: TSurveyQuestion) => {
|
||||
if ((recallQuestion.headline as TI18nString)[selectedLanguageCode].trim() === "") {
|
||||
// Adds a new recall question to the recallItems array, updates fallbacks, modifies the text with recall details.
|
||||
const addRecallItem = (recallItem: TSurveyRecallItem) => {
|
||||
if (recallItem.label.trim() === "") {
|
||||
toast.error("Cannot add question with empty headline as recall");
|
||||
return;
|
||||
}
|
||||
let recallQuestionTemp = structuredClone(recallQuestion);
|
||||
recallQuestionTemp = replaceRecallInfoWithUnderline(recallQuestionTemp, selectedLanguageCode);
|
||||
setRecallQuestions((prevQuestions) => {
|
||||
const updatedQuestions = [...prevQuestions, recallQuestionTemp];
|
||||
let recallItemTemp = structuredClone(recallItem);
|
||||
recallItemTemp.label = replaceRecallInfoWithUnderline(recallItem.label);
|
||||
setRecallItems((prevQuestions) => {
|
||||
const updatedQuestions = [...prevQuestions, recallItemTemp];
|
||||
return updatedQuestions;
|
||||
});
|
||||
if (!Object.keys(fallbacks).includes(recallQuestion.id)) {
|
||||
if (!Object.keys(fallbacks).includes(recallItem.id)) {
|
||||
setFallbacks((prevFallbacks) => ({
|
||||
...prevFallbacks,
|
||||
[recallQuestion.id]: "",
|
||||
[recallItem.id]: "",
|
||||
}));
|
||||
}
|
||||
setShowQuestionSelect(false);
|
||||
setShowRecallItemSelect(false);
|
||||
let modifiedHeadlineWithId = { ...getElementTextBasedOnType() };
|
||||
modifiedHeadlineWithId[selectedLanguageCode] = getLocalizedValue(
|
||||
modifiedHeadlineWithId,
|
||||
selectedLanguageCode
|
||||
).replace("@", `#recall:${recallQuestion.id}/fallback:# `);
|
||||
).replace(/(?<=^|\s)@(?=\s|$)/g, `#recall:${recallItem.id}/fallback:# `);
|
||||
handleUpdate(getLocalizedValue(modifiedHeadlineWithId, selectedLanguageCode));
|
||||
const modifiedHeadlineWithName = recallToHeadline(
|
||||
modifiedHeadlineWithId,
|
||||
localSurvey,
|
||||
false,
|
||||
selectedLanguageCode
|
||||
selectedLanguageCode,
|
||||
attributeClasses
|
||||
);
|
||||
setText(modifiedHeadlineWithName);
|
||||
setShowFallbackInput(true);
|
||||
};
|
||||
|
||||
// Filters and updates the list of recall questions based on their presence in the given text, also managing related text and fallback states.
|
||||
const filterRecallQuestions = (remainingText: string) => {
|
||||
let includedQuestions: TSurveyQuestion[] = [];
|
||||
recallQuestions.forEach((recallQuestion) => {
|
||||
if (remainingText.includes(`@${getLocalizedValue(recallQuestion.headline, selectedLanguageCode)}`)) {
|
||||
includedQuestions.push(recallQuestion);
|
||||
const filterRecallItems = (remainingText: string) => {
|
||||
let includedRecallItems: TSurveyRecallItem[] = [];
|
||||
recallItems.forEach((recallItem) => {
|
||||
if (remainingText.includes(`@${recallItem.label}`)) {
|
||||
includedRecallItems.push(recallItem);
|
||||
} else {
|
||||
const questionToRemove = getLocalizedValue(recallQuestion.headline, selectedLanguageCode).slice(
|
||||
0,
|
||||
-1
|
||||
);
|
||||
const recallItemToRemove = recallItem.label.slice(0, -1);
|
||||
const newText = { ...text };
|
||||
newText[selectedLanguageCode] = text[selectedLanguageCode].replace(`@${questionToRemove}`, "");
|
||||
newText[selectedLanguageCode] = text[selectedLanguageCode].replace(`@${recallItemToRemove}`, "");
|
||||
setText(newText);
|
||||
handleUpdate(text[selectedLanguageCode].replace(`@${questionToRemove}`, ""));
|
||||
handleUpdate(text[selectedLanguageCode].replace(`@${recallItemToRemove}`, ""));
|
||||
let updatedFallback = { ...fallbacks };
|
||||
delete updatedFallback[recallQuestion.id];
|
||||
delete updatedFallback[recallItem.id];
|
||||
setFallbacks(updatedFallback);
|
||||
}
|
||||
});
|
||||
setRecallQuestions(includedQuestions);
|
||||
setRecallItems(includedRecallItems);
|
||||
};
|
||||
|
||||
const addFallback = () => {
|
||||
let headlineWithFallback = getElementTextBasedOnType();
|
||||
filteredRecallQuestions.forEach((recallQuestion) => {
|
||||
filteredRecallItems.forEach((recallQuestion) => {
|
||||
if (recallQuestion) {
|
||||
const recallInfo = findRecallInfoById(
|
||||
getLocalizedValue(headlineWithFallback, selectedLanguageCode),
|
||||
@@ -436,8 +448,12 @@ export const QuestionFormInput = ({
|
||||
id={id}
|
||||
name={id}
|
||||
aria-label={label || getLabelById(id)}
|
||||
autoComplete={showQuestionSelect ? "off" : "on"}
|
||||
value={recallToHeadline(text, localSurvey, false, selectedLanguageCode)[selectedLanguageCode]}
|
||||
autoComplete={showRecallItemSelect ? "off" : "on"}
|
||||
value={
|
||||
recallToHeadline(text, localSurvey, false, selectedLanguageCode, attributeClasses)[
|
||||
selectedLanguageCode
|
||||
]
|
||||
}
|
||||
ref={inputRef}
|
||||
onBlur={onBlur}
|
||||
onChange={(e) => {
|
||||
@@ -445,10 +461,16 @@ export const QuestionFormInput = ({
|
||||
...getElementTextBasedOnType(),
|
||||
[selectedLanguageCode]: e.target.value,
|
||||
};
|
||||
setText(recallToHeadline(translatedText, localSurvey, false, selectedLanguageCode));
|
||||
handleUpdate(
|
||||
headlineToRecall(e.target.value, recallQuestions, fallbacks, selectedLanguageCode)
|
||||
setText(
|
||||
recallToHeadline(
|
||||
translatedText,
|
||||
localSurvey,
|
||||
false,
|
||||
selectedLanguageCode,
|
||||
attributeClasses
|
||||
)
|
||||
);
|
||||
handleUpdate(headlineToRecall(e.target.value, recallItems, fallbacks));
|
||||
}}
|
||||
maxLength={maxLength ?? undefined}
|
||||
isInvalid={
|
||||
@@ -465,9 +487,9 @@ export const QuestionFormInput = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
)}
|
||||
{!showQuestionSelect && showFallbackInput && recallQuestions.length > 0 && (
|
||||
{!showRecallItemSelect && showFallbackInput && recallItems.length > 0 && (
|
||||
<FallbackInput
|
||||
filteredRecallQuestions={filteredRecallQuestions}
|
||||
filteredRecallItems={filteredRecallItems}
|
||||
fallbacks={fallbacks}
|
||||
setFallbacks={setFallbacks}
|
||||
fallbackInputRef={fallbackInputRef}
|
||||
@@ -484,22 +506,23 @@ export const QuestionFormInput = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showQuestionSelect && (
|
||||
<RecallQuestionSelect
|
||||
{showRecallItemSelect && (
|
||||
<RecallItemSelect
|
||||
localSurvey={localSurvey}
|
||||
questionId={questionId}
|
||||
addRecallQuestion={addRecallQuestion}
|
||||
setShowQuestionSelect={setShowQuestionSelect}
|
||||
showQuestionSelect={showQuestionSelect}
|
||||
inputRef={inputRef}
|
||||
recallQuestions={recallQuestions}
|
||||
addRecallItem={addRecallItem}
|
||||
setShowRecallItemSelect={setShowRecallItemSelect}
|
||||
recallItems={recallItems}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
hiddenFields={localSurvey.hiddenFields}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{selectedLanguageCode !== "default" && value && typeof value["default"] !== undefined && (
|
||||
<div className="mt-1 text-xs text-gray-500">
|
||||
<strong>Translate:</strong> {recallToHeadline(value, localSurvey, false, "default")["default"]}
|
||||
<strong>Translate:</strong>{" "}
|
||||
{recallToHeadline(value, localSurvey, false, "default", attributeClasses)["default"]}
|
||||
</div>
|
||||
)}
|
||||
{selectedLanguageCode === "default" && localSurvey.languages?.length > 1 && isTranslationIncomplete && (
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { isLabelValidForAllLanguages } from "@formbricks/lib/i18n/utils";
|
||||
import { TI18nString } from "@formbricks/types/surveys";
|
||||
|
||||
export const isValueIncomplete = (
|
||||
id: string,
|
||||
isInvalid: boolean,
|
||||
surveyLanguageCodes: string[],
|
||||
value?: TI18nString
|
||||
) => {
|
||||
// Define a list of IDs for which a default value needs to be checked.
|
||||
const labelIds = [
|
||||
"label",
|
||||
"headline",
|
||||
"subheader",
|
||||
"lowerLabel",
|
||||
"upperLabel",
|
||||
"buttonLabel",
|
||||
"placeholder",
|
||||
"backButtonLabel",
|
||||
"dismissButtonLabel",
|
||||
];
|
||||
|
||||
// If value is not provided, immediately return false as it cannot be incomplete.
|
||||
if (value === undefined) return false;
|
||||
|
||||
// Check if the default value is incomplete. This applies only to specific label IDs.
|
||||
// For these IDs, the default value should not be an empty string.
|
||||
const isDefaultIncomplete = labelIds.includes(id) ? value["default"]?.trim() !== "" : false;
|
||||
|
||||
// Return true if all the following conditions are met:
|
||||
// 1. The field is marked as invalid.
|
||||
// 2. The label is not valid for all provided language codes in the survey.
|
||||
// 4. For specific label IDs, the default value is incomplete as defined above.
|
||||
return isInvalid && !isLabelValidForAllLanguages(value, surveyLanguageCodes) && isDefaultIncomplete;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createI18nString } from "@formbricks/lib/i18n/utils";
|
||||
import { isLabelValidForAllLanguages } from "@formbricks/lib/i18n/utils";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
@@ -92,3 +93,36 @@ export const getPlaceHolderById = (id: string) => {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
export const isValueIncomplete = (
|
||||
id: string,
|
||||
isInvalid: boolean,
|
||||
surveyLanguageCodes: string[],
|
||||
value?: TI18nString
|
||||
) => {
|
||||
// Define a list of IDs for which a default value needs to be checked.
|
||||
const labelIds = [
|
||||
"label",
|
||||
"headline",
|
||||
"subheader",
|
||||
"lowerLabel",
|
||||
"upperLabel",
|
||||
"buttonLabel",
|
||||
"placeholder",
|
||||
"backButtonLabel",
|
||||
"dismissButtonLabel",
|
||||
];
|
||||
|
||||
// If value is not provided, immediately return false as it cannot be incomplete.
|
||||
if (value === undefined) return false;
|
||||
|
||||
// Check if the default value is incomplete. This applies only to specific label IDs.
|
||||
// For these IDs, the default value should not be an empty string.
|
||||
const isDefaultIncomplete = labelIds.includes(id) ? value["default"]?.trim() !== "" : false;
|
||||
|
||||
// Return true if all the following conditions are met:
|
||||
// 1. The field is marked as invalid.
|
||||
// 2. The label is not valid for all provided language codes in the survey.
|
||||
// 4. For specific label IDs, the default value is incomplete as defined above.
|
||||
return isInvalid && !isLabelValidForAllLanguages(value, surveyLanguageCodes) && isDefaultIncomplete;
|
||||
};
|
||||
|
||||
44
packages/ui/SingleResponseCard/components/HiddenFields.tsx
Normal file
44
packages/ui/SingleResponseCard/components/HiddenFields.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { EyeOffIcon } from "lucide-react";
|
||||
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TSurveyHiddenFields } from "@formbricks/types/surveys";
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../Tooltip";
|
||||
|
||||
interface HiddenFieldsProps {
|
||||
hiddenFields: TSurveyHiddenFields;
|
||||
responseData: TResponseData;
|
||||
}
|
||||
|
||||
export const HiddenFields = ({ hiddenFields, responseData }: HiddenFieldsProps) => {
|
||||
const fieldIds = hiddenFields.fieldIds ?? [];
|
||||
return (
|
||||
<div className="mt-6 flex flex-col gap-6">
|
||||
{fieldIds.map((field) => {
|
||||
if (!responseData[field]) return;
|
||||
return (
|
||||
<div key={field}>
|
||||
<div className="flex space-x-2 text-sm text-slate-500">
|
||||
<p>{field}</p>
|
||||
<div className="flex items-center space-x-2 rounded-full bg-slate-100 px-2">
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<EyeOffIcon className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[300px]" side="top">
|
||||
Hidden field
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
<p className="ph-no-capture mt-2 font-semibold text-slate-700">
|
||||
{typeof responseData[field] === "string" ? (responseData[field] as string) : ""}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
import { CheckCircle2Icon, ChevronsDownIcon, XCircleIcon } from "lucide-react";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../Tooltip";
|
||||
@@ -10,6 +12,7 @@ interface QuestionSkipProps {
|
||||
status: string;
|
||||
questions: TSurveyQuestion[];
|
||||
isFirstQuestionAnswered?: boolean;
|
||||
responseData: TResponseData;
|
||||
}
|
||||
|
||||
export const QuestionSkip = ({
|
||||
@@ -17,6 +20,7 @@ export const QuestionSkip = ({
|
||||
status,
|
||||
questions,
|
||||
isFirstQuestionAnswered,
|
||||
responseData,
|
||||
}: QuestionSkipProps) => {
|
||||
return (
|
||||
<div>
|
||||
@@ -65,9 +69,13 @@ export const QuestionSkip = ({
|
||||
skippedQuestions.map((questionId) => {
|
||||
return (
|
||||
<p className="my-2" key={questionId}>
|
||||
{getLocalizedValue(
|
||||
questions.find((question) => question.id === questionId)!.headline,
|
||||
"default"
|
||||
{parseRecallInfo(
|
||||
getLocalizedValue(
|
||||
questions.find((question) => question.id === questionId)!.headline,
|
||||
"default"
|
||||
),
|
||||
{},
|
||||
responseData
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
@@ -95,9 +103,13 @@ export const QuestionSkip = ({
|
||||
skippedQuestions.map((questionId) => {
|
||||
return (
|
||||
<p className="my-2" key={questionId}>
|
||||
{getLocalizedValue(
|
||||
questions.find((question) => question.id === questionId)!.headline,
|
||||
"default"
|
||||
{parseRecallInfo(
|
||||
getLocalizedValue(
|
||||
questions.find((question) => question.id === questionId)!.headline,
|
||||
"default"
|
||||
),
|
||||
{},
|
||||
responseData
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { CheckCircle2Icon } from "lucide-react";
|
||||
|
||||
import { getLanguageCode, getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { formatDateWithOrdinal } from "@formbricks/lib/utils/datetime";
|
||||
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyMatrixQuestion,
|
||||
TSurveyPictureSelectionQuestion,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionType,
|
||||
} from "@formbricks/types/surveys";
|
||||
|
||||
import { AddressResponse } from "../../AddressResponse";
|
||||
import { FileUploadResponse } from "../../FileUploadResponse";
|
||||
import { PictureSelectionResponse } from "../../PictureSelectionResponse";
|
||||
import { RatingResponse } from "../../RatingResponse";
|
||||
import { isValidValue } from "../util";
|
||||
import { HiddenFields } from "./HiddenFields";
|
||||
import { QuestionSkip } from "./QuestionSkip";
|
||||
import { VerifiedEmail } from "./VerifiedEmail";
|
||||
|
||||
interface SingleResponseCardBodyProps {
|
||||
survey: TSurvey;
|
||||
response: TResponse;
|
||||
skippedQuestions: string[][];
|
||||
}
|
||||
|
||||
export const SingleResponseCardBody = ({
|
||||
survey,
|
||||
response,
|
||||
skippedQuestions,
|
||||
}: SingleResponseCardBodyProps) => {
|
||||
const isFirstQuestionAnswered = response.data[survey.questions[0].id] ? true : false;
|
||||
|
||||
const handleArray = (data: string | number | string[]): string => {
|
||||
if (Array.isArray(data)) {
|
||||
return data.join(", ");
|
||||
} else {
|
||||
return String(data);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTextWithSlashes = (text: string) => {
|
||||
// Updated regex to match content between #/ and \#
|
||||
const regex = /#\/(.*?)\\#/g;
|
||||
const parts = text.split(regex);
|
||||
|
||||
return parts.map((part, index) => {
|
||||
// Check if the part was inside #/ and \#
|
||||
if (index % 2 === 1) {
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
className="ml-0.5 mr-0.5 rounded-md border border-slate-200 bg-slate-50 px-1 py-0.5 text-sm first:ml-0 ">
|
||||
@{part}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return part;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renderResponse = (
|
||||
questionType: TSurveyQuestionType,
|
||||
responseData: string | number | string[] | Record<string, string>,
|
||||
question: TSurveyQuestion
|
||||
) => {
|
||||
switch (questionType) {
|
||||
case TSurveyQuestionType.Rating:
|
||||
if (typeof responseData === "number")
|
||||
return <RatingResponse scale={question.scale} answer={responseData} range={question.range} />;
|
||||
case TSurveyQuestionType.Date:
|
||||
if (typeof responseData === "string") {
|
||||
const formattedDateString = formatDateWithOrdinal(new Date(responseData));
|
||||
return <p className="ph-no-capture my-1 font-semibold text-slate-700">{formattedDateString}</p>;
|
||||
}
|
||||
case TSurveyQuestionType.Cal:
|
||||
if (typeof responseData === "string")
|
||||
return <p className="ph-no-capture my-1 font-semibold capitalize text-slate-700">{responseData}</p>;
|
||||
case TSurveyQuestionType.PictureSelection:
|
||||
if (Array.isArray(responseData))
|
||||
return (
|
||||
<PictureSelectionResponse
|
||||
choices={(question as TSurveyPictureSelectionQuestion).choices}
|
||||
selected={responseData}
|
||||
/>
|
||||
);
|
||||
case TSurveyQuestionType.FileUpload:
|
||||
if (Array.isArray(responseData)) return <FileUploadResponse selected={responseData} />;
|
||||
case TSurveyQuestionType.Matrix:
|
||||
if (typeof responseData === "object" && !Array.isArray(responseData)) {
|
||||
return (question as TSurveyMatrixQuestion).rows.map((row) => {
|
||||
const languagCode = getLanguageCode(survey.languages, response.language);
|
||||
const rowValueInSelectedLanguage = getLocalizedValue(row, languagCode);
|
||||
if (!responseData[rowValueInSelectedLanguage]) return;
|
||||
return (
|
||||
<p className="ph-no-capture my-1 font-semibold capitalize text-slate-700">
|
||||
{rowValueInSelectedLanguage}: {responseData[rowValueInSelectedLanguage]}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
}
|
||||
case TSurveyQuestionType.Address:
|
||||
if (Array.isArray(responseData)) {
|
||||
return <AddressResponse value={responseData} />;
|
||||
}
|
||||
default:
|
||||
if (
|
||||
typeof responseData === "string" ||
|
||||
typeof responseData === "number" ||
|
||||
Array.isArray(responseData)
|
||||
)
|
||||
return (
|
||||
<p className="ph-no-capture my-1 whitespace-pre-line font-semibold text-slate-700">
|
||||
{Array.isArray(responseData) ? handleArray(responseData) : responseData}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="p-6">
|
||||
{survey.welcomeCard.enabled && (
|
||||
<QuestionSkip
|
||||
skippedQuestions={[]}
|
||||
questions={survey.questions}
|
||||
status={"welcomeCard"}
|
||||
isFirstQuestionAnswered={isFirstQuestionAnswered}
|
||||
responseData={response.data}
|
||||
/>
|
||||
)}
|
||||
<div className="space-y-6">
|
||||
{survey.verifyEmail && response.data["verifiedEmail"] && (
|
||||
<VerifiedEmail responseData={response.data} />
|
||||
)}
|
||||
{survey.questions.map((question) => {
|
||||
const skipped = skippedQuestions.find((skippedQuestionElement) =>
|
||||
skippedQuestionElement.includes(question.id)
|
||||
);
|
||||
|
||||
// If found, remove it from the list
|
||||
if (skipped) {
|
||||
skippedQuestions = skippedQuestions.filter((item) => item !== skipped);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={`${question.id}`}>
|
||||
{isValidValue(response.data[question.id]) ? (
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">
|
||||
{formatTextWithSlashes(
|
||||
parseRecallInfo(
|
||||
getLocalizedValue(question.headline, "default"),
|
||||
{},
|
||||
response.data,
|
||||
true
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
{renderResponse(question.type, response.data[question.id], question)}
|
||||
</div>
|
||||
) : (
|
||||
<QuestionSkip
|
||||
skippedQuestions={skipped}
|
||||
questions={survey.questions}
|
||||
responseData={response.data}
|
||||
status={
|
||||
response.finished ||
|
||||
(skippedQuestions.length > 0 &&
|
||||
!skippedQuestions[skippedQuestions.length - 1].includes(question.id))
|
||||
? "skipped"
|
||||
: "aborted"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{survey.hiddenFields.enabled && survey.hiddenFields.fieldIds && (
|
||||
<HiddenFields hiddenFields={survey.hiddenFields} responseData={response.data} />
|
||||
)}
|
||||
{response.finished && (
|
||||
<div className="mt-4 flex items-center">
|
||||
<CheckCircle2Icon className="h-6 w-6 text-slate-400" />
|
||||
<p className="mx-2 rounded-lg bg-slate-100 px-2 text-sm font-medium text-slate-700">Completed</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,194 @@
|
||||
import { LanguagesIcon, TrashIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
|
||||
import { getLanguageLabel } from "../../../ee/multiLanguage/lib/isoLanguages";
|
||||
import { PersonAvatar } from "../../Avatars";
|
||||
import { SurveyStatusIndicator } from "../../SurveyStatusIndicator";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../Tooltip";
|
||||
import { isSubmissionTimeMoreThan5Minutes } from "../util";
|
||||
|
||||
interface TooltipRendererProps {
|
||||
shouldRender: boolean;
|
||||
tooltipContent: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface SingleResponseCardHeaderProps {
|
||||
pageType: "people" | "response";
|
||||
response: TResponse;
|
||||
survey: TSurvey;
|
||||
environment: TEnvironment;
|
||||
user?: TUser;
|
||||
isViewer: boolean;
|
||||
setDeleteDialogOpen: (deleteDialogOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const SingleResponseCardHeader = ({
|
||||
pageType,
|
||||
response,
|
||||
survey,
|
||||
environment,
|
||||
user,
|
||||
isViewer,
|
||||
setDeleteDialogOpen,
|
||||
}: SingleResponseCardHeaderProps) => {
|
||||
const displayIdentifier = response.person
|
||||
? getPersonIdentifier(response.person, response.personAttributes)
|
||||
: null;
|
||||
const environmentId = survey.environmentId;
|
||||
const canResponseBeDeleted = response.finished
|
||||
? true
|
||||
: isSubmissionTimeMoreThan5Minutes(response.updatedAt);
|
||||
const TooltipRenderer = (props: TooltipRendererProps) => {
|
||||
const { children, shouldRender, tooltipContent } = props;
|
||||
if (shouldRender) {
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{children}</TooltipTrigger>
|
||||
<TooltipContent>{tooltipContent}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const renderTooltip = Boolean(
|
||||
(response.personAttributes && Object.keys(response.personAttributes).length > 0) ||
|
||||
(response.meta.userAgent && Object.keys(response.meta.userAgent).length > 0)
|
||||
);
|
||||
|
||||
const tooltipContent = (
|
||||
<>
|
||||
{response.singleUseId && (
|
||||
<div>
|
||||
<p className="py-1 font-bold text-slate-700">SingleUse ID:</p>
|
||||
<span>{response.singleUseId}</span>
|
||||
</div>
|
||||
)}
|
||||
{response.personAttributes && Object.keys(response.personAttributes).length > 0 && (
|
||||
<div>
|
||||
<p className="py-1 font-bold text-slate-700">Person attributes:</p>
|
||||
{Object.keys(response.personAttributes).map((key) => (
|
||||
<p key={key}>
|
||||
{key}:{" "}
|
||||
<span className="font-bold">{response.personAttributes && response.personAttributes[key]}</span>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{response.meta.userAgent && Object.keys(response.meta.userAgent).length > 0 && (
|
||||
<div className="text-slate-600">
|
||||
{response.personAttributes && Object.keys(response.personAttributes).length > 0 && (
|
||||
<hr className="my-2 border-slate-200" />
|
||||
)}
|
||||
<p className="py-1 font-bold text-slate-700">Device info:</p>
|
||||
{response.meta.userAgent?.browser && <p>Browser: {response.meta.userAgent.browser}</p>}
|
||||
{response.meta.userAgent?.os && <p>OS: {response.meta.userAgent.os}</p>}
|
||||
{response.meta.userAgent && (
|
||||
<p>
|
||||
Device:{" "}
|
||||
{response.meta.userAgent.device ? response.meta.userAgent.device : "PC / Generic device"}
|
||||
</p>
|
||||
)}
|
||||
{response.meta.url && <p>URL: {response.meta.url}</p>}
|
||||
{response.meta.action && <p>Action: {response.meta.action}</p>}
|
||||
{response.meta.source && <p>Source: {response.meta.source}</p>}
|
||||
{response.meta.country && <p>Country: {response.meta.country}</p>}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const deleteSubmissionToolTip = <>This response is in progress.</>;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 border-b border-slate-200 px-6 pb-4 pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
{pageType === "response" && (
|
||||
<TooltipRenderer shouldRender={renderTooltip} tooltipContent={tooltipContent}>
|
||||
<div className="group">
|
||||
{response.person?.id ? (
|
||||
user ? (
|
||||
<Link
|
||||
className="flex items-center"
|
||||
href={`/environments/${environmentId}/people/${response.person.id}`}>
|
||||
<PersonAvatar personId={response.person.id} />
|
||||
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600 hover:underline">
|
||||
{displayIdentifier}
|
||||
</h3>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<PersonAvatar personId={response.person.id} />
|
||||
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600">
|
||||
{displayIdentifier}
|
||||
</h3>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
<h3 className="ml-4 pb-1 font-semibold text-slate-600">Anonymous</h3>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
|
||||
{pageType === "people" && (
|
||||
<div className="flex items-center justify-center space-x-2 rounded-full bg-slate-100 p-1 px-2 text-sm text-slate-600">
|
||||
{(survey.type === "link" || environment.widgetSetupCompleted) && (
|
||||
<SurveyStatusIndicator status={survey.status} />
|
||||
)}
|
||||
<Link
|
||||
className="hover:underline"
|
||||
href={`/environments/${environmentId}/surveys/${survey.id}/summary`}>
|
||||
{survey.name}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{response.language && response.language !== "default" && (
|
||||
<div className="flex space-x-2 rounded-full bg-slate-700 px-2 py-1 text-xs text-white">
|
||||
<div>{getLanguageLabel(response.language)}</div>
|
||||
<LanguagesIcon className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4 text-sm">
|
||||
<time className="text-slate-500" dateTime={timeSince(response.updatedAt.toISOString())}>
|
||||
{timeSince(response.updatedAt.toISOString())}
|
||||
</time>
|
||||
{user && !isViewer && (
|
||||
<TooltipRenderer shouldRender={!canResponseBeDeleted} tooltipContent={deleteSubmissionToolTip}>
|
||||
<TrashIcon
|
||||
onClick={() => {
|
||||
if (canResponseBeDeleted) {
|
||||
setDeleteDialogOpen(true);
|
||||
}
|
||||
}}
|
||||
className={`h-4 w-4 ${
|
||||
canResponseBeDeleted
|
||||
? "cursor-pointer text-slate-500 hover:text-red-700"
|
||||
: "cursor-not-allowed text-slate-400"
|
||||
} `}
|
||||
/>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
20
packages/ui/SingleResponseCard/components/VerifiedEmail.tsx
Normal file
20
packages/ui/SingleResponseCard/components/VerifiedEmail.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MailIcon } from "lucide-react";
|
||||
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
|
||||
interface VerifiedEmailProps {
|
||||
responseData: TResponseData;
|
||||
}
|
||||
export const VerifiedEmail = ({ responseData }: VerifiedEmailProps) => {
|
||||
return (
|
||||
<div>
|
||||
<p className="flex items-center space-x-2 text-sm text-slate-500">
|
||||
<MailIcon className="h-4 w-4" />
|
||||
<span>Verified Email</span>
|
||||
</p>
|
||||
<p className="ph-no-capture my-1 font-semibold text-slate-700">
|
||||
{typeof responseData["verifiedEmail"] === "string" ? responseData["verifiedEmail"] : ""}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,51 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { CheckCircle2Icon, LanguagesIcon, MailIcon, TrashIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { getLanguageCode, getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { formatDateWithOrdinal } from "@formbricks/lib/utils/datetime";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyMatrixQuestion,
|
||||
TSurveyPictureSelectionQuestion,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionType,
|
||||
} from "@formbricks/types/surveys";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
|
||||
import { getLanguageLabel } from "../../ee/multiLanguage/lib/isoLanguages";
|
||||
import { AddressResponse } from "../AddressResponse";
|
||||
import { PersonAvatar } from "../Avatars";
|
||||
import { DeleteDialog } from "../DeleteDialog";
|
||||
import { FileUploadResponse } from "../FileUploadResponse";
|
||||
import { PictureSelectionResponse } from "../PictureSelectionResponse";
|
||||
import { RatingResponse } from "../RatingResponse";
|
||||
import { SurveyStatusIndicator } from "../SurveyStatusIndicator";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../Tooltip";
|
||||
import { deleteResponseAction, getResponseAction } from "./actions";
|
||||
import { QuestionSkip } from "./components/QuestionSkip";
|
||||
import { ResponseNotes } from "./components/ResponseNote";
|
||||
import { ResponseTagsWrapper } from "./components/ResponseTagsWrapper";
|
||||
import { SingleResponseCardBody } from "./components/SingleResponseCardBody";
|
||||
import { SingleResponseCardHeader } from "./components/SingleResponseCardHeader";
|
||||
import { isValidValue } from "./util";
|
||||
|
||||
const isSubmissionTimeMoreThan5Minutes = (submissionTimeISOString: Date) => {
|
||||
const submissionTime: Date = new Date(submissionTimeISOString);
|
||||
const currentTime: Date = new Date();
|
||||
const timeDifference: number = (currentTime.getTime() - submissionTime.getTime()) / (1000 * 60); // Convert milliseconds to minutes
|
||||
return timeDifference > 5;
|
||||
};
|
||||
|
||||
export interface SingleResponseCardProps {
|
||||
interface SingleResponseCardProps {
|
||||
survey: TSurvey;
|
||||
response: TResponse;
|
||||
user?: TUser;
|
||||
@@ -57,35 +32,6 @@ export interface SingleResponseCardProps {
|
||||
isViewer: boolean;
|
||||
}
|
||||
|
||||
interface TooltipRendererProps {
|
||||
shouldRender: boolean;
|
||||
tooltipContent: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const TooltipRenderer = (props: TooltipRendererProps) => {
|
||||
const { children, shouldRender, tooltipContent } = props;
|
||||
if (shouldRender) {
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>{children}</TooltipTrigger>
|
||||
<TooltipContent>{tooltipContent}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const DateResponse = ({ date }: { date?: string }) => {
|
||||
if (!date) return null;
|
||||
|
||||
const formattedDateString = formatDateWithOrdinal(new Date(date));
|
||||
return <p className="ph-no-capture my-1 font-semibold text-slate-700">{formattedDateString}</p>;
|
||||
};
|
||||
|
||||
export const SingleResponseCard = ({
|
||||
survey,
|
||||
response,
|
||||
@@ -99,29 +45,13 @@ export const SingleResponseCard = ({
|
||||
}: SingleResponseCardProps) => {
|
||||
const environmentId = survey.environmentId;
|
||||
const router = useRouter();
|
||||
const displayIdentifier = response.person
|
||||
? getPersonIdentifier(response.person, response.personAttributes)
|
||||
: null;
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const canResponseBeDeleted = response.finished
|
||||
? true
|
||||
: isSubmissionTimeMoreThan5Minutes(response.updatedAt);
|
||||
|
||||
let skippedQuestions: string[][] = [];
|
||||
let temp: string[] = [];
|
||||
|
||||
const isFirstQuestionAnswered = response.data[survey.questions[0].id] ? true : false;
|
||||
|
||||
const isValidValue = (value: any) => {
|
||||
return (
|
||||
(typeof value === "string" && value.trim() !== "") ||
|
||||
(Array.isArray(value) && value.length > 0) ||
|
||||
typeof value === "number" ||
|
||||
(typeof value === "object" && Object.entries(value).length > 0)
|
||||
);
|
||||
};
|
||||
|
||||
if (response.finished) {
|
||||
survey.questions.forEach((question) => {
|
||||
if (!isValidValue(response.data[question.id])) {
|
||||
@@ -156,14 +86,6 @@ export const SingleResponseCard = ({
|
||||
skippedQuestions.push(temp);
|
||||
}
|
||||
|
||||
const handleArray = (data: string | number | string[]): string => {
|
||||
if (Array.isArray(data)) {
|
||||
return data.join(", ");
|
||||
} else {
|
||||
return String(data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteResponse = async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
@@ -183,58 +105,6 @@ export const SingleResponseCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
const renderTooltip = Boolean(
|
||||
(response.personAttributes && Object.keys(response.personAttributes).length > 0) ||
|
||||
(response.meta.userAgent && Object.keys(response.meta.userAgent).length > 0)
|
||||
);
|
||||
|
||||
const tooltipContent = (
|
||||
<>
|
||||
{response.singleUseId && (
|
||||
<div>
|
||||
<p className="py-1 font-bold text-slate-700">SingleUse ID:</p>
|
||||
<span>{response.singleUseId}</span>
|
||||
</div>
|
||||
)}
|
||||
{response.personAttributes && Object.keys(response.personAttributes).length > 0 && (
|
||||
<div>
|
||||
<p className="py-1 font-bold text-slate-700">Person attributes:</p>
|
||||
{Object.keys(response.personAttributes).map((key) => (
|
||||
<p key={key}>
|
||||
{key}:{" "}
|
||||
<span className="font-bold">{response.personAttributes && response.personAttributes[key]}</span>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{response.meta.userAgent && Object.keys(response.meta.userAgent).length > 0 && (
|
||||
<div className="text-slate-600">
|
||||
{response.personAttributes && Object.keys(response.personAttributes).length > 0 && (
|
||||
<hr className="my-2 border-slate-200" />
|
||||
)}
|
||||
<p className="py-1 font-bold text-slate-700">Device info:</p>
|
||||
{response.meta.userAgent?.browser && <p>Browser: {response.meta.userAgent.browser}</p>}
|
||||
{response.meta.userAgent?.os && <p>OS: {response.meta.userAgent.os}</p>}
|
||||
{response.meta.userAgent && (
|
||||
<p>
|
||||
Device:{" "}
|
||||
{response.meta.userAgent.device ? response.meta.userAgent.device : "PC / Generic device"}
|
||||
</p>
|
||||
)}
|
||||
{response.meta.url && <p>URL: {response.meta.url}</p>}
|
||||
{response.meta.action && <p>Action: {response.meta.action}</p>}
|
||||
{response.meta.source && <p>Source: {response.meta.source}</p>}
|
||||
{response.meta.country && <p>Country: {response.meta.country}</p>}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
const deleteSubmissionToolTip = <>This response is in progress.</>;
|
||||
const hasHiddenFieldsEnabled = survey.hiddenFields?.enabled;
|
||||
const fieldIds = survey.hiddenFields?.fieldIds || [];
|
||||
const hasFieldIds = !!fieldIds.length;
|
||||
|
||||
const updateFetchedResponses = async () => {
|
||||
const updatedResponse = await getResponseAction(response.id);
|
||||
if (updatedResponse !== null && updateResponse) {
|
||||
@@ -242,61 +112,6 @@ export const SingleResponseCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
const renderResponse = (
|
||||
questionType: TSurveyQuestionType,
|
||||
responseData: string | number | string[] | Record<string, string>,
|
||||
question: TSurveyQuestion
|
||||
) => {
|
||||
switch (questionType) {
|
||||
case TSurveyQuestionType.Rating:
|
||||
if (typeof responseData === "number")
|
||||
return <RatingResponse scale={question.scale} answer={responseData} range={question.range} />;
|
||||
case TSurveyQuestionType.Date:
|
||||
if (typeof responseData === "string") return <DateResponse date={responseData} />;
|
||||
case TSurveyQuestionType.Cal:
|
||||
if (typeof responseData === "string")
|
||||
return <p className="ph-no-capture my-1 font-semibold capitalize text-slate-700">{responseData}</p>;
|
||||
case TSurveyQuestionType.PictureSelection:
|
||||
if (Array.isArray(responseData))
|
||||
return (
|
||||
<PictureSelectionResponse
|
||||
choices={(question as TSurveyPictureSelectionQuestion).choices}
|
||||
selected={responseData}
|
||||
/>
|
||||
);
|
||||
case TSurveyQuestionType.FileUpload:
|
||||
if (Array.isArray(responseData)) return <FileUploadResponse selected={responseData} />;
|
||||
case TSurveyQuestionType.Matrix:
|
||||
if (typeof responseData === "object" && !Array.isArray(responseData)) {
|
||||
return (question as TSurveyMatrixQuestion).rows.map((row) => {
|
||||
const languagCode = getLanguageCode(survey.languages, response.language);
|
||||
const rowValueInSelectedLanguage = getLocalizedValue(row, languagCode);
|
||||
if (!responseData[rowValueInSelectedLanguage]) return;
|
||||
return (
|
||||
<p className="ph-no-capture my-1 font-semibold capitalize text-slate-700">
|
||||
{rowValueInSelectedLanguage}: {responseData[rowValueInSelectedLanguage]}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
}
|
||||
case TSurveyQuestionType.Address:
|
||||
if (Array.isArray(responseData)) {
|
||||
return <AddressResponse value={responseData} />;
|
||||
}
|
||||
default:
|
||||
if (
|
||||
typeof responseData === "string" ||
|
||||
typeof responseData === "number" ||
|
||||
Array.isArray(responseData)
|
||||
)
|
||||
return (
|
||||
<p className="ph-no-capture my-1 whitespace-pre-line font-semibold text-slate-700">
|
||||
{Array.isArray(responseData) ? handleArray(responseData) : responseData}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx("group relative", isOpen && "min-h-[300px]")}>
|
||||
<div
|
||||
@@ -309,166 +124,17 @@ export const SingleResponseCard = ({
|
||||
? "w-[96.5%]"
|
||||
: cn("w-full", user ? "group-hover:w-[96.5%]" : ""))
|
||||
)}>
|
||||
<div className="space-y-2 border-b border-slate-200 px-6 pb-4 pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
{pageType === "response" && (
|
||||
<TooltipRenderer shouldRender={renderTooltip} tooltipContent={tooltipContent}>
|
||||
<div className="group">
|
||||
{response.person?.id ? (
|
||||
user ? (
|
||||
<Link
|
||||
className="flex items-center"
|
||||
href={`/environments/${environmentId}/people/${response.person.id}`}>
|
||||
<PersonAvatar personId={response.person.id} />
|
||||
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600 hover:underline">
|
||||
{displayIdentifier}
|
||||
</h3>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<PersonAvatar personId={response.person.id} />
|
||||
<h3 className="ph-no-capture ml-4 pb-1 font-semibold text-slate-600">
|
||||
{displayIdentifier}
|
||||
</h3>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
<h3 className="ml-4 pb-1 font-semibold text-slate-600">Anonymous</h3>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
<SingleResponseCardHeader
|
||||
pageType="response"
|
||||
response={response}
|
||||
survey={survey}
|
||||
environment={environment}
|
||||
user={user}
|
||||
isViewer={isViewer}
|
||||
setDeleteDialogOpen={setDeleteDialogOpen}
|
||||
/>
|
||||
|
||||
{pageType === "people" && (
|
||||
<div className="flex items-center justify-center space-x-2 rounded-full bg-slate-100 p-1 px-2 text-sm text-slate-600">
|
||||
{(survey.type === "link" || environment.widgetSetupCompleted) && (
|
||||
<SurveyStatusIndicator status={survey.status} />
|
||||
)}
|
||||
<Link
|
||||
className="hover:underline"
|
||||
href={`/environments/${environmentId}/surveys/${survey.id}/summary`}>
|
||||
{survey.name}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{response.language && response.language !== "default" && (
|
||||
<div className="flex space-x-2 rounded-full bg-slate-700 px-2 py-1 text-xs text-white">
|
||||
<div>{getLanguageLabel(response.language)}</div>
|
||||
<LanguagesIcon className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4 text-sm">
|
||||
<time className="text-slate-500" dateTime={timeSince(response.updatedAt.toISOString())}>
|
||||
{timeSince(response.updatedAt.toISOString())}
|
||||
</time>
|
||||
{user && !isViewer && (
|
||||
<TooltipRenderer
|
||||
shouldRender={!canResponseBeDeleted}
|
||||
tooltipContent={deleteSubmissionToolTip}>
|
||||
<TrashIcon
|
||||
onClick={() => {
|
||||
if (canResponseBeDeleted) {
|
||||
setDeleteDialogOpen(true);
|
||||
}
|
||||
}}
|
||||
className={`h-4 w-4 ${
|
||||
canResponseBeDeleted
|
||||
? "cursor-pointer text-slate-500 hover:text-red-700"
|
||||
: "cursor-not-allowed text-slate-400"
|
||||
} `}
|
||||
/>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{survey.welcomeCard.enabled && (
|
||||
<QuestionSkip
|
||||
skippedQuestions={[]}
|
||||
questions={survey.questions}
|
||||
status={"welcomeCard"}
|
||||
isFirstQuestionAnswered={isFirstQuestionAnswered}
|
||||
/>
|
||||
)}
|
||||
<div className="space-y-6">
|
||||
{survey.verifyEmail && response.data["verifiedEmail"] && (
|
||||
<div>
|
||||
<p className="flex items-center space-x-2 text-sm text-slate-500">
|
||||
<MailIcon className="h-4 w-4" />
|
||||
|
||||
<span>Verified Email</span>
|
||||
</p>
|
||||
<p className="ph-no-capture my-1 font-semibold text-slate-700">
|
||||
{typeof response.data["verifiedEmail"] === "string" ? response.data["verifiedEmail"] : ""}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{survey.questions.map((question) => {
|
||||
const skipped = skippedQuestions.find((skippedQuestionElement) =>
|
||||
skippedQuestionElement.includes(question.id)
|
||||
);
|
||||
|
||||
// If found, remove it from the list
|
||||
if (skipped) {
|
||||
skippedQuestions = skippedQuestions.filter((item) => item !== skipped);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={`${question.id}`}>
|
||||
{isValidValue(response.data[question.id]) ? (
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">
|
||||
{getLocalizedValue(question.headline, "default")}
|
||||
</p>
|
||||
{renderResponse(question.type, response.data[question.id], question)}
|
||||
</div>
|
||||
) : (
|
||||
<QuestionSkip
|
||||
skippedQuestions={skipped}
|
||||
questions={survey.questions}
|
||||
status={
|
||||
response.finished ||
|
||||
(skippedQuestions.length > 0 &&
|
||||
!skippedQuestions[skippedQuestions.length - 1].includes(question.id))
|
||||
? "skipped"
|
||||
: "aborted"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{hasHiddenFieldsEnabled && hasFieldIds && (
|
||||
<div className="mt-6 flex flex-col gap-6">
|
||||
{fieldIds.map((field) => {
|
||||
return (
|
||||
<div key={field}>
|
||||
<p className="text-sm text-slate-500">Hidden Field: {field}</p>
|
||||
<p className="ph-no-capture my-1 font-semibold text-slate-700">
|
||||
{typeof response.data[field] === "string" ? (response.data[field] as string) : ""}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{response.finished && (
|
||||
<div className="mt-4 flex items-center">
|
||||
<CheckCircle2Icon className="h-6 w-6 text-slate-400" />
|
||||
<p className="mx-2 rounded-lg bg-slate-100 px-2 text-sm font-medium text-slate-700">
|
||||
Completed
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SingleResponseCardBody survey={survey} response={response} skippedQuestions={skippedQuestions} />
|
||||
|
||||
<ResponseTagsWrapper
|
||||
environmentId={environmentId}
|
||||
|
||||
15
packages/ui/SingleResponseCard/util.ts
Normal file
15
packages/ui/SingleResponseCard/util.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const isValidValue = (value: string | number | Record<string, string> | string[]) => {
|
||||
return (
|
||||
(typeof value === "string" && value.trim() !== "") ||
|
||||
(Array.isArray(value) && value.length > 0) ||
|
||||
typeof value === "number" ||
|
||||
(typeof value === "object" && Object.entries(value).length > 0)
|
||||
);
|
||||
};
|
||||
|
||||
export const isSubmissionTimeMoreThan5Minutes = (submissionTimeISOString: Date) => {
|
||||
const submissionTime: Date = new Date(submissionTimeISOString);
|
||||
const currentTime: Date = new Date();
|
||||
const timeDifference: number = (currentTime.getTime() - submissionTime.getTime()) / (1000 * 60); // Convert milliseconds to minutes
|
||||
return timeDifference > 5;
|
||||
};
|
||||
Reference in New Issue
Block a user