mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-19 03:04:39 -05:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05d7d1165a | |||
| eea7df81b4 | |||
| c172e2a33c | |||
| 9a5780d510 | |||
| e333d8ba02 | |||
| 16463960ad |
+1
-1
@@ -25,7 +25,7 @@ const Page = async (props: ModePageProps) => {
|
||||
}
|
||||
|
||||
const experimentVariant =
|
||||
(await getPostHogFeatureFlag(session.user.id, "onboarding-mode-experiment")) || "control";
|
||||
(await getPostHogFeatureFlag(session.user.id, "a-b_onboarding_skip-first-screen")) || "control";
|
||||
|
||||
if (experimentVariant === "remove-cx-and-surveys-mode") {
|
||||
return redirect(`/organizations/${params.organizationId}/workspaces/new/channel`);
|
||||
|
||||
+8
-39
@@ -5,35 +5,29 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyElementSummaryOpenText } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { renderHyperlinkedContent } from "@/modules/analysis/utils";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/modules/ui/components/table";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
import { ResponseSampleModal } from "./ResponseSampleModal";
|
||||
|
||||
interface OpenTextSummaryProps {
|
||||
elementSummary: TSurveyElementSummaryOpenText;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const OpenTextSummary = ({
|
||||
elementSummary,
|
||||
survey,
|
||||
locale,
|
||||
isReadOnly,
|
||||
}: Readonly<OpenTextSummaryProps>) => {
|
||||
export const OpenTextSummary = ({ elementSummary, survey, locale }: OpenTextSummaryProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
const [visibleResponses, setVisibleResponses] = useState(10);
|
||||
const [selectedResponseId, setSelectedResponseId] = useState<string | null>(null);
|
||||
|
||||
const handleLoadMore = () => {
|
||||
// Increase the number of visible responses by 10, not exceeding the total number of responses
|
||||
setVisibleResponses((prevVisibleResponses) =>
|
||||
Math.min(prevVisibleResponses + 10, elementSummary.samples.length)
|
||||
);
|
||||
@@ -54,31 +48,17 @@ export const OpenTextSummary = ({
|
||||
<TableRow>
|
||||
<TableHead className="w-1/4">{t("common.user")}</TableHead>
|
||||
<TableHead className="w-2/4">{t("common.response")}</TableHead>
|
||||
<TableHead className="w-1/6">{t("common.time")}</TableHead>
|
||||
<TableHead className="w-1/6">{t("common.response_id")}</TableHead>
|
||||
<TableHead className="w-1/4">{t("common.time")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{elementSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
<TableRow
|
||||
key={response.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t("workspace.surveys.summary.open_response_details")}
|
||||
className="cursor-pointer hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-400"
|
||||
onClick={() => setSelectedResponseId(response.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
setSelectedResponseId(response.id);
|
||||
}
|
||||
}}>
|
||||
<TableRow key={response.id}>
|
||||
<TableCell className="w-1/4">
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/workspaces/${survey.workspaceId}/contacts/${response.contact.id}`}
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
href={`/workspaces/${workspace?.id}/contacts/${response.contact.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
@@ -100,12 +80,9 @@ export const OpenTextSummary = ({
|
||||
? renderHyperlinkedContent(response.value)
|
||||
: response.value}
|
||||
</TableCell>
|
||||
<TableCell className="w-1/6">
|
||||
<TableCell className="w-1/4">
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
</TableCell>
|
||||
<TableCell className="w-1/6" onClick={(e) => e.stopPropagation()}>
|
||||
<IdBadge id={response.id} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -119,14 +96,6 @@ export const OpenTextSummary = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ResponseSampleModal
|
||||
responseId={selectedResponseId}
|
||||
onClose={() => setSelectedResponseId(null)}
|
||||
survey={survey}
|
||||
isReadOnly={isReadOnly}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
-144
@@ -1,144 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TResponseWithQuotas } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { SingleResponseCard } from "@/modules/analysis/components/SingleResponseCard";
|
||||
import {
|
||||
getResponseAction,
|
||||
getTagsByWorkspaceIdAction,
|
||||
} from "@/modules/analysis/components/SingleResponseCard/actions";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from "@/modules/ui/components/dialog";
|
||||
import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
|
||||
|
||||
interface ResponseSampleModalProps {
|
||||
responseId: string | null;
|
||||
onClose: () => void;
|
||||
survey: TSurvey;
|
||||
isReadOnly: boolean;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const ResponseSampleModal = ({
|
||||
responseId,
|
||||
onClose,
|
||||
survey,
|
||||
isReadOnly,
|
||||
locale,
|
||||
}: Readonly<ResponseSampleModalProps>) => {
|
||||
const { t } = useTranslation();
|
||||
const [response, setResponse] = useState<TResponseWithQuotas | null>(null);
|
||||
const [tags, setTags] = useState<TTag[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Cache fetched data per response ID to avoid re-fetching on re-open
|
||||
const cache = useRef<Map<string, { response: TResponseWithQuotas; tags: TTag[] }>>(new Map());
|
||||
// Track the in-flight request so stale resolutions can be ignored when the user
|
||||
// switches rows quickly.
|
||||
const latestRequestId = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!responseId) return;
|
||||
|
||||
const cached = cache.current.get(responseId);
|
||||
if (cached) {
|
||||
setResponse(cached.response);
|
||||
setTags(cached.tags);
|
||||
setErrorMessage(null);
|
||||
return;
|
||||
}
|
||||
|
||||
latestRequestId.current = responseId;
|
||||
setIsLoading(true);
|
||||
setResponse(null);
|
||||
setErrorMessage(null);
|
||||
|
||||
Promise.all([
|
||||
getResponseAction({ responseId }),
|
||||
getTagsByWorkspaceIdAction({ workspaceId: survey.workspaceId }),
|
||||
])
|
||||
.then(([responseResult, tagsResult]) => {
|
||||
// Discard if a newer request has started or the modal has been closed.
|
||||
if (latestRequestId.current !== responseId) return;
|
||||
|
||||
const responseError = getFormattedErrorMessage(responseResult);
|
||||
const tagsError = getFormattedErrorMessage(tagsResult);
|
||||
const fetchedResponse = responseResult?.data ?? null;
|
||||
const fetchedTags = tagsResult?.data ?? [];
|
||||
|
||||
if (responseError || tagsError || !fetchedResponse) {
|
||||
const message = responseError || tagsError || t("common.something_went_wrong");
|
||||
toast.error(message);
|
||||
setErrorMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = { response: fetchedResponse, tags: fetchedTags };
|
||||
cache.current.set(responseId, entry);
|
||||
setResponse(entry.response);
|
||||
setTags(entry.tags);
|
||||
})
|
||||
.catch(() => {
|
||||
if (latestRequestId.current !== responseId) return;
|
||||
const message = t("common.something_went_wrong");
|
||||
toast.error(message);
|
||||
setErrorMessage(message);
|
||||
})
|
||||
.finally(() => {
|
||||
if (latestRequestId.current !== responseId) return;
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [responseId, survey.workspaceId, t]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
// Drop any in-flight request so it can't commit after close.
|
||||
latestRequestId.current = null;
|
||||
setErrorMessage(null);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={!!responseId} onOpenChange={handleOpenChange}>
|
||||
<DialogContent width="wide">
|
||||
<VisuallyHidden asChild>
|
||||
<DialogTitle>{t("common.response")}</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
<VisuallyHidden asChild>
|
||||
<DialogDescription>{t("common.response")}</DialogDescription>
|
||||
</VisuallyHidden>
|
||||
<DialogBody>
|
||||
{isLoading ? (
|
||||
<div className="py-12">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
) : errorMessage ? (
|
||||
<div className="py-12 text-center text-sm text-slate-600">{errorMessage}</div>
|
||||
) : response ? (
|
||||
<SingleResponseCard
|
||||
survey={survey}
|
||||
response={response}
|
||||
environmentTags={tags}
|
||||
isReadOnly={isReadOnly}
|
||||
locale={locale}
|
||||
/>
|
||||
) : null}
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+1
-9
@@ -41,16 +41,9 @@ interface SummaryListProps {
|
||||
responseCount: number | null;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const SummaryList = ({
|
||||
summary,
|
||||
responseCount,
|
||||
survey,
|
||||
locale,
|
||||
isReadOnly,
|
||||
}: Readonly<SummaryListProps>) => {
|
||||
export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryListProps) => {
|
||||
const { workspace } = useWorkspaceContext();
|
||||
const { setSelectedFilter, selectedFilter } = useResponseFilter();
|
||||
const { t } = useTranslation();
|
||||
@@ -123,7 +116,6 @@ export const SummaryList = ({
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
locale={locale}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-4
@@ -49,7 +49,6 @@ interface SummaryPageProps {
|
||||
locale: TUserLocale;
|
||||
initialSurveySummary?: TSurveySummary;
|
||||
isQuotasAllowed: boolean;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const SummaryPage = ({
|
||||
@@ -58,8 +57,7 @@ export const SummaryPage = ({
|
||||
locale,
|
||||
initialSurveySummary,
|
||||
isQuotasAllowed,
|
||||
isReadOnly,
|
||||
}: Readonly<SummaryPageProps>) => {
|
||||
}: SummaryPageProps) => {
|
||||
const { t } = useTranslation();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -227,7 +225,6 @@ export const SummaryPage = ({
|
||||
responseCount={surveySummary.meta.totalResponses}
|
||||
survey={surveyMemoized}
|
||||
locale={locale}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+1
-2
@@ -22,7 +22,7 @@ import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
|
||||
|
||||
const SurveyPage = async (props: Readonly<{ params: Promise<{ workspaceId: string; surveyId: string }> }>) => {
|
||||
const SurveyPage = async (props: { params: Promise<{ workspaceId: string; surveyId: string }> }) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
|
||||
@@ -88,7 +88,6 @@ const SurveyPage = async (props: Readonly<{ params: Promise<{ workspaceId: strin
|
||||
locale={user.locale ?? DEFAULT_LOCALE}
|
||||
initialSurveySummary={initialSurveySummary}
|
||||
isQuotasAllowed={isQuotasAllowed}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
|
||||
<IdBadge id={surveyId} label={t("common.survey_id")} variant="column" />
|
||||
|
||||
@@ -104,7 +104,11 @@ export const createResponse = async (
|
||||
|
||||
const ttc = initialTtc ? (finished ? calculateTtcTotal(initialTtc) : initialTtc) : {};
|
||||
|
||||
const prismaData = buildPrismaResponseData(responseInput, contact, ttc);
|
||||
const prismaData = buildPrismaResponseData(
|
||||
{ ...responseInput, createdAt: undefined, updatedAt: undefined },
|
||||
contact,
|
||||
ttc
|
||||
);
|
||||
|
||||
const prismaClient = tx ?? prisma;
|
||||
|
||||
|
||||
@@ -49,18 +49,7 @@ const buildPrismaResponseData = (
|
||||
contact: { id: string; attributes: TContactAttributes } | null,
|
||||
ttc: Record<string, number>
|
||||
): Prisma.ResponseCreateInput => {
|
||||
const {
|
||||
surveyId,
|
||||
displayId,
|
||||
finished,
|
||||
data,
|
||||
language,
|
||||
meta,
|
||||
singleUseId,
|
||||
variables,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
} = responseInput;
|
||||
const { surveyId, displayId, finished, data, language, meta, singleUseId, variables } = responseInput;
|
||||
|
||||
return {
|
||||
survey: {
|
||||
@@ -84,8 +73,6 @@ const buildPrismaResponseData = (
|
||||
singleUseId,
|
||||
...(variables && { variables }),
|
||||
ttc: ttc,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+2
-1
@@ -2902,6 +2902,7 @@ checksums:
|
||||
workspace/surveys/edit/hidden_field_used_in_recall: 15d959528c3e817dce95640173d5d6a8
|
||||
workspace/surveys/edit/hidden_field_used_in_recall_ending_card: ea0d0b12ca1c9400690658cb1b537025
|
||||
workspace/surveys/edit/hidden_field_used_in_recall_welcome: bb498b6ee69c6311a3977d454866b610
|
||||
workspace/surveys/edit/hidden_fields_description: e9221cd00ae2944602c19ffbc82358a4
|
||||
workspace/surveys/edit/hide_back_button: 91355864b3032c3f57689074e2173544
|
||||
workspace/surveys/edit/hide_back_button_description: caaa30cf43c5611577933a1c9f44b9ee
|
||||
workspace/surveys/edit/hide_block_settings: c24c3d3892c251792e297cdc036d2fde
|
||||
@@ -3203,6 +3204,7 @@ checksums:
|
||||
workspace/surveys/edit/variable_used_in_recall: 1979c231569117297d1a19972b349617
|
||||
workspace/surveys/edit/variable_used_in_recall_ending_card: e6ab9a124985708dd77067c014b7c514
|
||||
workspace/surveys/edit/variable_used_in_recall_welcome: 60b995389b488366d8f6f53df35b6d8d
|
||||
workspace/surveys/edit/variables_description: 4a55faa279acc675228f54dccf63db6a
|
||||
workspace/surveys/edit/verify_email_before_submission: c05d345dc35f2d33839e4cfd72d11eb2
|
||||
workspace/surveys/edit/verify_email_before_submission_description: 434ab3ee6134367513b633a9d4f7d772
|
||||
workspace/surveys/edit/visibility_and_recontact: c27cb4ff3a4262266902a335c3ad5d84
|
||||
@@ -3446,7 +3448,6 @@ checksums:
|
||||
workspace/surveys/summary/no_identified_impressions: c3bc42e6feb9010ced905ded51c5afc4
|
||||
workspace/surveys/summary/no_responses_found: f10190cffdda4ca1bed479acbb89b13f
|
||||
workspace/surveys/summary/nps_promoters_tooltip: dea6a683c0c36189e325656d5a7596b8
|
||||
workspace/surveys/summary/open_response_details: 0e5de115b5e605f68ea857cf8ef5533a
|
||||
workspace/surveys/summary/other_values_found: 48a74ee68c05f7fb162072b50c683b6a
|
||||
workspace/surveys/summary/overall: 6c6d6533013d4739766af84b2871bca6
|
||||
workspace/surveys/summary/promoters: 41fbb8d0439227661253a82fda39f521
|
||||
|
||||
@@ -216,43 +216,6 @@ export const getResponse = reactCache(async (responseId: string): Promise<TRespo
|
||||
}
|
||||
});
|
||||
|
||||
export const getResponseWithQuotas = reactCache(
|
||||
async (responseId: string): Promise<TResponseWithQuotas | null> => {
|
||||
validateInputs([responseId, ZId]);
|
||||
|
||||
try {
|
||||
const responsePrisma = await prisma.response.findUnique({
|
||||
where: {
|
||||
id: responseId,
|
||||
},
|
||||
select: {
|
||||
...responseSelection,
|
||||
quotaLinks: {
|
||||
where: { status: "screenedIn" },
|
||||
include: { quota: { select: { id: true, name: true } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!responsePrisma) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { quotaLinks, ...rest } = responsePrisma;
|
||||
return {
|
||||
...mapResponsePrismaToResponse(rest),
|
||||
quotas: quotaLinks.map((ql) => ql.quota),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getResponseSnapshotForPipeline = async (responseId: string): Promise<TResponse | null> => {
|
||||
validateInputs([responseId, ZId]);
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
getResponseBySingleUseId,
|
||||
getResponseCountBySurveyId,
|
||||
getResponseDownloadFile,
|
||||
getResponseWithQuotas,
|
||||
getResponsesByWorkspaceId,
|
||||
responseSelection,
|
||||
updateResponse,
|
||||
@@ -171,70 +170,6 @@ describe("Tests for getResponse service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for getResponseWithQuotas service", () => {
|
||||
describe("Happy Path", () => {
|
||||
test("Returns the response with screened-in quotas", async () => {
|
||||
prisma.response.findUnique.mockResolvedValue(mockResponseWithQuotas);
|
||||
|
||||
const result = await getResponseWithQuotas(mockResponseWithQuotas.id);
|
||||
|
||||
expect(result).toEqual({
|
||||
...expectedResponseWithoutPerson,
|
||||
quotas: mockResponseWithQuotas.quotaLinks.map(
|
||||
(ql: { quota: { id: string; name: string } }) => ql.quota
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
test("Returns an empty quotas array when no quotaLinks are screened in", async () => {
|
||||
prisma.response.findUnique.mockResolvedValue({ ...mockResponse, quotaLinks: [] } as any);
|
||||
|
||||
const result = await getResponseWithQuotas(mockResponse.id);
|
||||
|
||||
expect(result).toEqual({ ...expectedResponseWithoutPerson, quotas: [] });
|
||||
});
|
||||
|
||||
test("Selects only screened-in quotaLinks", async () => {
|
||||
prisma.response.findUnique.mockResolvedValue({ ...mockResponse, quotaLinks: [] } as any);
|
||||
|
||||
await getResponseWithQuotas(mockResponse.id);
|
||||
|
||||
const findUniqueCall = prisma.response.findUnique.mock.calls.at(-1)?.[0];
|
||||
expect(findUniqueCall?.select?.quotaLinks).toEqual({
|
||||
where: { status: "screenedIn" },
|
||||
include: { quota: { select: { id: true, name: true } } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
testInputValidation(getResponseWithQuotas, "123#");
|
||||
|
||||
test("Returns null when no response is found", async () => {
|
||||
prisma.response.findUnique.mockResolvedValue(null);
|
||||
|
||||
const result = await getResponseWithQuotas(mockResponse.id);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("Throws DatabaseError on PrismaClientKnownRequestError", async () => {
|
||||
const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error", {
|
||||
code: PrismaErrorType.UniqueConstraintViolation,
|
||||
clientVersion: "0.0.1",
|
||||
});
|
||||
prisma.response.findUnique.mockRejectedValue(errToThrow);
|
||||
|
||||
await expect(getResponseWithQuotas(mockResponse.id)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
test("Rethrows generic errors", async () => {
|
||||
prisma.response.findUnique.mockRejectedValue(new Error("boom"));
|
||||
|
||||
await expect(getResponseWithQuotas(mockResponse.id)).rejects.toThrow("boom");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for getSurveySummary service", () => {
|
||||
describe("Happy Path", () => {
|
||||
test("Returns a summary of the survey responses", async () => {
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "Verborgenes Feld \"{hiddenField}\" wird in Frage {questionIndex} abgerufen.",
|
||||
"hidden_field_used_in_recall_ending_card": "Verborgenes Feld \"{hiddenField}\" wird in der Abschlusskarte abgerufen",
|
||||
"hidden_field_used_in_recall_welcome": "Verborgenes Feld \"{hiddenField}\" wird in der Willkommenskarte abgerufen.",
|
||||
"hidden_fields_description": "Übergib versteckte Daten an deine Umfrage, ohne sie den Teilnehmern zu zeigen.",
|
||||
"hide_back_button": "\"Zurück\"-Button ausblenden",
|
||||
"hide_back_button_description": "Zeige den Zurück-Button in der Umfrage nicht an",
|
||||
"hide_block_settings": "Block-Einstellungen ausblenden",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "Die Variable \"{variable}\" wird in Frage {questionIndex} abgerufen.",
|
||||
"variable_used_in_recall_ending_card": "Die Variable {variable} wird in der Abschlusskarte abgerufen",
|
||||
"variable_used_in_recall_welcome": "Die Variable \"{variable}\" wird in der Willkommenskarte abgerufen.",
|
||||
"variables_description": "Definiere und berechne Werte während deiner Umfrage.",
|
||||
"verify_email_before_submission": "E-Mail vor dem Absenden verifizieren",
|
||||
"verify_email_before_submission_description": "Lass nur Personen mit einer echten E-Mail-Adresse antworten.",
|
||||
"visibility_and_recontact": "Sichtbarkeit & erneute Kontaktaufnahme",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "Keine Impressionen von identifizierten Kontakten",
|
||||
"no_responses_found": "Keine Antworten gefunden",
|
||||
"nps_promoters_tooltip": "{percentage}% der Befragten haben eine Bewertung von 9 oder 10 gegeben (NPS-Promotoren).",
|
||||
"open_response_details": "Details zu offenen Antworten",
|
||||
"other_values_found": "Andere Werte gefunden",
|
||||
"overall": "Gesamt",
|
||||
"promoters": "Promotoren",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "Hidden field “{hiddenField}” is being recalled in question {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Hidden field “{hiddenField}” is being recalled in Ending Card",
|
||||
"hidden_field_used_in_recall_welcome": "Hidden field “{hiddenField}” is being recalled in Welcome card.",
|
||||
"hidden_fields_description": "Pass hidden data into your survey without showing it to respondents.",
|
||||
"hide_back_button": "Hide “Back” button",
|
||||
"hide_back_button_description": "Do not display the back button in the survey",
|
||||
"hide_block_settings": "Hide Block settings",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "Variable “{variable}” is being recalled in question {questionIndex}.",
|
||||
"variable_used_in_recall_ending_card": "Variable {variable} is being recalled in Ending Card",
|
||||
"variable_used_in_recall_welcome": "Variable “{variable}” is being recalled in Welcome Card.",
|
||||
"variables_description": "Define and compute values throughout your survey.",
|
||||
"verify_email_before_submission": "Verify email before submission",
|
||||
"verify_email_before_submission_description": "Only let people with a real email respond.",
|
||||
"visibility_and_recontact": "Visibility & Recontact",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "No impressions from identified contacts",
|
||||
"no_responses_found": "No responses found",
|
||||
"nps_promoters_tooltip": "{percentage}% of respondents gave a rating of 9 or 10 (NPS promoters).",
|
||||
"open_response_details": "Open response details",
|
||||
"other_values_found": "Other values found",
|
||||
"overall": "Overall",
|
||||
"promoters": "Promoters",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "El campo oculto \"{hiddenField}\" se está recordando en la pregunta {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "El campo oculto \"{hiddenField}\" se está recordando en la tarjeta final.",
|
||||
"hidden_field_used_in_recall_welcome": "El campo oculto \"{hiddenField}\" se está recordando en la tarjeta de bienvenida.",
|
||||
"hidden_fields_description": "Pasa datos ocultos a tu encuesta sin mostrárselos a los encuestados.",
|
||||
"hide_back_button": "Ocultar botón 'Atrás'",
|
||||
"hide_back_button_description": "No mostrar el botón de retroceso en la encuesta",
|
||||
"hide_block_settings": "Ocultar ajustes del bloque",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "La variable \"{variable}\" se está recuperando en la pregunta {questionIndex}.",
|
||||
"variable_used_in_recall_ending_card": "La variable {variable} se está recuperando en la tarjeta final",
|
||||
"variable_used_in_recall_welcome": "La variable \"{variable}\" se está recuperando en la tarjeta de bienvenida.",
|
||||
"variables_description": "Define y calcula valores a lo largo de tu encuesta.",
|
||||
"verify_email_before_submission": "Verificar correo electrónico antes del envío",
|
||||
"verify_email_before_submission_description": "Solo permite responder a personas con un correo electrónico real.",
|
||||
"visibility_and_recontact": "Visibilidad y recontacto",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "No hay impresiones de contactos identificados",
|
||||
"no_responses_found": "No se han encontrado respuestas",
|
||||
"nps_promoters_tooltip": "El {percentage}% de los encuestados dieron una puntuación de 9 o 10 (promotores NPS).",
|
||||
"open_response_details": "Detalles de respuesta abierta",
|
||||
"other_values_found": "Otros valores encontrados",
|
||||
"overall": "General",
|
||||
"promoters": "Promotores",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "Le champ caché \"{hiddenField}\" est rappelé dans la question {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Le champ caché \"{hiddenField}\" est rappelé dans la carte de fin.",
|
||||
"hidden_field_used_in_recall_welcome": "Le champ caché \"{hiddenField}\" est rappelé dans la carte de bienvenue.",
|
||||
"hidden_fields_description": "Transmets des données masquées dans ton questionnaire sans les montrer aux répondants.",
|
||||
"hide_back_button": "Masquer le bouton 'Retour'",
|
||||
"hide_back_button_description": "Ne pas afficher le bouton retour dans l'enquête",
|
||||
"hide_block_settings": "Masquer les paramètres du bloc",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "La variable \"{variable}\" est rappelée dans la question {questionIndex}.",
|
||||
"variable_used_in_recall_ending_card": "La variable {variable} est rappelée dans la carte de fin.",
|
||||
"variable_used_in_recall_welcome": "La variable \"{variable}\" est rappelée dans la carte de bienvenue.",
|
||||
"variables_description": "Définis et calcule des valeurs tout au long de ton questionnaire.",
|
||||
"verify_email_before_submission": "Vérifiez l'email avant la soumission",
|
||||
"verify_email_before_submission_description": "Ne laissez répondre que les personnes ayant une véritable adresse e-mail.",
|
||||
"visibility_and_recontact": "Visibilité et recontact",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "Aucune impression des contacts identifiés",
|
||||
"no_responses_found": "Aucune réponse trouvée",
|
||||
"nps_promoters_tooltip": "{percentage} % des répondants ont donné une note de 9 ou 10 (promoteurs NPS).",
|
||||
"open_response_details": "Détails des réponses ouvertes",
|
||||
"other_values_found": "D'autres valeurs trouvées",
|
||||
"overall": "Globalement",
|
||||
"promoters": "Promoteurs",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "A(z) „{hiddenField}” rejtett mező visszahívásra kerül a(z) {questionIndex}. kérdésben.",
|
||||
"hidden_field_used_in_recall_ending_card": "A(z) „{hiddenField}” rejtett mező visszahívásra kerül a befejező kártyában",
|
||||
"hidden_field_used_in_recall_welcome": "A(z) „{hiddenField}” rejtett mező visszahívásra kerül az üdvözlő kártyában.",
|
||||
"hidden_fields_description": "Rejtett adatokat továbbíthat a felmérésbe anélkül, hogy azokat a válaszadók látnák.",
|
||||
"hide_back_button": "A „Vissza” gomb elrejtése",
|
||||
"hide_back_button_description": "Ne jelenjen meg a vissza gomb a kérdőívben",
|
||||
"hide_block_settings": "Blokkbeállítások elrejtése",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "A(z) „{variable}” változó visszahívásra kerül a(z) {questionIndex}. kérdésben.",
|
||||
"variable_used_in_recall_ending_card": "A(z) {variable} változó visszahívásra kerül a befejező kártyában",
|
||||
"variable_used_in_recall_welcome": "A(z) „{variable}” változó visszahívásra kerül az üdvözlő kártyában.",
|
||||
"variables_description": "Értékeket határozhat meg és számíthat ki a felmérés során.",
|
||||
"verify_email_before_submission": "E-mail-cím ellenőrzése a beküldés előtt",
|
||||
"verify_email_before_submission_description": "Csak valódi e-mail-címmel rendelkező személyek válaszolhassanak.",
|
||||
"visibility_and_recontact": "Láthatóság és újbóli kapcsolatfelvétel",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "Nincsenek azonosított partnerektől származó megtekintések",
|
||||
"no_responses_found": "Nem találhatók válaszok",
|
||||
"nps_promoters_tooltip": "A válaszadók {percentage}%-a 9-es vagy 10-es értékelést adott (NPS promoters).",
|
||||
"open_response_details": "Nyitott válasz részletei",
|
||||
"other_values_found": "Más értékek találhatók",
|
||||
"overall": "Összesen",
|
||||
"promoters": "Népszerűsítők",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "隠し フィールド \"{hiddenField}\" が 質問 {questionIndex} で 呼び出され て います 。",
|
||||
"hidden_field_used_in_recall_ending_card": "隠し フィールド \"{hiddenField}\" が エンディング カード で 呼び出され て います。",
|
||||
"hidden_field_used_in_recall_welcome": "隠し フィールド \"{hiddenField}\" が ウェルカム カード で 呼び出され て います。",
|
||||
"hidden_fields_description": "回答者に表示せずに、非表示データをアンケートに渡すことができます。",
|
||||
"hide_back_button": "「戻る」ボタンを非表示",
|
||||
"hide_back_button_description": "フォームに「戻る」ボタンを表示しない",
|
||||
"hide_block_settings": "ブロック設定を非表示",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "変数 \"{variable}\" が 質問 {questionIndex} で 呼び出され て います 。",
|
||||
"variable_used_in_recall_ending_card": "変数 {variable} が エンディング カード で 呼び出され て います。",
|
||||
"variable_used_in_recall_welcome": "変数 \"{variable}\" が ウェルカム カード で 呼び出され て います。",
|
||||
"variables_description": "アンケート全体で値を定義し、計算できます。",
|
||||
"verify_email_before_submission": "送信前にメールアドレスを認証",
|
||||
"verify_email_before_submission_description": "有効なメールアドレスを持つ人のみが回答できるようにする",
|
||||
"visibility_and_recontact": "表示と再接触",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "識別済みコンタクトからのインプレッションはありません",
|
||||
"no_responses_found": "回答が見つかりません",
|
||||
"nps_promoters_tooltip": "回答者の{percentage}%が9または10の評価をしました(NPSプロモーター)。",
|
||||
"open_response_details": "自由回答の詳細",
|
||||
"other_values_found": "他の値が見つかりました",
|
||||
"overall": "全体",
|
||||
"promoters": "推奨者",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "Verborgen veld \"{hiddenField}\" wordt opgeroepen in vraag {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Verborgen veld \"{hiddenField}\" wordt opgeroepen in de eindkaart",
|
||||
"hidden_field_used_in_recall_welcome": "Verborgen veld \"{hiddenField}\" wordt opgeroepen in de welkomstkaart.",
|
||||
"hidden_fields_description": "Geef verborgen gegevens door aan je enquête zonder dat respondenten het zien.",
|
||||
"hide_back_button": "Knop 'Terug' verbergen",
|
||||
"hide_back_button_description": "Geef de terugknop niet weer in de enquête",
|
||||
"hide_block_settings": "Blokinstellingen verbergen",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "Variabele \"{variable}\" wordt opgeroepen in vraag {questionIndex}.",
|
||||
"variable_used_in_recall_ending_card": "Variabele {variable} wordt opgeroepen in de eindkaart",
|
||||
"variable_used_in_recall_welcome": "Variabele \"{variable}\" wordt opgeroepen in de welkomstkaart.",
|
||||
"variables_description": "Definieer en bereken waarden tijdens je enquête.",
|
||||
"verify_email_before_submission": "Verifieer uw e-mailadres voordat u het verzendt",
|
||||
"verify_email_before_submission_description": "Laat alleen mensen met een echte e-mail reageren.",
|
||||
"visibility_and_recontact": "Zichtbaarheid & opnieuw contact",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "Geen weergaven van geïdentificeerde contacten",
|
||||
"no_responses_found": "Geen reacties gevonden",
|
||||
"nps_promoters_tooltip": "{percentage}% van de respondenten gaf een beoordeling van 9 of 10 (NPS promoters).",
|
||||
"open_response_details": "Details open antwoorden",
|
||||
"other_values_found": "Andere waarden gevonden",
|
||||
"overall": "Algemeen",
|
||||
"promoters": "Promoters",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "Campo oculto \"{hiddenField}\" está sendo recordado na pergunta {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Campo oculto \"{hiddenField}\" está sendo recordado no card de Encerramento.",
|
||||
"hidden_field_used_in_recall_welcome": "Campo oculto \"{hiddenField}\" está sendo recordado no card de Boas-Vindas.",
|
||||
"hidden_fields_description": "Passe dados ocultos para sua pesquisa sem mostrá-los aos respondentes.",
|
||||
"hide_back_button": "Ocultar botão 'Voltar'",
|
||||
"hide_back_button_description": "Não exibir o botão de voltar na pesquisa",
|
||||
"hide_block_settings": "Ocultar configurações do bloco",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "Variável \"{variable}\" está sendo recordada na pergunta {questionIndex}.",
|
||||
"variable_used_in_recall_ending_card": "Variável {variable} está sendo recordada no card de Encerramento",
|
||||
"variable_used_in_recall_welcome": "Variável \"{variable}\" está sendo recordada no Card de Boas-Vindas.",
|
||||
"variables_description": "Defina e calcule valores ao longo da sua pesquisa.",
|
||||
"verify_email_before_submission": "Verifique o e-mail antes de enviar",
|
||||
"verify_email_before_submission_description": "Deixe só quem tem um email real responder.",
|
||||
"visibility_and_recontact": "Visibilidade e recontato",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "Nenhuma impressão de contatos identificados",
|
||||
"no_responses_found": "Nenhuma resposta encontrada",
|
||||
"nps_promoters_tooltip": "{percentage}% dos entrevistados deram uma nota de 9 ou 10 (promotores NPS).",
|
||||
"open_response_details": "Detalhes das respostas abertas",
|
||||
"other_values_found": "Outros valores encontrados",
|
||||
"overall": "No geral",
|
||||
"promoters": "Promotores",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "Campo oculto \"{hiddenField}\" está a ser recordado na pergunta {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Campo oculto \"{hiddenField}\" está a ser recordado no Cartão de Conclusão",
|
||||
"hidden_field_used_in_recall_welcome": "Campo oculto \"{hiddenField}\" está a ser recordado no cartão de boas-vindas.",
|
||||
"hidden_fields_description": "Passa dados ocultos para o teu inquérito sem os mostrar aos inquiridos.",
|
||||
"hide_back_button": "Ocultar botão 'Retroceder'",
|
||||
"hide_back_button_description": "Não mostrar o botão de retroceder no inquérito",
|
||||
"hide_block_settings": "Ocultar definições do bloco",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "Variável \"{variable}\" está a ser recordada na pergunta {questionIndex}.",
|
||||
"variable_used_in_recall_ending_card": "Variável {variable} está a ser recordada no Cartão de Conclusão",
|
||||
"variable_used_in_recall_welcome": "Variável \"{variable}\" está a ser recordada no cartão de boas-vindas.",
|
||||
"variables_description": "Define e calcula valores ao longo do teu inquérito.",
|
||||
"verify_email_before_submission": "Verificar email antes da submissão",
|
||||
"verify_email_before_submission_description": "Permitir apenas que pessoas com um email real respondam.",
|
||||
"visibility_and_recontact": "Visibilidade e Recontacto",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "Sem impressões de contactos identificados",
|
||||
"no_responses_found": "Nenhuma resposta encontrada",
|
||||
"nps_promoters_tooltip": "{percentage}% dos inquiridos deram uma classificação de 9 ou 10 (promotores NPS).",
|
||||
"open_response_details": "Detalhes de respostas abertas",
|
||||
"other_values_found": "Outros valores encontrados",
|
||||
"overall": "Geral",
|
||||
"promoters": "Promotores",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "Câmpul ascuns \"{hiddenField}\" este reamintit în întrebarea {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Câmpul ascuns \"{hiddenField}\" este reamintit în Cardul de Încheiere.",
|
||||
"hidden_field_used_in_recall_welcome": "Câmpul ascuns \"{hiddenField}\" este reamintit în cardul de bun venit.",
|
||||
"hidden_fields_description": "Transmite date ascunse în sondajul tău fără a le afișa respondenților.",
|
||||
"hide_back_button": "Ascunde butonul 'Înapoi'",
|
||||
"hide_back_button_description": "Nu afișa butonul Înapoi în sondaj",
|
||||
"hide_block_settings": "Ascunde setările blocului",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "Variabila \"{variable}\" este reamintită în întrebarea {questionIndex}.",
|
||||
"variable_used_in_recall_ending_card": "Variabila {variable} este reamintită în Cardul de Încheiere.",
|
||||
"variable_used_in_recall_welcome": "Variabila \"{variable}\" este reamintită în cardul de bun venit.",
|
||||
"variables_description": "Definește și calculează valori pe parcursul sondajului tău.",
|
||||
"verify_email_before_submission": "Verifică emailul înainte de trimitere",
|
||||
"verify_email_before_submission_description": "Permite doar persoanelor cu un email real să răspundă.",
|
||||
"visibility_and_recontact": "Vizibilitate și recontactare",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "Nicio impresie de la contactele identificate",
|
||||
"no_responses_found": "Nu s-au găsit răspunsuri",
|
||||
"nps_promoters_tooltip": "{percentage}% dintre respondenți au acordat o evaluare de 9 sau 10 (promotori NPS).",
|
||||
"open_response_details": "Detalii răspunsuri deschise",
|
||||
"other_values_found": "Alte valori găsite",
|
||||
"overall": "General",
|
||||
"promoters": "Promotori",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "Скрытое поле «{hiddenField}» используется в вопросе {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Скрытое поле «{hiddenField}» используется в финальной карточке",
|
||||
"hidden_field_used_in_recall_welcome": "Скрытое поле «{hiddenField}» используется в приветственной карточке.",
|
||||
"hidden_fields_description": "Передавайте скрытые данные в опрос, не показывая их респондентам.",
|
||||
"hide_back_button": "Скрыть кнопку «Назад»",
|
||||
"hide_back_button_description": "Не отображать кнопку «Назад» в опросе",
|
||||
"hide_block_settings": "Скрыть настройки блока",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "Переменная «{variable}» используется в вопросе {questionIndex}.",
|
||||
"variable_used_in_recall_ending_card": "Переменная {variable} используется в финальной карточке",
|
||||
"variable_used_in_recall_welcome": "Переменная «{variable}» используется в приветственной карточке.",
|
||||
"variables_description": "Определяйте и вычисляйте значения на протяжении всего опроса.",
|
||||
"verify_email_before_submission": "Проверять email перед отправкой",
|
||||
"verify_email_before_submission_description": "Разрешить отвечать только пользователям с реальным email.",
|
||||
"visibility_and_recontact": "Видимость и повторный контакт",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "Нет показов от идентифицированных контактов",
|
||||
"no_responses_found": "Ответы не найдены",
|
||||
"nps_promoters_tooltip": "{percentage}% респондентов дали оценку 9 или 10 (промоутеры NPS).",
|
||||
"open_response_details": "Детали открытых ответов",
|
||||
"other_values_found": "Найдены другие значения",
|
||||
"overall": "В целом",
|
||||
"promoters": "Сторонники",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "Dolt fält \"{hiddenField}\" återkallas i fråga {questionIndex}.",
|
||||
"hidden_field_used_in_recall_ending_card": "Dolt fält \"{hiddenField}\" återkallas i avslutningskortet",
|
||||
"hidden_field_used_in_recall_welcome": "Dolt fält \"{hiddenField}\" återkallas i välkomstkortet.",
|
||||
"hidden_fields_description": "Skicka dold data till din enkät utan att visa den för respondenter.",
|
||||
"hide_back_button": "Dölj 'Tillbaka'-knapp",
|
||||
"hide_back_button_description": "Visa inte tillbakaknappen i enkäten",
|
||||
"hide_block_settings": "Dölj blockinställningar",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "Variabel \"{variable}\" återkallas i fråga {questionIndex}.",
|
||||
"variable_used_in_recall_ending_card": "Variabel {variable} återkallas i avslutningskortet",
|
||||
"variable_used_in_recall_welcome": "Variabel \"{variable}\" återkallas i välkomstkortet.",
|
||||
"variables_description": "Definiera och beräkna värden genom hela din enkät.",
|
||||
"verify_email_before_submission": "Verifiera e-post före inskickning",
|
||||
"verify_email_before_submission_description": "Låt endast personer med en riktig e-post svara.",
|
||||
"visibility_and_recontact": "Synlighet och återkontakt",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "Inga visningar från identifierade kontakter",
|
||||
"no_responses_found": "Inga svar hittades",
|
||||
"nps_promoters_tooltip": "{percentage}% av respondenterna gav ett betyg på 9 eller 10 (NPS-ambassadörer).",
|
||||
"open_response_details": "Detaljer för öppna svar",
|
||||
"other_values_found": "Andra värden hittades",
|
||||
"overall": "Övergripande",
|
||||
"promoters": "Ambassadörer",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "Gizli alan \"{hiddenField}\" soru {questionIndex}'te hatırlatılıyor.",
|
||||
"hidden_field_used_in_recall_ending_card": "Gizli alan \"{hiddenField}\" Bitiş Kartında hatırlatılıyor",
|
||||
"hidden_field_used_in_recall_welcome": "Gizli alan \"{hiddenField}\" Hoş Geldiniz kartında hatırlatılıyor.",
|
||||
"hidden_fields_description": "Gizli verileri yanıtlayanlara göstermeden anketine aktar.",
|
||||
"hide_back_button": "\"Geri\" düğmesini gizle",
|
||||
"hide_back_button_description": "Ankette geri düğmesini gösterme",
|
||||
"hide_block_settings": "Blok ayarlarını gizle",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "“{variable}” değişkeni, {questionIndex}. soruda geri çağrılıyor.",
|
||||
"variable_used_in_recall_ending_card": "{variable} değişkeni Bitiş Kartı'nda geri çağrılıyor.",
|
||||
"variable_used_in_recall_welcome": "“{variable}” değişkeni Hoş Geldin Kartı'nda geri çağrılıyor.",
|
||||
"variables_description": "Anketin boyunca değerleri tanımla ve hesapla.",
|
||||
"verify_email_before_submission": "Göndermeden önce e-posta doğrula",
|
||||
"verify_email_before_submission_description": "Sadece gerçek bir e-postaya sahip kişilerin yanıt vermesine izin ver.",
|
||||
"visibility_and_recontact": "Görünürlük & Yeniden İletişim",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "Tanımlanmış kişilerden gösterim yok",
|
||||
"no_responses_found": "Yanıt bulunamadı",
|
||||
"nps_promoters_tooltip": "Yanıt verenlerin %{percentage}'si 9 veya 10 puan verdi (NPS tavsiye edenler).",
|
||||
"open_response_details": "Açık yanıt detayları",
|
||||
"other_values_found": "Diğer değerler bulundu",
|
||||
"overall": "Genel",
|
||||
"promoters": "Tavsiye edenler",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "隐藏 字段 \"{hiddenField}\" 正在召回于问题 {questionIndex}。",
|
||||
"hidden_field_used_in_recall_ending_card": "隐藏 字段 \"{hiddenField}\" 正在召回于结束 卡",
|
||||
"hidden_field_used_in_recall_welcome": "隐藏 字段 \"{hiddenField}\" 正在召回于欢迎 卡 。",
|
||||
"hidden_fields_description": "将隐藏数据传递到您的调查中,而不向受访者显示。",
|
||||
"hide_back_button": "隐藏 \"返回\" 按钮",
|
||||
"hide_back_button_description": "不 显示 调查 中 的 返回 按钮",
|
||||
"hide_block_settings": "隐藏区块设置",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "变量 \"{variable}\" 正在召回于问题 {questionIndex}。",
|
||||
"variable_used_in_recall_ending_card": "变量 {variable} 正在召回于结束 卡片",
|
||||
"variable_used_in_recall_welcome": "变量 \"{variable}\" 正在召回于欢迎 卡 。",
|
||||
"variables_description": "在整个调查过程中定义和计算值。",
|
||||
"verify_email_before_submission": "提交 之前 验证电子邮件",
|
||||
"verify_email_before_submission_description": "仅允许 拥有 有效 电子邮件 的 人 回应。",
|
||||
"visibility_and_recontact": "可见性与重新联系",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "没有已识别联系人的展示次数",
|
||||
"no_responses_found": "未找到响应",
|
||||
"nps_promoters_tooltip": "{percentage}% 的受访者给出了 9 或 10 分的评价(NPS 推荐者)。",
|
||||
"open_response_details": "开放式回答详情",
|
||||
"other_values_found": "找到其他值",
|
||||
"overall": "整体",
|
||||
"promoters": "推荐者",
|
||||
|
||||
@@ -3021,6 +3021,7 @@
|
||||
"hidden_field_used_in_recall": "隱藏欄位 \"{hiddenField}\" 於問題 {questionIndex} 中被召回。",
|
||||
"hidden_field_used_in_recall_ending_card": "隱藏欄位 \"{hiddenField}\" 於結束卡中被召回。",
|
||||
"hidden_field_used_in_recall_welcome": "隱藏欄位 \"{hiddenField}\" 於歡迎卡中被召回。",
|
||||
"hidden_fields_description": "將隱藏資料傳入你的問卷中,而不會顯示給受訪者。",
|
||||
"hide_back_button": "隱藏「Back」按鈕",
|
||||
"hide_back_button_description": "不要在問卷中顯示返回按鈕",
|
||||
"hide_block_settings": "隱藏區塊設定",
|
||||
@@ -3326,6 +3327,7 @@
|
||||
"variable_used_in_recall": "變數 \"{variable}\" 於問題 {questionIndex} 中被召回。",
|
||||
"variable_used_in_recall_ending_card": "變數 {variable} 於 結束 卡 中被召回。",
|
||||
"variable_used_in_recall_welcome": "變數 \"{variable}\" 於 歡迎 Card 中被召回。",
|
||||
"variables_description": "在整個問卷中定義和計算數值。",
|
||||
"verify_email_before_submission": "提交前驗證電子郵件",
|
||||
"verify_email_before_submission_description": "僅允許擁有真實電子郵件的人員回應。",
|
||||
"visibility_and_recontact": "可見性與重新聯絡",
|
||||
@@ -3597,7 +3599,6 @@
|
||||
"no_identified_impressions": "沒有來自已識別聯絡人的曝光次數",
|
||||
"no_responses_found": "找不到回應",
|
||||
"nps_promoters_tooltip": "{percentage}% 的受訪者給予 9 或 10 分評價(NPS 推薦者)。",
|
||||
"open_response_details": "開放式回覆詳情",
|
||||
"other_values_found": "找到其他值",
|
||||
"overall": "整體",
|
||||
"promoters": "推廣者",
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { deleteResponse, getResponseWithQuotas } from "@/lib/response/service";
|
||||
import { createTag, getTagsByWorkspaceId } from "@/lib/tag/service";
|
||||
import { deleteResponse, getResponse } from "@/lib/response/service";
|
||||
import { createTag } from "@/lib/tag/service";
|
||||
import { addTagToRespone, deleteTagOnResponse } from "@/lib/tagOnResponse/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
@@ -173,32 +173,6 @@ export const deleteResponseAction = authenticatedActionClient.inputSchema(ZDelet
|
||||
})
|
||||
);
|
||||
|
||||
const ZGetTagsByWorkspaceIdAction = z.object({
|
||||
workspaceId: ZId,
|
||||
});
|
||||
|
||||
export const getTagsByWorkspaceIdAction = authenticatedActionClient
|
||||
.inputSchema(ZGetTagsByWorkspaceIdAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromWorkspaceId(parsedInput.workspaceId),
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
minPermission: "read",
|
||||
workspaceId: parsedInput.workspaceId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return await getTagsByWorkspaceId(parsedInput.workspaceId);
|
||||
});
|
||||
|
||||
const ZGetResponseAction = z.object({
|
||||
responseId: ZId,
|
||||
});
|
||||
@@ -222,5 +196,5 @@ export const getResponseAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
return await getResponseWithQuotas(parsedInput.responseId);
|
||||
return await getResponse(parsedInput.responseId);
|
||||
});
|
||||
|
||||
@@ -72,6 +72,7 @@ interface ElementsViewProps {
|
||||
isStorageConfigured: boolean;
|
||||
quotas: TSurveyQuota[];
|
||||
isExternalUrlsAllowed: boolean;
|
||||
moveHiddenFieldsToSettingsTab?: boolean;
|
||||
}
|
||||
|
||||
export const ElementsView = ({
|
||||
@@ -91,6 +92,7 @@ export const ElementsView = ({
|
||||
isStorageConfigured = true,
|
||||
quotas,
|
||||
isExternalUrlsAllowed,
|
||||
moveHiddenFieldsToSettingsTab = false,
|
||||
}: ElementsViewProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [logicDeletionWarning, setLogicDeletionWarning] = React.useState<{
|
||||
@@ -919,23 +921,25 @@ export const ElementsView = ({
|
||||
{!isCxMode && (
|
||||
<>
|
||||
<AddEndingCardButton localSurvey={localSurvey} addEndingCard={addEndingCard} />
|
||||
<hr />
|
||||
|
||||
<HiddenFieldsCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
setActiveElementId={setActiveElementId}
|
||||
activeElementId={activeElementId}
|
||||
quotas={quotas}
|
||||
/>
|
||||
|
||||
<SurveyVariablesCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
activeElementId={activeElementId}
|
||||
setActiveElementId={setActiveElementId}
|
||||
quotas={quotas}
|
||||
/>
|
||||
{!moveHiddenFieldsToSettingsTab && (
|
||||
<>
|
||||
<hr />
|
||||
<HiddenFieldsCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
setActiveElementId={setActiveElementId}
|
||||
activeElementId={activeElementId}
|
||||
quotas={quotas}
|
||||
/>
|
||||
<SurveyVariablesCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
activeElementId={activeElementId}
|
||||
setActiveElementId={setActiveElementId}
|
||||
quotas={quotas}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { EyeOff } from "lucide-react";
|
||||
import { CheckIcon, EyeOff } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -25,6 +25,7 @@ interface HiddenFieldsCardProps {
|
||||
activeElementId: string | null;
|
||||
setActiveElementId: (elementId: string | null) => void;
|
||||
quotas: TSurveyQuota[];
|
||||
inSettings?: boolean;
|
||||
}
|
||||
|
||||
export const HiddenFieldsCard = ({
|
||||
@@ -33,6 +34,7 @@ export const HiddenFieldsCard = ({
|
||||
setActiveElementId,
|
||||
setLocalSurvey,
|
||||
quotas,
|
||||
inSettings = false,
|
||||
}: HiddenFieldsCardProps) => {
|
||||
const open = activeElementId == "hidden";
|
||||
const [hiddenField, setHiddenField] = useState<string>("");
|
||||
@@ -149,6 +151,105 @@ export const HiddenFieldsCard = ({
|
||||
// Auto Animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const content = (
|
||||
<Collapsible.CollapsibleContent
|
||||
className={inSettings ? "flex flex-col" : `flex flex-col px-4 ${open && "pb-6"}`}
|
||||
ref={parent}>
|
||||
{inSettings && <hr className="py-1 text-slate-600" />}
|
||||
<div className={cn("flex flex-wrap gap-2", inSettings ? "p-3" : "")} ref={parent}>
|
||||
{localSurvey.hiddenFields?.fieldIds && localSurvey.hiddenFields?.fieldIds?.length > 0 ? (
|
||||
localSurvey.hiddenFields?.fieldIds?.map((fieldId) => {
|
||||
return (
|
||||
<Tag
|
||||
key={fieldId}
|
||||
onDelete={(fieldId) => handleDeleteHiddenField(fieldId)}
|
||||
tagId={fieldId}
|
||||
tagName={fieldId}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="mt-2 text-sm italic text-slate-500">
|
||||
{t("workspace.surveys.edit.no_hidden_fields_yet_add_first_one_below")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
className={inSettings ? "mt-5 p-3 pt-0" : "mt-5"}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const existingElementIds = elements.map((element) => element.id);
|
||||
const existingEndingCardIds = localSurvey.endings.map((ending) => ending.id);
|
||||
const existingHiddenFieldIds = localSurvey.hiddenFields.fieldIds ?? [];
|
||||
const existingVariableNames = localSurvey.variables.map((v) => v.name);
|
||||
const validateIdError = validateId(
|
||||
hiddenField,
|
||||
existingElementIds,
|
||||
existingEndingCardIds,
|
||||
existingHiddenFieldIds,
|
||||
existingVariableNames
|
||||
);
|
||||
|
||||
if (validateIdError) {
|
||||
toast.error(getValidateIdErrorMessage(validateIdError, "hiddenField", t));
|
||||
return;
|
||||
}
|
||||
|
||||
updateSurvey({
|
||||
fieldIds: [...(localSurvey.hiddenFields?.fieldIds || []), hiddenField],
|
||||
enabled: true,
|
||||
});
|
||||
toast.success(t("workspace.surveys.edit.hidden_field_added_successfully"));
|
||||
setHiddenField("");
|
||||
}}>
|
||||
<Label htmlFor="hiddenField">{t("common.hidden_field")}</Label>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="hiddenField"
|
||||
name="hiddenField"
|
||||
value={hiddenField}
|
||||
onChange={(e) => setHiddenField(e.target.value.trim())}
|
||||
placeholder={t("workspace.surveys.edit.type_field_id") + "..."}
|
||||
/>
|
||||
<Button variant="secondary" type="submit" className="h-10 whitespace-nowrap">
|
||||
{t("workspace.surveys.edit.add_hidden_field_id")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Collapsible.CollapsibleContent>
|
||||
);
|
||||
|
||||
if (inSettings) {
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
className={cn(
|
||||
open ? "" : "hover:bg-slate-50",
|
||||
"w-full space-y-2 rounded-lg border border-slate-300 bg-white"
|
||||
)}>
|
||||
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
|
||||
<div className="inline-flex px-4 py-4">
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<CheckIcon
|
||||
strokeWidth={3}
|
||||
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-800">{t("common.hidden_fields")}</p>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
{t("workspace.surveys.edit.hidden_fields_description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
{content}
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(open ? "shadow-lg" : "shadow-md", "group z-10 flex flex-row rounded-lg bg-white")}>
|
||||
<div
|
||||
@@ -173,69 +274,7 @@ export const HiddenFieldsCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-6"}`} ref={parent}>
|
||||
<div className="flex flex-wrap gap-2" ref={parent}>
|
||||
{localSurvey.hiddenFields?.fieldIds && localSurvey.hiddenFields?.fieldIds?.length > 0 ? (
|
||||
localSurvey.hiddenFields?.fieldIds?.map((fieldId) => {
|
||||
return (
|
||||
<Tag
|
||||
key={fieldId}
|
||||
onDelete={(fieldId) => handleDeleteHiddenField(fieldId)}
|
||||
tagId={fieldId}
|
||||
tagName={fieldId}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="mt-2 text-sm italic text-slate-500">
|
||||
{t("workspace.surveys.edit.no_hidden_fields_yet_add_first_one_below")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
className="mt-5"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const existingElementIds = elements.map((element) => element.id);
|
||||
const existingEndingCardIds = localSurvey.endings.map((ending) => ending.id);
|
||||
const existingHiddenFieldIds = localSurvey.hiddenFields.fieldIds ?? [];
|
||||
const existingVariableNames = localSurvey.variables.map((v) => v.name);
|
||||
const validateIdError = validateId(
|
||||
hiddenField,
|
||||
existingElementIds,
|
||||
existingEndingCardIds,
|
||||
existingHiddenFieldIds,
|
||||
existingVariableNames
|
||||
);
|
||||
|
||||
if (validateIdError) {
|
||||
toast.error(getValidateIdErrorMessage(validateIdError, "hiddenField", t));
|
||||
return;
|
||||
}
|
||||
|
||||
updateSurvey({
|
||||
fieldIds: [...(localSurvey.hiddenFields?.fieldIds || []), hiddenField],
|
||||
enabled: true,
|
||||
});
|
||||
toast.success(t("workspace.surveys.edit.hidden_field_added_successfully"));
|
||||
setHiddenField("");
|
||||
}}>
|
||||
<Label htmlFor="hiddenField">{t("common.hidden_field")}</Label>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="hiddenField"
|
||||
name="hiddenField"
|
||||
value={hiddenField}
|
||||
onChange={(e) => setHiddenField(e.target.value.trim())}
|
||||
placeholder={t("workspace.surveys.edit.type_field_id") + "..."}
|
||||
/>
|
||||
<Button variant="secondary" type="submit" className="h-10 whitespace-nowrap">
|
||||
{t("workspace.surveys.edit.add_hidden_field_id")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Collapsible.CollapsibleContent>
|
||||
{content}
|
||||
</Collapsible.Root>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,10 +8,12 @@ import { TUserLocale } from "@formbricks/types/user";
|
||||
import { TargetingCard } from "@/modules/ee/contacts/segments/components/targeting-card";
|
||||
import { QuotasCard } from "@/modules/ee/quotas/components/quotas-card";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/workspace-teams/types/team";
|
||||
import { HiddenFieldsCard } from "@/modules/survey/editor/components/hidden-fields-card";
|
||||
import { HowToSendCard } from "@/modules/survey/editor/components/how-to-send-card";
|
||||
import { RecontactOptionsCard } from "@/modules/survey/editor/components/recontact-options-card";
|
||||
import { ResponseOptionsCard } from "@/modules/survey/editor/components/response-options-card";
|
||||
import { SurveyPlacementCard } from "@/modules/survey/editor/components/survey-placement-card";
|
||||
import { SurveyVariablesCard } from "@/modules/survey/editor/components/survey-variables-card";
|
||||
import { TargetingLockedCard } from "@/modules/survey/editor/components/targeting-locked-card";
|
||||
import { WhenToSendCard } from "@/modules/survey/editor/components/when-to-send-card";
|
||||
|
||||
@@ -32,6 +34,9 @@ interface SettingsViewProps {
|
||||
locale: TUserLocale;
|
||||
appSetupCompleted: boolean;
|
||||
enterpriseLicenseRequestFormUrl: string;
|
||||
moveHiddenFieldsToSettingsTab?: boolean;
|
||||
activeElementId?: string | null;
|
||||
setActiveElementId?: (elementId: string | null) => void;
|
||||
}
|
||||
|
||||
export const SettingsView = ({
|
||||
@@ -51,6 +56,9 @@ export const SettingsView = ({
|
||||
locale,
|
||||
appSetupCompleted,
|
||||
enterpriseLicenseRequestFormUrl,
|
||||
moveHiddenFieldsToSettingsTab = false,
|
||||
activeElementId,
|
||||
setActiveElementId,
|
||||
}: SettingsViewProps) => {
|
||||
const isAppSurvey = localSurvey.type === "app";
|
||||
|
||||
@@ -116,6 +124,27 @@ export const SettingsView = ({
|
||||
<RecontactOptionsCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
|
||||
|
||||
{isAppSurvey && <SurveyPlacementCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />}
|
||||
|
||||
{moveHiddenFieldsToSettingsTab && setActiveElementId && (
|
||||
<>
|
||||
<HiddenFieldsCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
setActiveElementId={setActiveElementId}
|
||||
activeElementId={activeElementId ?? null}
|
||||
quotas={quotas}
|
||||
inSettings
|
||||
/>
|
||||
<SurveyVariablesCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
activeElementId={activeElementId ?? null}
|
||||
setActiveElementId={setActiveElementId}
|
||||
quotas={quotas}
|
||||
inSettings
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -50,6 +50,7 @@ interface SurveyEditorProps {
|
||||
quotas: TSurveyQuota[];
|
||||
isExternalUrlsAllowed: boolean;
|
||||
publicDomain: string;
|
||||
moveHiddenFieldsToSettingsTab?: boolean;
|
||||
enterpriseLicenseRequestFormUrl: string;
|
||||
}
|
||||
|
||||
@@ -79,6 +80,7 @@ export const SurveyEditor = ({
|
||||
quotas,
|
||||
isExternalUrlsAllowed,
|
||||
publicDomain,
|
||||
moveHiddenFieldsToSettingsTab = false,
|
||||
enterpriseLicenseRequestFormUrl,
|
||||
}: SurveyEditorProps) => {
|
||||
const [activeView, setActiveView] = useState<TSurveyEditorTabs>("elements");
|
||||
@@ -221,6 +223,7 @@ export const SurveyEditor = ({
|
||||
isStorageConfigured={isStorageConfigured}
|
||||
quotas={quotas}
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
moveHiddenFieldsToSettingsTab={moveHiddenFieldsToSettingsTab}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -269,6 +272,9 @@ export const SurveyEditor = ({
|
||||
locale={locale}
|
||||
appSetupCompleted={localWorkspace.appSetupCompleted}
|
||||
enterpriseLicenseRequestFormUrl={enterpriseLicenseRequestFormUrl}
|
||||
moveHiddenFieldsToSettingsTab={moveHiddenFieldsToSettingsTab}
|
||||
activeElementId={activeElementId}
|
||||
setActiveElementId={setActiveElementId}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { FileDigitIcon } from "lucide-react";
|
||||
import { CheckIcon, FileDigitIcon } from "lucide-react";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurveyQuota } from "@formbricks/types/quota";
|
||||
@@ -17,6 +17,7 @@ interface SurveyVariablesCardProps {
|
||||
activeElementId: string | null;
|
||||
setActiveElementId: (id: string | null) => void;
|
||||
quotas: TSurveyQuota[];
|
||||
inSettings?: boolean;
|
||||
}
|
||||
|
||||
const variablesCardId = `fb-variables-${Date.now()}`;
|
||||
@@ -27,6 +28,7 @@ export const SurveyVariablesCard = ({
|
||||
activeElementId,
|
||||
setActiveElementId,
|
||||
quotas,
|
||||
inSettings = false,
|
||||
}: SurveyVariablesCardProps) => {
|
||||
const open = activeElementId === variablesCardId;
|
||||
const { t } = useTranslation();
|
||||
@@ -41,6 +43,77 @@ export const SurveyVariablesCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
const content = (
|
||||
<Collapsible.CollapsibleContent
|
||||
className={inSettings ? "flex flex-col" : `flex flex-col px-4 ${open && "pb-6"}`}
|
||||
ref={parent}>
|
||||
{inSettings && <hr className="py-1 text-slate-600" />}
|
||||
<div className={cn("flex flex-col gap-2", inSettings ? "p-3" : "")} ref={parent}>
|
||||
{localSurvey.variables.length > 0 ? (
|
||||
localSurvey.variables.map((variable) => (
|
||||
<SurveyVariablesCardItem
|
||||
key={variable.id}
|
||||
mode="edit"
|
||||
variable={variable}
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
quotas={quotas}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="mt-2 text-sm italic text-slate-500">
|
||||
{t("workspace.surveys.edit.no_variables_yet_add_first_one_below")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={inSettings ? "p-3 pt-0" : ""}>
|
||||
<SurveyVariablesCardItem
|
||||
mode="create"
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
quotas={quotas}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{localSurvey.variables.length > 0 && (
|
||||
<div className={cn("mt-6", inSettings ? "p-3 pt-0" : "")}>
|
||||
<OptionIds type="variables" variables={localSurvey.variables} />
|
||||
</div>
|
||||
)}
|
||||
</Collapsible.CollapsibleContent>
|
||||
);
|
||||
|
||||
if (inSettings) {
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
onOpenChange={setOpenState}
|
||||
className={cn(
|
||||
open ? "" : "hover:bg-slate-50",
|
||||
"w-full space-y-2 rounded-lg border border-slate-300 bg-white"
|
||||
)}>
|
||||
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
|
||||
<div className="inline-flex px-4 py-4">
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<CheckIcon
|
||||
strokeWidth={3}
|
||||
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-800">{t("common.variables")}</p>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
{t("workspace.surveys.edit.variables_description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
{content}
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(open ? "shadow-lg" : "shadow-md", "group z-10 flex flex-row rounded-lg bg-white")}>
|
||||
<div
|
||||
@@ -67,39 +140,7 @@ export const SurveyVariablesCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-6"}`} ref={parent}>
|
||||
<div className="flex flex-col gap-2" ref={parent}>
|
||||
{localSurvey.variables.length > 0 ? (
|
||||
localSurvey.variables.map((variable) => (
|
||||
<SurveyVariablesCardItem
|
||||
key={variable.id}
|
||||
mode="edit"
|
||||
variable={variable}
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
quotas={quotas}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="mt-2 text-sm italic text-slate-500">
|
||||
{t("workspace.surveys.edit.no_variables_yet_add_first_one_below")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SurveyVariablesCardItem
|
||||
mode="create"
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
quotas={quotas}
|
||||
/>
|
||||
|
||||
{localSurvey.variables.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<OptionIds type="variables" variables={localSurvey.variables} />
|
||||
</div>
|
||||
)}
|
||||
</Collapsible.CollapsibleContent>
|
||||
{content}
|
||||
</Collapsible.Root>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
UNSPLASH_ACCESS_KEY,
|
||||
} from "@/lib/constants";
|
||||
import { getPublicDomain } from "@/lib/getPublicUrl";
|
||||
import { getPostHogFeatureFlag } from "@/lib/posthog";
|
||||
import { getTranslate } from "@/lingodotdev/server";
|
||||
import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contact-attribute-keys";
|
||||
import { getSegments } from "@/modules/ee/contacts/segments/lib/segments";
|
||||
@@ -92,10 +93,12 @@ export const SurveyEditorPage = async (props: {
|
||||
]);
|
||||
|
||||
const quotas = isQuotasAllowed && survey ? await getQuotas(survey.id) : [];
|
||||
const [workspaceLanguages, teamMemberDetails] = await Promise.all([
|
||||
const [workspaceLanguages, teamMemberDetails, moveHiddenFieldsToSettingsTabFlag] = await Promise.all([
|
||||
getWorkspaceLanguages(workspaceWithTeamIds.id),
|
||||
getTeamMemberDetails(workspaceWithTeamIds.teamIds),
|
||||
getPostHogFeatureFlag(session.user.id, "a-b_survey-editor_move-hidden-fields-to-settings"),
|
||||
]);
|
||||
const moveHiddenFieldsToSettingsTab = moveHiddenFieldsToSettingsTabFlag === "in-settings";
|
||||
|
||||
if (
|
||||
!survey ||
|
||||
@@ -139,6 +142,7 @@ export const SurveyEditorPage = async (props: {
|
||||
isExternalUrlsAllowed={isExternalUrlsAllowed}
|
||||
publicDomain={publicDomain}
|
||||
enterpriseLicenseRequestFormUrl={ENTERPRISE_LICENSE_REQUEST_FORM_URL}
|
||||
moveHiddenFieldsToSettingsTab={moveHiddenFieldsToSettingsTab}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -118,6 +118,26 @@ describe("workspace lib", () => {
|
||||
expectNoFrdSideEffects();
|
||||
});
|
||||
|
||||
test("seeds the default contact attribute keys when creating a workspace", async () => {
|
||||
const createdWorkspace = { ...baseWorkspace, id: "p-defaults" };
|
||||
vi.mocked(prisma.workspace.create).mockResolvedValueOnce(createdWorkspace as any);
|
||||
|
||||
await createWorkspace("org1", { name: "Workspace defaults" });
|
||||
|
||||
const createArgs = vi.mocked(prisma.workspace.create).mock.calls[0][0];
|
||||
const attributeCreate = (createArgs.data as any).contactAttributeKeys.create as Array<{
|
||||
key: string;
|
||||
type: string;
|
||||
isUnique?: boolean;
|
||||
}>;
|
||||
expect(attributeCreate.map((a) => a.key).sort()).toEqual(
|
||||
["email", "firstName", "language", "lastName", "userId"].sort()
|
||||
);
|
||||
expect(attributeCreate.every((a) => a.type === "default")).toBe(true);
|
||||
const uniqueKeys = attributeCreate.filter((a) => a.isUnique).map((a) => a.key);
|
||||
expect(uniqueKeys.sort()).toEqual(["email", "userId"].sort());
|
||||
});
|
||||
|
||||
test("creates workspace without teams and does not auto-link any FRD", async () => {
|
||||
const createdWorkspace = { ...baseWorkspace, id: "p3" };
|
||||
vi.mocked(prisma.workspace.create).mockResolvedValueOnce(createdWorkspace as any);
|
||||
|
||||
@@ -9,6 +9,41 @@ import { TWorkspace, TWorkspaceUpdateInput, ZWorkspaceUpdateInput } from "@formb
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { deleteFilesByWorkspaceId } from "@/modules/storage/service";
|
||||
|
||||
const DEFAULT_CONTACT_ATTRIBUTE_KEYS: Prisma.ContactAttributeKeyCreateWithoutWorkspaceInput[] = [
|
||||
{
|
||||
key: "userId",
|
||||
name: "User Id",
|
||||
description: "The user id of a contact",
|
||||
type: "default",
|
||||
isUnique: true,
|
||||
},
|
||||
{
|
||||
key: "email",
|
||||
name: "Email",
|
||||
description: "The email of a contact",
|
||||
type: "default",
|
||||
isUnique: true,
|
||||
},
|
||||
{
|
||||
key: "firstName",
|
||||
name: "First Name",
|
||||
description: "Your contact's first name",
|
||||
type: "default",
|
||||
},
|
||||
{
|
||||
key: "lastName",
|
||||
name: "Last Name",
|
||||
description: "Your contact's last name",
|
||||
type: "default",
|
||||
},
|
||||
{
|
||||
key: "language",
|
||||
name: "Language",
|
||||
description: "The language preference of a contact",
|
||||
type: "default",
|
||||
},
|
||||
];
|
||||
|
||||
const selectWorkspace = {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
@@ -76,6 +111,9 @@ export const createWorkspace = async (
|
||||
...data,
|
||||
name: workspaceInput.name,
|
||||
organizationId,
|
||||
contactAttributeKeys: {
|
||||
create: DEFAULT_CONTACT_ATTRIBUTE_KEYS,
|
||||
},
|
||||
},
|
||||
select: selectWorkspace,
|
||||
});
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"xm-and-surveys/surveys/website-app-surveys/workspace-id-migration",
|
||||
"xm-and-surveys/surveys/website-app-surveys/framework-guides",
|
||||
"xm-and-surveys/surveys/website-app-surveys/google-tag-manager",
|
||||
"xm-and-surveys/surveys/website-app-surveys/custom-css",
|
||||
{
|
||||
"group": "Features",
|
||||
"icon": "wrench",
|
||||
|
||||
@@ -87,14 +87,14 @@ Action is of the following types:
|
||||
|
||||

|
||||
|
||||
* **Jump to Question**: Skip to a specific question. The user will be redirected to the specified question based on the condition.
|
||||
* **Jump to Block**: Skip to a specific block. The user will be redirected to the specified block based on the condition.
|
||||
|
||||

|
||||
|
||||
* **Save Logic**: Click the `Save` button to save the logic block.
|
||||
|
||||
## Question Logic
|
||||
## Block Logic
|
||||
|
||||
This logic is executed when the user answers the question. Logic can be as simple as showing a follow-up question based on the answer or as complex as calculating a score based on multiple answers.
|
||||
This logic is executed when the user reaches the block. Logic can be as simple as showing a follow-up block based on earlier answers or as complex as calculating a score based on multiple answers.
|
||||
|
||||

|
||||

|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
title: "Custom CSS"
|
||||
description: "Use scoped global CSS to customize Website & App Surveys without breaking your app styles."
|
||||
icon: "palette"
|
||||
---
|
||||
|
||||
Yes, custom CSS for Website & App Surveys is supported.
|
||||
|
||||
Use this when the Styling UI is not enough and you need tighter brand control.
|
||||
|
||||
<Note>
|
||||
Start with the built-in [Styling Theme](/xm-and-surveys/core-features/styling-theme) first. Use custom CSS only for advanced overrides.
|
||||
</Note>
|
||||
|
||||
## Problem
|
||||
|
||||
Website & App Surveys are rendered inside your product, so your app's global CSS can compete with survey styles.
|
||||
|
||||
Common examples:
|
||||
|
||||
- A global rule like `button { ... !important; }` changes survey buttons.
|
||||
- Typography resets change survey text spacing and sizing.
|
||||
- Utility-heavy global styles create unexpected visual differences between pages.
|
||||
|
||||
## Solution
|
||||
|
||||
Scope your overrides to the survey root (`#fbjs`) and use `!important` for the exact targets you want to control.
|
||||
|
||||
This gives you two important guarantees:
|
||||
|
||||
1. Your custom rules apply only to the survey.
|
||||
2. Your rules reliably win against conflicting global styles.
|
||||
|
||||
## Add Scoped Overrides In Global CSS
|
||||
|
||||
<Steps>
|
||||
<Step title="Open your global stylesheet">
|
||||
Use the stylesheet that is loaded on pages where surveys are shown (for example `globals.css`).
|
||||
</Step>
|
||||
<Step title="Add scoped rules under #fbjs">
|
||||
Keep all custom rules prefixed with `#fbjs`.
|
||||
</Step>
|
||||
<Step title="Use !important only where needed">
|
||||
Add `!important` to properties that are still being overridden by your app-wide CSS.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
```css globals.css
|
||||
/* 1) Theme-level variables */
|
||||
#fbjs {
|
||||
--fb-brand-color: #0ea5e9 !important;
|
||||
--fb-brand-text-color: #ffffff !important;
|
||||
--fb-heading-color: #0f172a !important;
|
||||
--fb-subheading-color: #334155 !important;
|
||||
--fb-survey-background-color: #ffffff !important;
|
||||
--fb-border-radius: 12px !important;
|
||||
}
|
||||
|
||||
/* 2) Targeted component overrides */
|
||||
#fbjs .button-custom,
|
||||
#fbjs button.button-custom {
|
||||
border: 1px solid #0284c7 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
#fbjs .label-headline,
|
||||
#fbjs .label-headline * {
|
||||
font-size: 1.125rem !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
#fbjs .bg-input-bg,
|
||||
#fbjs .border-input-border,
|
||||
#fbjs .text-input-text {
|
||||
background: #f8fafc !important;
|
||||
border-color: #cbd5e1 !important;
|
||||
color: #0f172a !important;
|
||||
}
|
||||
```
|
||||
|
||||
<Info>
|
||||
Internal Tailwind utility classes are implementation details and may change over time. Prefer CSS variables and stable survey selectors scoped under `#fbjs`.
|
||||
</Info>
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Keep all survey overrides in one section or file for easier maintenance.
|
||||
- Avoid global selectors without `#fbjs` (for example `button`, `input`, `p`) when styling surveys.
|
||||
- Document why each `!important` exists so future cleanup is easy.
|
||||
- After changes, hard refresh your page to clear cached SDK assets.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**My app styles still win over survey styles**
|
||||
|
||||
- Increase selector specificity under `#fbjs`.
|
||||
- Add `!important` only on the conflicting property.
|
||||
- Check the browser inspector to confirm which rule is winning.
|
||||
|
||||
**Survey styles are affecting the rest of my app**
|
||||
|
||||
- This usually means a selector is missing `#fbjs`.
|
||||
- Prefix every rule with `#fbjs` to keep styles isolated.
|
||||
|
||||
## Survey UI CSS Class Reference
|
||||
|
||||
The following classes are used by `packages/survey-ui` and are safe to target when scoped with `#fbjs`.
|
||||
|
||||
| CSS class | Element(s) it styles | Notes |
|
||||
| --- | --- | --- |
|
||||
| `.button-custom` | Survey action buttons (submit, CTA, navigation buttons with custom variant) | Applies `--fb-button-*` styling tokens. |
|
||||
| `.label-headline` | Question headlines and headline HTML content | Used by `Label` variant `headline`. |
|
||||
| `.label-description` | Question descriptions and helper copy | Used by `Label` variant `description`. |
|
||||
| `.label-default` | Default label text content | Used by `Label` variant `default`. |
|
||||
| `.label-card` | Upper labels (for example, required label text) | Used by `Label` variant `card`. |
|
||||
| `.progress-track` | Progress bar track container | Uses `--fb-progress-track-*` tokens. |
|
||||
| `.progress-indicator` | Progress bar fill indicator | Uses `--fb-progress-indicator-*` tokens. |
|
||||
| `.rounded-input` | Input-like controls (text inputs, dropdown triggers, date inputs, rating/NPS options) | Controls input border radius token. |
|
||||
| `.bg-input-bg` | Input-like control backgrounds | Maps to `--fb-input-bg-color`. |
|
||||
| `.border-input-border` | Input-like control borders | Maps to `--fb-input-border-color`. |
|
||||
| `.text-input` | Input-like text size | Maps to `--fb-input-font-size`. |
|
||||
| `.text-input-text` | Input text and some input icons | Maps to `--fb-input-color`. |
|
||||
| `.text-input-placeholder` | Placeholder and empty-state text | Maps to `--fb-input-placeholder-color`. |
|
||||
| `.font-input` | Input-like font family | Maps to `--fb-input-font-family`. |
|
||||
| `.font-input-weight` | Input-like font weight | Maps to `--fb-input-font-weight`. |
|
||||
| `.w-input` | Input width | Maps to `--fb-input-width`. |
|
||||
| `.min-h-input` | Input minimum height | Maps to `--fb-input-height`. |
|
||||
| `.px-input-x` | Input horizontal padding | Maps to `--fb-input-padding-x`. |
|
||||
| `.py-input-y` | Input vertical padding | Maps to `--fb-input-padding-y`. |
|
||||
| `.shadow-input` | Input shadow | Maps to `--fb-input-shadow`. |
|
||||
| `.rounded-option` | Select/multi-select/ranking/picture-select option containers | Controls option border radius token. |
|
||||
| `.bg-option-bg` | Unselected option backgrounds | Maps to `--fb-option-bg-color`. |
|
||||
| `.bg-option-selected-bg` | Selected option backgrounds | Used for selected states. |
|
||||
| `.bg-option-hover-bg` | Option hover background | Used for hover states. |
|
||||
| `.border-option-border` | Option borders and dropdown search divider | Maps to option border token. |
|
||||
| `.text-option` | Option label font size | Maps to `--fb-option-font-size`. |
|
||||
| `.text-option-label` | Option label text color | Maps to `--fb-option-label-color`. |
|
||||
| `.font-option` | Option label font family | Maps to `--fb-option-font-family`. |
|
||||
| `.font-option-weight` | Option label font weight | Maps to `--fb-option-font-weight`. |
|
||||
| `.px-option-x` | Option horizontal padding | Maps to `--fb-option-padding-x`. |
|
||||
| `.py-option-y` | Option vertical padding | Maps to `--fb-option-padding-y`. |
|
||||
| `.rounded-button` | Button radius (base button component) | Maps to `--fb-button-border-radius`. |
|
||||
| `.text-button` | Button text size | Maps to `--fb-button-font-size`. |
|
||||
| `.font-button-weight` | Button font weight | Maps to `--fb-button-font-weight`. |
|
||||
| `.border-brand` | Selected/active option borders | Uses survey brand color token. |
|
||||
Reference in New Issue
Block a user