mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-09 02:28:38 -05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8629d640b0 | |||
| bb494e8cc1 |
+1
-1
@@ -81,7 +81,7 @@ export const OrganizationBreadcrumb = ({
|
|||||||
getOrganizationsForSwitcherAction({ organizationId: currentOrganizationId }).then((result) => {
|
getOrganizationsForSwitcherAction({ organizationId: currentOrganizationId }).then((result) => {
|
||||||
if (result?.data) {
|
if (result?.data) {
|
||||||
// Sort organizations by name
|
// Sort organizations by name
|
||||||
const sorted = [...result.data].sort((a, b) => a.name.localeCompare(b.name));
|
const sorted = result.data.toSorted((a, b) => a.name.localeCompare(b.name));
|
||||||
setOrganizations(sorted);
|
setOrganizations(sorted);
|
||||||
} else {
|
} else {
|
||||||
// Handle server errors or validation errors
|
// Handle server errors or validation errors
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export const ProjectBreadcrumb = ({
|
|||||||
getProjectsForSwitcherAction({ organizationId: currentOrganizationId }).then((result) => {
|
getProjectsForSwitcherAction({ organizationId: currentOrganizationId }).then((result) => {
|
||||||
if (result?.data) {
|
if (result?.data) {
|
||||||
// Sort projects by name
|
// Sort projects by name
|
||||||
const sorted = [...result.data].sort((a, b) => a.name.localeCompare(b.name));
|
const sorted = result.data.toSorted((a, b) => a.name.localeCompare(b.name));
|
||||||
setProjects(sorted);
|
setProjects(sorted);
|
||||||
} else {
|
} else {
|
||||||
// Handle server errors or validation errors
|
// Handle server errors or validation errors
|
||||||
|
|||||||
-29
@@ -4,7 +4,6 @@ import { revalidatePath } from "next/cache";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { ZResponseFilterCriteria } from "@formbricks/types/responses";
|
import { ZResponseFilterCriteria } from "@formbricks/types/responses";
|
||||||
import { getDisplaysBySurveyIdWithContact } from "@/lib/display/service";
|
|
||||||
import { getResponseCountBySurveyId, getResponses } from "@/lib/response/service";
|
import { getResponseCountBySurveyId, getResponses } from "@/lib/response/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||||
@@ -107,31 +106,3 @@ export const getResponseCountAction = authenticatedActionClient
|
|||||||
|
|
||||||
return getResponseCountBySurveyId(parsedInput.surveyId, parsedInput.filterCriteria);
|
return getResponseCountBySurveyId(parsedInput.surveyId, parsedInput.filterCriteria);
|
||||||
});
|
});
|
||||||
|
|
||||||
const ZGetDisplaysWithContactAction = z.object({
|
|
||||||
surveyId: ZId,
|
|
||||||
limit: z.number().int().min(1).max(100),
|
|
||||||
offset: z.number().int().nonnegative(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getDisplaysWithContactAction = authenticatedActionClient
|
|
||||||
.schema(ZGetDisplaysWithContactAction)
|
|
||||||
.action(async ({ ctx, parsedInput }) => {
|
|
||||||
await checkAuthorizationUpdated({
|
|
||||||
userId: ctx.user.id,
|
|
||||||
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
|
||||||
access: [
|
|
||||||
{
|
|
||||||
type: "organization",
|
|
||||||
roles: ["owner", "manager"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "projectTeam",
|
|
||||||
minPermission: "read",
|
|
||||||
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
return getDisplaysBySurveyIdWithContact(parsedInput.surveyId, parsedInput.limit, parsedInput.offset);
|
|
||||||
});
|
|
||||||
|
|||||||
-125
@@ -1,125 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { AlertCircleIcon, InfoIcon } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { TDisplayWithContact } from "@formbricks/types/displays";
|
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
|
||||||
import { timeSince } from "@/lib/time";
|
|
||||||
import { Button } from "@/modules/ui/components/button";
|
|
||||||
|
|
||||||
interface SummaryImpressionsProps {
|
|
||||||
displays: TDisplayWithContact[];
|
|
||||||
isLoading: boolean;
|
|
||||||
hasMore: boolean;
|
|
||||||
displaysError: string | null;
|
|
||||||
environmentId: string;
|
|
||||||
locale: TUserLocale;
|
|
||||||
onLoadMore: () => void;
|
|
||||||
onRetry: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDisplayContactIdentifier = (display: TDisplayWithContact): string => {
|
|
||||||
if (!display.contact) return "";
|
|
||||||
return display.contact.attributes?.email || display.contact.attributes?.userId || display.contact.id;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SummaryImpressions = ({
|
|
||||||
displays,
|
|
||||||
isLoading,
|
|
||||||
hasMore,
|
|
||||||
displaysError,
|
|
||||||
environmentId,
|
|
||||||
locale,
|
|
||||||
onLoadMore,
|
|
||||||
onRetry,
|
|
||||||
}: SummaryImpressionsProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const renderContent = () => {
|
|
||||||
if (displaysError) {
|
|
||||||
return (
|
|
||||||
<div className="p-8">
|
|
||||||
<div className="flex flex-col items-center gap-4 text-center">
|
|
||||||
<div className="flex items-center gap-2 text-red-600">
|
|
||||||
<AlertCircleIcon className="h-5 w-5" />
|
|
||||||
<span className="text-sm font-medium">{t("common.error_loading_data")}</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-slate-500">{displaysError}</p>
|
|
||||||
<Button onClick={onRetry} variant="secondary" size="sm">
|
|
||||||
{t("common.try_again")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (displays.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="p-8 text-center text-sm text-slate-500">
|
|
||||||
{t("environments.surveys.summary.no_identified_impressions")}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="grid min-h-10 grid-cols-4 items-center border-b border-slate-200 bg-slate-100 text-sm font-semibold text-slate-600">
|
|
||||||
<div className="col-span-2 px-4 md:px-6">{t("common.user")}</div>
|
|
||||||
<div className="col-span-2 px-4 md:px-6">{t("environments.contacts.survey_viewed_at")}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-h-[62vh] overflow-y-auto">
|
|
||||||
{displays.map((display) => (
|
|
||||||
<div
|
|
||||||
key={display.id}
|
|
||||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-xs text-slate-800 last:border-transparent md:text-sm">
|
|
||||||
<div className="col-span-2 pl-4 md:pl-6">
|
|
||||||
{display.contact ? (
|
|
||||||
<Link
|
|
||||||
className="ph-no-capture break-all text-slate-600 hover:underline"
|
|
||||||
href={`/environments/${environmentId}/contacts/${display.contact.id}`}>
|
|
||||||
{getDisplayContactIdentifier(display)}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span className="break-all text-slate-600">{t("common.anonymous")}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2 px-4 text-slate-500 md:px-6">
|
|
||||||
{timeSince(display.createdAt.toString(), locale)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasMore && (
|
|
||||||
<div className="flex justify-center border-t border-slate-100 py-4">
|
|
||||||
<Button onClick={onLoadMore} variant="secondary" size="sm">
|
|
||||||
{t("common.load_more")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white p-8 shadow-sm">
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<div className="h-6 w-32 animate-pulse rounded-full bg-slate-200"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
|
||||||
<div className="flex items-center gap-2 rounded-t-xl border-b border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
|
|
||||||
<InfoIcon className="h-4 w-4 shrink-0" />
|
|
||||||
<span>{t("environments.surveys.summary.impressions_identified_only")}</span>
|
|
||||||
</div>
|
|
||||||
{renderContent()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+4
-8
@@ -10,8 +10,8 @@ interface SummaryMetadataProps {
|
|||||||
surveySummary: TSurveySummary["meta"];
|
surveySummary: TSurveySummary["meta"];
|
||||||
quotasCount: number;
|
quotasCount: number;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
tab: "dropOffs" | "quotas" | "impressions" | undefined;
|
tab: "dropOffs" | "quotas" | undefined;
|
||||||
setTab: React.Dispatch<React.SetStateAction<"dropOffs" | "quotas" | "impressions" | undefined>>;
|
setTab: React.Dispatch<React.SetStateAction<"dropOffs" | "quotas" | undefined>>;
|
||||||
isQuotasAllowed: boolean;
|
isQuotasAllowed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export const SummaryMetadata = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dropoffCountValue = dropOffCount === 0 ? <span>-</span> : dropOffCount;
|
const dropoffCountValue = dropOffCount === 0 ? <span>-</span> : dropOffCount;
|
||||||
|
|
||||||
const handleTabChange = (val: "dropOffs" | "quotas" | "impressions") => {
|
const handleTabChange = (val: "dropOffs" | "quotas") => {
|
||||||
const change = tab === val ? undefined : val;
|
const change = tab === val ? undefined : val;
|
||||||
setTab(change);
|
setTab(change);
|
||||||
};
|
};
|
||||||
@@ -65,16 +65,12 @@ export const SummaryMetadata = ({
|
|||||||
`grid gap-4 sm:grid-cols-2 md:grid-cols-3 md:gap-x-2 lg:grid-cols-3 2xl:grid-cols-5`,
|
`grid gap-4 sm:grid-cols-2 md:grid-cols-3 md:gap-x-2 lg:grid-cols-3 2xl:grid-cols-5`,
|
||||||
isQuotasAllowed && quotasCount > 0 && "2xl:grid-cols-6"
|
isQuotasAllowed && quotasCount > 0 && "2xl:grid-cols-6"
|
||||||
)}>
|
)}>
|
||||||
<InteractiveCard
|
<StatCard
|
||||||
key="impressions"
|
|
||||||
tab="impressions"
|
|
||||||
label={t("environments.surveys.summary.impressions")}
|
label={t("environments.surveys.summary.impressions")}
|
||||||
percentage={null}
|
percentage={null}
|
||||||
value={displayCount === 0 ? <span>-</span> : displayCount}
|
value={displayCount === 0 ? <span>-</span> : displayCount}
|
||||||
tooltipText={t("environments.surveys.summary.impressions_tooltip")}
|
tooltipText={t("environments.surveys.summary.impressions_tooltip")}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onClick={() => handleTabChange("impressions")}
|
|
||||||
isActive={tab === "impressions"}
|
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label={t("environments.surveys.summary.starts")}
|
label={t("environments.surveys.summary.starts")}
|
||||||
|
|||||||
+3
-84
@@ -1,31 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { TDisplayWithContact } from "@formbricks/types/displays";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
|
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
import {
|
import { getSurveySummaryAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||||
getDisplaysWithContactAction,
|
|
||||||
getSurveySummaryAction,
|
|
||||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
|
||||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/response-filter-context";
|
||||||
import ScrollToTop from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop";
|
import ScrollToTop from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ScrollToTop";
|
||||||
import { SummaryDropOffs } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs";
|
import { SummaryDropOffs } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs";
|
||||||
import { SummaryImpressions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryImpressions";
|
|
||||||
import { CustomFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter";
|
import { CustomFilter } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter";
|
||||||
import { getFormattedFilters } from "@/app/lib/surveys/surveys";
|
import { getFormattedFilters } from "@/app/lib/surveys/surveys";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
|
||||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||||
import { QuotasSummary } from "@/modules/ee/quotas/components/quotas-summary";
|
import { QuotasSummary } from "@/modules/ee/quotas/components/quotas-summary";
|
||||||
import { SummaryList } from "./SummaryList";
|
import { SummaryList } from "./SummaryList";
|
||||||
import { SummaryMetadata } from "./SummaryMetadata";
|
import { SummaryMetadata } from "./SummaryMetadata";
|
||||||
|
|
||||||
const DISPLAYS_PER_PAGE = 15;
|
|
||||||
|
|
||||||
const defaultSurveySummary: TSurveySummary = {
|
const defaultSurveySummary: TSurveySummary = {
|
||||||
meta: {
|
meta: {
|
||||||
completedPercentage: 0,
|
completedPercentage: 0,
|
||||||
@@ -61,76 +51,17 @@ export const SummaryPage = ({
|
|||||||
initialSurveySummary,
|
initialSurveySummary,
|
||||||
isQuotasAllowed,
|
isQuotasAllowed,
|
||||||
}: SummaryPageProps) => {
|
}: SummaryPageProps) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const [surveySummary, setSurveySummary] = useState<TSurveySummary>(
|
const [surveySummary, setSurveySummary] = useState<TSurveySummary>(
|
||||||
initialSurveySummary || defaultSurveySummary
|
initialSurveySummary || defaultSurveySummary
|
||||||
);
|
);
|
||||||
|
|
||||||
const [tab, setTab] = useState<"dropOffs" | "quotas" | "impressions" | undefined>(undefined);
|
const [tab, setTab] = useState<"dropOffs" | "quotas" | undefined>(undefined);
|
||||||
const [isLoading, setIsLoading] = useState(!initialSurveySummary);
|
const [isLoading, setIsLoading] = useState(!initialSurveySummary);
|
||||||
|
|
||||||
const { selectedFilter, dateRange, resetState } = useResponseFilter();
|
const { selectedFilter, dateRange, resetState } = useResponseFilter();
|
||||||
|
|
||||||
const [displays, setDisplays] = useState<TDisplayWithContact[]>([]);
|
|
||||||
const [isDisplaysLoading, setIsDisplaysLoading] = useState(false);
|
|
||||||
const [hasMoreDisplays, setHasMoreDisplays] = useState(true);
|
|
||||||
const [displaysError, setDisplaysError] = useState<string | null>(null);
|
|
||||||
const displaysFetchedRef = useRef(false);
|
|
||||||
|
|
||||||
const fetchDisplays = useCallback(
|
|
||||||
async (offset: number) => {
|
|
||||||
const response = await getDisplaysWithContactAction({
|
|
||||||
surveyId,
|
|
||||||
limit: DISPLAYS_PER_PAGE,
|
|
||||||
offset,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response?.data) {
|
|
||||||
const errorMessage = getFormattedErrorMessage(response);
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response?.data ?? [];
|
|
||||||
},
|
|
||||||
[surveyId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadInitialDisplays = useCallback(async () => {
|
|
||||||
setIsDisplaysLoading(true);
|
|
||||||
setDisplaysError(null);
|
|
||||||
try {
|
|
||||||
const data = await fetchDisplays(0);
|
|
||||||
setDisplays(data);
|
|
||||||
setHasMoreDisplays(data.length === DISPLAYS_PER_PAGE);
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(error);
|
|
||||||
setDisplays([]);
|
|
||||||
setHasMoreDisplays(false);
|
|
||||||
} finally {
|
|
||||||
setIsDisplaysLoading(false);
|
|
||||||
}
|
|
||||||
}, [fetchDisplays, t]);
|
|
||||||
|
|
||||||
const handleLoadMoreDisplays = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const data = await fetchDisplays(displays.length);
|
|
||||||
setDisplays((prev) => [...prev, ...data]);
|
|
||||||
setHasMoreDisplays(data.length === DISPLAYS_PER_PAGE);
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : t("common.something_went_wrong");
|
|
||||||
toast.error(errorMessage);
|
|
||||||
}
|
|
||||||
}, [fetchDisplays, displays.length, t]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (tab === "impressions" && !displaysFetchedRef.current) {
|
|
||||||
displaysFetchedRef.current = true;
|
|
||||||
loadInitialDisplays();
|
|
||||||
}
|
|
||||||
}, [tab, loadInitialDisplays]);
|
|
||||||
|
|
||||||
// Only fetch data when filters change or when there's no initial data
|
// Only fetch data when filters change or when there's no initial data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If we have initial data and no filters are applied, don't fetch
|
// If we have initial data and no filters are applied, don't fetch
|
||||||
@@ -190,18 +121,6 @@ export const SummaryPage = ({
|
|||||||
setTab={setTab}
|
setTab={setTab}
|
||||||
isQuotasAllowed={isQuotasAllowed}
|
isQuotasAllowed={isQuotasAllowed}
|
||||||
/>
|
/>
|
||||||
{tab === "impressions" && (
|
|
||||||
<SummaryImpressions
|
|
||||||
displays={displays}
|
|
||||||
isLoading={isDisplaysLoading}
|
|
||||||
hasMore={hasMoreDisplays}
|
|
||||||
displaysError={displaysError}
|
|
||||||
environmentId={environment.id}
|
|
||||||
locale={locale}
|
|
||||||
onLoadMore={handleLoadMoreDisplays}
|
|
||||||
onRetry={loadInitialDisplays}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{tab === "dropOffs" && <SummaryDropOffs dropOff={surveySummary.dropOff} survey={surveyMemoized} />}
|
{tab === "dropOffs" && <SummaryDropOffs dropOff={surveySummary.dropOff} survey={surveyMemoized} />}
|
||||||
{isQuotasAllowed && tab === "quotas" && <QuotasSummary quotas={surveySummary.quotas} />}
|
{isQuotasAllowed && tab === "quotas" && <QuotasSummary quotas={surveySummary.quotas} />}
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
|
|||||||
+2
-2
@@ -4,9 +4,9 @@ import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
|||||||
import { BaseCard } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/base-card";
|
import { BaseCard } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/base-card";
|
||||||
|
|
||||||
interface InteractiveCardProps {
|
interface InteractiveCardProps {
|
||||||
tab: "dropOffs" | "quotas" | "impressions";
|
tab: "dropOffs" | "quotas";
|
||||||
label: string;
|
label: string;
|
||||||
percentage: number | null;
|
percentage: number;
|
||||||
value: React.ReactNode;
|
value: React.ReactNode;
|
||||||
tooltipText: string;
|
tooltipText: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
|||||||
+1
-1
@@ -352,7 +352,7 @@ export const AnonymousLinksTab = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("environments.surveys.share.anonymous_links.custom_start_point"),
|
title: t("environments.surveys.share.anonymous_links.custom_start_point"),
|
||||||
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/start-at-block",
|
href: "https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/start-at-question",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+2
-2
@@ -241,7 +241,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
|||||||
<Popover open={isOpen} onOpenChange={handleOpenChange}>
|
<Popover open={isOpen} onOpenChange={handleOpenChange}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<PopoverTriggerButton isOpen={isOpen}>
|
<PopoverTriggerButton isOpen={isOpen}>
|
||||||
{t("common.filter")} <b>{activeFilterCount > 0 && `(${activeFilterCount})`}</b>
|
Filter <b>{activeFilterCount > 0 && `(${activeFilterCount})`}</b>
|
||||||
</PopoverTriggerButton>
|
</PopoverTriggerButton>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
@@ -329,7 +329,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
|||||||
</div>
|
</div>
|
||||||
{i !== filterValue.filter.length - 1 && (
|
{i !== filterValue.filter.length - 1 && (
|
||||||
<div className="my-4 flex items-center">
|
<div className="my-4 flex items-center">
|
||||||
<p className="mr-4 font-semibold text-slate-800">{t("common.and")}</p>
|
<p className="mr-4 font-semibold text-slate-800">and</p>
|
||||||
<hr className="w-full text-slate-600" />
|
<hr className="w-full text-slate-600" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "@formbricks/types/integration/slack";
|
} from "@formbricks/types/integration/slack";
|
||||||
import { responses } from "@/app/lib/api/response";
|
import { responses } from "@/app/lib/api/response";
|
||||||
import { TSessionAuthentication, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
import { TSessionAuthentication, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||||
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_REDIRECT_URI, WEBAPP_URL } from "@/lib/constants";
|
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@/lib/constants";
|
||||||
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
|
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
|
||||||
import { createOrUpdateIntegration, getIntegrationByType } from "@/lib/integration/service";
|
import { createOrUpdateIntegration, getIntegrationByType } from "@/lib/integration/service";
|
||||||
|
|
||||||
@@ -56,7 +56,6 @@ export const GET = withV1ApiWrapper({
|
|||||||
code,
|
code,
|
||||||
client_id: SLACK_CLIENT_ID,
|
client_id: SLACK_CLIENT_ID,
|
||||||
client_secret: SLACK_CLIENT_SECRET,
|
client_secret: SLACK_CLIENT_SECRET,
|
||||||
redirect_uri: SLACK_REDIRECT_URI,
|
|
||||||
};
|
};
|
||||||
const formBody: string[] = [];
|
const formBody: string[] = [];
|
||||||
for (const property in formData) {
|
for (const property in formData) {
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ checksums:
|
|||||||
common/error: 3c95bcb32c2104b99a46f5b3dd015248
|
common/error: 3c95bcb32c2104b99a46f5b3dd015248
|
||||||
common/error_component_description: fa9eee04f864c3fe6e6681f716caa015
|
common/error_component_description: fa9eee04f864c3fe6e6681f716caa015
|
||||||
common/error_component_title: ae68fa341a143aaa13a5ea30dd57a63e
|
common/error_component_title: ae68fa341a143aaa13a5ea30dd57a63e
|
||||||
common/error_loading_data: aaeffbfe4a2c2145442a57de524494be
|
|
||||||
common/error_rate_limit_description: 37791a33a947204662ee9c6544e90f51
|
common/error_rate_limit_description: 37791a33a947204662ee9c6544e90f51
|
||||||
common/error_rate_limit_title: 23ac9419e267e610e1bfd38e1dc35dc0
|
common/error_rate_limit_title: 23ac9419e267e610e1bfd38e1dc35dc0
|
||||||
common/expand_rows: b6e06327cb8718dfd6651720843e4dad
|
common/expand_rows: b6e06327cb8718dfd6651720843e4dad
|
||||||
@@ -199,7 +198,6 @@ checksums:
|
|||||||
common/failed_to_load_organizations: 512808a2b674c7c28bca73f8f91fd87e
|
common/failed_to_load_organizations: 512808a2b674c7c28bca73f8f91fd87e
|
||||||
common/failed_to_load_workspaces: 6ee3448097394517dc605074cd4e6ea4
|
common/failed_to_load_workspaces: 6ee3448097394517dc605074cd4e6ea4
|
||||||
common/finish: ffa7a10f71182b48fefed7135bee24fa
|
common/finish: ffa7a10f71182b48fefed7135bee24fa
|
||||||
common/first_name: cf040a5d6a9fd696be400380cc99f54b
|
|
||||||
common/follow_these: 3a730b242bb17a3f95e01bf0dae86885
|
common/follow_these: 3a730b242bb17a3f95e01bf0dae86885
|
||||||
common/formbricks_version: d9967c797f3e49ca0cae78bc0ebd19cb
|
common/formbricks_version: d9967c797f3e49ca0cae78bc0ebd19cb
|
||||||
common/full_name: f45991923345e8322c9ff8cd6b7e2b16
|
common/full_name: f45991923345e8322c9ff8cd6b7e2b16
|
||||||
@@ -212,7 +210,6 @@ checksums:
|
|||||||
common/hidden_field: 3ed5c58d0ed359e558cdf7bd33606d2d
|
common/hidden_field: 3ed5c58d0ed359e558cdf7bd33606d2d
|
||||||
common/hidden_fields: 3de6cfd308293a826cb8679fd1d49972
|
common/hidden_fields: 3de6cfd308293a826cb8679fd1d49972
|
||||||
common/hide_column: 23ce94db148f2d8e4a0923defead6cf1
|
common/hide_column: 23ce94db148f2d8e4a0923defead6cf1
|
||||||
common/id: c8886d38aeea2ed5f785aba4fc96784b
|
|
||||||
common/image: 048ba7a239de0fbd883ade8558415830
|
common/image: 048ba7a239de0fbd883ade8558415830
|
||||||
common/images: 9305827c28694866f49db42b4c51831f
|
common/images: 9305827c28694866f49db42b4c51831f
|
||||||
common/import: 348b8ab981de5b7f1fca6d7302263bbd
|
common/import: 348b8ab981de5b7f1fca6d7302263bbd
|
||||||
@@ -230,7 +227,6 @@ checksums:
|
|||||||
common/key: 3d1065ab98a1c2f1210507fd5c7bf515
|
common/key: 3d1065ab98a1c2f1210507fd5c7bf515
|
||||||
common/label: a5c71bf158481233f8215dbd38cc196b
|
common/label: a5c71bf158481233f8215dbd38cc196b
|
||||||
common/language: 277fd1a41cc237a437cd1d5e4a80463b
|
common/language: 277fd1a41cc237a437cd1d5e4a80463b
|
||||||
common/last_name: 2c9a7de7738ca007ba9023c385149c26
|
|
||||||
common/learn_more: e598091d132f890c37a6d4ed94f6d794
|
common/learn_more: e598091d132f890c37a6d4ed94f6d794
|
||||||
common/license_expired: 7af13535e320e4197989472c01387d2c
|
common/license_expired: 7af13535e320e4197989472c01387d2c
|
||||||
common/light_overlay: 0499907ea7b8405f4267b117998b5a78
|
common/light_overlay: 0499907ea7b8405f4267b117998b5a78
|
||||||
@@ -405,7 +401,6 @@ checksums:
|
|||||||
common/top_right: 241f95c923846911aaf13af6109333e5
|
common/top_right: 241f95c923846911aaf13af6109333e5
|
||||||
common/try_again: 33dd8820e743e35a66e6977f69e9d3b5
|
common/try_again: 33dd8820e743e35a66e6977f69e9d3b5
|
||||||
common/type: f04471a7ddac844b9ad145eb9911ef75
|
common/type: f04471a7ddac844b9ad145eb9911ef75
|
||||||
common/unknown_survey: dd8f6985e17ccf19fac1776e18b2c498
|
|
||||||
common/unlock_more_workspaces_with_a_higher_plan: fe1590075b855bb4306c9388b65143b0
|
common/unlock_more_workspaces_with_a_higher_plan: fe1590075b855bb4306c9388b65143b0
|
||||||
common/update: 079fc039262fd31b10532929685c2d1b
|
common/update: 079fc039262fd31b10532929685c2d1b
|
||||||
common/updated: 8aa8ff2dc2977ca4b269e80a513100b4
|
common/updated: 8aa8ff2dc2977ca4b269e80a513100b4
|
||||||
@@ -624,7 +619,6 @@ checksums:
|
|||||||
environments/contacts/delete_attribute_confirmation: 01d99b89eb3d27ff468d0db1b4aeb394
|
environments/contacts/delete_attribute_confirmation: 01d99b89eb3d27ff468d0db1b4aeb394
|
||||||
environments/contacts/delete_contact_confirmation: 2d45579e0bb4bc40fb1ee75b43c0e7a4
|
environments/contacts/delete_contact_confirmation: 2d45579e0bb4bc40fb1ee75b43c0e7a4
|
||||||
environments/contacts/delete_contact_confirmation_with_quotas: d3d17f13ae46ce04c126c82bf01299ac
|
environments/contacts/delete_contact_confirmation_with_quotas: d3d17f13ae46ce04c126c82bf01299ac
|
||||||
environments/contacts/displays: fcc4527002bd045021882be463b8ac72
|
|
||||||
environments/contacts/edit_attribute: 92a83c96a5d850e7d39002e8fd5898f4
|
environments/contacts/edit_attribute: 92a83c96a5d850e7d39002e8fd5898f4
|
||||||
environments/contacts/edit_attribute_description: 073a3084bb2f3b34ed1320ed1cd6db3c
|
environments/contacts/edit_attribute_description: 073a3084bb2f3b34ed1320ed1cd6db3c
|
||||||
environments/contacts/edit_attribute_values: 44e4e7a661cc1b59200bb07c710072a7
|
environments/contacts/edit_attribute_values: 44e4e7a661cc1b59200bb07c710072a7
|
||||||
@@ -636,7 +630,6 @@ checksums:
|
|||||||
environments/contacts/invalid_csv_column_names: dcb8534e7d4c00b9ea7bdaf389f72328
|
environments/contacts/invalid_csv_column_names: dcb8534e7d4c00b9ea7bdaf389f72328
|
||||||
environments/contacts/invalid_date_format: 5bad9730ac5a5bacd0792098f712b1c4
|
environments/contacts/invalid_date_format: 5bad9730ac5a5bacd0792098f712b1c4
|
||||||
environments/contacts/invalid_number_format: bd0422507385f671c3046730a6febc64
|
environments/contacts/invalid_number_format: bd0422507385f671c3046730a6febc64
|
||||||
environments/contacts/no_activity_yet: f88897ac05afd6bf8af0d4834ad24ffc
|
|
||||||
environments/contacts/no_published_link_surveys_available: 9c1abc5b21aba827443cdf87dd6c8bfe
|
environments/contacts/no_published_link_surveys_available: 9c1abc5b21aba827443cdf87dd6c8bfe
|
||||||
environments/contacts/no_published_surveys: bd945b0e2e2328c17615c94143bdd62b
|
environments/contacts/no_published_surveys: bd945b0e2e2328c17615c94143bdd62b
|
||||||
environments/contacts/no_responses_found: f10190cffdda4ca1bed479acbb89b13f
|
environments/contacts/no_responses_found: f10190cffdda4ca1bed479acbb89b13f
|
||||||
@@ -651,8 +644,6 @@ checksums:
|
|||||||
environments/contacts/select_a_survey: 1f49086dfb874307aae1136e88c3d514
|
environments/contacts/select_a_survey: 1f49086dfb874307aae1136e88c3d514
|
||||||
environments/contacts/select_attribute: d93fb60eb4fbb42bf13a22f6216fbd79
|
environments/contacts/select_attribute: d93fb60eb4fbb42bf13a22f6216fbd79
|
||||||
environments/contacts/select_attribute_key: 673a6683fab41b387d921841cded7e38
|
environments/contacts/select_attribute_key: 673a6683fab41b387d921841cded7e38
|
||||||
environments/contacts/survey_viewed: 646d413218626787b0373ffd71cb7451
|
|
||||||
environments/contacts/survey_viewed_at: 2ab535237af5c3c3f33acc792a7e70a4
|
|
||||||
environments/contacts/system_attributes: eadb6a8888c7b32c0e68881f945ae9b6
|
environments/contacts/system_attributes: eadb6a8888c7b32c0e68881f945ae9b6
|
||||||
environments/contacts/unlock_contacts_description: c5572047f02b4c39e5109f9de715499d
|
environments/contacts/unlock_contacts_description: c5572047f02b4c39e5109f9de715499d
|
||||||
environments/contacts/unlock_contacts_title: a8b3d7db03eb404d9267fd5cdd6d5ddb
|
environments/contacts/unlock_contacts_title: a8b3d7db03eb404d9267fd5cdd6d5ddb
|
||||||
@@ -1859,7 +1850,6 @@ checksums:
|
|||||||
environments/surveys/summary/filtered_responses_excel: 06e57bae9e41979fd7fc4b8bfe3466f9
|
environments/surveys/summary/filtered_responses_excel: 06e57bae9e41979fd7fc4b8bfe3466f9
|
||||||
environments/surveys/summary/generating_qr_code: 5026d4a76f995db458195e5215d9bbd9
|
environments/surveys/summary/generating_qr_code: 5026d4a76f995db458195e5215d9bbd9
|
||||||
environments/surveys/summary/impressions: 7fe38d42d68a64d3fd8436a063751584
|
environments/surveys/summary/impressions: 7fe38d42d68a64d3fd8436a063751584
|
||||||
environments/surveys/summary/impressions_identified_only: 10f8c491463c73b8e6534314ee00d165
|
|
||||||
environments/surveys/summary/impressions_tooltip: 4d0823cbf360304770c7c5913e33fdc8
|
environments/surveys/summary/impressions_tooltip: 4d0823cbf360304770c7c5913e33fdc8
|
||||||
environments/surveys/summary/in_app/connection_description: 9710bbf8048a8a5c3b2b56db9d946b73
|
environments/surveys/summary/in_app/connection_description: 9710bbf8048a8a5c3b2b56db9d946b73
|
||||||
environments/surveys/summary/in_app/connection_title: 29e8a40ad6a7fdb5af5ee9451a70a9aa
|
environments/surveys/summary/in_app/connection_title: 29e8a40ad6a7fdb5af5ee9451a70a9aa
|
||||||
@@ -1900,7 +1890,6 @@ checksums:
|
|||||||
environments/surveys/summary/last_quarter: 2e565a81de9b3d7b1ee709ebb6f6eda1
|
environments/surveys/summary/last_quarter: 2e565a81de9b3d7b1ee709ebb6f6eda1
|
||||||
environments/surveys/summary/last_year: fe7c268a48bf85bc40da000e6e437637
|
environments/surveys/summary/last_year: fe7c268a48bf85bc40da000e6e437637
|
||||||
environments/surveys/summary/limit: 347051f1a068e01e8c4e4f6744d8e727
|
environments/surveys/summary/limit: 347051f1a068e01e8c4e4f6744d8e727
|
||||||
environments/surveys/summary/no_identified_impressions: c3bc42e6feb9010ced905ded51c5afc4
|
|
||||||
environments/surveys/summary/no_responses_found: f10190cffdda4ca1bed479acbb89b13f
|
environments/surveys/summary/no_responses_found: f10190cffdda4ca1bed479acbb89b13f
|
||||||
environments/surveys/summary/other_values_found: 48a74ee68c05f7fb162072b50c683b6a
|
environments/surveys/summary/other_values_found: 48a74ee68c05f7fb162072b50c683b6a
|
||||||
environments/surveys/summary/overall: 6c6d6533013d4739766af84b2871bca6
|
environments/surveys/summary/overall: 6c6d6533013d4739766af84b2871bca6
|
||||||
|
|||||||
@@ -63,8 +63,7 @@ export const INVITE_DISABLED = env.INVITE_DISABLED === "1";
|
|||||||
|
|
||||||
export const SLACK_CLIENT_SECRET = env.SLACK_CLIENT_SECRET;
|
export const SLACK_CLIENT_SECRET = env.SLACK_CLIENT_SECRET;
|
||||||
export const SLACK_CLIENT_ID = env.SLACK_CLIENT_ID;
|
export const SLACK_CLIENT_ID = env.SLACK_CLIENT_ID;
|
||||||
export const SLACK_REDIRECT_URI = `${WEBAPP_URL}/api/v1/integrations/slack/callback`;
|
export const SLACK_AUTH_URL = `https://slack.com/oauth/v2/authorize?client_id=${env.SLACK_CLIENT_ID}&scope=channels:read,chat:write,chat:write.public,chat:write.customize,groups:read`;
|
||||||
export const SLACK_AUTH_URL = `https://slack.com/oauth/v2/authorize?client_id=${env.SLACK_CLIENT_ID}&scope=channels:read,chat:write,chat:write.public,chat:write.customize,groups:read&redirect_uri=${SLACK_REDIRECT_URI}`;
|
|
||||||
|
|
||||||
export const GOOGLE_SHEETS_CLIENT_ID = env.GOOGLE_SHEETS_CLIENT_ID;
|
export const GOOGLE_SHEETS_CLIENT_ID = env.GOOGLE_SHEETS_CLIENT_ID;
|
||||||
export const GOOGLE_SHEETS_CLIENT_SECRET = env.GOOGLE_SHEETS_CLIENT_SECRET;
|
export const GOOGLE_SHEETS_CLIENT_SECRET = env.GOOGLE_SHEETS_CLIENT_SECRET;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { z } from "zod";
|
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { TDisplay, TDisplayFilters, TDisplayWithContact } from "@formbricks/types/displays";
|
import { TDisplay, TDisplayFilters } from "@formbricks/types/displays";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
import { validateInputs } from "../utils/validate";
|
import { validateInputs } from "../utils/validate";
|
||||||
|
|
||||||
@@ -24,12 +23,13 @@ export const getDisplayCountBySurveyId = reactCache(
|
|||||||
const displayCount = await prisma.display.count({
|
const displayCount = await prisma.display.count({
|
||||||
where: {
|
where: {
|
||||||
surveyId: surveyId,
|
surveyId: surveyId,
|
||||||
...(filters?.createdAt && {
|
...(filters &&
|
||||||
createdAt: {
|
filters.createdAt && {
|
||||||
gte: filters.createdAt.min,
|
createdAt: {
|
||||||
lte: filters.createdAt.max,
|
gte: filters.createdAt.min,
|
||||||
},
|
lte: filters.createdAt.max,
|
||||||
}),
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return displayCount;
|
return displayCount;
|
||||||
@@ -42,97 +42,6 @@ export const getDisplayCountBySurveyId = reactCache(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getDisplaysByContactId = reactCache(
|
|
||||||
async (contactId: string): Promise<Pick<TDisplay, "id" | "createdAt" | "surveyId">[]> => {
|
|
||||||
validateInputs([contactId, ZId]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const displays = await prisma.display.findMany({
|
|
||||||
where: { contactId },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
surveyId: true,
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: "desc" },
|
|
||||||
});
|
|
||||||
|
|
||||||
return displays;
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseError(error.message);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const getDisplaysBySurveyIdWithContact = reactCache(
|
|
||||||
async (surveyId: string, limit?: number, offset?: number): Promise<TDisplayWithContact[]> => {
|
|
||||||
validateInputs(
|
|
||||||
[surveyId, ZId],
|
|
||||||
[limit, z.number().int().min(1).optional()],
|
|
||||||
[offset, z.number().int().nonnegative().optional()]
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const displays = await prisma.display.findMany({
|
|
||||||
where: {
|
|
||||||
surveyId,
|
|
||||||
contactId: { not: null },
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
surveyId: true,
|
|
||||||
contact: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
attributes: {
|
|
||||||
where: {
|
|
||||||
attributeKey: {
|
|
||||||
key: { in: ["email", "userId"] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
attributeKey: { select: { key: true } },
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: "desc" },
|
|
||||||
take: limit,
|
|
||||||
skip: offset,
|
|
||||||
});
|
|
||||||
|
|
||||||
return displays.map((display) => ({
|
|
||||||
id: display.id,
|
|
||||||
createdAt: display.createdAt,
|
|
||||||
surveyId: display.surveyId,
|
|
||||||
contact: display.contact
|
|
||||||
? {
|
|
||||||
id: display.contact.id,
|
|
||||||
attributes: display.contact.attributes.reduce(
|
|
||||||
(acc, attr) => {
|
|
||||||
acc[attr.attributeKey.key] = attr.value;
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, string>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseError(error.message);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const deleteDisplay = async (displayId: string, tx?: Prisma.TransactionClient): Promise<TDisplay> => {
|
export const deleteDisplay = async (displayId: string, tx?: Prisma.TransactionClient): Promise<TDisplay> => {
|
||||||
validateInputs([displayId, ZId]);
|
validateInputs([displayId, ZId]);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
import { mockDisplayId, mockSurveyId } from "./__mocks__/data.mock";
|
|
||||||
import { prisma } from "@/lib/__mocks__/database";
|
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import { describe, expect, test, vi } from "vitest";
|
|
||||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
|
||||||
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
|
|
||||||
import { getDisplaysByContactId, getDisplaysBySurveyIdWithContact } from "../service";
|
|
||||||
|
|
||||||
const mockContactId = "clqnj99r9000008lebgf8734j";
|
|
||||||
|
|
||||||
const mockDisplaysForContact = [
|
|
||||||
{
|
|
||||||
id: mockDisplayId,
|
|
||||||
createdAt: new Date("2024-01-15T10:00:00Z"),
|
|
||||||
surveyId: mockSurveyId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "clqkr5smu000208jy50v6g5k5",
|
|
||||||
createdAt: new Date("2024-01-14T10:00:00Z"),
|
|
||||||
surveyId: "clqkr8dlv000308jybb08evgs",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockDisplaysWithContact = [
|
|
||||||
{
|
|
||||||
id: mockDisplayId,
|
|
||||||
createdAt: new Date("2024-01-15T10:00:00Z"),
|
|
||||||
surveyId: mockSurveyId,
|
|
||||||
contact: {
|
|
||||||
id: mockContactId,
|
|
||||||
attributes: [
|
|
||||||
{ attributeKey: { key: "email" }, value: "test@example.com" },
|
|
||||||
{ attributeKey: { key: "userId" }, value: "user-123" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "clqkr5smu000208jy50v6g5k5",
|
|
||||||
createdAt: new Date("2024-01-14T10:00:00Z"),
|
|
||||||
surveyId: "clqkr8dlv000308jybb08evgs",
|
|
||||||
contact: {
|
|
||||||
id: "clqnj99r9000008lebgf8734k",
|
|
||||||
attributes: [{ attributeKey: { key: "userId" }, value: "user-456" }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe("getDisplaysByContactId", () => {
|
|
||||||
describe("Happy Path", () => {
|
|
||||||
test("returns displays for a contact ordered by createdAt desc", async () => {
|
|
||||||
vi.mocked(prisma.display.findMany).mockResolvedValue(mockDisplaysForContact as any);
|
|
||||||
|
|
||||||
const result = await getDisplaysByContactId(mockContactId);
|
|
||||||
|
|
||||||
expect(result).toEqual(mockDisplaysForContact);
|
|
||||||
expect(prisma.display.findMany).toHaveBeenCalledWith({
|
|
||||||
where: { contactId: mockContactId },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
surveyId: true,
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: "desc" },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns empty array when contact has no displays", async () => {
|
|
||||||
vi.mocked(prisma.display.findMany).mockResolvedValue([]);
|
|
||||||
|
|
||||||
const result = await getDisplaysByContactId(mockContactId);
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Sad Path", () => {
|
|
||||||
test("throws a ValidationError if the contactId is invalid", async () => {
|
|
||||||
await expect(getDisplaysByContactId("not-a-cuid")).rejects.toThrow(ValidationError);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("throws DatabaseError on PrismaClientKnownRequestError", async () => {
|
|
||||||
const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error", {
|
|
||||||
code: PrismaErrorType.UniqueConstraintViolation,
|
|
||||||
clientVersion: "0.0.1",
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mocked(prisma.display.findMany).mockRejectedValue(errToThrow);
|
|
||||||
|
|
||||||
await expect(getDisplaysByContactId(mockContactId)).rejects.toThrow(DatabaseError);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("throws generic Error for other exceptions", async () => {
|
|
||||||
vi.mocked(prisma.display.findMany).mockRejectedValue(new Error("Mock error"));
|
|
||||||
|
|
||||||
await expect(getDisplaysByContactId(mockContactId)).rejects.toThrow(Error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getDisplaysBySurveyIdWithContact", () => {
|
|
||||||
describe("Happy Path", () => {
|
|
||||||
test("returns displays with contact attributes transformed", async () => {
|
|
||||||
vi.mocked(prisma.display.findMany).mockResolvedValue(mockDisplaysWithContact as any);
|
|
||||||
|
|
||||||
const result = await getDisplaysBySurveyIdWithContact(mockSurveyId, 15, 0);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
{
|
|
||||||
id: mockDisplayId,
|
|
||||||
createdAt: new Date("2024-01-15T10:00:00Z"),
|
|
||||||
surveyId: mockSurveyId,
|
|
||||||
contact: {
|
|
||||||
id: mockContactId,
|
|
||||||
attributes: { email: "test@example.com", userId: "user-123" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "clqkr5smu000208jy50v6g5k5",
|
|
||||||
createdAt: new Date("2024-01-14T10:00:00Z"),
|
|
||||||
surveyId: "clqkr8dlv000308jybb08evgs",
|
|
||||||
contact: {
|
|
||||||
id: "clqnj99r9000008lebgf8734k",
|
|
||||||
attributes: { userId: "user-456" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("calls prisma with correct where clause and pagination", async () => {
|
|
||||||
vi.mocked(prisma.display.findMany).mockResolvedValue([]);
|
|
||||||
|
|
||||||
await getDisplaysBySurveyIdWithContact(mockSurveyId, 15, 0);
|
|
||||||
|
|
||||||
expect(prisma.display.findMany).toHaveBeenCalledWith({
|
|
||||||
where: {
|
|
||||||
surveyId: mockSurveyId,
|
|
||||||
contactId: { not: null },
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
surveyId: true,
|
|
||||||
contact: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
attributes: {
|
|
||||||
where: {
|
|
||||||
attributeKey: {
|
|
||||||
key: { in: ["email", "userId"] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
attributeKey: { select: { key: true } },
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: "desc" },
|
|
||||||
take: 15,
|
|
||||||
skip: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns empty array when no displays found", async () => {
|
|
||||||
vi.mocked(prisma.display.findMany).mockResolvedValue([]);
|
|
||||||
|
|
||||||
const result = await getDisplaysBySurveyIdWithContact(mockSurveyId);
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("handles display with null contact", async () => {
|
|
||||||
vi.mocked(prisma.display.findMany).mockResolvedValue([
|
|
||||||
{
|
|
||||||
id: mockDisplayId,
|
|
||||||
createdAt: new Date("2024-01-15T10:00:00Z"),
|
|
||||||
surveyId: mockSurveyId,
|
|
||||||
contact: null,
|
|
||||||
},
|
|
||||||
] as any);
|
|
||||||
|
|
||||||
const result = await getDisplaysBySurveyIdWithContact(mockSurveyId);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
{
|
|
||||||
id: mockDisplayId,
|
|
||||||
createdAt: new Date("2024-01-15T10:00:00Z"),
|
|
||||||
surveyId: mockSurveyId,
|
|
||||||
contact: null,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Sad Path", () => {
|
|
||||||
test("throws a ValidationError if the surveyId is invalid", async () => {
|
|
||||||
await expect(getDisplaysBySurveyIdWithContact("not-a-cuid")).rejects.toThrow(ValidationError);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("throws DatabaseError on PrismaClientKnownRequestError", async () => {
|
|
||||||
const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error", {
|
|
||||||
code: PrismaErrorType.UniqueConstraintViolation,
|
|
||||||
clientVersion: "0.0.1",
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mocked(prisma.display.findMany).mockRejectedValue(errToThrow);
|
|
||||||
|
|
||||||
await expect(getDisplaysBySurveyIdWithContact(mockSurveyId)).rejects.toThrow(DatabaseError);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("throws generic Error for other exceptions", async () => {
|
|
||||||
vi.mocked(prisma.display.findMany).mockRejectedValue(new Error("Mock error"));
|
|
||||||
|
|
||||||
await expect(getDisplaysBySurveyIdWithContact(mockSurveyId)).rejects.toThrow(Error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -218,16 +218,13 @@
|
|||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
"error_component_description": "Diese Ressource existiert nicht oder Du hast nicht die notwendigen Rechte, um darauf zuzugreifen.",
|
"error_component_description": "Diese Ressource existiert nicht oder Du hast nicht die notwendigen Rechte, um darauf zuzugreifen.",
|
||||||
"error_component_title": "Fehler beim Laden der Ressourcen",
|
"error_component_title": "Fehler beim Laden der Ressourcen",
|
||||||
"error_loading_data": "Fehler beim Laden der Daten",
|
|
||||||
"error_rate_limit_description": "Maximale Anzahl an Anfragen erreicht. Bitte später erneut versuchen.",
|
"error_rate_limit_description": "Maximale Anzahl an Anfragen erreicht. Bitte später erneut versuchen.",
|
||||||
"error_rate_limit_title": "Rate Limit Überschritten",
|
"error_rate_limit_title": "Rate Limit Überschritten",
|
||||||
"expand_rows": "Zeilen erweitern",
|
"expand_rows": "Zeilen erweitern",
|
||||||
"failed_to_copy_to_clipboard": "Fehler beim Kopieren in die Zwischenablage",
|
"failed_to_copy_to_clipboard": "Fehler beim Kopieren in die Zwischenablage",
|
||||||
"failed_to_load_organizations": "Fehler beim Laden der Organisationen",
|
"failed_to_load_organizations": "Fehler beim Laden der Organisationen",
|
||||||
"failed_to_load_workspaces": "Projekte konnten nicht geladen werden",
|
"failed_to_load_workspaces": "Projekte konnten nicht geladen werden",
|
||||||
"filter": "Filter",
|
|
||||||
"finish": "Fertigstellen",
|
"finish": "Fertigstellen",
|
||||||
"first_name": "Vorname",
|
|
||||||
"follow_these": "Folge diesen",
|
"follow_these": "Folge diesen",
|
||||||
"formbricks_version": "Formbricks Version",
|
"formbricks_version": "Formbricks Version",
|
||||||
"full_name": "Name",
|
"full_name": "Name",
|
||||||
@@ -240,7 +237,6 @@
|
|||||||
"hidden_field": "Verstecktes Feld",
|
"hidden_field": "Verstecktes Feld",
|
||||||
"hidden_fields": "Versteckte Felder",
|
"hidden_fields": "Versteckte Felder",
|
||||||
"hide_column": "Spalte ausblenden",
|
"hide_column": "Spalte ausblenden",
|
||||||
"id": "ID",
|
|
||||||
"image": "Bild",
|
"image": "Bild",
|
||||||
"images": "Bilder",
|
"images": "Bilder",
|
||||||
"import": "Importieren",
|
"import": "Importieren",
|
||||||
@@ -258,7 +254,6 @@
|
|||||||
"key": "Schlüssel",
|
"key": "Schlüssel",
|
||||||
"label": "Bezeichnung",
|
"label": "Bezeichnung",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"last_name": "Nachname",
|
|
||||||
"learn_more": "Mehr erfahren",
|
"learn_more": "Mehr erfahren",
|
||||||
"license_expired": "License Expired",
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Helle Überlagerung",
|
"light_overlay": "Helle Überlagerung",
|
||||||
@@ -433,7 +428,6 @@
|
|||||||
"top_right": "Oben rechts",
|
"top_right": "Oben rechts",
|
||||||
"try_again": "Versuch's nochmal",
|
"try_again": "Versuch's nochmal",
|
||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
"unknown_survey": "Unbekannte Umfrage",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "Schalten Sie mehr Projekte mit einem höheren Tarif frei.",
|
"unlock_more_workspaces_with_a_higher_plan": "Schalten Sie mehr Projekte mit einem höheren Tarif frei.",
|
||||||
"update": "Aktualisierung",
|
"update": "Aktualisierung",
|
||||||
"updated": "Aktualisiert",
|
"updated": "Aktualisiert",
|
||||||
@@ -661,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {Dadurch wird das ausgewählte Attribut gelöscht. Alle mit diesem Attribut verknüpften Kontaktdaten gehen verloren.} other {Dadurch werden die ausgewählten Attribute gelöscht. Alle mit diesen Attributen verknüpften Kontaktdaten gehen verloren.}}",
|
"delete_attribute_confirmation": "{value, plural, one {Dadurch wird das ausgewählte Attribut gelöscht. Alle mit diesem Attribut verknüpften Kontaktdaten gehen verloren.} other {Dadurch werden die ausgewählten Attribute gelöscht. Alle mit diesen Attributen verknüpften Kontaktdaten gehen verloren.}}",
|
||||||
"delete_contact_confirmation": "Dies wird alle Umfrageantworten und Kontaktattribute löschen, die mit diesem Kontakt verbunden sind. Jegliche zielgerichtete Kommunikation und Personalisierung basierend auf den Daten dieses Kontakts gehen verloren.",
|
"delete_contact_confirmation": "Dies wird alle Umfrageantworten und Kontaktattribute löschen, die mit diesem Kontakt verbunden sind. Jegliche zielgerichtete Kommunikation und Personalisierung basierend auf den Daten dieses Kontakts gehen verloren.",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {Dies wird alle Umfrageantworten und Kontaktattribute löschen, die mit diesem Kontakt verbunden sind. Jegliche zielgerichtete Kommunikation und Personalisierung basierend auf den Daten dieses Kontakts gehen verloren. Wenn dieser Kontakt Antworten hat, die zu den Umfragequoten zählen, werden die Quotenstände reduziert, aber die Quotenlimits bleiben unverändert.} other {Dies wird alle Umfrageantworten und Kontaktattribute löschen, die mit diesen Kontakten verbunden sind. Jegliche zielgerichtete Kommunikation und Personalisierung basierend auf den Daten dieses Kontakts gehen verloren. Wenn diesen Kontakten Antworten haben, die zu den Umfragequoten zählen, werden die Quotenstände reduziert, aber die Quotenlimits bleiben unverändert.}}",
|
"delete_contact_confirmation_with_quotas": "{value, plural, one {Dies wird alle Umfrageantworten und Kontaktattribute löschen, die mit diesem Kontakt verbunden sind. Jegliche zielgerichtete Kommunikation und Personalisierung basierend auf den Daten dieses Kontakts gehen verloren. Wenn dieser Kontakt Antworten hat, die zu den Umfragequoten zählen, werden die Quotenstände reduziert, aber die Quotenlimits bleiben unverändert.} other {Dies wird alle Umfrageantworten und Kontaktattribute löschen, die mit diesen Kontakten verbunden sind. Jegliche zielgerichtete Kommunikation und Personalisierung basierend auf den Daten dieses Kontakts gehen verloren. Wenn diesen Kontakten Antworten haben, die zu den Umfragequoten zählen, werden die Quotenstände reduziert, aber die Quotenlimits bleiben unverändert.}}",
|
||||||
"displays": "Anzeigen",
|
|
||||||
"edit_attribute": "Attribut bearbeiten",
|
"edit_attribute": "Attribut bearbeiten",
|
||||||
"edit_attribute_description": "Aktualisieren Sie die Bezeichnung und Beschreibung für dieses Attribut.",
|
"edit_attribute_description": "Aktualisieren Sie die Bezeichnung und Beschreibung für dieses Attribut.",
|
||||||
"edit_attribute_values": "Attribute bearbeiten",
|
"edit_attribute_values": "Attribute bearbeiten",
|
||||||
@@ -673,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "Ungültige CSV-Spaltennamen: {columns}. Spaltennamen, die zu neuen Attributen werden, dürfen nur Kleinbuchstaben, Zahlen und Unterstriche enthalten und müssen mit einem Buchstaben beginnen.",
|
"invalid_csv_column_names": "Ungültige CSV-Spaltennamen: {columns}. Spaltennamen, die zu neuen Attributen werden, dürfen nur Kleinbuchstaben, Zahlen und Unterstriche enthalten und müssen mit einem Buchstaben beginnen.",
|
||||||
"invalid_date_format": "Ungültiges Datumsformat. Bitte verwende ein gültiges Datum.",
|
"invalid_date_format": "Ungültiges Datumsformat. Bitte verwende ein gültiges Datum.",
|
||||||
"invalid_number_format": "Ungültiges Zahlenformat. Bitte gib eine gültige Zahl ein.",
|
"invalid_number_format": "Ungültiges Zahlenformat. Bitte gib eine gültige Zahl ein.",
|
||||||
"no_activity_yet": "Noch keine Aktivität",
|
|
||||||
"no_published_link_surveys_available": "Keine veröffentlichten Link-Umfragen verfügbar. Bitte veröffentliche zuerst eine Link-Umfrage.",
|
"no_published_link_surveys_available": "Keine veröffentlichten Link-Umfragen verfügbar. Bitte veröffentliche zuerst eine Link-Umfrage.",
|
||||||
"no_published_surveys": "Keine veröffentlichten Umfragen",
|
"no_published_surveys": "Keine veröffentlichten Umfragen",
|
||||||
"no_responses_found": "Keine Antworten gefunden",
|
"no_responses_found": "Keine Antworten gefunden",
|
||||||
@@ -688,8 +680,6 @@
|
|||||||
"select_a_survey": "Wähle eine Umfrage aus",
|
"select_a_survey": "Wähle eine Umfrage aus",
|
||||||
"select_attribute": "Attribut auswählen",
|
"select_attribute": "Attribut auswählen",
|
||||||
"select_attribute_key": "Attributschlüssel auswählen",
|
"select_attribute_key": "Attributschlüssel auswählen",
|
||||||
"survey_viewed": "Umfrage angesehen",
|
|
||||||
"survey_viewed_at": "Angesehen am",
|
|
||||||
"system_attributes": "Systemattribute",
|
"system_attributes": "Systemattribute",
|
||||||
"unlock_contacts_description": "Verwalte Kontakte und sende gezielte Umfragen",
|
"unlock_contacts_description": "Verwalte Kontakte und sende gezielte Umfragen",
|
||||||
"unlock_contacts_title": "Kontakte mit einem höheren Plan freischalten",
|
"unlock_contacts_title": "Kontakte mit einem höheren Plan freischalten",
|
||||||
@@ -1961,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "Gefilterte Antworten (Excel)",
|
"filtered_responses_excel": "Gefilterte Antworten (Excel)",
|
||||||
"generating_qr_code": "QR-Code wird generiert",
|
"generating_qr_code": "QR-Code wird generiert",
|
||||||
"impressions": "Eindrücke",
|
"impressions": "Eindrücke",
|
||||||
"impressions_identified_only": "Zeigt nur Impressionen von identifizierten Kontakten",
|
|
||||||
"impressions_tooltip": "Anzahl der Aufrufe der Umfrage.",
|
"impressions_tooltip": "Anzahl der Aufrufe der Umfrage.",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "Die Umfrage wird den Nutzern Ihrer Website angezeigt, die den unten aufgeführten Kriterien entsprechen",
|
"connection_description": "Die Umfrage wird den Nutzern Ihrer Website angezeigt, die den unten aufgeführten Kriterien entsprechen",
|
||||||
@@ -2004,7 +1993,6 @@
|
|||||||
"last_quarter": "Letztes Quartal",
|
"last_quarter": "Letztes Quartal",
|
||||||
"last_year": "Letztes Jahr",
|
"last_year": "Letztes Jahr",
|
||||||
"limit": "Limit",
|
"limit": "Limit",
|
||||||
"no_identified_impressions": "Keine Impressionen von identifizierten Kontakten",
|
|
||||||
"no_responses_found": "Keine Antworten gefunden",
|
"no_responses_found": "Keine Antworten gefunden",
|
||||||
"other_values_found": "Andere Werte gefunden",
|
"other_values_found": "Andere Werte gefunden",
|
||||||
"overall": "Insgesamt",
|
"overall": "Insgesamt",
|
||||||
|
|||||||
@@ -218,16 +218,13 @@
|
|||||||
"error": "Error",
|
"error": "Error",
|
||||||
"error_component_description": "This resource does not exist or you do not have the necessary rights to access it.",
|
"error_component_description": "This resource does not exist or you do not have the necessary rights to access it.",
|
||||||
"error_component_title": "Error loading resources",
|
"error_component_title": "Error loading resources",
|
||||||
"error_loading_data": "Error loading data",
|
|
||||||
"error_rate_limit_description": "Maximum number of requests reached. Please try again later.",
|
"error_rate_limit_description": "Maximum number of requests reached. Please try again later.",
|
||||||
"error_rate_limit_title": "Rate Limit Exceeded",
|
"error_rate_limit_title": "Rate Limit Exceeded",
|
||||||
"expand_rows": "Expand rows",
|
"expand_rows": "Expand rows",
|
||||||
"failed_to_copy_to_clipboard": "Failed to copy to clipboard",
|
"failed_to_copy_to_clipboard": "Failed to copy to clipboard",
|
||||||
"failed_to_load_organizations": "Failed to load organizations",
|
"failed_to_load_organizations": "Failed to load organizations",
|
||||||
"failed_to_load_workspaces": "Failed to load workspaces",
|
"failed_to_load_workspaces": "Failed to load workspaces",
|
||||||
"filter": "Filter",
|
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"first_name": "First Name",
|
|
||||||
"follow_these": "Follow these",
|
"follow_these": "Follow these",
|
||||||
"formbricks_version": "Formbricks Version",
|
"formbricks_version": "Formbricks Version",
|
||||||
"full_name": "Full name",
|
"full_name": "Full name",
|
||||||
@@ -240,7 +237,6 @@
|
|||||||
"hidden_field": "Hidden field",
|
"hidden_field": "Hidden field",
|
||||||
"hidden_fields": "Hidden fields",
|
"hidden_fields": "Hidden fields",
|
||||||
"hide_column": "Hide column",
|
"hide_column": "Hide column",
|
||||||
"id": "ID",
|
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"images": "Images",
|
"images": "Images",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
@@ -258,7 +254,6 @@
|
|||||||
"key": "Key",
|
"key": "Key",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"last_name": "Last Name",
|
|
||||||
"learn_more": "Learn more",
|
"learn_more": "Learn more",
|
||||||
"license_expired": "License Expired",
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Light overlay",
|
"light_overlay": "Light overlay",
|
||||||
@@ -433,7 +428,6 @@
|
|||||||
"top_right": "Top Right",
|
"top_right": "Top Right",
|
||||||
"try_again": "Try again",
|
"try_again": "Try again",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"unknown_survey": "Unknown survey",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "Unlock more workspaces with a higher plan.",
|
"unlock_more_workspaces_with_a_higher_plan": "Unlock more workspaces with a higher plan.",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"updated": "Updated",
|
"updated": "Updated",
|
||||||
@@ -661,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {This will delete the selected attribute. Any contact data associated with this attribute will be lost.} other {This will delete the selected attributes. Any contact data associated with these attributes will be lost.}}",
|
"delete_attribute_confirmation": "{value, plural, one {This will delete the selected attribute. Any contact data associated with this attribute will be lost.} other {This will delete the selected attributes. Any contact data associated with these attributes will be lost.}}",
|
||||||
"delete_contact_confirmation": "This will delete all survey responses and contact attributes associated with this contact. Any targeting and personalization based on this contact’s data will be lost.",
|
"delete_contact_confirmation": "This will delete all survey responses and contact attributes associated with this contact. Any targeting and personalization based on this contact’s data will be lost.",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {This will delete all survey responses and contact attributes associated with this contact. Any targeting and personalization based on this contact’s data will be lost. If this contact has responses that count towards survey quotas, the quota counts will be reduced but the quota limits will remain unchanged.} other {This will delete all survey responses and contact attributes associated with these contacts. Any targeting and personalization based on these contacts’ data will be lost. If these contacts have responses that count towards survey quotas, the quota counts will be reduced but the quota limits will remain unchanged.}}",
|
"delete_contact_confirmation_with_quotas": "{value, plural, one {This will delete all survey responses and contact attributes associated with this contact. Any targeting and personalization based on this contact’s data will be lost. If this contact has responses that count towards survey quotas, the quota counts will be reduced but the quota limits will remain unchanged.} other {This will delete all survey responses and contact attributes associated with these contacts. Any targeting and personalization based on these contacts’ data will be lost. If these contacts have responses that count towards survey quotas, the quota counts will be reduced but the quota limits will remain unchanged.}}",
|
||||||
"displays": "Displays",
|
|
||||||
"edit_attribute": "Edit attribute",
|
"edit_attribute": "Edit attribute",
|
||||||
"edit_attribute_description": "Update the label and description for this attribute.",
|
"edit_attribute_description": "Update the label and description for this attribute.",
|
||||||
"edit_attribute_values": "Edit attributes",
|
"edit_attribute_values": "Edit attributes",
|
||||||
@@ -673,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "Invalid CSV column name(s): {columns}. Column names that will become new attributes must only contain lowercase letters, numbers, and underscores, and must start with a letter.",
|
"invalid_csv_column_names": "Invalid CSV column name(s): {columns}. Column names that will become new attributes must only contain lowercase letters, numbers, and underscores, and must start with a letter.",
|
||||||
"invalid_date_format": "Invalid date format. Please use a valid date.",
|
"invalid_date_format": "Invalid date format. Please use a valid date.",
|
||||||
"invalid_number_format": "Invalid number format. Please enter a valid number.",
|
"invalid_number_format": "Invalid number format. Please enter a valid number.",
|
||||||
"no_activity_yet": "No activity yet",
|
|
||||||
"no_published_link_surveys_available": "No published link surveys available. Please publish a link survey first.",
|
"no_published_link_surveys_available": "No published link surveys available. Please publish a link survey first.",
|
||||||
"no_published_surveys": "No published surveys",
|
"no_published_surveys": "No published surveys",
|
||||||
"no_responses_found": "No responses found",
|
"no_responses_found": "No responses found",
|
||||||
@@ -688,8 +680,6 @@
|
|||||||
"select_a_survey": "Select a survey",
|
"select_a_survey": "Select a survey",
|
||||||
"select_attribute": "Select Attribute",
|
"select_attribute": "Select Attribute",
|
||||||
"select_attribute_key": "Select attribute key",
|
"select_attribute_key": "Select attribute key",
|
||||||
"survey_viewed": "Survey viewed",
|
|
||||||
"survey_viewed_at": "Viewed At",
|
|
||||||
"system_attributes": "System Attributes",
|
"system_attributes": "System Attributes",
|
||||||
"unlock_contacts_description": "Manage contacts and send out targeted surveys",
|
"unlock_contacts_description": "Manage contacts and send out targeted surveys",
|
||||||
"unlock_contacts_title": "Unlock contacts with a higher plan",
|
"unlock_contacts_title": "Unlock contacts with a higher plan",
|
||||||
@@ -1961,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "Filtered responses (Excel)",
|
"filtered_responses_excel": "Filtered responses (Excel)",
|
||||||
"generating_qr_code": "Generating QR code",
|
"generating_qr_code": "Generating QR code",
|
||||||
"impressions": "Impressions",
|
"impressions": "Impressions",
|
||||||
"impressions_identified_only": "Only showing impressions from identified contacts",
|
|
||||||
"impressions_tooltip": "Number of times the survey has been viewed.",
|
"impressions_tooltip": "Number of times the survey has been viewed.",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "The survey will be shown to users of your website, that match the criteria listed below",
|
"connection_description": "The survey will be shown to users of your website, that match the criteria listed below",
|
||||||
@@ -2004,7 +1993,6 @@
|
|||||||
"last_quarter": "Last quarter",
|
"last_quarter": "Last quarter",
|
||||||
"last_year": "Last year",
|
"last_year": "Last year",
|
||||||
"limit": "Limit",
|
"limit": "Limit",
|
||||||
"no_identified_impressions": "No impressions from identified contacts",
|
|
||||||
"no_responses_found": "No responses found",
|
"no_responses_found": "No responses found",
|
||||||
"other_values_found": "Other values found",
|
"other_values_found": "Other values found",
|
||||||
"overall": "Overall",
|
"overall": "Overall",
|
||||||
|
|||||||
@@ -218,16 +218,13 @@
|
|||||||
"error": "Error",
|
"error": "Error",
|
||||||
"error_component_description": "Este recurso no existe o no tienes los derechos necesarios para acceder a él.",
|
"error_component_description": "Este recurso no existe o no tienes los derechos necesarios para acceder a él.",
|
||||||
"error_component_title": "Error al cargar recursos",
|
"error_component_title": "Error al cargar recursos",
|
||||||
"error_loading_data": "Error al cargar los datos",
|
|
||||||
"error_rate_limit_description": "Número máximo de solicitudes alcanzado. Por favor, inténtalo de nuevo más tarde.",
|
"error_rate_limit_description": "Número máximo de solicitudes alcanzado. Por favor, inténtalo de nuevo más tarde.",
|
||||||
"error_rate_limit_title": "Límite de frecuencia excedido",
|
"error_rate_limit_title": "Límite de frecuencia excedido",
|
||||||
"expand_rows": "Expandir filas",
|
"expand_rows": "Expandir filas",
|
||||||
"failed_to_copy_to_clipboard": "Error al copiar al portapapeles",
|
"failed_to_copy_to_clipboard": "Error al copiar al portapapeles",
|
||||||
"failed_to_load_organizations": "Error al cargar organizaciones",
|
"failed_to_load_organizations": "Error al cargar organizaciones",
|
||||||
"failed_to_load_workspaces": "Error al cargar los proyectos",
|
"failed_to_load_workspaces": "Error al cargar los proyectos",
|
||||||
"filter": "Filtro",
|
|
||||||
"finish": "Finalizar",
|
"finish": "Finalizar",
|
||||||
"first_name": "Nombre",
|
|
||||||
"follow_these": "Sigue estos",
|
"follow_these": "Sigue estos",
|
||||||
"formbricks_version": "Versión de Formbricks",
|
"formbricks_version": "Versión de Formbricks",
|
||||||
"full_name": "Nombre completo",
|
"full_name": "Nombre completo",
|
||||||
@@ -240,7 +237,6 @@
|
|||||||
"hidden_field": "Campo oculto",
|
"hidden_field": "Campo oculto",
|
||||||
"hidden_fields": "Campos ocultos",
|
"hidden_fields": "Campos ocultos",
|
||||||
"hide_column": "Ocultar columna",
|
"hide_column": "Ocultar columna",
|
||||||
"id": "ID",
|
|
||||||
"image": "Imagen",
|
"image": "Imagen",
|
||||||
"images": "Imágenes",
|
"images": "Imágenes",
|
||||||
"import": "Importar",
|
"import": "Importar",
|
||||||
@@ -258,7 +254,6 @@
|
|||||||
"key": "Clave",
|
"key": "Clave",
|
||||||
"label": "Etiqueta",
|
"label": "Etiqueta",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"last_name": "Apellido",
|
|
||||||
"learn_more": "Saber más",
|
"learn_more": "Saber más",
|
||||||
"license_expired": "License Expired",
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Superposición clara",
|
"light_overlay": "Superposición clara",
|
||||||
@@ -433,7 +428,6 @@
|
|||||||
"top_right": "Superior derecha",
|
"top_right": "Superior derecha",
|
||||||
"try_again": "Intentar de nuevo",
|
"try_again": "Intentar de nuevo",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
"unknown_survey": "Encuesta desconocida",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "Desbloquea más proyectos con un plan superior.",
|
"unlock_more_workspaces_with_a_higher_plan": "Desbloquea más proyectos con un plan superior.",
|
||||||
"update": "Actualizar",
|
"update": "Actualizar",
|
||||||
"updated": "Actualizado",
|
"updated": "Actualizado",
|
||||||
@@ -661,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {Esto eliminará el atributo seleccionado. Se perderán todos los datos de contacto asociados con este atributo.} other {Esto eliminará los atributos seleccionados. Se perderán todos los datos de contacto asociados con estos atributos.}}",
|
"delete_attribute_confirmation": "{value, plural, one {Esto eliminará el atributo seleccionado. Se perderán todos los datos de contacto asociados con este atributo.} other {Esto eliminará los atributos seleccionados. Se perderán todos los datos de contacto asociados con estos atributos.}}",
|
||||||
"delete_contact_confirmation": "Esto eliminará todas las respuestas de encuestas y atributos de contacto asociados con este contacto. Cualquier segmentación y personalización basada en los datos de este contacto se perderá.",
|
"delete_contact_confirmation": "Esto eliminará todas las respuestas de encuestas y atributos de contacto asociados con este contacto. Cualquier segmentación y personalización basada en los datos de este contacto se perderá.",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {Esto eliminará todas las respuestas de encuestas y atributos de contacto asociados con este contacto. Cualquier segmentación y personalización basada en los datos de este contacto se perderá. Si este contacto tiene respuestas que cuentan para las cuotas de encuesta, los recuentos de cuota se reducirán pero los límites de cuota permanecerán sin cambios.} other {Esto eliminará todas las respuestas de encuestas y atributos de contacto asociados con estos contactos. Cualquier segmentación y personalización basada en los datos de estos contactos se perderá. Si estos contactos tienen respuestas que cuentan para las cuotas de encuesta, los recuentos de cuota se reducirán pero los límites de cuota permanecerán sin cambios.}}",
|
"delete_contact_confirmation_with_quotas": "{value, plural, one {Esto eliminará todas las respuestas de encuestas y atributos de contacto asociados con este contacto. Cualquier segmentación y personalización basada en los datos de este contacto se perderá. Si este contacto tiene respuestas que cuentan para las cuotas de encuesta, los recuentos de cuota se reducirán pero los límites de cuota permanecerán sin cambios.} other {Esto eliminará todas las respuestas de encuestas y atributos de contacto asociados con estos contactos. Cualquier segmentación y personalización basada en los datos de estos contactos se perderá. Si estos contactos tienen respuestas que cuentan para las cuotas de encuesta, los recuentos de cuota se reducirán pero los límites de cuota permanecerán sin cambios.}}",
|
||||||
"displays": "Visualizaciones",
|
|
||||||
"edit_attribute": "Editar atributo",
|
"edit_attribute": "Editar atributo",
|
||||||
"edit_attribute_description": "Actualiza la etiqueta y la descripción de este atributo.",
|
"edit_attribute_description": "Actualiza la etiqueta y la descripción de este atributo.",
|
||||||
"edit_attribute_values": "Editar atributos",
|
"edit_attribute_values": "Editar atributos",
|
||||||
@@ -673,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "Nombre(s) de columna CSV no válido(s): {columns}. Los nombres de columna que se convertirán en nuevos atributos solo deben contener letras minúsculas, números y guiones bajos, y deben comenzar con una letra.",
|
"invalid_csv_column_names": "Nombre(s) de columna CSV no válido(s): {columns}. Los nombres de columna que se convertirán en nuevos atributos solo deben contener letras minúsculas, números y guiones bajos, y deben comenzar con una letra.",
|
||||||
"invalid_date_format": "Formato de fecha no válido. Por favor, usa una fecha válida.",
|
"invalid_date_format": "Formato de fecha no válido. Por favor, usa una fecha válida.",
|
||||||
"invalid_number_format": "Formato de número no válido. Por favor, introduce un número válido.",
|
"invalid_number_format": "Formato de número no válido. Por favor, introduce un número válido.",
|
||||||
"no_activity_yet": "Aún no hay actividad",
|
|
||||||
"no_published_link_surveys_available": "No hay encuestas de enlace publicadas disponibles. Por favor, publica primero una encuesta de enlace.",
|
"no_published_link_surveys_available": "No hay encuestas de enlace publicadas disponibles. Por favor, publica primero una encuesta de enlace.",
|
||||||
"no_published_surveys": "No hay encuestas publicadas",
|
"no_published_surveys": "No hay encuestas publicadas",
|
||||||
"no_responses_found": "No se encontraron respuestas",
|
"no_responses_found": "No se encontraron respuestas",
|
||||||
@@ -688,8 +680,6 @@
|
|||||||
"select_a_survey": "Selecciona una encuesta",
|
"select_a_survey": "Selecciona una encuesta",
|
||||||
"select_attribute": "Seleccionar atributo",
|
"select_attribute": "Seleccionar atributo",
|
||||||
"select_attribute_key": "Seleccionar clave de atributo",
|
"select_attribute_key": "Seleccionar clave de atributo",
|
||||||
"survey_viewed": "Encuesta vista",
|
|
||||||
"survey_viewed_at": "Vista el",
|
|
||||||
"system_attributes": "Atributos del sistema",
|
"system_attributes": "Atributos del sistema",
|
||||||
"unlock_contacts_description": "Gestiona contactos y envía encuestas dirigidas",
|
"unlock_contacts_description": "Gestiona contactos y envía encuestas dirigidas",
|
||||||
"unlock_contacts_title": "Desbloquea contactos con un plan superior",
|
"unlock_contacts_title": "Desbloquea contactos con un plan superior",
|
||||||
@@ -1961,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "Respuestas filtradas (Excel)",
|
"filtered_responses_excel": "Respuestas filtradas (Excel)",
|
||||||
"generating_qr_code": "Generando código QR",
|
"generating_qr_code": "Generando código QR",
|
||||||
"impressions": "Impresiones",
|
"impressions": "Impresiones",
|
||||||
"impressions_identified_only": "Solo se muestran impresiones de contactos identificados",
|
|
||||||
"impressions_tooltip": "Número de veces que se ha visto la encuesta.",
|
"impressions_tooltip": "Número de veces que se ha visto la encuesta.",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "La encuesta se mostrará a los usuarios de tu sitio web que cumplan con los criterios enumerados a continuación",
|
"connection_description": "La encuesta se mostrará a los usuarios de tu sitio web que cumplan con los criterios enumerados a continuación",
|
||||||
@@ -2004,7 +1993,6 @@
|
|||||||
"last_quarter": "Último trimestre",
|
"last_quarter": "Último trimestre",
|
||||||
"last_year": "Último año",
|
"last_year": "Último año",
|
||||||
"limit": "Límite",
|
"limit": "Límite",
|
||||||
"no_identified_impressions": "No hay impresiones de contactos identificados",
|
|
||||||
"no_responses_found": "No se han encontrado respuestas",
|
"no_responses_found": "No se han encontrado respuestas",
|
||||||
"other_values_found": "Otros valores encontrados",
|
"other_values_found": "Otros valores encontrados",
|
||||||
"overall": "General",
|
"overall": "General",
|
||||||
|
|||||||
@@ -112,7 +112,6 @@
|
|||||||
"link_expired_description": "Le lien que vous avez utilisé n'est plus valide."
|
"link_expired_description": "Le lien que vous avez utilisé n'est plus valide."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"Filter": "Filtrer",
|
|
||||||
"accepted": "Accepté",
|
"accepted": "Accepté",
|
||||||
"account": "Compte",
|
"account": "Compte",
|
||||||
"account_settings": "Paramètres du compte",
|
"account_settings": "Paramètres du compte",
|
||||||
@@ -219,16 +218,13 @@
|
|||||||
"error": "Erreur",
|
"error": "Erreur",
|
||||||
"error_component_description": "Cette ressource n'existe pas ou vous n'avez pas les droits nécessaires pour y accéder.",
|
"error_component_description": "Cette ressource n'existe pas ou vous n'avez pas les droits nécessaires pour y accéder.",
|
||||||
"error_component_title": "Erreur de chargement des ressources",
|
"error_component_title": "Erreur de chargement des ressources",
|
||||||
"error_loading_data": "Erreur lors du chargement des données",
|
|
||||||
"error_rate_limit_description": "Nombre maximal de demandes atteint. Veuillez réessayer plus tard.",
|
"error_rate_limit_description": "Nombre maximal de demandes atteint. Veuillez réessayer plus tard.",
|
||||||
"error_rate_limit_title": "Limite de Taux Dépassée",
|
"error_rate_limit_title": "Limite de Taux Dépassée",
|
||||||
"expand_rows": "Développer les lignes",
|
"expand_rows": "Développer les lignes",
|
||||||
"failed_to_copy_to_clipboard": "Échec de la copie dans le presse-papiers",
|
"failed_to_copy_to_clipboard": "Échec de la copie dans le presse-papiers",
|
||||||
"failed_to_load_organizations": "Échec du chargement des organisations",
|
"failed_to_load_organizations": "Échec du chargement des organisations",
|
||||||
"failed_to_load_workspaces": "Échec du chargement des projets",
|
"failed_to_load_workspaces": "Échec du chargement des projets",
|
||||||
"filter": "Filtre",
|
|
||||||
"finish": "Terminer",
|
"finish": "Terminer",
|
||||||
"first_name": "Prénom",
|
|
||||||
"follow_these": "Suivez ceci",
|
"follow_these": "Suivez ceci",
|
||||||
"formbricks_version": "Version de Formbricks",
|
"formbricks_version": "Version de Formbricks",
|
||||||
"full_name": "Nom complet",
|
"full_name": "Nom complet",
|
||||||
@@ -241,7 +237,6 @@
|
|||||||
"hidden_field": "Champ caché",
|
"hidden_field": "Champ caché",
|
||||||
"hidden_fields": "Champs cachés",
|
"hidden_fields": "Champs cachés",
|
||||||
"hide_column": "Cacher la colonne",
|
"hide_column": "Cacher la colonne",
|
||||||
"id": "ID",
|
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
"images": "Images",
|
"images": "Images",
|
||||||
"import": "Importer",
|
"import": "Importer",
|
||||||
@@ -259,7 +254,6 @@
|
|||||||
"key": "Clé",
|
"key": "Clé",
|
||||||
"label": "Étiquette",
|
"label": "Étiquette",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
"last_name": "Nom de famille",
|
|
||||||
"learn_more": "En savoir plus",
|
"learn_more": "En savoir plus",
|
||||||
"license_expired": "License Expired",
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Claire",
|
"light_overlay": "Claire",
|
||||||
@@ -434,7 +428,6 @@
|
|||||||
"top_right": "En haut à droite",
|
"top_right": "En haut à droite",
|
||||||
"try_again": "Réessayer",
|
"try_again": "Réessayer",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"unknown_survey": "Enquête inconnue",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "Débloquez plus de projets avec un forfait supérieur.",
|
"unlock_more_workspaces_with_a_higher_plan": "Débloquez plus de projets avec un forfait supérieur.",
|
||||||
"update": "Mise à jour",
|
"update": "Mise à jour",
|
||||||
"updated": "Mise à jour",
|
"updated": "Mise à jour",
|
||||||
@@ -662,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {Cela supprimera l'attribut sélectionné. Toutes les données de contact associées à cet attribut seront perdues.} other {Cela supprimera les attributs sélectionnés. Toutes les données de contact associées à ces attributs seront perdues.}}",
|
"delete_attribute_confirmation": "{value, plural, one {Cela supprimera l'attribut sélectionné. Toutes les données de contact associées à cet attribut seront perdues.} other {Cela supprimera les attributs sélectionnés. Toutes les données de contact associées à ces attributs seront perdues.}}",
|
||||||
"delete_contact_confirmation": "Cela supprimera toutes les réponses aux enquêtes et les attributs de contact associés à ce contact. Toute la personnalisation et le ciblage basés sur les données de ce contact seront perdus.",
|
"delete_contact_confirmation": "Cela supprimera toutes les réponses aux enquêtes et les attributs de contact associés à ce contact. Toute la personnalisation et le ciblage basés sur les données de ce contact seront perdus.",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, other {Cela supprimera toutes les réponses aux enquêtes et les attributs de contact associés à ce contact. Toute la personnalisation et le ciblage basés sur les données de ce contact seront perdus. Si ce contact a des réponses qui comptent dans les quotas de l'enquête, les comptes de quotas seront réduits mais les limites de quota resteront inchangées.}}",
|
"delete_contact_confirmation_with_quotas": "{value, plural, other {Cela supprimera toutes les réponses aux enquêtes et les attributs de contact associés à ce contact. Toute la personnalisation et le ciblage basés sur les données de ce contact seront perdus. Si ce contact a des réponses qui comptent dans les quotas de l'enquête, les comptes de quotas seront réduits mais les limites de quota resteront inchangées.}}",
|
||||||
"displays": "Affichages",
|
|
||||||
"edit_attribute": "Modifier l'attribut",
|
"edit_attribute": "Modifier l'attribut",
|
||||||
"edit_attribute_description": "Mettez à jour l'étiquette et la description de cet attribut.",
|
"edit_attribute_description": "Mettez à jour l'étiquette et la description de cet attribut.",
|
||||||
"edit_attribute_values": "Modifier les attributs",
|
"edit_attribute_values": "Modifier les attributs",
|
||||||
@@ -674,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "Nom(s) de colonne CSV invalide(s) : {columns}. Les noms de colonnes qui deviendront de nouveaux attributs ne doivent contenir que des lettres minuscules, des chiffres et des underscores, et doivent commencer par une lettre.",
|
"invalid_csv_column_names": "Nom(s) de colonne CSV invalide(s) : {columns}. Les noms de colonnes qui deviendront de nouveaux attributs ne doivent contenir que des lettres minuscules, des chiffres et des underscores, et doivent commencer par une lettre.",
|
||||||
"invalid_date_format": "Format de date invalide. Merci d'utiliser une date valide.",
|
"invalid_date_format": "Format de date invalide. Merci d'utiliser une date valide.",
|
||||||
"invalid_number_format": "Format de nombre invalide. Veuillez saisir un nombre valide.",
|
"invalid_number_format": "Format de nombre invalide. Veuillez saisir un nombre valide.",
|
||||||
"no_activity_yet": "Aucune activité pour le moment",
|
|
||||||
"no_published_link_surveys_available": "Aucune enquête par lien publiée n'est disponible. Veuillez d'abord publier une enquête par lien.",
|
"no_published_link_surveys_available": "Aucune enquête par lien publiée n'est disponible. Veuillez d'abord publier une enquête par lien.",
|
||||||
"no_published_surveys": "Aucune enquête publiée",
|
"no_published_surveys": "Aucune enquête publiée",
|
||||||
"no_responses_found": "Aucune réponse trouvée",
|
"no_responses_found": "Aucune réponse trouvée",
|
||||||
@@ -689,8 +680,6 @@
|
|||||||
"select_a_survey": "Sélectionner une enquête",
|
"select_a_survey": "Sélectionner une enquête",
|
||||||
"select_attribute": "Sélectionner un attribut",
|
"select_attribute": "Sélectionner un attribut",
|
||||||
"select_attribute_key": "Sélectionner une clé d'attribut",
|
"select_attribute_key": "Sélectionner une clé d'attribut",
|
||||||
"survey_viewed": "Enquête consultée",
|
|
||||||
"survey_viewed_at": "Consultée le",
|
|
||||||
"system_attributes": "Attributs système",
|
"system_attributes": "Attributs système",
|
||||||
"unlock_contacts_description": "Gérer les contacts et envoyer des enquêtes ciblées",
|
"unlock_contacts_description": "Gérer les contacts et envoyer des enquêtes ciblées",
|
||||||
"unlock_contacts_title": "Débloquez des contacts avec un plan supérieur.",
|
"unlock_contacts_title": "Débloquez des contacts avec un plan supérieur.",
|
||||||
@@ -1962,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "Réponses filtrées (Excel)",
|
"filtered_responses_excel": "Réponses filtrées (Excel)",
|
||||||
"generating_qr_code": "Génération du code QR",
|
"generating_qr_code": "Génération du code QR",
|
||||||
"impressions": "Impressions",
|
"impressions": "Impressions",
|
||||||
"impressions_identified_only": "Affichage uniquement des impressions des contacts identifiés",
|
|
||||||
"impressions_tooltip": "Nombre de fois que l'enquête a été consultée.",
|
"impressions_tooltip": "Nombre de fois que l'enquête a été consultée.",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "Le sondage sera affiché aux utilisateurs de votre site web, qui correspondent aux critères listés ci-dessous",
|
"connection_description": "Le sondage sera affiché aux utilisateurs de votre site web, qui correspondent aux critères listés ci-dessous",
|
||||||
@@ -2005,7 +1993,6 @@
|
|||||||
"last_quarter": "dernier trimestre",
|
"last_quarter": "dernier trimestre",
|
||||||
"last_year": "l'année dernière",
|
"last_year": "l'année dernière",
|
||||||
"limit": "Limite",
|
"limit": "Limite",
|
||||||
"no_identified_impressions": "Aucune impression des contacts identifiés",
|
|
||||||
"no_responses_found": "Aucune réponse trouvée",
|
"no_responses_found": "Aucune réponse trouvée",
|
||||||
"other_values_found": "D'autres valeurs trouvées",
|
"other_values_found": "D'autres valeurs trouvées",
|
||||||
"overall": "Globalement",
|
"overall": "Globalement",
|
||||||
|
|||||||
@@ -218,16 +218,13 @@
|
|||||||
"error": "Hiba",
|
"error": "Hiba",
|
||||||
"error_component_description": "Ez az erőforrás nem létezik, vagy nem rendelkezik a hozzáféréshez szükséges jogosultságokkal.",
|
"error_component_description": "Ez az erőforrás nem létezik, vagy nem rendelkezik a hozzáféréshez szükséges jogosultságokkal.",
|
||||||
"error_component_title": "Hiba az erőforrások betöltésekor",
|
"error_component_title": "Hiba az erőforrások betöltésekor",
|
||||||
"error_loading_data": "Hiba az adatok betöltése során",
|
|
||||||
"error_rate_limit_description": "A kérések legnagyobb száma elérve. Próbálja meg később újra.",
|
"error_rate_limit_description": "A kérések legnagyobb száma elérve. Próbálja meg később újra.",
|
||||||
"error_rate_limit_title": "A sebességkorlát elérve",
|
"error_rate_limit_title": "A sebességkorlát elérve",
|
||||||
"expand_rows": "Sorok kinyitása",
|
"expand_rows": "Sorok kinyitása",
|
||||||
"failed_to_copy_to_clipboard": "Nem sikerült másolni a vágólapra",
|
"failed_to_copy_to_clipboard": "Nem sikerült másolni a vágólapra",
|
||||||
"failed_to_load_organizations": "Nem sikerült betölteni a szervezeteket",
|
"failed_to_load_organizations": "Nem sikerült betölteni a szervezeteket",
|
||||||
"failed_to_load_workspaces": "Nem sikerült a munkaterületek betöltése",
|
"failed_to_load_workspaces": "Nem sikerült a munkaterületek betöltése",
|
||||||
"filter": "Szűrő",
|
|
||||||
"finish": "Befejezés",
|
"finish": "Befejezés",
|
||||||
"first_name": "Keresztnév",
|
|
||||||
"follow_these": "Ezek követése",
|
"follow_these": "Ezek követése",
|
||||||
"formbricks_version": "Formbricks verziója",
|
"formbricks_version": "Formbricks verziója",
|
||||||
"full_name": "Teljes név",
|
"full_name": "Teljes név",
|
||||||
@@ -240,7 +237,6 @@
|
|||||||
"hidden_field": "Rejtett mező",
|
"hidden_field": "Rejtett mező",
|
||||||
"hidden_fields": "Rejtett mezők",
|
"hidden_fields": "Rejtett mezők",
|
||||||
"hide_column": "Oszlop elrejtése",
|
"hide_column": "Oszlop elrejtése",
|
||||||
"id": "ID",
|
|
||||||
"image": "Kép",
|
"image": "Kép",
|
||||||
"images": "Képek",
|
"images": "Képek",
|
||||||
"import": "Importálás",
|
"import": "Importálás",
|
||||||
@@ -258,7 +254,6 @@
|
|||||||
"key": "Kulcs",
|
"key": "Kulcs",
|
||||||
"label": "Címke",
|
"label": "Címke",
|
||||||
"language": "Nyelv",
|
"language": "Nyelv",
|
||||||
"last_name": "Vezetéknév",
|
|
||||||
"learn_more": "Tudjon meg többet",
|
"learn_more": "Tudjon meg többet",
|
||||||
"license_expired": "A licenc lejárt",
|
"license_expired": "A licenc lejárt",
|
||||||
"light_overlay": "Világos rávetítés",
|
"light_overlay": "Világos rávetítés",
|
||||||
@@ -433,7 +428,6 @@
|
|||||||
"top_right": "Jobbra fent",
|
"top_right": "Jobbra fent",
|
||||||
"try_again": "Próbálja újra",
|
"try_again": "Próbálja újra",
|
||||||
"type": "Típus",
|
"type": "Típus",
|
||||||
"unknown_survey": "Ismeretlen kérdőív",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "Több munkaterület feloldása egy magasabb csomaggal.",
|
"unlock_more_workspaces_with_a_higher_plan": "Több munkaterület feloldása egy magasabb csomaggal.",
|
||||||
"update": "Frissítés",
|
"update": "Frissítés",
|
||||||
"updated": "Frissítve",
|
"updated": "Frissítve",
|
||||||
@@ -661,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {Ez törölni fogja a kiválasztott attribútumot. Az ehhez az attribútumhoz hozzárendelt összes partneradat el fog veszni.} other {Ez törölni fogja a kiválasztott attribútumokat. Az ezekhez az attribútumokhoz hozzárendelt összes partneradat el fog veszni.}}",
|
"delete_attribute_confirmation": "{value, plural, one {Ez törölni fogja a kiválasztott attribútumot. Az ehhez az attribútumhoz hozzárendelt összes partneradat el fog veszni.} other {Ez törölni fogja a kiválasztott attribútumokat. Az ezekhez az attribútumokhoz hozzárendelt összes partneradat el fog veszni.}}",
|
||||||
"delete_contact_confirmation": "Ez törölni fogja az ehhez a partnerhez tartozó összes kérdőívválaszt és partnerattribútumot. A partner adatain alapuló bármilyen célzás és személyre szabás el fog veszni.",
|
"delete_contact_confirmation": "Ez törölni fogja az ehhez a partnerhez tartozó összes kérdőívválaszt és partnerattribútumot. A partner adatain alapuló bármilyen célzás és személyre szabás el fog veszni.",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {Ez törölni fogja az ehhez a partnerhez tartozó összes kérdőívválaszt és partnerattribútumot. A partner adatain alapuló bármilyen célzás és személyre szabás el fog veszni. Ha ez a partner olyan válaszokkal rendelkezik, amelyek a kérdőívkvótákba beletartoznak, akkor a kvóta számlálója csökkentve lesz, de a kvóta korlátai változatlanok maradnak.} other {Ez törölni fogja az ezekhez a partnerekhez tartozó összes kérdőívválaszt és partnerattribútumot. A partnerek adatain alapuló bármilyen célzás és személyre szabás el fog veszni. Ha ezek a partnerek olyan válaszokkal rendelkeznek, amelyek a kérdőívkvótákba beletartoznak, akkor a kvóta számlálója csökkentve lesz, de a kvóta korlátai változatlanok maradnak.}}",
|
"delete_contact_confirmation_with_quotas": "{value, plural, one {Ez törölni fogja az ehhez a partnerhez tartozó összes kérdőívválaszt és partnerattribútumot. A partner adatain alapuló bármilyen célzás és személyre szabás el fog veszni. Ha ez a partner olyan válaszokkal rendelkezik, amelyek a kérdőívkvótákba beletartoznak, akkor a kvóta számlálója csökkentve lesz, de a kvóta korlátai változatlanok maradnak.} other {Ez törölni fogja az ezekhez a partnerekhez tartozó összes kérdőívválaszt és partnerattribútumot. A partnerek adatain alapuló bármilyen célzás és személyre szabás el fog veszni. Ha ezek a partnerek olyan válaszokkal rendelkeznek, amelyek a kérdőívkvótákba beletartoznak, akkor a kvóta számlálója csökkentve lesz, de a kvóta korlátai változatlanok maradnak.}}",
|
||||||
"displays": "Megjelenítések",
|
|
||||||
"edit_attribute": "Attribútum szerkesztése",
|
"edit_attribute": "Attribútum szerkesztése",
|
||||||
"edit_attribute_description": "Az attribútum címkéjének és leírásának frissítése.",
|
"edit_attribute_description": "Az attribútum címkéjének és leírásának frissítése.",
|
||||||
"edit_attribute_values": "Attribútumok szerkesztése",
|
"edit_attribute_values": "Attribútumok szerkesztése",
|
||||||
@@ -673,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "Érvénytelen CSV oszlopnév(nevek): {columns}. Az új attribútumokká váló oszlopnevek csak kisbetűket, számokat és aláhúzásjeleket tartalmazhatnak, és betűvel kell kezdődniük.",
|
"invalid_csv_column_names": "Érvénytelen CSV oszlopnév(nevek): {columns}. Az új attribútumokká váló oszlopnevek csak kisbetűket, számokat és aláhúzásjeleket tartalmazhatnak, és betűvel kell kezdődniük.",
|
||||||
"invalid_date_format": "Érvénytelen dátumformátum. Kérlek, adj meg egy érvényes dátumot.",
|
"invalid_date_format": "Érvénytelen dátumformátum. Kérlek, adj meg egy érvényes dátumot.",
|
||||||
"invalid_number_format": "Érvénytelen számformátum. Kérlek, adj meg egy érvényes számot.",
|
"invalid_number_format": "Érvénytelen számformátum. Kérlek, adj meg egy érvényes számot.",
|
||||||
"no_activity_yet": "Még nincs aktivitás",
|
|
||||||
"no_published_link_surveys_available": "Nem érhetők el közzétett hivatkozás-kérdőívek. Először tegyen közzé egy hivatkozás-kérdőívet.",
|
"no_published_link_surveys_available": "Nem érhetők el közzétett hivatkozás-kérdőívek. Először tegyen közzé egy hivatkozás-kérdőívet.",
|
||||||
"no_published_surveys": "Nincsenek közzétett kérdőívek",
|
"no_published_surveys": "Nincsenek közzétett kérdőívek",
|
||||||
"no_responses_found": "Nem találhatók válaszok",
|
"no_responses_found": "Nem találhatók válaszok",
|
||||||
@@ -688,8 +680,6 @@
|
|||||||
"select_a_survey": "Kérdőív kiválasztása",
|
"select_a_survey": "Kérdőív kiválasztása",
|
||||||
"select_attribute": "Attribútum kiválasztása",
|
"select_attribute": "Attribútum kiválasztása",
|
||||||
"select_attribute_key": "Attribútum kulcs kiválasztása",
|
"select_attribute_key": "Attribútum kulcs kiválasztása",
|
||||||
"survey_viewed": "Kérdőív megtekintve",
|
|
||||||
"survey_viewed_at": "Megtekintve",
|
|
||||||
"system_attributes": "Rendszer attribútumok",
|
"system_attributes": "Rendszer attribútumok",
|
||||||
"unlock_contacts_description": "Partnerek kezelése és célzott kérdőívek kiküldése",
|
"unlock_contacts_description": "Partnerek kezelése és célzott kérdőívek kiküldése",
|
||||||
"unlock_contacts_title": "Partnerek feloldása egy magasabb csomaggal",
|
"unlock_contacts_title": "Partnerek feloldása egy magasabb csomaggal",
|
||||||
@@ -1961,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "Szűrt válaszok (Excel)",
|
"filtered_responses_excel": "Szűrt válaszok (Excel)",
|
||||||
"generating_qr_code": "QR-kód előállítása",
|
"generating_qr_code": "QR-kód előállítása",
|
||||||
"impressions": "Benyomások",
|
"impressions": "Benyomások",
|
||||||
"impressions_identified_only": "Csak az azonosított kapcsolatok megjelenítései láthatók",
|
|
||||||
"impressions_tooltip": "A kérdőív megtekintési alkalmainak száma.",
|
"impressions_tooltip": "A kérdőív megtekintési alkalmainak száma.",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "A kérdőív a webhelye azon felhasználóinak lesz megjelenítve, akik megfelelnek az alább felsorolt feltételeknek",
|
"connection_description": "A kérdőív a webhelye azon felhasználóinak lesz megjelenítve, akik megfelelnek az alább felsorolt feltételeknek",
|
||||||
@@ -2004,7 +1993,6 @@
|
|||||||
"last_quarter": "Elmúlt negyedév",
|
"last_quarter": "Elmúlt negyedév",
|
||||||
"last_year": "Elmúlt év",
|
"last_year": "Elmúlt év",
|
||||||
"limit": "Korlát",
|
"limit": "Korlát",
|
||||||
"no_identified_impressions": "Nincsenek megjelenítések azonosított kapcsolatoktól",
|
|
||||||
"no_responses_found": "Nem találhatók válaszok",
|
"no_responses_found": "Nem találhatók válaszok",
|
||||||
"other_values_found": "Más értékek találhatók",
|
"other_values_found": "Más értékek találhatók",
|
||||||
"overall": "Összesen",
|
"overall": "Összesen",
|
||||||
|
|||||||
@@ -218,16 +218,13 @@
|
|||||||
"error": "エラー",
|
"error": "エラー",
|
||||||
"error_component_description": "この リソース は 存在 しない か、アクセス する ための 必要な 権限 が ありません。",
|
"error_component_description": "この リソース は 存在 しない か、アクセス する ための 必要な 権限 が ありません。",
|
||||||
"error_component_title": "リソース の 読み込み エラー",
|
"error_component_title": "リソース の 読み込み エラー",
|
||||||
"error_loading_data": "データの読み込みエラー",
|
|
||||||
"error_rate_limit_description": "リクエストの最大数に達しました。後でもう一度試してください。",
|
"error_rate_limit_description": "リクエストの最大数に達しました。後でもう一度試してください。",
|
||||||
"error_rate_limit_title": "レート制限を超えました",
|
"error_rate_limit_title": "レート制限を超えました",
|
||||||
"expand_rows": "行を展開",
|
"expand_rows": "行を展開",
|
||||||
"failed_to_copy_to_clipboard": "クリップボードへのコピーに失敗しました",
|
"failed_to_copy_to_clipboard": "クリップボードへのコピーに失敗しました",
|
||||||
"failed_to_load_organizations": "組織の読み込みに失敗しました",
|
"failed_to_load_organizations": "組織の読み込みに失敗しました",
|
||||||
"failed_to_load_workspaces": "ワークスペースの読み込みに失敗しました",
|
"failed_to_load_workspaces": "ワークスペースの読み込みに失敗しました",
|
||||||
"filter": "フィルター",
|
|
||||||
"finish": "完了",
|
"finish": "完了",
|
||||||
"first_name": "名",
|
|
||||||
"follow_these": "こちらの手順に従って",
|
"follow_these": "こちらの手順に従って",
|
||||||
"formbricks_version": "Formbricksバージョン",
|
"formbricks_version": "Formbricksバージョン",
|
||||||
"full_name": "氏名",
|
"full_name": "氏名",
|
||||||
@@ -240,7 +237,6 @@
|
|||||||
"hidden_field": "非表示フィールド",
|
"hidden_field": "非表示フィールド",
|
||||||
"hidden_fields": "非表示フィールド",
|
"hidden_fields": "非表示フィールド",
|
||||||
"hide_column": "列を非表示",
|
"hide_column": "列を非表示",
|
||||||
"id": "ID",
|
|
||||||
"image": "画像",
|
"image": "画像",
|
||||||
"images": "画像",
|
"images": "画像",
|
||||||
"import": "インポート",
|
"import": "インポート",
|
||||||
@@ -258,7 +254,6 @@
|
|||||||
"key": "キー",
|
"key": "キー",
|
||||||
"label": "ラベル",
|
"label": "ラベル",
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
"last_name": "姓",
|
|
||||||
"learn_more": "詳細を見る",
|
"learn_more": "詳細を見る",
|
||||||
"license_expired": "License Expired",
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "明るいオーバーレイ",
|
"light_overlay": "明るいオーバーレイ",
|
||||||
@@ -433,7 +428,6 @@
|
|||||||
"top_right": "右上",
|
"top_right": "右上",
|
||||||
"try_again": "もう一度お試しください",
|
"try_again": "もう一度お試しください",
|
||||||
"type": "種類",
|
"type": "種類",
|
||||||
"unknown_survey": "不明なフォーム",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "上位プランでより多くのワークスペースを利用できます。",
|
"unlock_more_workspaces_with_a_higher_plan": "上位プランでより多くのワークスペースを利用できます。",
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
"updated": "更新済み",
|
"updated": "更新済み",
|
||||||
@@ -661,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {選択した属性を削除します。この属性に関連付けられたすべてのコンタクトデータは失われます。} other {選択した属性を削除します。これらの属性に関連付けられたすべてのコンタクトデータは失われます。}}",
|
"delete_attribute_confirmation": "{value, plural, one {選択した属性を削除します。この属性に関連付けられたすべてのコンタクトデータは失われます。} other {選択した属性を削除します。これらの属性に関連付けられたすべてのコンタクトデータは失われます。}}",
|
||||||
"delete_contact_confirmation": "これにより、この連絡先に関連付けられているすべてのフォーム回答と連絡先属性が削除されます。この連絡先のデータに基づいたターゲティングとパーソナライゼーションはすべて失われます。",
|
"delete_contact_confirmation": "これにより、この連絡先に関連付けられているすべてのフォーム回答と連絡先属性が削除されます。この連絡先のデータに基づいたターゲティングとパーソナライゼーションはすべて失われます。",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {これにより この連絡先に関連するすべてのアンケート応答と連絡先属性が削除されます。この連絡先のデータに基づくターゲティングとパーソナライゼーションが失われます。この連絡先がアンケートの割当量を考慮した回答を持っている場合、割当量カウントは減少しますが、割当量の制限は変更されません。} other {これにより これらの連絡先に関連するすべてのアンケート応答と連絡先属性が削除されます。これらの連絡先のデータに基づくターゲティングとパーソナライゼーションが失われます。これらの連絡先がアンケートの割当量を考慮した回答を持っている場合、割当量カウントは減少しますが、割当量の制限は変更されません。}}",
|
"delete_contact_confirmation_with_quotas": "{value, plural, one {これにより この連絡先に関連するすべてのアンケート応答と連絡先属性が削除されます。この連絡先のデータに基づくターゲティングとパーソナライゼーションが失われます。この連絡先がアンケートの割当量を考慮した回答を持っている場合、割当量カウントは減少しますが、割当量の制限は変更されません。} other {これにより これらの連絡先に関連するすべてのアンケート応答と連絡先属性が削除されます。これらの連絡先のデータに基づくターゲティングとパーソナライゼーションが失われます。これらの連絡先がアンケートの割当量を考慮した回答を持っている場合、割当量カウントは減少しますが、割当量の制限は変更されません。}}",
|
||||||
"displays": "表示回数",
|
|
||||||
"edit_attribute": "属性を編集",
|
"edit_attribute": "属性を編集",
|
||||||
"edit_attribute_description": "この属性のラベルと説明を更新します。",
|
"edit_attribute_description": "この属性のラベルと説明を更新します。",
|
||||||
"edit_attribute_values": "属性を編集",
|
"edit_attribute_values": "属性を編集",
|
||||||
@@ -673,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "無効なCSV列名: {columns}。新しい属性となる列名は、小文字、数字、アンダースコアのみを含み、文字で始まる必要があります。",
|
"invalid_csv_column_names": "無効なCSV列名: {columns}。新しい属性となる列名は、小文字、数字、アンダースコアのみを含み、文字で始まる必要があります。",
|
||||||
"invalid_date_format": "無効な日付形式です。有効な日付を使用してください。",
|
"invalid_date_format": "無効な日付形式です。有効な日付を使用してください。",
|
||||||
"invalid_number_format": "無効な数値形式です。有効な数値を入力してください。",
|
"invalid_number_format": "無効な数値形式です。有効な数値を入力してください。",
|
||||||
"no_activity_yet": "まだアクティビティがありません",
|
|
||||||
"no_published_link_surveys_available": "公開されたリンクフォームはありません。まずリンクフォームを公開してください。",
|
"no_published_link_surveys_available": "公開されたリンクフォームはありません。まずリンクフォームを公開してください。",
|
||||||
"no_published_surveys": "公開されたフォームはありません",
|
"no_published_surveys": "公開されたフォームはありません",
|
||||||
"no_responses_found": "回答が見つかりません",
|
"no_responses_found": "回答が見つかりません",
|
||||||
@@ -688,8 +680,6 @@
|
|||||||
"select_a_survey": "フォームを選択",
|
"select_a_survey": "フォームを選択",
|
||||||
"select_attribute": "属性を選択",
|
"select_attribute": "属性を選択",
|
||||||
"select_attribute_key": "属性キーを選択",
|
"select_attribute_key": "属性キーを選択",
|
||||||
"survey_viewed": "フォームを閲覧",
|
|
||||||
"survey_viewed_at": "閲覧日時",
|
|
||||||
"system_attributes": "システム属性",
|
"system_attributes": "システム属性",
|
||||||
"unlock_contacts_description": "連絡先を管理し、特定のフォームを送信します",
|
"unlock_contacts_description": "連絡先を管理し、特定のフォームを送信します",
|
||||||
"unlock_contacts_title": "上位プランで連絡先をアンロック",
|
"unlock_contacts_title": "上位プランで連絡先をアンロック",
|
||||||
@@ -1961,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "フィルター済み回答 (Excel)",
|
"filtered_responses_excel": "フィルター済み回答 (Excel)",
|
||||||
"generating_qr_code": "QRコードを生成中",
|
"generating_qr_code": "QRコードを生成中",
|
||||||
"impressions": "表示回数",
|
"impressions": "表示回数",
|
||||||
"impressions_identified_only": "識別済みコンタクトからのインプレッションのみを表示しています",
|
|
||||||
"impressions_tooltip": "フォームが表示された回数。",
|
"impressions_tooltip": "フォームが表示された回数。",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "このフォームは、以下の条件に一致するあなたのウェブサイトのユーザーに表示されます",
|
"connection_description": "このフォームは、以下の条件に一致するあなたのウェブサイトのユーザーに表示されます",
|
||||||
@@ -2004,7 +1993,6 @@
|
|||||||
"last_quarter": "前四半期",
|
"last_quarter": "前四半期",
|
||||||
"last_year": "昨年",
|
"last_year": "昨年",
|
||||||
"limit": "制限",
|
"limit": "制限",
|
||||||
"no_identified_impressions": "識別済みコンタクトからのインプレッションはありません",
|
|
||||||
"no_responses_found": "回答が見つかりません",
|
"no_responses_found": "回答が見つかりません",
|
||||||
"other_values_found": "他の値が見つかりました",
|
"other_values_found": "他の値が見つかりました",
|
||||||
"overall": "全体",
|
"overall": "全体",
|
||||||
|
|||||||
@@ -218,16 +218,13 @@
|
|||||||
"error": "Fout",
|
"error": "Fout",
|
||||||
"error_component_description": "Deze bron bestaat niet of u beschikt niet over de benodigde toegangsrechten.",
|
"error_component_description": "Deze bron bestaat niet of u beschikt niet over de benodigde toegangsrechten.",
|
||||||
"error_component_title": "Fout bij het laden van bronnen",
|
"error_component_title": "Fout bij het laden van bronnen",
|
||||||
"error_loading_data": "Fout bij het laden van gegevens",
|
|
||||||
"error_rate_limit_description": "Maximaal aantal verzoeken bereikt. Probeer het later opnieuw.",
|
"error_rate_limit_description": "Maximaal aantal verzoeken bereikt. Probeer het later opnieuw.",
|
||||||
"error_rate_limit_title": "Tarieflimiet overschreden",
|
"error_rate_limit_title": "Tarieflimiet overschreden",
|
||||||
"expand_rows": "Vouw rijen uit",
|
"expand_rows": "Vouw rijen uit",
|
||||||
"failed_to_copy_to_clipboard": "Kopiëren naar klembord mislukt",
|
"failed_to_copy_to_clipboard": "Kopiëren naar klembord mislukt",
|
||||||
"failed_to_load_organizations": "Laden van organisaties mislukt",
|
"failed_to_load_organizations": "Laden van organisaties mislukt",
|
||||||
"failed_to_load_workspaces": "Laden van werkruimtes mislukt",
|
"failed_to_load_workspaces": "Laden van werkruimtes mislukt",
|
||||||
"filter": "Filter",
|
|
||||||
"finish": "Finish",
|
"finish": "Finish",
|
||||||
"first_name": "Voornaam",
|
|
||||||
"follow_these": "Volg deze",
|
"follow_these": "Volg deze",
|
||||||
"formbricks_version": "Formbricks-versie",
|
"formbricks_version": "Formbricks-versie",
|
||||||
"full_name": "Volledige naam",
|
"full_name": "Volledige naam",
|
||||||
@@ -240,7 +237,6 @@
|
|||||||
"hidden_field": "Verborgen veld",
|
"hidden_field": "Verborgen veld",
|
||||||
"hidden_fields": "Verborgen velden",
|
"hidden_fields": "Verborgen velden",
|
||||||
"hide_column": "Kolom verbergen",
|
"hide_column": "Kolom verbergen",
|
||||||
"id": "ID",
|
|
||||||
"image": "Afbeelding",
|
"image": "Afbeelding",
|
||||||
"images": "Afbeeldingen",
|
"images": "Afbeeldingen",
|
||||||
"import": "Importeren",
|
"import": "Importeren",
|
||||||
@@ -258,7 +254,6 @@
|
|||||||
"key": "Sleutel",
|
"key": "Sleutel",
|
||||||
"label": "Label",
|
"label": "Label",
|
||||||
"language": "Taal",
|
"language": "Taal",
|
||||||
"last_name": "Achternaam",
|
|
||||||
"learn_more": "Meer informatie",
|
"learn_more": "Meer informatie",
|
||||||
"license_expired": "License Expired",
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Lichte overlay",
|
"light_overlay": "Lichte overlay",
|
||||||
@@ -433,7 +428,6 @@
|
|||||||
"top_right": "Rechtsboven",
|
"top_right": "Rechtsboven",
|
||||||
"try_again": "Probeer het opnieuw",
|
"try_again": "Probeer het opnieuw",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"unknown_survey": "Onbekende enquête",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "Ontgrendel meer werkruimtes met een hoger abonnement.",
|
"unlock_more_workspaces_with_a_higher_plan": "Ontgrendel meer werkruimtes met een hoger abonnement.",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"updated": "Bijgewerkt",
|
"updated": "Bijgewerkt",
|
||||||
@@ -661,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {Dit verwijdert het geselecteerde attribuut. Alle contactgegevens die aan dit attribuut zijn gekoppeld, gaan verloren.} other {Dit verwijdert de geselecteerde attributen. Alle contactgegevens die aan deze attributen zijn gekoppeld, gaan verloren.}}",
|
"delete_attribute_confirmation": "{value, plural, one {Dit verwijdert het geselecteerde attribuut. Alle contactgegevens die aan dit attribuut zijn gekoppeld, gaan verloren.} other {Dit verwijdert de geselecteerde attributen. Alle contactgegevens die aan deze attributen zijn gekoppeld, gaan verloren.}}",
|
||||||
"delete_contact_confirmation": "Hierdoor worden alle enquêtereacties en contactkenmerken verwijderd die aan dit contact zijn gekoppeld. Elke targeting en personalisatie op basis van de gegevens van dit contact gaat verloren.",
|
"delete_contact_confirmation": "Hierdoor worden alle enquêtereacties en contactkenmerken verwijderd die aan dit contact zijn gekoppeld. Elke targeting en personalisatie op basis van de gegevens van dit contact gaat verloren.",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {Dit verwijdert alle enquêteresultaten en contactattributen die aan dit contact zijn gekoppeld. Alle targeting en personalisatie op basis van de gegevens van dit contact gaan verloren. Als dit contact reacties heeft die meetellen voor enquêtekvota, worden de quotawaarden verlaagd maar blijven de limieten ongewijzigd.} other {Dit verwijdert alle enquêteresultaten en contactattributen die aan deze contacten zijn gekoppeld. Alle targeting en personalisatie op basis van de gegevens van deze contacten gaan verloren. Als deze contacten reacties hebben die meetellen voor enquêtekvota, worden de quotawaarden verlaagd maar blijven de limieten ongewijzigd.}}",
|
"delete_contact_confirmation_with_quotas": "{value, plural, one {Dit verwijdert alle enquêteresultaten en contactattributen die aan dit contact zijn gekoppeld. Alle targeting en personalisatie op basis van de gegevens van dit contact gaan verloren. Als dit contact reacties heeft die meetellen voor enquêtekvota, worden de quotawaarden verlaagd maar blijven de limieten ongewijzigd.} other {Dit verwijdert alle enquêteresultaten en contactattributen die aan deze contacten zijn gekoppeld. Alle targeting en personalisatie op basis van de gegevens van deze contacten gaan verloren. Als deze contacten reacties hebben die meetellen voor enquêtekvota, worden de quotawaarden verlaagd maar blijven de limieten ongewijzigd.}}",
|
||||||
"displays": "Weergaven",
|
|
||||||
"edit_attribute": "Attribuut bewerken",
|
"edit_attribute": "Attribuut bewerken",
|
||||||
"edit_attribute_description": "Werk het label en de beschrijving voor dit attribuut bij.",
|
"edit_attribute_description": "Werk het label en de beschrijving voor dit attribuut bij.",
|
||||||
"edit_attribute_values": "Attributen bewerken",
|
"edit_attribute_values": "Attributen bewerken",
|
||||||
@@ -673,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "Ongeldige CSV-kolomna(a)m(en): {columns}. Kolomnamen die nieuwe kenmerken worden, mogen alleen kleine letters, cijfers en underscores bevatten en moeten beginnen met een letter.",
|
"invalid_csv_column_names": "Ongeldige CSV-kolomna(a)m(en): {columns}. Kolomnamen die nieuwe kenmerken worden, mogen alleen kleine letters, cijfers en underscores bevatten en moeten beginnen met een letter.",
|
||||||
"invalid_date_format": "Ongeldig datumformaat. Gebruik een geldige datum.",
|
"invalid_date_format": "Ongeldig datumformaat. Gebruik een geldige datum.",
|
||||||
"invalid_number_format": "Ongeldig getalformaat. Voer een geldig getal in.",
|
"invalid_number_format": "Ongeldig getalformaat. Voer een geldig getal in.",
|
||||||
"no_activity_yet": "Nog geen activiteit",
|
|
||||||
"no_published_link_surveys_available": "Geen gepubliceerde link-enquêtes beschikbaar. Publiceer eerst een link-enquête.",
|
"no_published_link_surveys_available": "Geen gepubliceerde link-enquêtes beschikbaar. Publiceer eerst een link-enquête.",
|
||||||
"no_published_surveys": "Geen gepubliceerde enquêtes",
|
"no_published_surveys": "Geen gepubliceerde enquêtes",
|
||||||
"no_responses_found": "Geen reacties gevonden",
|
"no_responses_found": "Geen reacties gevonden",
|
||||||
@@ -688,8 +680,6 @@
|
|||||||
"select_a_survey": "Selecteer een enquête",
|
"select_a_survey": "Selecteer een enquête",
|
||||||
"select_attribute": "Selecteer Kenmerk",
|
"select_attribute": "Selecteer Kenmerk",
|
||||||
"select_attribute_key": "Selecteer kenmerksleutel",
|
"select_attribute_key": "Selecteer kenmerksleutel",
|
||||||
"survey_viewed": "Enquête bekeken",
|
|
||||||
"survey_viewed_at": "Bekeken op",
|
|
||||||
"system_attributes": "Systeemkenmerken",
|
"system_attributes": "Systeemkenmerken",
|
||||||
"unlock_contacts_description": "Beheer contacten en verstuur gerichte enquêtes",
|
"unlock_contacts_description": "Beheer contacten en verstuur gerichte enquêtes",
|
||||||
"unlock_contacts_title": "Ontgrendel contacten met een hoger abonnement",
|
"unlock_contacts_title": "Ontgrendel contacten met een hoger abonnement",
|
||||||
@@ -1961,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "Gefilterde reacties (Excel)",
|
"filtered_responses_excel": "Gefilterde reacties (Excel)",
|
||||||
"generating_qr_code": "QR-code genereren",
|
"generating_qr_code": "QR-code genereren",
|
||||||
"impressions": "Indrukken",
|
"impressions": "Indrukken",
|
||||||
"impressions_identified_only": "Alleen weergaven van geïdentificeerde contacten worden getoond",
|
|
||||||
"impressions_tooltip": "Aantal keren dat de enquête is bekeken.",
|
"impressions_tooltip": "Aantal keren dat de enquête is bekeken.",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "De enquête wordt getoond aan gebruikers van uw website die voldoen aan de onderstaande criteria",
|
"connection_description": "De enquête wordt getoond aan gebruikers van uw website die voldoen aan de onderstaande criteria",
|
||||||
@@ -2004,7 +1993,6 @@
|
|||||||
"last_quarter": "Laatste kwartaal",
|
"last_quarter": "Laatste kwartaal",
|
||||||
"last_year": "Vorig jaar",
|
"last_year": "Vorig jaar",
|
||||||
"limit": "Beperken",
|
"limit": "Beperken",
|
||||||
"no_identified_impressions": "Geen weergaven van geïdentificeerde contacten",
|
|
||||||
"no_responses_found": "Geen reacties gevonden",
|
"no_responses_found": "Geen reacties gevonden",
|
||||||
"other_values_found": "Andere waarden gevonden",
|
"other_values_found": "Andere waarden gevonden",
|
||||||
"overall": "Algemeen",
|
"overall": "Algemeen",
|
||||||
|
|||||||
@@ -218,16 +218,13 @@
|
|||||||
"error": "Erro",
|
"error": "Erro",
|
||||||
"error_component_description": "Esse recurso não existe ou você não tem permissão para acessá-lo.",
|
"error_component_description": "Esse recurso não existe ou você não tem permissão para acessá-lo.",
|
||||||
"error_component_title": "Erro ao carregar recursos",
|
"error_component_title": "Erro ao carregar recursos",
|
||||||
"error_loading_data": "Erro ao carregar dados",
|
|
||||||
"error_rate_limit_description": "Número máximo de requisições atingido. Por favor, tente novamente mais tarde.",
|
"error_rate_limit_description": "Número máximo de requisições atingido. Por favor, tente novamente mais tarde.",
|
||||||
"error_rate_limit_title": "Limite de Taxa Excedido",
|
"error_rate_limit_title": "Limite de Taxa Excedido",
|
||||||
"expand_rows": "Expandir linhas",
|
"expand_rows": "Expandir linhas",
|
||||||
"failed_to_copy_to_clipboard": "Falha ao copiar para a área de transferência",
|
"failed_to_copy_to_clipboard": "Falha ao copiar para a área de transferência",
|
||||||
"failed_to_load_organizations": "Falha ao carregar organizações",
|
"failed_to_load_organizations": "Falha ao carregar organizações",
|
||||||
"failed_to_load_workspaces": "Falha ao carregar projetos",
|
"failed_to_load_workspaces": "Falha ao carregar projetos",
|
||||||
"filter": "Filtro",
|
|
||||||
"finish": "Terminar",
|
"finish": "Terminar",
|
||||||
"first_name": "Primeiro nome",
|
|
||||||
"follow_these": "Siga esses",
|
"follow_these": "Siga esses",
|
||||||
"formbricks_version": "Versão do Formbricks",
|
"formbricks_version": "Versão do Formbricks",
|
||||||
"full_name": "Nome completo",
|
"full_name": "Nome completo",
|
||||||
@@ -240,7 +237,6 @@
|
|||||||
"hidden_field": "Campo oculto",
|
"hidden_field": "Campo oculto",
|
||||||
"hidden_fields": "Campos ocultos",
|
"hidden_fields": "Campos ocultos",
|
||||||
"hide_column": "Ocultar coluna",
|
"hide_column": "Ocultar coluna",
|
||||||
"id": "ID",
|
|
||||||
"image": "imagem",
|
"image": "imagem",
|
||||||
"images": "Imagens",
|
"images": "Imagens",
|
||||||
"import": "importar",
|
"import": "importar",
|
||||||
@@ -258,7 +254,6 @@
|
|||||||
"key": "Chave",
|
"key": "Chave",
|
||||||
"label": "Etiqueta",
|
"label": "Etiqueta",
|
||||||
"language": "Língua",
|
"language": "Língua",
|
||||||
"last_name": "Sobrenome",
|
|
||||||
"learn_more": "Saiba mais",
|
"learn_more": "Saiba mais",
|
||||||
"license_expired": "License Expired",
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "sobreposição leve",
|
"light_overlay": "sobreposição leve",
|
||||||
@@ -433,7 +428,6 @@
|
|||||||
"top_right": "Canto Superior Direito",
|
"top_right": "Canto Superior Direito",
|
||||||
"try_again": "Tenta de novo",
|
"try_again": "Tenta de novo",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
"unknown_survey": "Pesquisa desconhecida",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "Desbloqueie mais projetos com um plano superior.",
|
"unlock_more_workspaces_with_a_higher_plan": "Desbloqueie mais projetos com um plano superior.",
|
||||||
"update": "atualizar",
|
"update": "atualizar",
|
||||||
"updated": "atualizado",
|
"updated": "atualizado",
|
||||||
@@ -661,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {Isso excluirá o atributo selecionado. Todos os dados de contato associados a este atributo serão perdidos.} other {Isso excluirá os atributos selecionados. Todos os dados de contato associados a estes atributos serão perdidos.}}",
|
"delete_attribute_confirmation": "{value, plural, one {Isso excluirá o atributo selecionado. Todos os dados de contato associados a este atributo serão perdidos.} other {Isso excluirá os atributos selecionados. Todos os dados de contato associados a estes atributos serão perdidos.}}",
|
||||||
"delete_contact_confirmation": "Isso irá apagar todas as respostas da pesquisa e atributos de contato associados a este contato. Qualquer direcionamento e personalização baseados nos dados deste contato serão perdidos.",
|
"delete_contact_confirmation": "Isso irá apagar todas as respostas da pesquisa e atributos de contato associados a este contato. Qualquer direcionamento e personalização baseados nos dados deste contato serão perdidos.",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, other {Isso irá apagar todas as respostas da pesquisa e atributos de contato associados a este contato. Qualquer direcionamento e personalização baseados nos dados deste contato serão perdidos. Se este contato tiver respostas que contam para cotas da pesquisa, as contagens das cotas serão reduzidas, mas os limites das cotas permanecerão inalterados.}}",
|
"delete_contact_confirmation_with_quotas": "{value, plural, other {Isso irá apagar todas as respostas da pesquisa e atributos de contato associados a este contato. Qualquer direcionamento e personalização baseados nos dados deste contato serão perdidos. Se este contato tiver respostas que contam para cotas da pesquisa, as contagens das cotas serão reduzidas, mas os limites das cotas permanecerão inalterados.}}",
|
||||||
"displays": "Exibições",
|
|
||||||
"edit_attribute": "Editar atributo",
|
"edit_attribute": "Editar atributo",
|
||||||
"edit_attribute_description": "Atualize a etiqueta e a descrição deste atributo.",
|
"edit_attribute_description": "Atualize a etiqueta e a descrição deste atributo.",
|
||||||
"edit_attribute_values": "Editar atributos",
|
"edit_attribute_values": "Editar atributos",
|
||||||
@@ -673,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "Nome(s) de coluna CSV inválido(s): {columns}. Os nomes de colunas que se tornarão novos atributos devem conter apenas letras minúsculas, números e sublinhados, e devem começar com uma letra.",
|
"invalid_csv_column_names": "Nome(s) de coluna CSV inválido(s): {columns}. Os nomes de colunas que se tornarão novos atributos devem conter apenas letras minúsculas, números e sublinhados, e devem começar com uma letra.",
|
||||||
"invalid_date_format": "Formato de data inválido. Por favor, use uma data válida.",
|
"invalid_date_format": "Formato de data inválido. Por favor, use uma data válida.",
|
||||||
"invalid_number_format": "Formato de número inválido. Por favor, insira um número válido.",
|
"invalid_number_format": "Formato de número inválido. Por favor, insira um número válido.",
|
||||||
"no_activity_yet": "Nenhuma atividade ainda",
|
|
||||||
"no_published_link_surveys_available": "Não há pesquisas de link publicadas disponíveis. Por favor, publique uma pesquisa de link primeiro.",
|
"no_published_link_surveys_available": "Não há pesquisas de link publicadas disponíveis. Por favor, publique uma pesquisa de link primeiro.",
|
||||||
"no_published_surveys": "Sem pesquisas publicadas",
|
"no_published_surveys": "Sem pesquisas publicadas",
|
||||||
"no_responses_found": "Nenhuma resposta encontrada",
|
"no_responses_found": "Nenhuma resposta encontrada",
|
||||||
@@ -688,8 +680,6 @@
|
|||||||
"select_a_survey": "Selecione uma pesquisa",
|
"select_a_survey": "Selecione uma pesquisa",
|
||||||
"select_attribute": "Selecionar Atributo",
|
"select_attribute": "Selecionar Atributo",
|
||||||
"select_attribute_key": "Selecionar chave de atributo",
|
"select_attribute_key": "Selecionar chave de atributo",
|
||||||
"survey_viewed": "Pesquisa visualizada",
|
|
||||||
"survey_viewed_at": "Visualizada em",
|
|
||||||
"system_attributes": "Atributos do sistema",
|
"system_attributes": "Atributos do sistema",
|
||||||
"unlock_contacts_description": "Gerencie contatos e envie pesquisas direcionadas",
|
"unlock_contacts_description": "Gerencie contatos e envie pesquisas direcionadas",
|
||||||
"unlock_contacts_title": "Desbloqueie contatos com um plano superior",
|
"unlock_contacts_title": "Desbloqueie contatos com um plano superior",
|
||||||
@@ -1961,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "Respostas filtradas (Excel)",
|
"filtered_responses_excel": "Respostas filtradas (Excel)",
|
||||||
"generating_qr_code": "Gerando código QR",
|
"generating_qr_code": "Gerando código QR",
|
||||||
"impressions": "Impressões",
|
"impressions": "Impressões",
|
||||||
"impressions_identified_only": "Mostrando apenas impressões de contatos identificados",
|
|
||||||
"impressions_tooltip": "Número de vezes que a pesquisa foi visualizada.",
|
"impressions_tooltip": "Número de vezes que a pesquisa foi visualizada.",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "A pesquisa será exibida para usuários do seu site, que atendam aos critérios listados abaixo",
|
"connection_description": "A pesquisa será exibida para usuários do seu site, que atendam aos critérios listados abaixo",
|
||||||
@@ -2004,7 +1993,6 @@
|
|||||||
"last_quarter": "Último trimestre",
|
"last_quarter": "Último trimestre",
|
||||||
"last_year": "Último ano",
|
"last_year": "Último ano",
|
||||||
"limit": "Limite",
|
"limit": "Limite",
|
||||||
"no_identified_impressions": "Nenhuma impressão de contatos identificados",
|
|
||||||
"no_responses_found": "Nenhuma resposta encontrada",
|
"no_responses_found": "Nenhuma resposta encontrada",
|
||||||
"other_values_found": "Outros valores encontrados",
|
"other_values_found": "Outros valores encontrados",
|
||||||
"overall": "No geral",
|
"overall": "No geral",
|
||||||
|
|||||||
@@ -218,16 +218,13 @@
|
|||||||
"error": "Erro",
|
"error": "Erro",
|
||||||
"error_component_description": "Este recurso não existe ou não tem os direitos necessários para aceder a ele.",
|
"error_component_description": "Este recurso não existe ou não tem os direitos necessários para aceder a ele.",
|
||||||
"error_component_title": "Erro ao carregar recursos",
|
"error_component_title": "Erro ao carregar recursos",
|
||||||
"error_loading_data": "Erro ao carregar dados",
|
|
||||||
"error_rate_limit_description": "Número máximo de pedidos alcançado. Por favor, tente novamente mais tarde.",
|
"error_rate_limit_description": "Número máximo de pedidos alcançado. Por favor, tente novamente mais tarde.",
|
||||||
"error_rate_limit_title": "Limite de Taxa Excedido",
|
"error_rate_limit_title": "Limite de Taxa Excedido",
|
||||||
"expand_rows": "Expandir linhas",
|
"expand_rows": "Expandir linhas",
|
||||||
"failed_to_copy_to_clipboard": "Falha ao copiar para a área de transferência",
|
"failed_to_copy_to_clipboard": "Falha ao copiar para a área de transferência",
|
||||||
"failed_to_load_organizations": "Falha ao carregar organizações",
|
"failed_to_load_organizations": "Falha ao carregar organizações",
|
||||||
"failed_to_load_workspaces": "Falha ao carregar projetos",
|
"failed_to_load_workspaces": "Falha ao carregar projetos",
|
||||||
"filter": "Filtro",
|
|
||||||
"finish": "Concluir",
|
"finish": "Concluir",
|
||||||
"first_name": "Primeiro nome",
|
|
||||||
"follow_these": "Siga estes",
|
"follow_these": "Siga estes",
|
||||||
"formbricks_version": "Versão do Formbricks",
|
"formbricks_version": "Versão do Formbricks",
|
||||||
"full_name": "Nome completo",
|
"full_name": "Nome completo",
|
||||||
@@ -240,7 +237,6 @@
|
|||||||
"hidden_field": "Campo oculto",
|
"hidden_field": "Campo oculto",
|
||||||
"hidden_fields": "Campos ocultos",
|
"hidden_fields": "Campos ocultos",
|
||||||
"hide_column": "Ocultar coluna",
|
"hide_column": "Ocultar coluna",
|
||||||
"id": "ID",
|
|
||||||
"image": "Imagem",
|
"image": "Imagem",
|
||||||
"images": "Imagens",
|
"images": "Imagens",
|
||||||
"import": "Importar",
|
"import": "Importar",
|
||||||
@@ -258,7 +254,6 @@
|
|||||||
"key": "Chave",
|
"key": "Chave",
|
||||||
"label": "Etiqueta",
|
"label": "Etiqueta",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"last_name": "Apelido",
|
|
||||||
"learn_more": "Saiba mais",
|
"learn_more": "Saiba mais",
|
||||||
"license_expired": "License Expired",
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Sobreposição leve",
|
"light_overlay": "Sobreposição leve",
|
||||||
@@ -433,7 +428,6 @@
|
|||||||
"top_right": "Superior Direito",
|
"top_right": "Superior Direito",
|
||||||
"try_again": "Tente novamente",
|
"try_again": "Tente novamente",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
"unknown_survey": "Inquérito desconhecido",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "Desbloqueie mais projetos com um plano superior.",
|
"unlock_more_workspaces_with_a_higher_plan": "Desbloqueie mais projetos com um plano superior.",
|
||||||
"update": "Atualizar",
|
"update": "Atualizar",
|
||||||
"updated": "Atualizado",
|
"updated": "Atualizado",
|
||||||
@@ -661,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {Isto irá eliminar o atributo selecionado. Todos os dados de contacto associados a este atributo serão perdidos.} other {Isto irá eliminar os atributos selecionados. Todos os dados de contacto associados a estes atributos serão perdidos.}}",
|
"delete_attribute_confirmation": "{value, plural, one {Isto irá eliminar o atributo selecionado. Todos os dados de contacto associados a este atributo serão perdidos.} other {Isto irá eliminar os atributos selecionados. Todos os dados de contacto associados a estes atributos serão perdidos.}}",
|
||||||
"delete_contact_confirmation": "Isto irá eliminar todas as respostas das pesquisas e os atributos de contato associados a este contato. Qualquer direcionamento e personalização baseados nos dados deste contato serão perdidos.",
|
"delete_contact_confirmation": "Isto irá eliminar todas as respostas das pesquisas e os atributos de contato associados a este contato. Qualquer direcionamento e personalização baseados nos dados deste contato serão perdidos.",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, other {Isto irá eliminar todas as respostas das pesquisas e os atributos de contacto associados a este contacto. Qualquer segmentação e personalização baseados nos dados deste contacto serão perdidos. Se este contacto tiver respostas que contribuam para as quotas das pesquisas, as contagens de quotas serão reduzidas, mas os limites das quotas permanecerão inalterados.}}",
|
"delete_contact_confirmation_with_quotas": "{value, plural, other {Isto irá eliminar todas as respostas das pesquisas e os atributos de contacto associados a este contacto. Qualquer segmentação e personalização baseados nos dados deste contacto serão perdidos. Se este contacto tiver respostas que contribuam para as quotas das pesquisas, as contagens de quotas serão reduzidas, mas os limites das quotas permanecerão inalterados.}}",
|
||||||
"displays": "Visualizações",
|
|
||||||
"edit_attribute": "Editar atributo",
|
"edit_attribute": "Editar atributo",
|
||||||
"edit_attribute_description": "Atualize a etiqueta e a descrição deste atributo.",
|
"edit_attribute_description": "Atualize a etiqueta e a descrição deste atributo.",
|
||||||
"edit_attribute_values": "Editar atributos",
|
"edit_attribute_values": "Editar atributos",
|
||||||
@@ -673,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "Nome(s) de coluna CSV inválido(s): {columns}. Os nomes de colunas que se tornarão novos atributos devem conter apenas letras minúsculas, números e underscores, e devem começar com uma letra.",
|
"invalid_csv_column_names": "Nome(s) de coluna CSV inválido(s): {columns}. Os nomes de colunas que se tornarão novos atributos devem conter apenas letras minúsculas, números e underscores, e devem começar com uma letra.",
|
||||||
"invalid_date_format": "Formato de data inválido. Por favor, usa uma data válida.",
|
"invalid_date_format": "Formato de data inválido. Por favor, usa uma data válida.",
|
||||||
"invalid_number_format": "Formato de número inválido. Por favor, introduz um número válido.",
|
"invalid_number_format": "Formato de número inválido. Por favor, introduz um número válido.",
|
||||||
"no_activity_yet": "Ainda sem atividade",
|
|
||||||
"no_published_link_surveys_available": "Não existem inquéritos de link publicados disponíveis. Por favor, publique primeiro um inquérito de link.",
|
"no_published_link_surveys_available": "Não existem inquéritos de link publicados disponíveis. Por favor, publique primeiro um inquérito de link.",
|
||||||
"no_published_surveys": "Sem inquéritos publicados",
|
"no_published_surveys": "Sem inquéritos publicados",
|
||||||
"no_responses_found": "Nenhuma resposta encontrada",
|
"no_responses_found": "Nenhuma resposta encontrada",
|
||||||
@@ -688,8 +680,6 @@
|
|||||||
"select_a_survey": "Selecione um inquérito",
|
"select_a_survey": "Selecione um inquérito",
|
||||||
"select_attribute": "Selecionar Atributo",
|
"select_attribute": "Selecionar Atributo",
|
||||||
"select_attribute_key": "Selecionar chave de atributo",
|
"select_attribute_key": "Selecionar chave de atributo",
|
||||||
"survey_viewed": "Inquérito visualizado",
|
|
||||||
"survey_viewed_at": "Visualizado em",
|
|
||||||
"system_attributes": "Atributos do sistema",
|
"system_attributes": "Atributos do sistema",
|
||||||
"unlock_contacts_description": "Gerir contactos e enviar inquéritos direcionados",
|
"unlock_contacts_description": "Gerir contactos e enviar inquéritos direcionados",
|
||||||
"unlock_contacts_title": "Desbloqueie os contactos com um plano superior",
|
"unlock_contacts_title": "Desbloqueie os contactos com um plano superior",
|
||||||
@@ -1961,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "Respostas filtradas (Excel)",
|
"filtered_responses_excel": "Respostas filtradas (Excel)",
|
||||||
"generating_qr_code": "A gerar código QR",
|
"generating_qr_code": "A gerar código QR",
|
||||||
"impressions": "Impressões",
|
"impressions": "Impressões",
|
||||||
"impressions_identified_only": "A mostrar apenas impressões de contactos identificados",
|
|
||||||
"impressions_tooltip": "Número de vezes que o inquérito foi visualizado.",
|
"impressions_tooltip": "Número de vezes que o inquérito foi visualizado.",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "O questionário será exibido aos utilizadores do seu website que correspondam aos critérios listados abaixo",
|
"connection_description": "O questionário será exibido aos utilizadores do seu website que correspondam aos critérios listados abaixo",
|
||||||
@@ -2004,7 +1993,6 @@
|
|||||||
"last_quarter": "Último trimestre",
|
"last_quarter": "Último trimestre",
|
||||||
"last_year": "Ano passado",
|
"last_year": "Ano passado",
|
||||||
"limit": "Limite",
|
"limit": "Limite",
|
||||||
"no_identified_impressions": "Sem impressões de contactos identificados",
|
|
||||||
"no_responses_found": "Nenhuma resposta encontrada",
|
"no_responses_found": "Nenhuma resposta encontrada",
|
||||||
"other_values_found": "Outros valores encontrados",
|
"other_values_found": "Outros valores encontrados",
|
||||||
"overall": "Geral",
|
"overall": "Geral",
|
||||||
|
|||||||
@@ -218,16 +218,13 @@
|
|||||||
"error": "Eroare",
|
"error": "Eroare",
|
||||||
"error_component_description": "Această resursă nu există sau nu aveți drepturile necesare pentru a o accesa.",
|
"error_component_description": "Această resursă nu există sau nu aveți drepturile necesare pentru a o accesa.",
|
||||||
"error_component_title": "Eroare la încărcarea resurselor",
|
"error_component_title": "Eroare la încărcarea resurselor",
|
||||||
"error_loading_data": "Eroare la încărcarea datelor",
|
|
||||||
"error_rate_limit_description": "Numărul maxim de cereri atins. Vă rugăm să încercați din nou mai târziu.",
|
"error_rate_limit_description": "Numărul maxim de cereri atins. Vă rugăm să încercați din nou mai târziu.",
|
||||||
"error_rate_limit_title": "Limită de cereri depășită",
|
"error_rate_limit_title": "Limită de cereri depășită",
|
||||||
"expand_rows": "Extinde rândurile",
|
"expand_rows": "Extinde rândurile",
|
||||||
"failed_to_copy_to_clipboard": "Nu s-a reușit copierea în clipboard",
|
"failed_to_copy_to_clipboard": "Nu s-a reușit copierea în clipboard",
|
||||||
"failed_to_load_organizations": "Nu s-a reușit încărcarea organizațiilor",
|
"failed_to_load_organizations": "Nu s-a reușit încărcarea organizațiilor",
|
||||||
"failed_to_load_workspaces": "Nu s-au putut încărca workspaces",
|
"failed_to_load_workspaces": "Nu s-au putut încărca workspaces",
|
||||||
"filter": "Filtru",
|
|
||||||
"finish": "Finalizează",
|
"finish": "Finalizează",
|
||||||
"first_name": "Prenume",
|
|
||||||
"follow_these": "Urmați acestea",
|
"follow_these": "Urmați acestea",
|
||||||
"formbricks_version": "Versiunea Formbricks",
|
"formbricks_version": "Versiunea Formbricks",
|
||||||
"full_name": "Nume complet",
|
"full_name": "Nume complet",
|
||||||
@@ -240,7 +237,6 @@
|
|||||||
"hidden_field": "Câmp ascuns",
|
"hidden_field": "Câmp ascuns",
|
||||||
"hidden_fields": "Câmpuri ascunse",
|
"hidden_fields": "Câmpuri ascunse",
|
||||||
"hide_column": "Ascunde coloana",
|
"hide_column": "Ascunde coloana",
|
||||||
"id": "ID",
|
|
||||||
"image": "Imagine",
|
"image": "Imagine",
|
||||||
"images": "Imagini",
|
"images": "Imagini",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
@@ -258,7 +254,6 @@
|
|||||||
"key": "Cheie",
|
"key": "Cheie",
|
||||||
"label": "Etichetă",
|
"label": "Etichetă",
|
||||||
"language": "Limba",
|
"language": "Limba",
|
||||||
"last_name": "Nume de familie",
|
|
||||||
"learn_more": "Află mai multe",
|
"learn_more": "Află mai multe",
|
||||||
"license_expired": "License Expired",
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Suprapunere ușoară",
|
"light_overlay": "Suprapunere ușoară",
|
||||||
@@ -433,7 +428,6 @@
|
|||||||
"top_right": "Dreapta Sus",
|
"top_right": "Dreapta Sus",
|
||||||
"try_again": "Încearcă din nou",
|
"try_again": "Încearcă din nou",
|
||||||
"type": "Tip",
|
"type": "Tip",
|
||||||
"unknown_survey": "Chestionar necunoscut",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "Deblochează mai multe workspaces cu un plan superior.",
|
"unlock_more_workspaces_with_a_higher_plan": "Deblochează mai multe workspaces cu un plan superior.",
|
||||||
"update": "Actualizare",
|
"update": "Actualizare",
|
||||||
"updated": "Actualizat",
|
"updated": "Actualizat",
|
||||||
@@ -661,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {Acest lucru va șterge atributul selectat. Orice date de contact asociate cu acest atribut vor fi pierdute.} few {Acest lucru va șterge atributele selectate. Orice date de contact asociate cu aceste atribute vor fi pierdute.} other {Acest lucru va șterge atributele selectate. Orice date de contact asociate cu aceste atribute vor fi pierdute.}}",
|
"delete_attribute_confirmation": "{value, plural, one {Acest lucru va șterge atributul selectat. Orice date de contact asociate cu acest atribut vor fi pierdute.} few {Acest lucru va șterge atributele selectate. Orice date de contact asociate cu aceste atribute vor fi pierdute.} other {Acest lucru va șterge atributele selectate. Orice date de contact asociate cu aceste atribute vor fi pierdute.}}",
|
||||||
"delete_contact_confirmation": "Acest lucru va șterge toate răspunsurile la sondaj și atributele de contact asociate cu acest contact. Orice țintire și personalizare bazată pe datele acestui contact vor fi pierdute.",
|
"delete_contact_confirmation": "Acest lucru va șterge toate răspunsurile la sondaj și atributele de contact asociate cu acest contact. Orice țintire și personalizare bazată pe datele acestui contact vor fi pierdute.",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {Această acțiune va șterge toate răspunsurile chestionarului și atributele de contact asociate cu acest contact. Orice țintire și personalizare bazată pe datele acestui contact vor fi pierdute. Dacă acest contact are răspunsuri care contează pentru cotele chestionarului, numărul cotelor va fi redus, dar limitele cotelor vor rămâne neschimbate.} other {Aceste acțiuni vor șterge toate răspunsurile chestionarului și atributele de contact asociate cu acești contacți. Orice țintire și personalizare bazată pe datele acestor contacți vor fi pierdute. Dacă acești contacți au răspunsuri care contează pentru cotele chestionarului, numărul cotelor va fi redus, dar limitele cotelor vor rămâne neschimbate.} }",
|
"delete_contact_confirmation_with_quotas": "{value, plural, one {Această acțiune va șterge toate răspunsurile chestionarului și atributele de contact asociate cu acest contact. Orice țintire și personalizare bazată pe datele acestui contact vor fi pierdute. Dacă acest contact are răspunsuri care contează pentru cotele chestionarului, numărul cotelor va fi redus, dar limitele cotelor vor rămâne neschimbate.} other {Aceste acțiuni vor șterge toate răspunsurile chestionarului și atributele de contact asociate cu acești contacți. Orice țintire și personalizare bazată pe datele acestor contacți vor fi pierdute. Dacă acești contacți au răspunsuri care contează pentru cotele chestionarului, numărul cotelor va fi redus, dar limitele cotelor vor rămâne neschimbate.} }",
|
||||||
"displays": "Afișări",
|
|
||||||
"edit_attribute": "Editează atributul",
|
"edit_attribute": "Editează atributul",
|
||||||
"edit_attribute_description": "Actualizează eticheta și descrierea acestui atribut.",
|
"edit_attribute_description": "Actualizează eticheta și descrierea acestui atribut.",
|
||||||
"edit_attribute_values": "Editează atributele",
|
"edit_attribute_values": "Editează atributele",
|
||||||
@@ -673,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "Nume de coloană CSV nevalide: {columns}. Numele coloanelor care vor deveni atribute noi trebuie să conțină doar litere mici, cifre și caractere de subliniere și trebuie să înceapă cu o literă.",
|
"invalid_csv_column_names": "Nume de coloană CSV nevalide: {columns}. Numele coloanelor care vor deveni atribute noi trebuie să conțină doar litere mici, cifre și caractere de subliniere și trebuie să înceapă cu o literă.",
|
||||||
"invalid_date_format": "Format de dată invalid. Te rugăm să folosești o dată validă.",
|
"invalid_date_format": "Format de dată invalid. Te rugăm să folosești o dată validă.",
|
||||||
"invalid_number_format": "Format de număr invalid. Te rugăm să introduci un număr valid.",
|
"invalid_number_format": "Format de număr invalid. Te rugăm să introduci un număr valid.",
|
||||||
"no_activity_yet": "Nicio activitate încă",
|
|
||||||
"no_published_link_surveys_available": "Nu există sondaje publicate pentru linkuri disponibile. Vă rugăm să publicați mai întâi un sondaj pentru linkuri.",
|
"no_published_link_surveys_available": "Nu există sondaje publicate pentru linkuri disponibile. Vă rugăm să publicați mai întâi un sondaj pentru linkuri.",
|
||||||
"no_published_surveys": "Nu există sondaje publicate",
|
"no_published_surveys": "Nu există sondaje publicate",
|
||||||
"no_responses_found": "Nu s-au găsit răspunsuri",
|
"no_responses_found": "Nu s-au găsit răspunsuri",
|
||||||
@@ -688,8 +680,6 @@
|
|||||||
"select_a_survey": "Selectați un sondaj",
|
"select_a_survey": "Selectați un sondaj",
|
||||||
"select_attribute": "Selectează atributul",
|
"select_attribute": "Selectează atributul",
|
||||||
"select_attribute_key": "Selectează cheia atributului",
|
"select_attribute_key": "Selectează cheia atributului",
|
||||||
"survey_viewed": "Chestionar vizualizat",
|
|
||||||
"survey_viewed_at": "Vizualizat la",
|
|
||||||
"system_attributes": "Atribute de sistem",
|
"system_attributes": "Atribute de sistem",
|
||||||
"unlock_contacts_description": "Gestionează contactele și trimite sondaje țintite",
|
"unlock_contacts_description": "Gestionează contactele și trimite sondaje țintite",
|
||||||
"unlock_contacts_title": "Deblocați contactele cu un plan superior.",
|
"unlock_contacts_title": "Deblocați contactele cu un plan superior.",
|
||||||
@@ -1961,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "Răspunsuri filtrate (Excel)",
|
"filtered_responses_excel": "Răspunsuri filtrate (Excel)",
|
||||||
"generating_qr_code": "Se generează codul QR",
|
"generating_qr_code": "Se generează codul QR",
|
||||||
"impressions": "Impresii",
|
"impressions": "Impresii",
|
||||||
"impressions_identified_only": "Se afișează doar impresiile de la contactele identificate",
|
|
||||||
"impressions_tooltip": "Număr de ori când sondajul a fost vizualizat.",
|
"impressions_tooltip": "Număr de ori când sondajul a fost vizualizat.",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "Sondajul va fi afișat utilizatorilor site-ului dvs. web, care îndeplinesc criteriile enumerate mai jos",
|
"connection_description": "Sondajul va fi afișat utilizatorilor site-ului dvs. web, care îndeplinesc criteriile enumerate mai jos",
|
||||||
@@ -2004,7 +1993,6 @@
|
|||||||
"last_quarter": "Ultimul trimestru",
|
"last_quarter": "Ultimul trimestru",
|
||||||
"last_year": "Anul trecut",
|
"last_year": "Anul trecut",
|
||||||
"limit": "Limită",
|
"limit": "Limită",
|
||||||
"no_identified_impressions": "Nicio impresie de la contactele identificate",
|
|
||||||
"no_responses_found": "Nu s-au găsit răspunsuri",
|
"no_responses_found": "Nu s-au găsit răspunsuri",
|
||||||
"other_values_found": "Alte valori găsite",
|
"other_values_found": "Alte valori găsite",
|
||||||
"overall": "General",
|
"overall": "General",
|
||||||
|
|||||||
@@ -112,7 +112,6 @@
|
|||||||
"link_expired_description": "Ссылка, которой вы воспользовались, больше не действительна."
|
"link_expired_description": "Ссылка, которой вы воспользовались, больше не действительна."
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"Filter": "Фильтр",
|
|
||||||
"accepted": "Принято",
|
"accepted": "Принято",
|
||||||
"account": "Аккаунт",
|
"account": "Аккаунт",
|
||||||
"account_settings": "Настройки аккаунта",
|
"account_settings": "Настройки аккаунта",
|
||||||
@@ -219,16 +218,13 @@
|
|||||||
"error": "Ошибка",
|
"error": "Ошибка",
|
||||||
"error_component_description": "Этот ресурс не существует или у вас нет необходимых прав для доступа к нему.",
|
"error_component_description": "Этот ресурс не существует или у вас нет необходимых прав для доступа к нему.",
|
||||||
"error_component_title": "Ошибка загрузки ресурсов",
|
"error_component_title": "Ошибка загрузки ресурсов",
|
||||||
"error_loading_data": "Ошибка загрузки данных",
|
|
||||||
"error_rate_limit_description": "Достигнуто максимальное количество запросов. Пожалуйста, попробуйте позже.",
|
"error_rate_limit_description": "Достигнуто максимальное количество запросов. Пожалуйста, попробуйте позже.",
|
||||||
"error_rate_limit_title": "Превышен лимит запросов",
|
"error_rate_limit_title": "Превышен лимит запросов",
|
||||||
"expand_rows": "Развернуть строки",
|
"expand_rows": "Развернуть строки",
|
||||||
"failed_to_copy_to_clipboard": "Не удалось скопировать в буфер обмена",
|
"failed_to_copy_to_clipboard": "Не удалось скопировать в буфер обмена",
|
||||||
"failed_to_load_organizations": "Не удалось загрузить организации",
|
"failed_to_load_organizations": "Не удалось загрузить организации",
|
||||||
"failed_to_load_workspaces": "Не удалось загрузить рабочие пространства",
|
"failed_to_load_workspaces": "Не удалось загрузить рабочие пространства",
|
||||||
"filter": "Фильтр",
|
|
||||||
"finish": "Завершить",
|
"finish": "Завершить",
|
||||||
"first_name": "Имя",
|
|
||||||
"follow_these": "Выполните следующие действия",
|
"follow_these": "Выполните следующие действия",
|
||||||
"formbricks_version": "Версия Formbricks",
|
"formbricks_version": "Версия Formbricks",
|
||||||
"full_name": "Полное имя",
|
"full_name": "Полное имя",
|
||||||
@@ -241,7 +237,6 @@
|
|||||||
"hidden_field": "Скрытое поле",
|
"hidden_field": "Скрытое поле",
|
||||||
"hidden_fields": "Скрытые поля",
|
"hidden_fields": "Скрытые поля",
|
||||||
"hide_column": "Скрыть столбец",
|
"hide_column": "Скрыть столбец",
|
||||||
"id": "ID",
|
|
||||||
"image": "Изображение",
|
"image": "Изображение",
|
||||||
"images": "Изображения",
|
"images": "Изображения",
|
||||||
"import": "Импорт",
|
"import": "Импорт",
|
||||||
@@ -259,7 +254,6 @@
|
|||||||
"key": "Ключ",
|
"key": "Ключ",
|
||||||
"label": "Метка",
|
"label": "Метка",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"last_name": "Фамилия",
|
|
||||||
"learn_more": "Подробнее",
|
"learn_more": "Подробнее",
|
||||||
"license_expired": "License Expired",
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Светлый оверлей",
|
"light_overlay": "Светлый оверлей",
|
||||||
@@ -434,7 +428,6 @@
|
|||||||
"top_right": "Вверху справа",
|
"top_right": "Вверху справа",
|
||||||
"try_again": "Попробуйте ещё раз",
|
"try_again": "Попробуйте ещё раз",
|
||||||
"type": "Тип",
|
"type": "Тип",
|
||||||
"unknown_survey": "Неизвестный опрос",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "Откройте больше рабочих пространств с более высоким тарифом.",
|
"unlock_more_workspaces_with_a_higher_plan": "Откройте больше рабочих пространств с более высоким тарифом.",
|
||||||
"update": "Обновить",
|
"update": "Обновить",
|
||||||
"updated": "Обновлено",
|
"updated": "Обновлено",
|
||||||
@@ -662,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {Будет удалён выбранный атрибут. Все данные контактов, связанные с этим атрибутом, будут потеряны.} few {Будут удалены выбранные атрибуты. Все данные контактов, связанные с этими атрибутами, будут потеряны.} many {Будут удалены выбранные атрибуты. Все данные контактов, связанные с этими атрибутами, будут потеряны.} other {Будут удалены выбранные атрибуты. Все данные контактов, связанные с этими атрибутами, будут потеряны.}}",
|
"delete_attribute_confirmation": "{value, plural, one {Будет удалён выбранный атрибут. Все данные контактов, связанные с этим атрибутом, будут потеряны.} few {Будут удалены выбранные атрибуты. Все данные контактов, связанные с этими атрибутами, будут потеряны.} many {Будут удалены выбранные атрибуты. Все данные контактов, связанные с этими атрибутами, будут потеряны.} other {Будут удалены выбранные атрибуты. Все данные контактов, связанные с этими атрибутами, будут потеряны.}}",
|
||||||
"delete_contact_confirmation": "Это удалит все ответы на опросы и атрибуты контакта, связанные с этим контактом. Любая таргетинг и персонализация на основе данных этого контакта будут потеряны.",
|
"delete_contact_confirmation": "Это удалит все ответы на опросы и атрибуты контакта, связанные с этим контактом. Любая таргетинг и персонализация на основе данных этого контакта будут потеряны.",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {Это удалит все ответы на опросы и атрибуты контакта, связанные с этим контактом. Любая таргетинг и персонализация на основе данных этого контакта будут потеряны. Если у этого контакта есть ответы, которые учитываются в квотах опроса, количество по квотам будет уменьшено, но лимиты квот останутся без изменений.} few {Это удалит все ответы на опросы и атрибуты контактов, связанные с этими контактами. Любая таргетинг и персонализация на основе данных этих контактов будут потеряны. Если у этих контактов есть ответы, которые учитываются в квотах опроса, количество по квотам будет уменьшено, но лимиты квот останутся без изменений.} many {Это удалит все ответы на опросы и атрибуты контактов, связанные с этими контактами. Любая таргетинг и персонализация на основе данных этих контактов будут потеряны. Если у этих контактов есть ответы, которые учитываются в квотах опроса, количество по квотам будет уменьшено, но лимиты квот останутся без изменений.} other {Это удалит все ответы на опросы и атрибуты контактов, связанные с этими контактами. Любая таргетинг и персонализация на основе данных этих контактов будут потеряны. Если у этих контактов есть ответы, которые учитываются в квотах опроса, количество по квотам будет уменьшено, но лимиты квот останутся без изменений.}}",
|
"delete_contact_confirmation_with_quotas": "{value, plural, one {Это удалит все ответы на опросы и атрибуты контакта, связанные с этим контактом. Любая таргетинг и персонализация на основе данных этого контакта будут потеряны. Если у этого контакта есть ответы, которые учитываются в квотах опроса, количество по квотам будет уменьшено, но лимиты квот останутся без изменений.} few {Это удалит все ответы на опросы и атрибуты контактов, связанные с этими контактами. Любая таргетинг и персонализация на основе данных этих контактов будут потеряны. Если у этих контактов есть ответы, которые учитываются в квотах опроса, количество по квотам будет уменьшено, но лимиты квот останутся без изменений.} many {Это удалит все ответы на опросы и атрибуты контактов, связанные с этими контактами. Любая таргетинг и персонализация на основе данных этих контактов будут потеряны. Если у этих контактов есть ответы, которые учитываются в квотах опроса, количество по квотам будет уменьшено, но лимиты квот останутся без изменений.} other {Это удалит все ответы на опросы и атрибуты контактов, связанные с этими контактами. Любая таргетинг и персонализация на основе данных этих контактов будут потеряны. Если у этих контактов есть ответы, которые учитываются в квотах опроса, количество по квотам будет уменьшено, но лимиты квот останутся без изменений.}}",
|
||||||
"displays": "Показы",
|
|
||||||
"edit_attribute": "Редактировать атрибут",
|
"edit_attribute": "Редактировать атрибут",
|
||||||
"edit_attribute_description": "Обновите метку и описание для этого атрибута.",
|
"edit_attribute_description": "Обновите метку и описание для этого атрибута.",
|
||||||
"edit_attribute_values": "Редактировать атрибуты",
|
"edit_attribute_values": "Редактировать атрибуты",
|
||||||
@@ -674,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "Недопустимые имена столбцов в CSV: {columns}. Имена столбцов, которые станут новыми атрибутами, должны содержать только строчные буквы, цифры и подчёркивания, а также начинаться с буквы.",
|
"invalid_csv_column_names": "Недопустимые имена столбцов в CSV: {columns}. Имена столбцов, которые станут новыми атрибутами, должны содержать только строчные буквы, цифры и подчёркивания, а также начинаться с буквы.",
|
||||||
"invalid_date_format": "Неверный формат даты. Пожалуйста, используйте корректную дату.",
|
"invalid_date_format": "Неверный формат даты. Пожалуйста, используйте корректную дату.",
|
||||||
"invalid_number_format": "Неверный формат числа. Пожалуйста, введите корректное число.",
|
"invalid_number_format": "Неверный формат числа. Пожалуйста, введите корректное число.",
|
||||||
"no_activity_yet": "Пока нет активности",
|
|
||||||
"no_published_link_surveys_available": "Нет доступных опубликованных опросов-ссылок. Пожалуйста, сначала опубликуйте опрос-ссылку.",
|
"no_published_link_surveys_available": "Нет доступных опубликованных опросов-ссылок. Пожалуйста, сначала опубликуйте опрос-ссылку.",
|
||||||
"no_published_surveys": "Нет опубликованных опросов",
|
"no_published_surveys": "Нет опубликованных опросов",
|
||||||
"no_responses_found": "Ответы не найдены",
|
"no_responses_found": "Ответы не найдены",
|
||||||
@@ -689,8 +680,6 @@
|
|||||||
"select_a_survey": "Выберите опрос",
|
"select_a_survey": "Выберите опрос",
|
||||||
"select_attribute": "Выберите атрибут",
|
"select_attribute": "Выберите атрибут",
|
||||||
"select_attribute_key": "Выберите ключ атрибута",
|
"select_attribute_key": "Выберите ключ атрибута",
|
||||||
"survey_viewed": "Опрос просмотрен",
|
|
||||||
"survey_viewed_at": "Просмотрено",
|
|
||||||
"system_attributes": "Системные атрибуты",
|
"system_attributes": "Системные атрибуты",
|
||||||
"unlock_contacts_description": "Управляйте контактами и отправляйте целевые опросы",
|
"unlock_contacts_description": "Управляйте контактами и отправляйте целевые опросы",
|
||||||
"unlock_contacts_title": "Откройте доступ к контактам с более высоким тарифом",
|
"unlock_contacts_title": "Откройте доступ к контактам с более высоким тарифом",
|
||||||
@@ -1962,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "Отфильтрованные ответы (Excel)",
|
"filtered_responses_excel": "Отфильтрованные ответы (Excel)",
|
||||||
"generating_qr_code": "Генерация QR-кода",
|
"generating_qr_code": "Генерация QR-кода",
|
||||||
"impressions": "Просмотры",
|
"impressions": "Просмотры",
|
||||||
"impressions_identified_only": "Показаны только показы от идентифицированных контактов",
|
|
||||||
"impressions_tooltip": "Количество раз, когда опрос был просмотрен.",
|
"impressions_tooltip": "Количество раз, когда опрос был просмотрен.",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "Опрос будет показан пользователям вашего сайта, которые соответствуют указанным ниже критериям",
|
"connection_description": "Опрос будет показан пользователям вашего сайта, которые соответствуют указанным ниже критериям",
|
||||||
@@ -2005,7 +1993,6 @@
|
|||||||
"last_quarter": "Прошлый квартал",
|
"last_quarter": "Прошлый квартал",
|
||||||
"last_year": "Прошлый год",
|
"last_year": "Прошлый год",
|
||||||
"limit": "Лимит",
|
"limit": "Лимит",
|
||||||
"no_identified_impressions": "Нет показов от идентифицированных контактов",
|
|
||||||
"no_responses_found": "Ответы не найдены",
|
"no_responses_found": "Ответы не найдены",
|
||||||
"other_values_found": "Найдены другие значения",
|
"other_values_found": "Найдены другие значения",
|
||||||
"overall": "В целом",
|
"overall": "В целом",
|
||||||
|
|||||||
@@ -218,16 +218,13 @@
|
|||||||
"error": "Fel",
|
"error": "Fel",
|
||||||
"error_component_description": "Denna resurs finns inte eller så har du inte de nödvändiga rättigheterna för att komma åt den.",
|
"error_component_description": "Denna resurs finns inte eller så har du inte de nödvändiga rättigheterna för att komma åt den.",
|
||||||
"error_component_title": "Fel vid laddning av resurser",
|
"error_component_title": "Fel vid laddning av resurser",
|
||||||
"error_loading_data": "Fel vid inläsning av data",
|
|
||||||
"error_rate_limit_description": "Maximalt antal förfrågningar har nåtts. Försök igen senare.",
|
"error_rate_limit_description": "Maximalt antal förfrågningar har nåtts. Försök igen senare.",
|
||||||
"error_rate_limit_title": "Begränsningsgräns överskriden",
|
"error_rate_limit_title": "Begränsningsgräns överskriden",
|
||||||
"expand_rows": "Visa rader",
|
"expand_rows": "Visa rader",
|
||||||
"failed_to_copy_to_clipboard": "Misslyckades att kopiera till urklipp",
|
"failed_to_copy_to_clipboard": "Misslyckades att kopiera till urklipp",
|
||||||
"failed_to_load_organizations": "Misslyckades att ladda organisationer",
|
"failed_to_load_organizations": "Misslyckades att ladda organisationer",
|
||||||
"failed_to_load_workspaces": "Det gick inte att ladda arbetsytor",
|
"failed_to_load_workspaces": "Det gick inte att ladda arbetsytor",
|
||||||
"filter": "Filter",
|
|
||||||
"finish": "Slutför",
|
"finish": "Slutför",
|
||||||
"first_name": "Förnamn",
|
|
||||||
"follow_these": "Följ dessa",
|
"follow_these": "Följ dessa",
|
||||||
"formbricks_version": "Formbricks-version",
|
"formbricks_version": "Formbricks-version",
|
||||||
"full_name": "Fullständigt namn",
|
"full_name": "Fullständigt namn",
|
||||||
@@ -240,7 +237,6 @@
|
|||||||
"hidden_field": "Dolt fält",
|
"hidden_field": "Dolt fält",
|
||||||
"hidden_fields": "Dolda fält",
|
"hidden_fields": "Dolda fält",
|
||||||
"hide_column": "Dölj kolumn",
|
"hide_column": "Dölj kolumn",
|
||||||
"id": "ID",
|
|
||||||
"image": "Bild",
|
"image": "Bild",
|
||||||
"images": "Bilder",
|
"images": "Bilder",
|
||||||
"import": "Importera",
|
"import": "Importera",
|
||||||
@@ -258,7 +254,6 @@
|
|||||||
"key": "Nyckel",
|
"key": "Nyckel",
|
||||||
"label": "Etikett",
|
"label": "Etikett",
|
||||||
"language": "Språk",
|
"language": "Språk",
|
||||||
"last_name": "Efternamn",
|
|
||||||
"learn_more": "Läs mer",
|
"learn_more": "Läs mer",
|
||||||
"license_expired": "License Expired",
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "Ljust överlägg",
|
"light_overlay": "Ljust överlägg",
|
||||||
@@ -433,7 +428,6 @@
|
|||||||
"top_right": "Övre höger",
|
"top_right": "Övre höger",
|
||||||
"try_again": "Försök igen",
|
"try_again": "Försök igen",
|
||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
"unknown_survey": "Okänd enkät",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "Lås upp fler arbetsytor med ett högre abonnemang.",
|
"unlock_more_workspaces_with_a_higher_plan": "Lås upp fler arbetsytor med ett högre abonnemang.",
|
||||||
"update": "Uppdatera",
|
"update": "Uppdatera",
|
||||||
"updated": "Uppdaterad",
|
"updated": "Uppdaterad",
|
||||||
@@ -661,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {Detta kommer att ta bort det valda attributet. All kontaktdata som är kopplad till detta attribut kommer att gå förlorad.} other {Detta kommer att ta bort de valda attributen. All kontaktdata som är kopplad till dessa attribut kommer att gå förlorad.}}",
|
"delete_attribute_confirmation": "{value, plural, one {Detta kommer att ta bort det valda attributet. All kontaktdata som är kopplad till detta attribut kommer att gå förlorad.} other {Detta kommer att ta bort de valda attributen. All kontaktdata som är kopplad till dessa attribut kommer att gå förlorad.}}",
|
||||||
"delete_contact_confirmation": "Detta kommer att ta bort alla enkätsvar och kontaktattribut som är kopplade till denna kontakt. All målgruppsinriktning och personalisering baserad på denna kontakts data kommer att gå förlorad.",
|
"delete_contact_confirmation": "Detta kommer att ta bort alla enkätsvar och kontaktattribut som är kopplade till denna kontakt. All målgruppsinriktning och personalisering baserad på denna kontakts data kommer att gå förlorad.",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {Detta kommer att ta bort alla enkätsvar och kontaktattribut som är kopplade till denna kontakt. All målgruppsinriktning och personalisering baserad på denna kontakts data kommer att gå förlorad. Om denna kontakt har svar som räknas mot enkätkvoter, kommer kvotantalet att minskas men kvotgränserna förblir oförändrade.} other {Detta kommer att ta bort alla enkätsvar och kontaktattribut som är kopplade till dessa kontakter. All målgruppsinriktning och personalisering baserad på dessa kontakters data kommer att gå förlorad. Om dessa kontakter har svar som räknas mot enkätkvoter, kommer kvotantalet att minskas men kvotgränserna förblir oförändrade.}}",
|
"delete_contact_confirmation_with_quotas": "{value, plural, one {Detta kommer att ta bort alla enkätsvar och kontaktattribut som är kopplade till denna kontakt. All målgruppsinriktning och personalisering baserad på denna kontakts data kommer att gå förlorad. Om denna kontakt har svar som räknas mot enkätkvoter, kommer kvotantalet att minskas men kvotgränserna förblir oförändrade.} other {Detta kommer att ta bort alla enkätsvar och kontaktattribut som är kopplade till dessa kontakter. All målgruppsinriktning och personalisering baserad på dessa kontakters data kommer att gå förlorad. Om dessa kontakter har svar som räknas mot enkätkvoter, kommer kvotantalet att minskas men kvotgränserna förblir oförändrade.}}",
|
||||||
"displays": "Visningar",
|
|
||||||
"edit_attribute": "Redigera attribut",
|
"edit_attribute": "Redigera attribut",
|
||||||
"edit_attribute_description": "Uppdatera etikett och beskrivning för detta attribut.",
|
"edit_attribute_description": "Uppdatera etikett och beskrivning för detta attribut.",
|
||||||
"edit_attribute_values": "Redigera attribut",
|
"edit_attribute_values": "Redigera attribut",
|
||||||
@@ -673,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "Ogiltiga CSV-kolumnnamn: {columns}. Kolumnnamn som ska bli nya attribut får bara innehålla små bokstäver, siffror och understreck, och måste börja med en bokstav.",
|
"invalid_csv_column_names": "Ogiltiga CSV-kolumnnamn: {columns}. Kolumnnamn som ska bli nya attribut får bara innehålla små bokstäver, siffror och understreck, och måste börja med en bokstav.",
|
||||||
"invalid_date_format": "Ogiltigt datumformat. Ange ett giltigt datum.",
|
"invalid_date_format": "Ogiltigt datumformat. Ange ett giltigt datum.",
|
||||||
"invalid_number_format": "Ogiltigt nummerformat. Ange ett giltigt nummer.",
|
"invalid_number_format": "Ogiltigt nummerformat. Ange ett giltigt nummer.",
|
||||||
"no_activity_yet": "Ingen aktivitet än",
|
|
||||||
"no_published_link_surveys_available": "Inga publicerade länkenkäter tillgängliga. Vänligen publicera en länkenkät först.",
|
"no_published_link_surveys_available": "Inga publicerade länkenkäter tillgängliga. Vänligen publicera en länkenkät först.",
|
||||||
"no_published_surveys": "Inga publicerade enkäter",
|
"no_published_surveys": "Inga publicerade enkäter",
|
||||||
"no_responses_found": "Inga svar hittades",
|
"no_responses_found": "Inga svar hittades",
|
||||||
@@ -688,8 +680,6 @@
|
|||||||
"select_a_survey": "Välj en enkät",
|
"select_a_survey": "Välj en enkät",
|
||||||
"select_attribute": "Välj attribut",
|
"select_attribute": "Välj attribut",
|
||||||
"select_attribute_key": "Välj attributnyckel",
|
"select_attribute_key": "Välj attributnyckel",
|
||||||
"survey_viewed": "Enkät visad",
|
|
||||||
"survey_viewed_at": "Visad kl.",
|
|
||||||
"system_attributes": "Systemattribut",
|
"system_attributes": "Systemattribut",
|
||||||
"unlock_contacts_description": "Hantera kontakter och skicka ut riktade enkäter",
|
"unlock_contacts_description": "Hantera kontakter och skicka ut riktade enkäter",
|
||||||
"unlock_contacts_title": "Lås upp kontakter med en högre plan",
|
"unlock_contacts_title": "Lås upp kontakter med en högre plan",
|
||||||
@@ -1961,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "Filtrerade svar (Excel)",
|
"filtered_responses_excel": "Filtrerade svar (Excel)",
|
||||||
"generating_qr_code": "Genererar QR-kod",
|
"generating_qr_code": "Genererar QR-kod",
|
||||||
"impressions": "Visningar",
|
"impressions": "Visningar",
|
||||||
"impressions_identified_only": "Visar bara visningar från identifierade kontakter",
|
|
||||||
"impressions_tooltip": "Antal gånger enkäten har visats.",
|
"impressions_tooltip": "Antal gånger enkäten har visats.",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "Enkäten kommer att visas för användare på din webbplats som matchar kriterierna nedan",
|
"connection_description": "Enkäten kommer att visas för användare på din webbplats som matchar kriterierna nedan",
|
||||||
@@ -2004,7 +1993,6 @@
|
|||||||
"last_quarter": "Senaste kvartalet",
|
"last_quarter": "Senaste kvartalet",
|
||||||
"last_year": "Senaste året",
|
"last_year": "Senaste året",
|
||||||
"limit": "Gräns",
|
"limit": "Gräns",
|
||||||
"no_identified_impressions": "Inga visningar från identifierade kontakter",
|
|
||||||
"no_responses_found": "Inga svar hittades",
|
"no_responses_found": "Inga svar hittades",
|
||||||
"other_values_found": "Andra värden hittades",
|
"other_values_found": "Andra värden hittades",
|
||||||
"overall": "Övergripande",
|
"overall": "Övergripande",
|
||||||
|
|||||||
@@ -218,16 +218,13 @@
|
|||||||
"error": "错误",
|
"error": "错误",
|
||||||
"error_component_description": "这个资源不存在或您没有权限访问它。",
|
"error_component_description": "这个资源不存在或您没有权限访问它。",
|
||||||
"error_component_title": "错误 加载 资源",
|
"error_component_title": "错误 加载 资源",
|
||||||
"error_loading_data": "数据加载出错",
|
|
||||||
"error_rate_limit_description": "请求 达到 最大 上限 , 请 稍后 再试 。",
|
"error_rate_limit_description": "请求 达到 最大 上限 , 请 稍后 再试 。",
|
||||||
"error_rate_limit_title": "速率 限制 超过",
|
"error_rate_limit_title": "速率 限制 超过",
|
||||||
"expand_rows": "展开 行",
|
"expand_rows": "展开 行",
|
||||||
"failed_to_copy_to_clipboard": "复制到剪贴板失败",
|
"failed_to_copy_to_clipboard": "复制到剪贴板失败",
|
||||||
"failed_to_load_organizations": "加载组织失败",
|
"failed_to_load_organizations": "加载组织失败",
|
||||||
"failed_to_load_workspaces": "加载工作区失败",
|
"failed_to_load_workspaces": "加载工作区失败",
|
||||||
"filter": "筛选",
|
|
||||||
"finish": "完成",
|
"finish": "完成",
|
||||||
"first_name": "名字",
|
|
||||||
"follow_these": "遵循 这些",
|
"follow_these": "遵循 这些",
|
||||||
"formbricks_version": "Formbricks 版本",
|
"formbricks_version": "Formbricks 版本",
|
||||||
"full_name": "全名",
|
"full_name": "全名",
|
||||||
@@ -240,7 +237,6 @@
|
|||||||
"hidden_field": "隐藏 字段",
|
"hidden_field": "隐藏 字段",
|
||||||
"hidden_fields": "隐藏 字段",
|
"hidden_fields": "隐藏 字段",
|
||||||
"hide_column": "隐藏 列",
|
"hide_column": "隐藏 列",
|
||||||
"id": "ID",
|
|
||||||
"image": "图片",
|
"image": "图片",
|
||||||
"images": "图片",
|
"images": "图片",
|
||||||
"import": "导入",
|
"import": "导入",
|
||||||
@@ -258,7 +254,6 @@
|
|||||||
"key": "键",
|
"key": "键",
|
||||||
"label": "标签",
|
"label": "标签",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"last_name": "姓",
|
|
||||||
"learn_more": "了解 更多",
|
"learn_more": "了解 更多",
|
||||||
"license_expired": "License Expired",
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "浅色遮罩层",
|
"light_overlay": "浅色遮罩层",
|
||||||
@@ -433,7 +428,6 @@
|
|||||||
"top_right": "右上",
|
"top_right": "右上",
|
||||||
"try_again": "再试一次",
|
"try_again": "再试一次",
|
||||||
"type": "类型",
|
"type": "类型",
|
||||||
"unknown_survey": "未知调查",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "升级套餐以解锁更多工作区。",
|
"unlock_more_workspaces_with_a_higher_plan": "升级套餐以解锁更多工作区。",
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
"updated": "已更新",
|
"updated": "已更新",
|
||||||
@@ -661,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {这将删除所选属性。与该属性相关的任何联系人数据都将丢失。} other {这将删除所选属性。与这些属性相关的任何联系人数据都将丢失。}}",
|
"delete_attribute_confirmation": "{value, plural, one {这将删除所选属性。与该属性相关的任何联系人数据都将丢失。} other {这将删除所选属性。与这些属性相关的任何联系人数据都将丢失。}}",
|
||||||
"delete_contact_confirmation": "这将删除与此联系人相关的所有调查问卷回复和联系人属性。基于此联系人数据的任何定位和个性化将会丢失。",
|
"delete_contact_confirmation": "这将删除与此联系人相关的所有调查问卷回复和联系人属性。基于此联系人数据的任何定位和个性化将会丢失。",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {这将删除与此联系人相关的所有调查回复和联系人属性。基于此联系人数据的任何定位和个性化将丢失。如果此联系人有影响调查配额的回复,配额数量将减少,但配额限制将保持不变。} other {这将删除与这些联系人相关的所有调查回复和联系人属性。基于这些联系人数据的任何定位和个性化将丢失。如果这些联系人有影响调查配额的回复,配额数量将减少,但配额限制将保持不变。}}",
|
"delete_contact_confirmation_with_quotas": "{value, plural, one {这将删除与此联系人相关的所有调查回复和联系人属性。基于此联系人数据的任何定位和个性化将丢失。如果此联系人有影响调查配额的回复,配额数量将减少,但配额限制将保持不变。} other {这将删除与这些联系人相关的所有调查回复和联系人属性。基于这些联系人数据的任何定位和个性化将丢失。如果这些联系人有影响调查配额的回复,配额数量将减少,但配额限制将保持不变。}}",
|
||||||
"displays": "展示次数",
|
|
||||||
"edit_attribute": "编辑属性",
|
"edit_attribute": "编辑属性",
|
||||||
"edit_attribute_description": "更新此属性的标签和描述。",
|
"edit_attribute_description": "更新此属性的标签和描述。",
|
||||||
"edit_attribute_values": "编辑属性",
|
"edit_attribute_values": "编辑属性",
|
||||||
@@ -673,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "无效的 CSV 列名:{columns}。作为新属性的列名只能包含小写字母、数字和下划线,并且必须以字母开头。",
|
"invalid_csv_column_names": "无效的 CSV 列名:{columns}。作为新属性的列名只能包含小写字母、数字和下划线,并且必须以字母开头。",
|
||||||
"invalid_date_format": "日期格式无效。请使用有效日期。",
|
"invalid_date_format": "日期格式无效。请使用有效日期。",
|
||||||
"invalid_number_format": "数字格式无效。请输入有效的数字。",
|
"invalid_number_format": "数字格式无效。请输入有效的数字。",
|
||||||
"no_activity_yet": "暂无活动",
|
|
||||||
"no_published_link_surveys_available": "没有可用的已发布链接调查。请先发布一个链接调查。",
|
"no_published_link_surveys_available": "没有可用的已发布链接调查。请先发布一个链接调查。",
|
||||||
"no_published_surveys": "没有已发布的调查",
|
"no_published_surveys": "没有已发布的调查",
|
||||||
"no_responses_found": "未找到 响应",
|
"no_responses_found": "未找到 响应",
|
||||||
@@ -688,8 +680,6 @@
|
|||||||
"select_a_survey": "选择一个调查",
|
"select_a_survey": "选择一个调查",
|
||||||
"select_attribute": "选择 属性",
|
"select_attribute": "选择 属性",
|
||||||
"select_attribute_key": "选择属性键",
|
"select_attribute_key": "选择属性键",
|
||||||
"survey_viewed": "已查看调查",
|
|
||||||
"survey_viewed_at": "查看时间",
|
|
||||||
"system_attributes": "系统属性",
|
"system_attributes": "系统属性",
|
||||||
"unlock_contacts_description": "管理 联系人 并 发送 定向 调查",
|
"unlock_contacts_description": "管理 联系人 并 发送 定向 调查",
|
||||||
"unlock_contacts_title": "通过 更 高级 划解锁 联系人",
|
"unlock_contacts_title": "通过 更 高级 划解锁 联系人",
|
||||||
@@ -1961,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "过滤 反馈 (Excel)",
|
"filtered_responses_excel": "过滤 反馈 (Excel)",
|
||||||
"generating_qr_code": "正在生成二维码",
|
"generating_qr_code": "正在生成二维码",
|
||||||
"impressions": "印象",
|
"impressions": "印象",
|
||||||
"impressions_identified_only": "仅显示已识别联系人的展示次数",
|
|
||||||
"impressions_tooltip": "调查 被 查看 的 次数",
|
"impressions_tooltip": "调查 被 查看 的 次数",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "调查将显示给符合以下条件的您网站用户",
|
"connection_description": "调查将显示给符合以下条件的您网站用户",
|
||||||
@@ -2004,7 +1993,6 @@
|
|||||||
"last_quarter": "上季度",
|
"last_quarter": "上季度",
|
||||||
"last_year": "去年",
|
"last_year": "去年",
|
||||||
"limit": "限额",
|
"limit": "限额",
|
||||||
"no_identified_impressions": "没有已识别联系人的展示次数",
|
|
||||||
"no_responses_found": "未找到响应",
|
"no_responses_found": "未找到响应",
|
||||||
"other_values_found": "找到其他值",
|
"other_values_found": "找到其他值",
|
||||||
"overall": "整体",
|
"overall": "整体",
|
||||||
|
|||||||
@@ -218,16 +218,13 @@
|
|||||||
"error": "錯誤",
|
"error": "錯誤",
|
||||||
"error_component_description": "此資源不存在或您沒有存取權限。",
|
"error_component_description": "此資源不存在或您沒有存取權限。",
|
||||||
"error_component_title": "載入資源錯誤",
|
"error_component_title": "載入資源錯誤",
|
||||||
"error_loading_data": "載入資料時發生錯誤",
|
|
||||||
"error_rate_limit_description": "已達 到最大 請求 次數。請 稍後 再試。",
|
"error_rate_limit_description": "已達 到最大 請求 次數。請 稍後 再試。",
|
||||||
"error_rate_limit_title": "限流超過",
|
"error_rate_limit_title": "限流超過",
|
||||||
"expand_rows": "展開列",
|
"expand_rows": "展開列",
|
||||||
"failed_to_copy_to_clipboard": "無法複製到剪貼簿",
|
"failed_to_copy_to_clipboard": "無法複製到剪貼簿",
|
||||||
"failed_to_load_organizations": "無法載入組織",
|
"failed_to_load_organizations": "無法載入組織",
|
||||||
"failed_to_load_workspaces": "載入工作區失敗",
|
"failed_to_load_workspaces": "載入工作區失敗",
|
||||||
"filter": "篩選",
|
|
||||||
"finish": "完成",
|
"finish": "完成",
|
||||||
"first_name": "名字",
|
|
||||||
"follow_these": "按照這些步驟",
|
"follow_these": "按照這些步驟",
|
||||||
"formbricks_version": "Formbricks 版本",
|
"formbricks_version": "Formbricks 版本",
|
||||||
"full_name": "全名",
|
"full_name": "全名",
|
||||||
@@ -240,7 +237,6 @@
|
|||||||
"hidden_field": "隱藏欄位",
|
"hidden_field": "隱藏欄位",
|
||||||
"hidden_fields": "隱藏欄位",
|
"hidden_fields": "隱藏欄位",
|
||||||
"hide_column": "隱藏欄位",
|
"hide_column": "隱藏欄位",
|
||||||
"id": "ID",
|
|
||||||
"image": "圖片",
|
"image": "圖片",
|
||||||
"images": "圖片",
|
"images": "圖片",
|
||||||
"import": "匯入",
|
"import": "匯入",
|
||||||
@@ -258,7 +254,6 @@
|
|||||||
"key": "金鑰",
|
"key": "金鑰",
|
||||||
"label": "標籤",
|
"label": "標籤",
|
||||||
"language": "語言",
|
"language": "語言",
|
||||||
"last_name": "姓氏",
|
|
||||||
"learn_more": "瞭解更多",
|
"learn_more": "瞭解更多",
|
||||||
"license_expired": "License Expired",
|
"license_expired": "License Expired",
|
||||||
"light_overlay": "淺色覆蓋",
|
"light_overlay": "淺色覆蓋",
|
||||||
@@ -433,7 +428,6 @@
|
|||||||
"top_right": "右上",
|
"top_right": "右上",
|
||||||
"try_again": "再試一次",
|
"try_again": "再試一次",
|
||||||
"type": "類型",
|
"type": "類型",
|
||||||
"unknown_survey": "未知問卷",
|
|
||||||
"unlock_more_workspaces_with_a_higher_plan": "升級方案以解鎖更多工作區。",
|
"unlock_more_workspaces_with_a_higher_plan": "升級方案以解鎖更多工作區。",
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
"updated": "已更新",
|
"updated": "已更新",
|
||||||
@@ -661,7 +655,6 @@
|
|||||||
"delete_attribute_confirmation": "{value, plural, one {這將刪除所選屬性。與此屬性相關的聯絡人資料將會遺失。} other {這將刪除所選屬性。與這些屬性相關的聯絡人資料將會遺失。}}",
|
"delete_attribute_confirmation": "{value, plural, one {這將刪除所選屬性。與此屬性相關的聯絡人資料將會遺失。} other {這將刪除所選屬性。與這些屬性相關的聯絡人資料將會遺失。}}",
|
||||||
"delete_contact_confirmation": "這將刪除與此聯繫人相關的所有調查回應和聯繫屬性。任何基於此聯繫人數據的定位和個性化將會丟失。",
|
"delete_contact_confirmation": "這將刪除與此聯繫人相關的所有調查回應和聯繫屬性。任何基於此聯繫人數據的定位和個性化將會丟失。",
|
||||||
"delete_contact_confirmation_with_quotas": "{value, plural, one {這將刪除與這個 contact 相關的所有調查響應和聯繫人屬性。基於這個 contact 數據的任何定向和個性化功能將會丟失。如果這個 contact 有作為調查配額依據的響應,配額計數將會減少,但配額限制將保持不變。} other {這將刪除與這些 contacts 相關的所有調查響應和聯繫人屬性。基於這些 contacts 數據的任何定向和個性化功能將會丟失。如果這些 contacts 有作為調查配額依據的響應,配額計數將會減少,但配額限制將保持不變。}}",
|
"delete_contact_confirmation_with_quotas": "{value, plural, one {這將刪除與這個 contact 相關的所有調查響應和聯繫人屬性。基於這個 contact 數據的任何定向和個性化功能將會丟失。如果這個 contact 有作為調查配額依據的響應,配額計數將會減少,但配額限制將保持不變。} other {這將刪除與這些 contacts 相關的所有調查響應和聯繫人屬性。基於這些 contacts 數據的任何定向和個性化功能將會丟失。如果這些 contacts 有作為調查配額依據的響應,配額計數將會減少,但配額限制將保持不變。}}",
|
||||||
"displays": "顯示次數",
|
|
||||||
"edit_attribute": "編輯屬性",
|
"edit_attribute": "編輯屬性",
|
||||||
"edit_attribute_description": "更新此屬性的標籤與描述。",
|
"edit_attribute_description": "更新此屬性的標籤與描述。",
|
||||||
"edit_attribute_values": "編輯屬性",
|
"edit_attribute_values": "編輯屬性",
|
||||||
@@ -673,7 +666,6 @@
|
|||||||
"invalid_csv_column_names": "無效的 CSV 欄位名稱:{columns}。作為新屬性的欄位名稱只能包含小寫字母、數字和底線,且必須以字母開頭。",
|
"invalid_csv_column_names": "無效的 CSV 欄位名稱:{columns}。作為新屬性的欄位名稱只能包含小寫字母、數字和底線,且必須以字母開頭。",
|
||||||
"invalid_date_format": "日期格式無效。請使用有效的日期。",
|
"invalid_date_format": "日期格式無效。請使用有效的日期。",
|
||||||
"invalid_number_format": "數字格式無效。請輸入有效的數字。",
|
"invalid_number_format": "數字格式無效。請輸入有效的數字。",
|
||||||
"no_activity_yet": "尚無活動",
|
|
||||||
"no_published_link_surveys_available": "沒有可用的已發佈連結問卷。請先發佈一個連結問卷。",
|
"no_published_link_surveys_available": "沒有可用的已發佈連結問卷。請先發佈一個連結問卷。",
|
||||||
"no_published_surveys": "沒有已發佈的問卷",
|
"no_published_surveys": "沒有已發佈的問卷",
|
||||||
"no_responses_found": "找不到回應",
|
"no_responses_found": "找不到回應",
|
||||||
@@ -688,8 +680,6 @@
|
|||||||
"select_a_survey": "選擇問卷",
|
"select_a_survey": "選擇問卷",
|
||||||
"select_attribute": "選取屬性",
|
"select_attribute": "選取屬性",
|
||||||
"select_attribute_key": "選取屬性鍵值",
|
"select_attribute_key": "選取屬性鍵值",
|
||||||
"survey_viewed": "已查看問卷",
|
|
||||||
"survey_viewed_at": "查看時間",
|
|
||||||
"system_attributes": "系統屬性",
|
"system_attributes": "系統屬性",
|
||||||
"unlock_contacts_description": "管理聯絡人並發送目標問卷",
|
"unlock_contacts_description": "管理聯絡人並發送目標問卷",
|
||||||
"unlock_contacts_title": "使用更高等級的方案解鎖聯絡人",
|
"unlock_contacts_title": "使用更高等級的方案解鎖聯絡人",
|
||||||
@@ -1961,7 +1951,6 @@
|
|||||||
"filtered_responses_excel": "篩選回應 (Excel)",
|
"filtered_responses_excel": "篩選回應 (Excel)",
|
||||||
"generating_qr_code": "正在生成 QR code",
|
"generating_qr_code": "正在生成 QR code",
|
||||||
"impressions": "曝光數",
|
"impressions": "曝光數",
|
||||||
"impressions_identified_only": "僅顯示已識別聯絡人的曝光次數",
|
|
||||||
"impressions_tooltip": "問卷已檢視的次數。",
|
"impressions_tooltip": "問卷已檢視的次數。",
|
||||||
"in_app": {
|
"in_app": {
|
||||||
"connection_description": "調查將顯示給符合以下列出條件的網站用戶",
|
"connection_description": "調查將顯示給符合以下列出條件的網站用戶",
|
||||||
@@ -2004,7 +1993,6 @@
|
|||||||
"last_quarter": "上一季",
|
"last_quarter": "上一季",
|
||||||
"last_year": "去年",
|
"last_year": "去年",
|
||||||
"limit": "限制",
|
"limit": "限制",
|
||||||
"no_identified_impressions": "沒有來自已識別聯絡人的曝光次數",
|
|
||||||
"no_responses_found": "找不到回應",
|
"no_responses_found": "找不到回應",
|
||||||
"other_values_found": "找到其他值",
|
"other_values_found": "找到其他值",
|
||||||
"overall": "整體",
|
"overall": "整體",
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { ArrowDownUpIcon } from "lucide-react";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { TDisplay } from "@formbricks/types/displays";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
|
||||||
import { TResponseWithQuotas } from "@formbricks/types/responses";
|
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
|
||||||
import { TTag } from "@formbricks/types/tags";
|
|
||||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
|
||||||
import { useMembershipRole } from "@/lib/membership/hooks/useMembershipRole";
|
|
||||||
import { getAccessFlags } from "@/lib/membership/utils";
|
|
||||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
|
||||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
|
||||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
|
||||||
import { DisplayCard } from "./display-card";
|
|
||||||
import { ResponseSurveyCard } from "./response-survey-card";
|
|
||||||
|
|
||||||
type TTimelineItem =
|
|
||||||
| { type: "display"; data: Pick<TDisplay, "id" | "createdAt" | "surveyId"> }
|
|
||||||
| { type: "response"; data: TResponseWithQuotas };
|
|
||||||
|
|
||||||
interface ActivityTimelineProps {
|
|
||||||
surveys: TSurvey[];
|
|
||||||
user: TUser;
|
|
||||||
responses: TResponseWithQuotas[];
|
|
||||||
displays: Pick<TDisplay, "id" | "createdAt" | "surveyId">[];
|
|
||||||
environment: TEnvironment;
|
|
||||||
environmentTags: TTag[];
|
|
||||||
locale: TUserLocale;
|
|
||||||
projectPermission: TTeamPermission | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ActivityTimeline = ({
|
|
||||||
surveys,
|
|
||||||
user,
|
|
||||||
responses: initialResponses,
|
|
||||||
displays,
|
|
||||||
environment,
|
|
||||||
environmentTags,
|
|
||||||
locale,
|
|
||||||
projectPermission,
|
|
||||||
}: ActivityTimelineProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [responses, setResponses] = useState(initialResponses);
|
|
||||||
const [isReversed, setIsReversed] = useState(false);
|
|
||||||
|
|
||||||
const { membershipRole } = useMembershipRole(environment.id, user.id);
|
|
||||||
|
|
||||||
const isReadOnly = useMemo(() => {
|
|
||||||
const { isMember } = getAccessFlags(membershipRole);
|
|
||||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
|
||||||
return isMember && hasReadAccess;
|
|
||||||
}, [membershipRole, projectPermission]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setResponses(initialResponses);
|
|
||||||
}, [initialResponses]);
|
|
||||||
|
|
||||||
const updateResponseList = (responseIds: string[]) => {
|
|
||||||
setResponses((prev) => prev.filter((r) => !responseIds.includes(r.id)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateResponse = (responseId: string, updatedResponse: TResponseWithQuotas) => {
|
|
||||||
setResponses((prev) => prev.map((r) => (r.id === responseId ? updatedResponse : r)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const timelineItems = useMemo(() => {
|
|
||||||
const displayItems: TTimelineItem[] = displays.map((d) => ({
|
|
||||||
type: "display" as const,
|
|
||||||
data: d,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const responseItems: TTimelineItem[] = responses.map((r) => ({
|
|
||||||
type: "response" as const,
|
|
||||||
data: r,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const merged = [...displayItems, ...responseItems].sort((a, b) => {
|
|
||||||
const aTime = new Date(a.data.createdAt).getTime();
|
|
||||||
const bTime = new Date(b.data.createdAt).getTime();
|
|
||||||
return bTime - aTime;
|
|
||||||
});
|
|
||||||
|
|
||||||
return isReversed ? [...merged].reverse() : merged;
|
|
||||||
}, [displays, responses, isReversed]);
|
|
||||||
|
|
||||||
const toggleSort = () => {
|
|
||||||
setIsReversed((prev) => !prev);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="col-span-3">
|
|
||||||
<div className="flex items-center justify-between pb-6">
|
|
||||||
<h2 className="text-lg font-bold text-slate-700">{t("common.activity")}</h2>
|
|
||||||
<div className="text-right">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={toggleSort}
|
|
||||||
className="hover:text-brand-dark flex items-center px-1 text-slate-800">
|
|
||||||
<ArrowDownUpIcon className="inline h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{timelineItems.length === 0 ? (
|
|
||||||
<EmptyState text={t("environments.contacts.no_activity_yet")} />
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{timelineItems.map((item) =>
|
|
||||||
item.type === "display" ? (
|
|
||||||
<DisplayCard
|
|
||||||
key={`display-${item.data.id}`}
|
|
||||||
display={item.data}
|
|
||||||
surveys={surveys}
|
|
||||||
environmentId={environment.id}
|
|
||||||
locale={locale}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ResponseSurveyCard
|
|
||||||
key={`response-${item.data.id}`}
|
|
||||||
response={item.data}
|
|
||||||
surveys={surveys}
|
|
||||||
user={user}
|
|
||||||
environmentTags={environmentTags}
|
|
||||||
environment={environment}
|
|
||||||
updateResponseList={updateResponseList}
|
|
||||||
updateResponse={updateResponse}
|
|
||||||
locale={locale}
|
|
||||||
isReadOnly={isReadOnly}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { getDisplaysByContactId } from "@/lib/display/service";
|
|
||||||
import { getResponsesByContactId } from "@/lib/response/service";
|
import { getResponsesByContactId } from "@/lib/response/service";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { getContactAttributesWithKeyInfo } from "@/modules/ee/contacts/lib/contact-attributes";
|
import { getContactAttributesWithKeyInfo } from "@/modules/ee/contacts/lib/contact-attributes";
|
||||||
@@ -18,12 +17,8 @@ export const AttributesSection = async ({ contactId }: { contactId: string }) =>
|
|||||||
throw new Error(t("environments.contacts.contact_not_found"));
|
throw new Error(t("environments.contacts.contact_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const [responses, displays] = await Promise.all([
|
const responses = await getResponsesByContactId(contactId);
|
||||||
getResponsesByContactId(contactId),
|
|
||||||
getDisplaysByContactId(contactId),
|
|
||||||
]);
|
|
||||||
const numberOfResponses = responses?.length || 0;
|
const numberOfResponses = responses?.length || 0;
|
||||||
const numberOfDisplays = displays?.length || 0;
|
|
||||||
|
|
||||||
const systemAttributes = attributesWithKeyInfo
|
const systemAttributes = attributesWithKeyInfo
|
||||||
.filter((attr) => attr.type === "default")
|
.filter((attr) => attr.type === "default")
|
||||||
@@ -90,11 +85,6 @@ export const AttributesSection = async ({ contactId }: { contactId: string }) =>
|
|||||||
<dt className="text-sm font-medium text-slate-500">{t("common.responses")}</dt>
|
<dt className="text-sm font-medium text-slate-500">{t("common.responses")}</dt>
|
||||||
<dd className="mt-1 text-sm text-slate-900">{numberOfResponses}</dd>
|
<dd className="mt-1 text-sm text-slate-900">{numberOfResponses}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<dt className="text-sm font-medium text-slate-500">{t("environments.contacts.displays")}</dt>
|
|
||||||
<dd className="mt-1 text-sm text-slate-900">{numberOfDisplays}</dd>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { EyeIcon } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { TDisplay } from "@formbricks/types/displays";
|
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
|
||||||
import { timeSince } from "@/lib/time";
|
|
||||||
|
|
||||||
interface DisplayCardProps {
|
|
||||||
display: Pick<TDisplay, "id" | "createdAt" | "surveyId">;
|
|
||||||
surveys: TSurvey[];
|
|
||||||
environmentId: string;
|
|
||||||
locale: TUserLocale;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DisplayCard = ({ display, surveys, environmentId, locale }: DisplayCardProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const survey = surveys.find((s) => s.id === display.surveyId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-slate-100">
|
|
||||||
<EyeIcon className="h-4 w-4 text-slate-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-slate-500">{t("environments.contacts.survey_viewed")}</p>
|
|
||||||
{survey ? (
|
|
||||||
<Link
|
|
||||||
href={`/environments/${environmentId}/surveys/${survey.id}/summary`}
|
|
||||||
className="text-sm font-medium text-slate-700 hover:underline">
|
|
||||||
{survey.name}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span className="text-sm font-medium text-slate-500">{t("common.unknown_survey")}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-slate-500">{timeSince(display.createdAt.toString(), locale)}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
import { TResponseWithQuotas } from "@formbricks/types/responses";
|
||||||
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TTag } from "@formbricks/types/tags";
|
||||||
|
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||||
|
import { useMembershipRole } from "@/lib/membership/hooks/useMembershipRole";
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
|
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||||
|
import { SingleResponseCard } from "@/modules/analysis/components/SingleResponseCard";
|
||||||
|
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||||
|
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||||
|
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||||
|
|
||||||
|
interface ResponseTimelineProps {
|
||||||
|
surveys: TSurvey[];
|
||||||
|
user: TUser;
|
||||||
|
responses: TResponseWithQuotas[];
|
||||||
|
environment: TEnvironment;
|
||||||
|
environmentTags: TTag[];
|
||||||
|
locale: TUserLocale;
|
||||||
|
projectPermission: TTeamPermission | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResponseFeed = ({
|
||||||
|
responses,
|
||||||
|
environment,
|
||||||
|
surveys,
|
||||||
|
user,
|
||||||
|
environmentTags,
|
||||||
|
locale,
|
||||||
|
projectPermission,
|
||||||
|
}: ResponseTimelineProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [fetchedResponses, setFetchedResponses] = useState(responses);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFetchedResponses(responses);
|
||||||
|
}, [responses]);
|
||||||
|
|
||||||
|
const updateResponseList = (responseIds: string[]) => {
|
||||||
|
setFetchedResponses((prev) => prev.filter((r) => !responseIds.includes(r.id)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateResponse = (responseId: string, updatedResponse: TResponseWithQuotas) => {
|
||||||
|
setFetchedResponses((prev) => prev.map((r) => (r.id === responseId ? updatedResponse : r)));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{fetchedResponses.length === 0 ? (
|
||||||
|
<EmptyState text={t("environments.contacts.no_responses_found")} />
|
||||||
|
) : (
|
||||||
|
fetchedResponses.map((response) => (
|
||||||
|
<ResponseSurveyCard
|
||||||
|
key={response.id}
|
||||||
|
response={response}
|
||||||
|
surveys={surveys}
|
||||||
|
user={user}
|
||||||
|
environmentTags={environmentTags}
|
||||||
|
environment={environment}
|
||||||
|
updateResponseList={updateResponseList}
|
||||||
|
updateResponse={updateResponse}
|
||||||
|
locale={locale}
|
||||||
|
projectPermission={projectPermission}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ResponseSurveyCard = ({
|
||||||
|
response,
|
||||||
|
surveys,
|
||||||
|
user,
|
||||||
|
environmentTags,
|
||||||
|
environment,
|
||||||
|
updateResponseList,
|
||||||
|
updateResponse,
|
||||||
|
locale,
|
||||||
|
projectPermission,
|
||||||
|
}: {
|
||||||
|
response: TResponseWithQuotas;
|
||||||
|
surveys: TSurvey[];
|
||||||
|
user: TUser;
|
||||||
|
environmentTags: TTag[];
|
||||||
|
environment: TEnvironment;
|
||||||
|
updateResponseList: (responseIds: string[]) => void;
|
||||||
|
updateResponse: (responseId: string, response: TResponseWithQuotas) => void;
|
||||||
|
locale: TUserLocale;
|
||||||
|
projectPermission: TTeamPermission | null;
|
||||||
|
}) => {
|
||||||
|
const survey = surveys.find((survey) => {
|
||||||
|
return survey.id === response.surveyId;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { membershipRole } = useMembershipRole(survey?.environmentId || "", user.id);
|
||||||
|
const { isMember } = getAccessFlags(membershipRole);
|
||||||
|
|
||||||
|
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||||
|
|
||||||
|
const isReadOnly = isMember && hasReadAccess;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={response.id}>
|
||||||
|
{survey && (
|
||||||
|
<SingleResponseCard
|
||||||
|
response={response}
|
||||||
|
survey={replaceHeadlineRecall(survey, "default")}
|
||||||
|
user={user}
|
||||||
|
environmentTags={environmentTags}
|
||||||
|
environment={environment}
|
||||||
|
updateResponseList={updateResponseList}
|
||||||
|
updateResponse={updateResponse}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+11
-18
@@ -2,7 +2,6 @@ import { getServerSession } from "next-auth";
|
|||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
import { TTag } from "@formbricks/types/tags";
|
import { TTag } from "@formbricks/types/tags";
|
||||||
import { getDisplaysByContactId } from "@/lib/display/service";
|
|
||||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { getResponsesByContactId } from "@/lib/response/service";
|
import { getResponsesByContactId } from "@/lib/response/service";
|
||||||
import { getSurveys } from "@/lib/survey/service";
|
import { getSurveys } from "@/lib/survey/service";
|
||||||
@@ -11,34 +10,27 @@ import { findMatchingLocale } from "@/lib/utils/locale";
|
|||||||
import { getTranslate } from "@/lingodotdev/server";
|
import { getTranslate } from "@/lingodotdev/server";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||||
import { ActivityTimeline } from "./activity-timeline";
|
import { ResponseTimeline } from "./response-timeline";
|
||||||
|
|
||||||
interface ActivitySectionProps {
|
interface ResponseSectionProps {
|
||||||
environment: TEnvironment;
|
environment: TEnvironment;
|
||||||
contactId: string;
|
contactId: string;
|
||||||
environmentTags: TTag[];
|
environmentTags: TTag[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActivitySection = async ({ environment, contactId, environmentTags }: ActivitySectionProps) => {
|
export const ResponseSection = async ({ environment, contactId, environmentTags }: ResponseSectionProps) => {
|
||||||
const [responses, displays] = await Promise.all([
|
const responses = await getResponsesByContactId(contactId);
|
||||||
getResponsesByContactId(contactId),
|
const surveyIds = responses?.map((response) => response.surveyId) || [];
|
||||||
getDisplaysByContactId(contactId),
|
const surveys: TSurvey[] = surveyIds.length === 0 ? [] : ((await getSurveys(environment.id)) ?? []);
|
||||||
]);
|
|
||||||
|
|
||||||
const allSurveyIds = [
|
|
||||||
...new Set([...(responses?.map((r) => r.surveyId) || []), ...displays.map((d) => d.surveyId)]),
|
|
||||||
];
|
|
||||||
|
|
||||||
const surveys: TSurvey[] = allSurveyIds.length === 0 ? [] : ((await getSurveys(environment.id)) ?? []);
|
|
||||||
|
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
const t = await getTranslate();
|
|
||||||
|
|
||||||
|
const t = await getTranslate();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new Error(t("common.session_not_found"));
|
throw new Error(t("common.session_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await getUser(session.user.id);
|
const user = await getUser(session.user.id);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("common.user_not_found"));
|
throw new Error(t("common.user_not_found"));
|
||||||
}
|
}
|
||||||
@@ -48,19 +40,20 @@ export const ActivitySection = async ({ environment, contactId, environmentTags
|
|||||||
}
|
}
|
||||||
|
|
||||||
const project = await getProjectByEnvironmentId(environment.id);
|
const project = await getProjectByEnvironmentId(environment.id);
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error(t("common.workspace_not_found"));
|
throw new Error(t("common.workspace_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
|
const projectPermission = await getProjectPermissionByUserId(session.user.id, project.id);
|
||||||
|
|
||||||
const locale = await findMatchingLocale();
|
const locale = await findMatchingLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActivityTimeline
|
<ResponseTimeline
|
||||||
user={user}
|
user={user}
|
||||||
surveys={surveys}
|
surveys={surveys}
|
||||||
responses={responses}
|
responses={responses}
|
||||||
displays={displays}
|
|
||||||
environment={environment}
|
environment={environment}
|
||||||
environmentTags={environmentTags}
|
environmentTags={environmentTags}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
|
||||||
import { TResponseWithQuotas } from "@formbricks/types/responses";
|
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
|
||||||
import { TTag } from "@formbricks/types/tags";
|
|
||||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
|
||||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
|
||||||
import { SingleResponseCard } from "@/modules/analysis/components/SingleResponseCard";
|
|
||||||
|
|
||||||
interface ResponseSurveyCardProps {
|
|
||||||
response: TResponseWithQuotas;
|
|
||||||
surveys: TSurvey[];
|
|
||||||
user: TUser;
|
|
||||||
environmentTags: TTag[];
|
|
||||||
environment: TEnvironment;
|
|
||||||
updateResponseList: (responseIds: string[]) => void;
|
|
||||||
updateResponse: (responseId: string, response: TResponseWithQuotas) => void;
|
|
||||||
locale: TUserLocale;
|
|
||||||
isReadOnly: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ResponseSurveyCard = ({
|
|
||||||
response,
|
|
||||||
surveys,
|
|
||||||
user,
|
|
||||||
environmentTags,
|
|
||||||
environment,
|
|
||||||
updateResponseList,
|
|
||||||
updateResponse,
|
|
||||||
locale,
|
|
||||||
isReadOnly,
|
|
||||||
}: ResponseSurveyCardProps) => {
|
|
||||||
const survey = surveys.find((s) => s.id === response.surveyId);
|
|
||||||
|
|
||||||
if (!survey) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SingleResponseCard
|
|
||||||
response={response}
|
|
||||||
survey={replaceHeadlineRecall(survey, "default")}
|
|
||||||
user={user}
|
|
||||||
environmentTags={environmentTags}
|
|
||||||
environment={environment}
|
|
||||||
updateResponseList={updateResponseList}
|
|
||||||
updateResponse={updateResponse}
|
|
||||||
isReadOnly={isReadOnly}
|
|
||||||
locale={locale}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ArrowDownUpIcon } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
import { TResponseWithQuotas } from "@formbricks/types/responses";
|
||||||
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
import { TTag } from "@formbricks/types/tags";
|
||||||
|
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||||
|
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||||
|
import { ResponseFeed } from "./response-feed";
|
||||||
|
|
||||||
|
interface ResponseTimelineProps {
|
||||||
|
surveys: TSurvey[];
|
||||||
|
user: TUser;
|
||||||
|
responses: TResponseWithQuotas[];
|
||||||
|
environment: TEnvironment;
|
||||||
|
environmentTags: TTag[];
|
||||||
|
locale: TUserLocale;
|
||||||
|
projectPermission: TTeamPermission | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResponseTimeline = ({
|
||||||
|
surveys,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
responses,
|
||||||
|
environmentTags,
|
||||||
|
locale,
|
||||||
|
projectPermission,
|
||||||
|
}: ResponseTimelineProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [sortedResponses, setSortedResponses] = useState(responses);
|
||||||
|
const toggleSortResponses = () => {
|
||||||
|
setSortedResponses([...sortedResponses].reverse());
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSortedResponses(responses);
|
||||||
|
}, [responses]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// <div className="md:col-span-3">
|
||||||
|
<div className="col-span-3">
|
||||||
|
<div className="flex items-center justify-between pb-6">
|
||||||
|
<h2 className="text-lg font-bold text-slate-700">{t("common.responses")}</h2>
|
||||||
|
<div className="text-right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleSortResponses}
|
||||||
|
className="hover:text-brand-dark flex items-center px-1 text-slate-800">
|
||||||
|
<ArrowDownUpIcon className="inline h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ResponseFeed
|
||||||
|
responses={sortedResponses}
|
||||||
|
environment={environment}
|
||||||
|
surveys={surveys}
|
||||||
|
user={user}
|
||||||
|
environmentTags={environmentTags}
|
||||||
|
locale={locale}
|
||||||
|
projectPermission={projectPermission}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -11,7 +11,7 @@ import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
|||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { ActivitySection } from "./components/activity-section";
|
import { ResponseSection } from "./components/response-section";
|
||||||
|
|
||||||
export const SingleContactPage = async (props: {
|
export const SingleContactPage = async (props: {
|
||||||
params: Promise<{ environmentId: string; contactId: string }>;
|
params: Promise<{ environmentId: string; contactId: string }>;
|
||||||
@@ -64,7 +64,7 @@ export const SingleContactPage = async (props: {
|
|||||||
<section className="pb-24 pt-6">
|
<section className="pb-24 pt-6">
|
||||||
<div className="grid grid-cols-4 gap-x-8">
|
<div className="grid grid-cols-4 gap-x-8">
|
||||||
<AttributesSection contactId={params.contactId} />
|
<AttributesSection contactId={params.contactId} />
|
||||||
<ActivitySection
|
<ResponseSection
|
||||||
environment={environment}
|
environment={environment}
|
||||||
contactId={params.contactId}
|
contactId={params.contactId}
|
||||||
environmentTags={environmentTags}
|
environmentTags={environmentTags}
|
||||||
|
|||||||
+26
-75
@@ -7,9 +7,10 @@ import { validateInputs } from "@/lib/utils/validate";
|
|||||||
import { segmentFilterToPrismaQuery } from "@/modules/ee/contacts/segments/lib/filter/prisma-query";
|
import { segmentFilterToPrismaQuery } from "@/modules/ee/contacts/segments/lib/filter/prisma-query";
|
||||||
import { getPersonSegmentIds, getSegments } from "./segments";
|
import { getPersonSegmentIds, getSegments } from "./segments";
|
||||||
|
|
||||||
|
// Mock the cache functions
|
||||||
vi.mock("@/lib/cache", () => ({
|
vi.mock("@/lib/cache", () => ({
|
||||||
cache: {
|
cache: {
|
||||||
withCache: vi.fn(async (fn) => await fn()),
|
withCache: vi.fn(async (fn) => await fn()), // Just execute the function without caching for tests
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -29,15 +30,15 @@ vi.mock("@formbricks/database", () => ({
|
|||||||
contact: {
|
contact: {
|
||||||
findFirst: vi.fn(),
|
findFirst: vi.fn(),
|
||||||
},
|
},
|
||||||
$transaction: vi.fn(),
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock React cache
|
||||||
vi.mock("react", async () => {
|
vi.mock("react", async () => {
|
||||||
const actual = await vi.importActual("react");
|
const actual = await vi.importActual("react");
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
cache: <T extends (...args: any[]) => any>(fn: T): T => fn,
|
cache: <T extends (...args: any[]) => any>(fn: T): T => fn, // Return the function with the same type signature
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,20 +97,22 @@ describe("segments lib", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("getPersonSegmentIds", () => {
|
describe("getPersonSegmentIds", () => {
|
||||||
const mockWhereClause = { AND: [{ environmentId: mockEnvironmentId }, {}] };
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(prisma.segment.findMany).mockResolvedValue(
|
vi.mocked(prisma.segment.findMany).mockResolvedValue(
|
||||||
mockSegmentsData as Prisma.Result<typeof prisma.segment, unknown, "findMany">
|
mockSegmentsData as Prisma.Result<typeof prisma.segment, unknown, "findMany">
|
||||||
);
|
);
|
||||||
vi.mocked(segmentFilterToPrismaQuery).mockResolvedValue({
|
vi.mocked(segmentFilterToPrismaQuery).mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
data: { whereClause: mockWhereClause },
|
data: { whereClause: { AND: [{ environmentId: mockEnvironmentId }, {}] } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return person segment IDs successfully", async () => {
|
test("should return person segment IDs successfully", async () => {
|
||||||
vi.mocked(prisma.$transaction).mockResolvedValue([{ id: mockContactId }, { id: mockContactId }]);
|
vi.mocked(prisma.contact.findFirst).mockResolvedValue({ id: mockContactId } as Prisma.Result<
|
||||||
|
typeof prisma.contact,
|
||||||
|
unknown,
|
||||||
|
"findFirst"
|
||||||
|
>);
|
||||||
|
|
||||||
const result = await getPersonSegmentIds(
|
const result = await getPersonSegmentIds(
|
||||||
mockEnvironmentId,
|
mockEnvironmentId,
|
||||||
@@ -125,12 +128,12 @@ describe("segments lib", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(segmentFilterToPrismaQuery).toHaveBeenCalledTimes(mockSegmentsData.length);
|
expect(segmentFilterToPrismaQuery).toHaveBeenCalledTimes(mockSegmentsData.length);
|
||||||
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
|
expect(prisma.contact.findFirst).toHaveBeenCalledTimes(mockSegmentsData.length);
|
||||||
expect(result).toEqual(mockSegmentsData.map((s) => s.id));
|
expect(result).toEqual(mockSegmentsData.map((s) => s.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return empty array if no segments exist", async () => {
|
test("should return empty array if no segments exist", async () => {
|
||||||
vi.mocked(prisma.segment.findMany).mockResolvedValue([]);
|
vi.mocked(prisma.segment.findMany).mockResolvedValue([]); // No segments
|
||||||
|
|
||||||
const result = await getPersonSegmentIds(
|
const result = await getPersonSegmentIds(
|
||||||
mockEnvironmentId,
|
mockEnvironmentId,
|
||||||
@@ -141,11 +144,10 @@ describe("segments lib", () => {
|
|||||||
|
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
expect(segmentFilterToPrismaQuery).not.toHaveBeenCalled();
|
expect(segmentFilterToPrismaQuery).not.toHaveBeenCalled();
|
||||||
expect(prisma.$transaction).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return empty array if segments exist but none match", async () => {
|
test("should return empty array if segments exist but none match", async () => {
|
||||||
vi.mocked(prisma.$transaction).mockResolvedValue([null, null]);
|
vi.mocked(prisma.contact.findFirst).mockResolvedValue(null);
|
||||||
|
|
||||||
const result = await getPersonSegmentIds(
|
const result = await getPersonSegmentIds(
|
||||||
mockEnvironmentId,
|
mockEnvironmentId,
|
||||||
@@ -153,14 +155,16 @@ describe("segments lib", () => {
|
|||||||
mockContactUserId,
|
mockContactUserId,
|
||||||
mockDeviceType
|
mockDeviceType
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
expect(segmentFilterToPrismaQuery).toHaveBeenCalledTimes(mockSegmentsData.length);
|
expect(segmentFilterToPrismaQuery).toHaveBeenCalledTimes(mockSegmentsData.length);
|
||||||
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should call validateInputs with correct parameters", async () => {
|
test("should call validateInputs with correct parameters", async () => {
|
||||||
vi.mocked(prisma.$transaction).mockResolvedValue([{ id: mockContactId }, { id: mockContactId }]);
|
vi.mocked(prisma.contact.findFirst).mockResolvedValue({ id: mockContactId } as Prisma.Result<
|
||||||
|
typeof prisma.contact,
|
||||||
|
unknown,
|
||||||
|
"findFirst"
|
||||||
|
>);
|
||||||
|
|
||||||
await getPersonSegmentIds(mockEnvironmentId, mockContactId, mockContactUserId, mockDeviceType);
|
await getPersonSegmentIds(mockEnvironmentId, mockContactId, mockContactUserId, mockDeviceType);
|
||||||
expect(validateInputs).toHaveBeenCalledWith(
|
expect(validateInputs).toHaveBeenCalledWith(
|
||||||
@@ -171,7 +175,14 @@ describe("segments lib", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should return only matching segment IDs", async () => {
|
test("should return only matching segment IDs", async () => {
|
||||||
vi.mocked(prisma.$transaction).mockResolvedValue([{ id: mockContactId }, null]);
|
// First segment matches, second doesn't
|
||||||
|
vi.mocked(prisma.contact.findFirst)
|
||||||
|
.mockResolvedValueOnce({ id: mockContactId } as Prisma.Result<
|
||||||
|
typeof prisma.contact,
|
||||||
|
unknown,
|
||||||
|
"findFirst"
|
||||||
|
>) // First segment matches
|
||||||
|
.mockResolvedValueOnce(null); // Second segment does not match
|
||||||
|
|
||||||
const result = await getPersonSegmentIds(
|
const result = await getPersonSegmentIds(
|
||||||
mockEnvironmentId,
|
mockEnvironmentId,
|
||||||
@@ -182,66 +193,6 @@ describe("segments lib", () => {
|
|||||||
|
|
||||||
expect(result).toEqual([mockSegmentsData[0].id]);
|
expect(result).toEqual([mockSegmentsData[0].id]);
|
||||||
expect(segmentFilterToPrismaQuery).toHaveBeenCalledTimes(mockSegmentsData.length);
|
expect(segmentFilterToPrismaQuery).toHaveBeenCalledTimes(mockSegmentsData.length);
|
||||||
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should include segments with no filters as always-matching", async () => {
|
|
||||||
const segmentsWithEmptyFilters = [
|
|
||||||
{ id: "segment-no-filter", filters: [] },
|
|
||||||
{ id: "segment-with-filter", filters: [{}] as TBaseFilter[] },
|
|
||||||
];
|
|
||||||
vi.mocked(prisma.segment.findMany).mockResolvedValue(
|
|
||||||
segmentsWithEmptyFilters as Prisma.Result<typeof prisma.segment, unknown, "findMany">
|
|
||||||
);
|
|
||||||
vi.mocked(prisma.$transaction).mockResolvedValue([{ id: mockContactId }]);
|
|
||||||
|
|
||||||
const result = await getPersonSegmentIds(
|
|
||||||
mockEnvironmentId,
|
|
||||||
mockContactId,
|
|
||||||
mockContactUserId,
|
|
||||||
mockDeviceType
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toContain("segment-no-filter");
|
|
||||||
expect(result).toContain("segment-with-filter");
|
|
||||||
expect(segmentFilterToPrismaQuery).toHaveBeenCalledTimes(1);
|
|
||||||
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should skip segments where filter query building fails", async () => {
|
|
||||||
vi.mocked(segmentFilterToPrismaQuery)
|
|
||||||
.mockResolvedValueOnce({
|
|
||||||
ok: true,
|
|
||||||
data: { whereClause: mockWhereClause },
|
|
||||||
})
|
|
||||||
.mockResolvedValueOnce({
|
|
||||||
ok: false,
|
|
||||||
error: { type: "bad_request", message: "Invalid filters", details: [] },
|
|
||||||
});
|
|
||||||
vi.mocked(prisma.$transaction).mockResolvedValue([{ id: mockContactId }]);
|
|
||||||
|
|
||||||
const result = await getPersonSegmentIds(
|
|
||||||
mockEnvironmentId,
|
|
||||||
mockContactId,
|
|
||||||
mockContactUserId,
|
|
||||||
mockDeviceType
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual(["segment1"]);
|
|
||||||
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return empty array on unexpected error", async () => {
|
|
||||||
vi.mocked(prisma.segment.findMany).mockRejectedValue(new Error("Unexpected"));
|
|
||||||
|
|
||||||
const result = await getPersonSegmentIds(
|
|
||||||
mockEnvironmentId,
|
|
||||||
mockContactId,
|
|
||||||
mockContactUserId,
|
|
||||||
mockDeviceType
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,6 +37,47 @@ export const getSegments = reactCache(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a contact matches a segment using Prisma query
|
||||||
|
* This leverages native DB types (valueDate, valueNumber) for accurate comparisons
|
||||||
|
* Device filters are evaluated at query build time using the provided deviceType
|
||||||
|
*/
|
||||||
|
const isContactInSegment = async (
|
||||||
|
contactId: string,
|
||||||
|
segmentId: string,
|
||||||
|
filters: TBaseFilters,
|
||||||
|
environmentId: string,
|
||||||
|
deviceType: "phone" | "desktop"
|
||||||
|
): Promise<boolean> => {
|
||||||
|
// If no filters, segment matches all contacts
|
||||||
|
if (!filters || filters.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryResult = await segmentFilterToPrismaQuery(segmentId, filters, environmentId, deviceType);
|
||||||
|
|
||||||
|
if (!queryResult.ok) {
|
||||||
|
logger.warn(
|
||||||
|
{ segmentId, environmentId, error: queryResult.error },
|
||||||
|
"Failed to build Prisma query for segment"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { whereClause } = queryResult.data;
|
||||||
|
|
||||||
|
// Check if this specific contact matches the segment filters
|
||||||
|
const matchingContact = await prisma.contact.findFirst({
|
||||||
|
where: {
|
||||||
|
id: contactId,
|
||||||
|
...whereClause,
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchingContact !== null;
|
||||||
|
};
|
||||||
|
|
||||||
export const getPersonSegmentIds = async (
|
export const getPersonSegmentIds = async (
|
||||||
environmentId: string,
|
environmentId: string,
|
||||||
contactId: string,
|
contactId: string,
|
||||||
@@ -48,58 +89,29 @@ export const getPersonSegmentIds = async (
|
|||||||
|
|
||||||
const segments = await getSegments(environmentId);
|
const segments = await getSegments(environmentId);
|
||||||
|
|
||||||
if (!segments || !Array.isArray(segments) || segments.length === 0) {
|
// fast path; if there are no segments, return an empty array
|
||||||
|
if (!segments || !Array.isArray(segments)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 1: Build WHERE clauses sequentially to avoid connection pool contention.
|
// Device filters are evaluated at query build time using the provided deviceType
|
||||||
// segmentFilterToPrismaQuery can itself hit the DB (e.g. unmigrated-row checks),
|
const segmentPromises = segments.map(async (segment) => {
|
||||||
// so running all builds concurrently would saturate the pool.
|
const filters = segment.filters;
|
||||||
const alwaysMatchIds: string[] = [];
|
const isIncluded = await isContactInSegment(contactId, segment.id, filters, environmentId, deviceType);
|
||||||
const dbChecks: { segmentId: string; whereClause: Prisma.ContactWhereInput }[] = [];
|
return isIncluded ? segment.id : null;
|
||||||
|
});
|
||||||
|
|
||||||
for (const segment of segments) {
|
const results = await Promise.all(segmentPromises);
|
||||||
const filters = segment.filters as TBaseFilters;
|
|
||||||
|
|
||||||
if (!filters?.length) {
|
return results.filter((id): id is string => id !== null);
|
||||||
alwaysMatchIds.push(segment.id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryResult = await segmentFilterToPrismaQuery(segment.id, filters, environmentId, deviceType);
|
|
||||||
|
|
||||||
if (!queryResult.ok) {
|
|
||||||
logger.warn(
|
|
||||||
{ segmentId: segment.id, environmentId, error: queryResult.error },
|
|
||||||
"Failed to build Prisma query for segment, skipping"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbChecks.push({ segmentId: segment.id, whereClause: queryResult.data.whereClause });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dbChecks.length === 0) {
|
|
||||||
return alwaysMatchIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: Execute all membership checks in a single transaction.
|
|
||||||
// Uses one connection instead of N concurrent ones, eliminating pool contention.
|
|
||||||
const txResults = await prisma.$transaction(
|
|
||||||
dbChecks.map(({ whereClause }) =>
|
|
||||||
prisma.contact.findFirst({
|
|
||||||
where: { id: contactId, ...whereClause },
|
|
||||||
select: { id: true },
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const matchedIds = dbChecks.filter((_, i) => txResults[i] !== null).map(({ segmentId }) => segmentId);
|
|
||||||
|
|
||||||
return [...alwaysMatchIds, ...matchedIds];
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Log error for debugging but don't throw to prevent "segments is not iterable" error
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ environmentId, contactId, error },
|
{
|
||||||
|
environmentId,
|
||||||
|
contactId,
|
||||||
|
error,
|
||||||
|
},
|
||||||
"Failed to get person segment IDs, returning empty array"
|
"Failed to get person segment IDs, returning empty array"
|
||||||
);
|
);
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { prisma } from "@formbricks/database";
|
||||||
|
import { TJsPersonState } from "@formbricks/types/js";
|
||||||
|
import { getPersonSegmentIds } from "./segments";
|
||||||
|
import { getUserState } from "./user-state";
|
||||||
|
|
||||||
|
vi.mock("@formbricks/database", () => ({
|
||||||
|
prisma: {
|
||||||
|
contact: {
|
||||||
|
findUniqueOrThrow: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./segments", () => ({
|
||||||
|
getPersonSegmentIds: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockEnvironmentId = "test-environment-id";
|
||||||
|
const mockUserId = "test-user-id";
|
||||||
|
const mockContactId = "test-contact-id";
|
||||||
|
const mockDevice = "desktop";
|
||||||
|
|
||||||
|
describe("getUserState", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return user state with empty responses and displays", async () => {
|
||||||
|
const mockContactData = {
|
||||||
|
id: mockContactId,
|
||||||
|
responses: [],
|
||||||
|
displays: [],
|
||||||
|
};
|
||||||
|
vi.mocked(prisma.contact.findUniqueOrThrow).mockResolvedValue(mockContactData as any);
|
||||||
|
vi.mocked(getPersonSegmentIds).mockResolvedValue(["segment1"]);
|
||||||
|
|
||||||
|
const result = await getUserState({
|
||||||
|
environmentId: mockEnvironmentId,
|
||||||
|
userId: mockUserId,
|
||||||
|
contactId: mockContactId,
|
||||||
|
device: mockDevice,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(prisma.contact.findUniqueOrThrow).toHaveBeenCalledWith({
|
||||||
|
where: { id: mockContactId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
responses: {
|
||||||
|
select: { surveyId: true },
|
||||||
|
},
|
||||||
|
displays: {
|
||||||
|
select: { surveyId: true, createdAt: true },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getPersonSegmentIds).toHaveBeenCalledWith(
|
||||||
|
mockEnvironmentId,
|
||||||
|
mockContactId,
|
||||||
|
mockUserId,
|
||||||
|
mockDevice
|
||||||
|
);
|
||||||
|
expect(result).toEqual<TJsPersonState["data"]>({
|
||||||
|
contactId: mockContactId,
|
||||||
|
userId: mockUserId,
|
||||||
|
segments: ["segment1"],
|
||||||
|
displays: [],
|
||||||
|
responses: [],
|
||||||
|
lastDisplayAt: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return user state with responses and displays, and sort displays by createdAt", async () => {
|
||||||
|
const mockDate1 = new Date("2023-01-01T00:00:00.000Z");
|
||||||
|
const mockDate2 = new Date("2023-01-02T00:00:00.000Z");
|
||||||
|
|
||||||
|
const mockContactData = {
|
||||||
|
id: mockContactId,
|
||||||
|
responses: [{ surveyId: "survey1" }, { surveyId: "survey2" }],
|
||||||
|
displays: [
|
||||||
|
{ surveyId: "survey4", createdAt: mockDate2 }, // most recent (already sorted by desc)
|
||||||
|
{ surveyId: "survey3", createdAt: mockDate1 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
vi.mocked(prisma.contact.findUniqueOrThrow).mockResolvedValue(mockContactData as any);
|
||||||
|
vi.mocked(getPersonSegmentIds).mockResolvedValue(["segment2", "segment3"]);
|
||||||
|
|
||||||
|
const result = await getUserState({
|
||||||
|
environmentId: mockEnvironmentId,
|
||||||
|
userId: mockUserId,
|
||||||
|
contactId: mockContactId,
|
||||||
|
device: mockDevice,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual<TJsPersonState["data"]>({
|
||||||
|
contactId: mockContactId,
|
||||||
|
userId: mockUserId,
|
||||||
|
segments: ["segment2", "segment3"],
|
||||||
|
displays: [
|
||||||
|
{ surveyId: "survey4", createdAt: mockDate2 },
|
||||||
|
{ surveyId: "survey3", createdAt: mockDate1 },
|
||||||
|
],
|
||||||
|
responses: ["survey1", "survey2"],
|
||||||
|
lastDisplayAt: mockDate2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle empty arrays from prisma", async () => {
|
||||||
|
// This case tests with proper empty arrays instead of null
|
||||||
|
const mockContactData = {
|
||||||
|
id: mockContactId,
|
||||||
|
responses: [],
|
||||||
|
displays: [],
|
||||||
|
};
|
||||||
|
vi.mocked(prisma.contact.findUniqueOrThrow).mockResolvedValue(mockContactData as any);
|
||||||
|
vi.mocked(getPersonSegmentIds).mockResolvedValue([]);
|
||||||
|
|
||||||
|
const result = await getUserState({
|
||||||
|
environmentId: mockEnvironmentId,
|
||||||
|
userId: mockUserId,
|
||||||
|
contactId: mockContactId,
|
||||||
|
device: mockDevice,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual<TJsPersonState["data"]>({
|
||||||
|
contactId: mockContactId,
|
||||||
|
userId: mockUserId,
|
||||||
|
segments: [],
|
||||||
|
displays: [],
|
||||||
|
responses: [],
|
||||||
|
lastDisplayAt: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { prisma } from "@formbricks/database";
|
||||||
|
import { TJsPersonState } from "@formbricks/types/js";
|
||||||
|
import { getPersonSegmentIds } from "./segments";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized single query to get all user state data
|
||||||
|
* Replaces multiple separate queries with one efficient query
|
||||||
|
*/
|
||||||
|
const getUserStateDataOptimized = async (contactId: string) => {
|
||||||
|
return prisma.contact.findUniqueOrThrow({
|
||||||
|
where: { id: contactId },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
responses: {
|
||||||
|
select: { surveyId: true },
|
||||||
|
},
|
||||||
|
displays: {
|
||||||
|
select: {
|
||||||
|
surveyId: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized user state fetcher without caching
|
||||||
|
* Uses single database query and efficient data processing
|
||||||
|
* NO CACHING - user state changes frequently with contact updates
|
||||||
|
*
|
||||||
|
* @param environmentId - The environment id
|
||||||
|
* @param userId - The user id
|
||||||
|
* @param device - The device type
|
||||||
|
* @returns The person state
|
||||||
|
* @throws {ValidationError} - If the input is invalid
|
||||||
|
* @throws {ResourceNotFoundError} - If the environment or organization is not found
|
||||||
|
*/
|
||||||
|
export const getUserState = async ({
|
||||||
|
environmentId,
|
||||||
|
userId,
|
||||||
|
contactId,
|
||||||
|
device,
|
||||||
|
}: {
|
||||||
|
environmentId: string;
|
||||||
|
userId: string;
|
||||||
|
contactId: string;
|
||||||
|
device: "phone" | "desktop";
|
||||||
|
}): Promise<TJsPersonState["data"]> => {
|
||||||
|
// Single optimized query for all contact data
|
||||||
|
const contactData = await getUserStateDataOptimized(contactId);
|
||||||
|
|
||||||
|
// Get segments using Prisma-based evaluation (no attributes needed - fetched from DB)
|
||||||
|
const segments = await getPersonSegmentIds(environmentId, contactId, userId, device);
|
||||||
|
|
||||||
|
// Process displays efficiently
|
||||||
|
const displays = (contactData.displays ?? []).map((display) => ({
|
||||||
|
surveyId: display.surveyId,
|
||||||
|
createdAt: display.createdAt,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get latest display date
|
||||||
|
const lastDisplayAt =
|
||||||
|
contactData.displays && contactData.displays.length > 0 ? contactData.displays[0].createdAt : null;
|
||||||
|
|
||||||
|
// Process responses efficiently
|
||||||
|
const responses = (contactData.responses ?? []).map((response) => response.surveyId);
|
||||||
|
|
||||||
|
const userState: TJsPersonState["data"] = {
|
||||||
|
contactId,
|
||||||
|
userId,
|
||||||
|
segments,
|
||||||
|
displays,
|
||||||
|
responses,
|
||||||
|
lastDisplayAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
return userState;
|
||||||
|
};
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { getLocale } from "@/lingodotdev/language";
|
import { getLocale } from "@/lingodotdev/language";
|
||||||
import { getTranslate } from "@/lingodotdev/server";
|
|
||||||
import { ContactsPageLayout } from "@/modules/ee/contacts/components/contacts-page-layout";
|
import { ContactsPageLayout } from "@/modules/ee/contacts/components/contacts-page-layout";
|
||||||
import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contact-attribute-keys";
|
import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contact-attribute-keys";
|
||||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||||
@@ -14,7 +13,7 @@ export const AttributesPage = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const params = await paramsProps;
|
const params = await paramsProps;
|
||||||
const locale = await getLocale();
|
const locale = await getLocale();
|
||||||
const t = await getTranslate();
|
|
||||||
const [{ isReadOnly }, contactAttributeKeys] = await Promise.all([
|
const [{ isReadOnly }, contactAttributeKeys] = await Promise.all([
|
||||||
getEnvironmentAuth(params.environmentId),
|
getEnvironmentAuth(params.environmentId),
|
||||||
getContactAttributeKeys(params.environmentId),
|
getContactAttributeKeys(params.environmentId),
|
||||||
@@ -24,7 +23,7 @@ export const AttributesPage = async ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ContactsPageLayout
|
<ContactsPageLayout
|
||||||
pageTitle={t("common.contacts")}
|
pageTitle="Contacts"
|
||||||
activeId="attributes"
|
activeId="attributes"
|
||||||
environmentId={params.environmentId}
|
environmentId={params.environmentId}
|
||||||
isContactsEnabled={isContactsEnabled}
|
isContactsEnabled={isContactsEnabled}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import { TFunction } from "i18next";
|
|
||||||
import { formatAttributeValue } from "@/modules/ee/contacts/lib/format-attribute-value";
|
import { formatAttributeValue } from "@/modules/ee/contacts/lib/format-attribute-value";
|
||||||
import { getSelectionColumn } from "@/modules/ui/components/data-table";
|
import { getSelectionColumn } from "@/modules/ui/components/data-table";
|
||||||
import { HighlightedText } from "@/modules/ui/components/highlighted-text";
|
import { HighlightedText } from "@/modules/ui/components/highlighted-text";
|
||||||
@@ -11,13 +10,12 @@ import { TContactTableData } from "../types/contact";
|
|||||||
export const generateContactTableColumns = (
|
export const generateContactTableColumns = (
|
||||||
searchValue: string,
|
searchValue: string,
|
||||||
data: TContactTableData[],
|
data: TContactTableData[],
|
||||||
isReadOnly: boolean,
|
isReadOnly: boolean
|
||||||
t: TFunction
|
|
||||||
): ColumnDef<TContactTableData>[] => {
|
): ColumnDef<TContactTableData>[] => {
|
||||||
const userColumn: ColumnDef<TContactTableData> = {
|
const userColumn: ColumnDef<TContactTableData> = {
|
||||||
id: "contactsTableUser",
|
id: "contactsTableUser",
|
||||||
accessorKey: "contactsTableUser",
|
accessorKey: "contactsTableUser",
|
||||||
header: t("common.id"),
|
header: "ID",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const contactId = row.original.id;
|
const contactId = row.original.id;
|
||||||
return <HighlightedText value={contactId} searchValue={searchValue} />;
|
return <HighlightedText value={contactId} searchValue={searchValue} />;
|
||||||
@@ -27,7 +25,7 @@ export const generateContactTableColumns = (
|
|||||||
const userIdColumn: ColumnDef<TContactTableData> = {
|
const userIdColumn: ColumnDef<TContactTableData> = {
|
||||||
id: "userId",
|
id: "userId",
|
||||||
accessorKey: "userId",
|
accessorKey: "userId",
|
||||||
header: t("common.user_id"),
|
header: "User ID",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const userId = row.original.userId;
|
const userId = row.original.userId;
|
||||||
return <IdBadge id={userId} />;
|
return <IdBadge id={userId} />;
|
||||||
@@ -37,7 +35,7 @@ export const generateContactTableColumns = (
|
|||||||
const emailColumn: ColumnDef<TContactTableData> = {
|
const emailColumn: ColumnDef<TContactTableData> = {
|
||||||
id: "email",
|
id: "email",
|
||||||
accessorKey: "email",
|
accessorKey: "email",
|
||||||
header: t("common.email"),
|
header: "Email",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const email = row.original.email;
|
const email = row.original.email;
|
||||||
if (email) {
|
if (email) {
|
||||||
@@ -49,7 +47,7 @@ export const generateContactTableColumns = (
|
|||||||
const firstNameColumn: ColumnDef<TContactTableData> = {
|
const firstNameColumn: ColumnDef<TContactTableData> = {
|
||||||
id: "firstName",
|
id: "firstName",
|
||||||
accessorKey: "firstName",
|
accessorKey: "firstName",
|
||||||
header: t("common.first_name"),
|
header: "First Name",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const firstName = row.original.firstName;
|
const firstName = row.original.firstName;
|
||||||
return <HighlightedText value={firstName} searchValue={searchValue} />;
|
return <HighlightedText value={firstName} searchValue={searchValue} />;
|
||||||
@@ -59,7 +57,7 @@ export const generateContactTableColumns = (
|
|||||||
const lastNameColumn: ColumnDef<TContactTableData> = {
|
const lastNameColumn: ColumnDef<TContactTableData> = {
|
||||||
id: "lastName",
|
id: "lastName",
|
||||||
accessorKey: "lastName",
|
accessorKey: "lastName",
|
||||||
header: t("common.last_name"),
|
header: "Last Name",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const lastName = row.original.lastName;
|
const lastName = row.original.lastName;
|
||||||
return <HighlightedText value={lastName} searchValue={searchValue} />;
|
return <HighlightedText value={lastName} searchValue={searchValue} />;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const ContactsTable = ({
|
|||||||
|
|
||||||
// Generate columns
|
// Generate columns
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
return generateContactTableColumns(searchValue, data, isReadOnly, t);
|
return generateContactTableColumns(searchValue, data, isReadOnly);
|
||||||
}, [searchValue, data, isReadOnly]);
|
}, [searchValue, data, isReadOnly]);
|
||||||
|
|
||||||
// Load saved settings from localStorage
|
// Load saved settings from localStorage
|
||||||
|
|||||||
@@ -2,15 +2,14 @@
|
|||||||
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import { format, formatDistanceToNow } from "date-fns";
|
import { format, formatDistanceToNow } from "date-fns";
|
||||||
import { TFunction } from "i18next";
|
|
||||||
import { UsersIcon } from "lucide-react";
|
import { UsersIcon } from "lucide-react";
|
||||||
import { TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
import { TSegmentWithSurveyNames } from "@formbricks/types/segment";
|
||||||
|
|
||||||
export const generateSegmentTableColumns = (t: TFunction): ColumnDef<TSegmentWithSurveyNames>[] => {
|
export const generateSegmentTableColumns = (): ColumnDef<TSegmentWithSurveyNames>[] => {
|
||||||
const titleColumn: ColumnDef<TSegmentWithSurveyNames> = {
|
const titleColumn: ColumnDef<TSegmentWithSurveyNames> = {
|
||||||
id: "title",
|
id: "title",
|
||||||
accessorKey: "title",
|
accessorKey: "title",
|
||||||
header: t("common.title"),
|
header: "Title",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -31,7 +30,7 @@ export const generateSegmentTableColumns = (t: TFunction): ColumnDef<TSegmentWit
|
|||||||
const updatedAtColumn: ColumnDef<TSegmentWithSurveyNames> = {
|
const updatedAtColumn: ColumnDef<TSegmentWithSurveyNames> = {
|
||||||
id: "updatedAt",
|
id: "updatedAt",
|
||||||
accessorKey: "updatedAt",
|
accessorKey: "updatedAt",
|
||||||
header: t("common.updated_at"),
|
header: "Updated",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return (
|
return (
|
||||||
<span className="text-sm text-slate-900">
|
<span className="text-sm text-slate-900">
|
||||||
@@ -44,7 +43,7 @@ export const generateSegmentTableColumns = (t: TFunction): ColumnDef<TSegmentWit
|
|||||||
const createdAtColumn: ColumnDef<TSegmentWithSurveyNames> = {
|
const createdAtColumn: ColumnDef<TSegmentWithSurveyNames> = {
|
||||||
id: "createdAt",
|
id: "createdAt",
|
||||||
accessorKey: "createdAt",
|
accessorKey: "createdAt",
|
||||||
header: t("common.created_at"),
|
header: "Created",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return (
|
return (
|
||||||
<span className="text-sm text-slate-900">{format(row.original.createdAt, "do 'of' MMMM, yyyy")}</span>
|
<span className="text-sm text-slate-900">{format(row.original.createdAt, "do 'of' MMMM, yyyy")}</span>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function SegmentTable({
|
|||||||
const [editingSegment, setEditingSegment] = useState<TSegmentWithSurveyNames | null>(null);
|
const [editingSegment, setEditingSegment] = useState<TSegmentWithSurveyNames | null>(null);
|
||||||
|
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
return generateSegmentTableColumns(t);
|
return generateSegmentTableColumns();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const SegmentsPage = async ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ContactsPageLayout
|
<ContactsPageLayout
|
||||||
pageTitle={t("common.contacts")}
|
pageTitle="Contacts"
|
||||||
activeId="segments"
|
activeId="segments"
|
||||||
environmentId={params.environmentId}
|
environmentId={params.environmentId}
|
||||||
isContactsEnabled={isContactsEnabled}
|
isContactsEnabled={isContactsEnabled}
|
||||||
|
|||||||
@@ -54,9 +54,9 @@
|
|||||||
"@opentelemetry/sdk-node": "0.211.0",
|
"@opentelemetry/sdk-node": "0.211.0",
|
||||||
"@opentelemetry/sdk-trace-base": "2.5.0",
|
"@opentelemetry/sdk-trace-base": "2.5.0",
|
||||||
"@opentelemetry/semantic-conventions": "1.38.0",
|
"@opentelemetry/semantic-conventions": "1.38.0",
|
||||||
|
"@prisma/instrumentation": "6.14.0",
|
||||||
"@paralleldrive/cuid2": "2.2.2",
|
"@paralleldrive/cuid2": "2.2.2",
|
||||||
"@prisma/client": "6.14.0",
|
"@prisma/client": "6.14.0",
|
||||||
"@prisma/instrumentation": "6.14.0",
|
|
||||||
"@radix-ui/react-accordion": "1.2.10",
|
"@radix-ui/react-accordion": "1.2.10",
|
||||||
"@radix-ui/react-checkbox": "1.3.1",
|
"@radix-ui/react-checkbox": "1.3.1",
|
||||||
"@radix-ui/react-collapsible": "1.1.10",
|
"@radix-ui/react-collapsible": "1.1.10",
|
||||||
@@ -114,12 +114,10 @@
|
|||||||
"prismjs": "1.30.0",
|
"prismjs": "1.30.0",
|
||||||
"qr-code-styling": "1.9.2",
|
"qr-code-styling": "1.9.2",
|
||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
"react": "19.2.3",
|
|
||||||
"react-calendar": "5.1.0",
|
"react-calendar": "5.1.0",
|
||||||
"react-colorful": "5.6.1",
|
"react-colorful": "5.6.1",
|
||||||
"react-confetti": "6.4.0",
|
"react-confetti": "6.4.0",
|
||||||
"react-day-picker": "9.6.7",
|
"react-day-picker": "9.6.7",
|
||||||
"react-dom": "19.2.3",
|
|
||||||
"react-hook-form": "7.56.2",
|
"react-hook-form": "7.56.2",
|
||||||
"react-hot-toast": "2.5.2",
|
"react-hot-toast": "2.5.2",
|
||||||
"react-i18next": "15.7.3",
|
"react-i18next": "15.7.3",
|
||||||
@@ -137,8 +135,10 @@
|
|||||||
"uuid": "11.1.0",
|
"uuid": "11.1.0",
|
||||||
"webpack": "5.99.8",
|
"webpack": "5.99.8",
|
||||||
"xlsx": "file:vendor/xlsx-0.20.3.tgz",
|
"xlsx": "file:vendor/xlsx-0.20.3.tgz",
|
||||||
"zod": "3.25.76",
|
"zod": "3.24.4",
|
||||||
"zod-openapi": "4.2.4"
|
"zod-openapi": "4.2.4",
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-dom": "19.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formbricks/config-typescript": "workspace:*",
|
"@formbricks/config-typescript": "workspace:*",
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ Scaling:
|
|||||||
kubectl get hpa -n {{ .Release.Namespace }} {{ include "formbricks.name" . }}
|
kubectl get hpa -n {{ .Release.Namespace }} {{ include "formbricks.name" . }}
|
||||||
```
|
```
|
||||||
{{- else }}
|
{{- else }}
|
||||||
HPA is **not enabled**. Your deployment has a fixed number of `{{ .Values.deployment.replicas }}` replicas.
|
HPA is **not enabled**. Your deployment has a fixed number of `{{ .Values.replicaCount }}` replicas.
|
||||||
Manually scale using:
|
Manually scale using:
|
||||||
```sh
|
```sh
|
||||||
kubectl scale deployment -n {{ .Release.Namespace }} {{ include "formbricks.name" . }} --replicas=<desired_number>
|
kubectl scale deployment -n {{ .Release.Namespace }} {{ include "formbricks.name" . }} --replicas=<desired_number>
|
||||||
@@ -127,34 +127,6 @@ Scaling:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Pod Disruption Budget:
|
|
||||||
|
|
||||||
{{- if .Values.pdb.enabled }}
|
|
||||||
A PodDisruptionBudget is active to protect against voluntary disruptions.
|
|
||||||
{{- if not (kindIs "invalid" .Values.pdb.minAvailable) }}
|
|
||||||
- **Min Available**: `{{ .Values.pdb.minAvailable }}`
|
|
||||||
{{- end }}
|
|
||||||
{{- if not (kindIs "invalid" .Values.pdb.maxUnavailable) }}
|
|
||||||
- **Max Unavailable**: `{{ .Values.pdb.maxUnavailable }}`
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
Check PDB status:
|
|
||||||
```sh
|
|
||||||
kubectl get pdb -n {{ .Release.Namespace }} {{ include "formbricks.name" . }}
|
|
||||||
```
|
|
||||||
{{- if and .Values.autoscaling.enabled (eq (int .Values.autoscaling.minReplicas) 1) }}
|
|
||||||
|
|
||||||
WARNING: autoscaling.minReplicas is 1. With minAvailable: 1, the PDB
|
|
||||||
will block all node drains when only 1 replica is running. Set
|
|
||||||
autoscaling.minReplicas to at least 2 for proper HA protection.
|
|
||||||
{{- end }}
|
|
||||||
{{- else }}
|
|
||||||
PDB is **not enabled**. Voluntary disruptions (node drains, upgrades) may
|
|
||||||
take down all pods simultaneously.
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
External Secrets:
|
External Secrets:
|
||||||
{{- if .Values.externalSecret.enabled }}
|
{{- if .Values.externalSecret.enabled }}
|
||||||
External secrets are enabled.
|
External secrets are enabled.
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
{{- if .Values.pdb.enabled }}
|
|
||||||
{{- $hasMinAvailable := not (kindIs "invalid" .Values.pdb.minAvailable) -}}
|
|
||||||
{{- $hasMaxUnavailable := not (kindIs "invalid" .Values.pdb.maxUnavailable) -}}
|
|
||||||
{{- if and $hasMinAvailable $hasMaxUnavailable }}
|
|
||||||
{{- fail "pdb.minAvailable and pdb.maxUnavailable are mutually exclusive; set only one" }}
|
|
||||||
{{- end }}
|
|
||||||
{{- if not (or $hasMinAvailable $hasMaxUnavailable) }}
|
|
||||||
{{- fail "pdb.enabled is true but neither pdb.minAvailable nor pdb.maxUnavailable is set; set exactly one" }}
|
|
||||||
{{- end }}
|
|
||||||
---
|
|
||||||
apiVersion: policy/v1
|
|
||||||
kind: PodDisruptionBudget
|
|
||||||
metadata:
|
|
||||||
name: {{ template "formbricks.name" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "formbricks.labels" . | nindent 4 }}
|
|
||||||
{{- with .Values.pdb.additionalLabels }}
|
|
||||||
{{- toYaml . | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- if .Values.pdb.annotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml .Values.pdb.annotations | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
spec:
|
|
||||||
{{- if $hasMinAvailable }}
|
|
||||||
minAvailable: {{ .Values.pdb.minAvailable }}
|
|
||||||
{{- end }}
|
|
||||||
{{- if $hasMaxUnavailable }}
|
|
||||||
maxUnavailable: {{ .Values.pdb.maxUnavailable }}
|
|
||||||
{{- end }}
|
|
||||||
{{- if .Values.pdb.unhealthyPodEvictionPolicy }}
|
|
||||||
unhealthyPodEvictionPolicy: {{ .Values.pdb.unhealthyPodEvictionPolicy }}
|
|
||||||
{{- end }}
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
{{- include "formbricks.selectorLabels" . | nindent 6 }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -214,42 +214,6 @@ autoscaling:
|
|||||||
value: 2
|
value: 2
|
||||||
periodSeconds: 60 # Add at most 2 pods every minute
|
periodSeconds: 60 # Add at most 2 pods every minute
|
||||||
|
|
||||||
##########################################################
|
|
||||||
# Pod Disruption Budget (PDB)
|
|
||||||
#
|
|
||||||
# Ensures a minimum number of pods remain available during
|
|
||||||
# voluntary disruptions (node drains, cluster upgrades, etc.).
|
|
||||||
#
|
|
||||||
# IMPORTANT:
|
|
||||||
# - minAvailable and maxUnavailable are MUTUALLY EXCLUSIVE.
|
|
||||||
# Setting both will cause a helm install/upgrade failure.
|
|
||||||
# To switch, set the unused one to null in your override file.
|
|
||||||
# - Accepts an integer (e.g., 1) or a percentage string (e.g., "25%").
|
|
||||||
# - For PDB to provide real HA protection, ensure
|
|
||||||
# autoscaling.minReplicas >= 2 (or deployment.replicas >= 2
|
|
||||||
# if HPA is disabled). With only 1 replica and minAvailable: 1,
|
|
||||||
# the PDB will block ALL node drains and cluster upgrades.
|
|
||||||
##########################################################
|
|
||||||
pdb:
|
|
||||||
enabled: true
|
|
||||||
additionalLabels: {}
|
|
||||||
annotations: {}
|
|
||||||
|
|
||||||
# Minimum pods that must remain available during disruptions.
|
|
||||||
# Set to null and configure maxUnavailable instead if preferred.
|
|
||||||
minAvailable: 1
|
|
||||||
|
|
||||||
# Maximum pods that can be unavailable during disruptions.
|
|
||||||
# Mutually exclusive with minAvailable — uncomment and set
|
|
||||||
# minAvailable to null to use this instead.
|
|
||||||
# maxUnavailable: 1
|
|
||||||
|
|
||||||
# Eviction policy for unhealthy pods (Kubernetes 1.27+).
|
|
||||||
# "IfHealthy" — unhealthy pods count toward the budget (default).
|
|
||||||
# "AlwaysAllow" — unhealthy pods can always be evicted,
|
|
||||||
# preventing them from blocking node drain.
|
|
||||||
# unhealthyPodEvictionPolicy: AlwaysAllow
|
|
||||||
|
|
||||||
##########################################################
|
##########################################################
|
||||||
# Service Configuration
|
# Service Configuration
|
||||||
##########################################################
|
##########################################################
|
||||||
|
|||||||
+2
-6
@@ -99,7 +99,7 @@
|
|||||||
"xm-and-surveys/surveys/link-surveys/personal-links",
|
"xm-and-surveys/surveys/link-surveys/personal-links",
|
||||||
"xm-and-surveys/surveys/link-surveys/single-use-links",
|
"xm-and-surveys/surveys/link-surveys/single-use-links",
|
||||||
"xm-and-surveys/surveys/link-surveys/source-tracking",
|
"xm-and-surveys/surveys/link-surveys/source-tracking",
|
||||||
"xm-and-surveys/surveys/link-surveys/start-at-block",
|
"xm-and-surveys/surveys/link-surveys/start-at-question",
|
||||||
"xm-and-surveys/surveys/link-surveys/verify-email-before-survey",
|
"xm-and-surveys/surveys/link-surveys/verify-email-before-survey",
|
||||||
"xm-and-surveys/surveys/link-surveys/market-research-panel",
|
"xm-and-surveys/surveys/link-surveys/market-research-panel",
|
||||||
"xm-and-surveys/surveys/link-surveys/pin-protected-surveys",
|
"xm-and-surveys/surveys/link-surveys/pin-protected-surveys",
|
||||||
@@ -516,13 +516,9 @@
|
|||||||
"source": "/docs/link-surveys/global/schedule-start-end-dates"
|
"source": "/docs/link-surveys/global/schedule-start-end-dates"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"destination": "/docs/xm-and-surveys/surveys/link-surveys/start-at-block",
|
"destination": "/docs/xm-and-surveys/surveys/link-surveys/start-at-question",
|
||||||
"source": "/docs/link-surveys/start-at-question"
|
"source": "/docs/link-surveys/start-at-question"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"destination": "/docs/xm-and-surveys/surveys/link-surveys/start-at-block",
|
|
||||||
"source": "/docs/xm-and-surveys/surveys/link-surveys/start-at-question"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"destination": "/docs/xm-and-surveys/surveys/general-features/metadata",
|
"destination": "/docs/xm-and-surveys/surveys/general-features/metadata",
|
||||||
"source": "/docs/link-surveys/global/metadata"
|
"source": "/docs/link-surveys/global/metadata"
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Start At Specific Block"
|
|
||||||
description:
|
|
||||||
"Start a survey at a specific block using the URL to skip earlier blocks."
|
|
||||||
icon: "arrow-right"
|
|
||||||
---
|
|
||||||
|
|
||||||
The `startAt` URL parameter lets you open a Link Survey at a specific **block** instead of from the beginning. This is useful when you want to link to a specific part of the survey from external sources or reuse the same survey at different points in the user journey.
|
|
||||||
|
|
||||||
## How it works
|
|
||||||
|
|
||||||
The survey navigation is block-based: each block can contain one or more questions. When you pass `startAt` with a Question ID, the survey finds the **block** that contains that question and opens at that block. All questions in that block are shown together.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
**Multi-question blocks:** When a block has multiple questions, `startAt` opens at the block—you will see all questions in that block, not only the question whose ID you used. For precise "start at this exact question" behavior, use one question per block.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## How to use it
|
|
||||||
|
|
||||||
1. In the Survey Editor, open the Questions Tab and ensure the survey is set as a **Link Survey**.
|
|
||||||
|
|
||||||
2. Find the question (or block) you want to start at, click on **Show Advanced Settings**, and copy the **Question ID** of any question in that block.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Each question has a unique Question ID. Since `startAt` resolves to the block containing the question, you can use any question ID from the target block—typically the first question in that block.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
3. Append `?startAt=question_id` to your survey's URL, replacing `question_id` with the copied Question ID.
|
|
||||||
|
|
||||||
4. Share this modified URL with your users to start the survey at the specified block.
|
|
||||||
|
|
||||||
### Sample Link Survey URL with `startAt`
|
|
||||||
|
|
||||||
```sh Example Link Survey URL with startAt configured
|
|
||||||
https://formbricks.com/clny997dj087ho30fdzyf4nkl?startAt=bqd29m94l9k0hnc3azbrexl8
|
|
||||||
```
|
|
||||||
|
|
||||||
## Use cases
|
|
||||||
|
|
||||||
- **Link to a specific block from an external source:** Direct users to a specific block in your survey from emails, chatbots, or web pages.
|
|
||||||
- **Use the same survey in different parts of the user journey:** Reuse the survey at different stages, starting at different blocks to gather insights.
|
|
||||||
- **Create a personalized survey experience:** Tailor the survey by starting at a particular block based on the user's past interactions or preferences.
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
title: "Start At Specific Question"
|
||||||
|
description:
|
||||||
|
"Start a survey at a specific question using the URL to skip the initial questions."
|
||||||
|
icon: "arrow-right"
|
||||||
|
---
|
||||||
|
|
||||||
|
You can start a survey at a specific question from the survey using the URL to skip the initial questions. This is useful when you want to link to a specific question from an external source or want to use the same survey in different parts of the user journey.
|
||||||
|
|
||||||
|
## How to Use it?
|
||||||
|
|
||||||
|
1. In the Survey Editor, open the Questions Tab and ensure the survey is set as a **Link Survey**.
|
||||||
|
|
||||||
|
2. Find the question you want to start at, click on **Show Advanced Settings**, and copy the **Question ID**.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Each question has a unique Question ID, which is used to identify it in the
|
||||||
|
survey. You can use different Question IDs for multiple **startAt** points in
|
||||||
|
the URL.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
3. Append `?startAt=question_id` to your survey's URL, replacing `question_id` with the copied Question ID.
|
||||||
|
|
||||||
|
4. Share this modified URL with your users to start the survey at the specified question.
|
||||||
|
|
||||||
|
### Sample Link Survey URL with `startAt`
|
||||||
|
|
||||||
|
```sh Example Link Survey URL with startAt configured
|
||||||
|
https://formbricks.com/clny997dj087ho30fdzyf4nkl?startAt=bqd29m94l9k0hnc3azbrexl8
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
- **Link to a specific question from an external source:** Use this feature to direct users to a specific question in your survey from emails, chatbots, or web pages, providing a seamless experience.
|
||||||
|
- **Use the same survey in different parts of the user journey:** Employ the same survey at various stages of the user journey, starting at different questions to gather comprehensive insights.
|
||||||
|
- **Create a personalized survey experience:** Tailor the survey experience by starting at a particular question based on the user's past interactions or preferences, enhancing engagement.
|
||||||
Vendored
+1
-1
@@ -39,7 +39,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formbricks/logger": "workspace:*",
|
"@formbricks/logger": "workspace:*",
|
||||||
"redis": "5.8.1",
|
"redis": "5.8.1",
|
||||||
"zod": "3.25.76"
|
"zod": "3.24.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formbricks/config-typescript": "workspace:*",
|
"@formbricks/config-typescript": "workspace:*",
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
"@prisma/client": "6.14.0",
|
"@prisma/client": "6.14.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"uuid": "11.1.0",
|
"uuid": "11.1.0",
|
||||||
"zod": "3.25.76",
|
"zod": "3.24.4",
|
||||||
"zod-openapi": "4.2.4"
|
"zod-openapi": "4.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -887,7 +887,6 @@ describe("utils.ts", () => {
|
|||||||
targetElement.className = "other";
|
targetElement.className = "other";
|
||||||
|
|
||||||
targetElement.matches = vi.fn(() => false);
|
targetElement.matches = vi.fn(() => false);
|
||||||
targetElement.closest = vi.fn(() => null); // no ancestor matches either
|
|
||||||
|
|
||||||
const action: TEnvironmentStateActionClass = {
|
const action: TEnvironmentStateActionClass = {
|
||||||
id: "clabc123abc",
|
id: "clabc123abc",
|
||||||
@@ -994,93 +993,13 @@ describe("utils.ts", () => {
|
|||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Regression tests for nested child click target (issue #7314) ---
|
|
||||||
// In this test environment document.createElement() returns a plain mock object,
|
|
||||||
// so we set .matches and .closest as vi.fn() — the same pattern used by existing tests.
|
|
||||||
// This exercises the exact code path of the fix: matches() fails → closest() succeeds.
|
|
||||||
|
|
||||||
test("returns true when clicking a child element inside a button matched by cssSelector", () => {
|
|
||||||
const button = document.createElement("button");
|
|
||||||
const icon = document.createElement("span");
|
|
||||||
|
|
||||||
// Simulate: icon does NOT directly match ".my-btn", but its closest ancestor does
|
|
||||||
(icon as unknown as { matches: ReturnType<typeof vi.fn> }).matches = vi.fn(() => false);
|
|
||||||
(icon as unknown as { closest: ReturnType<typeof vi.fn> }).closest = vi.fn(() => button);
|
|
||||||
|
|
||||||
const action: TEnvironmentStateActionClass = {
|
|
||||||
id: "clabc123abc",
|
|
||||||
name: "Test Action",
|
|
||||||
type: "noCode",
|
|
||||||
key: null,
|
|
||||||
noCodeConfig: {
|
|
||||||
type: "click",
|
|
||||||
urlFilters: [],
|
|
||||||
elementSelector: { cssSelector: ".my-btn" },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Before fix: matches() → false → returns false (bug)
|
|
||||||
// After fix: matches() → false → closest() → button → returns true (correct)
|
|
||||||
const result = evaluateNoCodeConfigClick(icon as unknown as HTMLElement, action);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("returns false when clicking a child element with no matching ancestor", () => {
|
|
||||||
const other = document.createElement("div");
|
|
||||||
|
|
||||||
// Simulate: element doesn't match, and no ancestor matches either
|
|
||||||
(other as unknown as { matches: ReturnType<typeof vi.fn> }).matches = vi.fn(() => false);
|
|
||||||
(other as unknown as { closest: ReturnType<typeof vi.fn> }).closest = vi.fn(() => null);
|
|
||||||
|
|
||||||
const action: TEnvironmentStateActionClass = {
|
|
||||||
id: "clabc123abc",
|
|
||||||
name: "Test Action",
|
|
||||||
type: "noCode",
|
|
||||||
key: null,
|
|
||||||
noCodeConfig: {
|
|
||||||
type: "click",
|
|
||||||
urlFilters: [],
|
|
||||||
elementSelector: { cssSelector: ".my-btn" },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = evaluateNoCodeConfigClick(other as unknown as HTMLElement, action);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("uses direct target (not closest) when target directly matches cssSelector", () => {
|
|
||||||
const button = document.createElement("button");
|
|
||||||
|
|
||||||
// Simulate: click on the button itself — matches() succeeds, closest() should NOT be called
|
|
||||||
(button as unknown as { matches: ReturnType<typeof vi.fn> }).matches = vi.fn(() => true);
|
|
||||||
const closestSpy = vi.fn();
|
|
||||||
(button as unknown as { closest: ReturnType<typeof vi.fn> }).closest = closestSpy;
|
|
||||||
|
|
||||||
const action: TEnvironmentStateActionClass = {
|
|
||||||
id: "clabc123abc",
|
|
||||||
name: "Test Action",
|
|
||||||
type: "noCode",
|
|
||||||
key: null,
|
|
||||||
noCodeConfig: {
|
|
||||||
type: "click",
|
|
||||||
urlFilters: [],
|
|
||||||
elementSelector: { cssSelector: ".my-btn" },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = evaluateNoCodeConfigClick(button as unknown as HTMLElement, action);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
expect(closestSpy).not.toHaveBeenCalled(); // closest() is only a fallback
|
|
||||||
});
|
|
||||||
|
|
||||||
test("handles multiple cssSelectors correctly", () => {
|
test("handles multiple cssSelectors correctly", () => {
|
||||||
const targetElement = document.createElement("div");
|
const targetElement = document.createElement("div");
|
||||||
targetElement.className = "test other";
|
targetElement.className = "test other";
|
||||||
|
|
||||||
targetElement.matches = vi.fn((selector) => {
|
targetElement.matches = vi.fn((selector) => {
|
||||||
return selector === ".test" || selector === ".other" || selector === ".test .other";
|
return selector === ".test" || selector === ".other";
|
||||||
});
|
});
|
||||||
targetElement.closest = vi.fn(() => null); // not needed but consistent with mock environment
|
|
||||||
|
|
||||||
const action: TEnvironmentStateActionClass = {
|
const action: TEnvironmentStateActionClass = {
|
||||||
id: "clabc123abc",
|
id: "clabc123abc",
|
||||||
|
|||||||
@@ -304,28 +304,20 @@ export const evaluateNoCodeConfigClick = (
|
|||||||
|
|
||||||
if (!innerHtml && !cssSelector) return false;
|
if (!innerHtml && !cssSelector) return false;
|
||||||
|
|
||||||
// Resolve the element to test: prefer the direct click target, but walk up to
|
if (innerHtml && targetElement.innerHTML !== innerHtml) return false;
|
||||||
// the nearest ancestor that matches the CSS selector (event delegation for nested markup,
|
|
||||||
// e.g. <svg> or <span> inside a <button class="my-btn">).
|
|
||||||
let matchedElement: HTMLElement = targetElement;
|
|
||||||
|
|
||||||
if (cssSelector) {
|
if (cssSelector) {
|
||||||
let matchesDirectly = false;
|
// Split selectors that start with a . or # including the . or #
|
||||||
try {
|
const individualSelectors = cssSelector
|
||||||
matchesDirectly = targetElement.matches(cssSelector);
|
.split(/(?=[.#])/) // split before each . or #
|
||||||
} catch {
|
.map((sel) => sel.trim()); // remove leftover whitespace
|
||||||
matchesDirectly = false;
|
for (const selector of individualSelectors) {
|
||||||
}
|
if (!targetElement.matches(selector)) {
|
||||||
if (!matchesDirectly) {
|
return false;
|
||||||
const ancestor = targetElement.closest(cssSelector);
|
}
|
||||||
if (!ancestor) return false;
|
|
||||||
matchedElement = ancestor as HTMLElement;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check innerHtml against the resolved element, not the raw click target
|
|
||||||
if (innerHtml && matchedElement.innerHTML !== innerHtml) return false;
|
|
||||||
|
|
||||||
const connector = action.noCodeConfig.urlFiltersConnector ?? "or";
|
const connector = action.noCodeConfig.urlFiltersConnector ?? "or";
|
||||||
const isValidUrl = handleUrlFilters(urlFilters, connector);
|
const isValidUrl = handleUrlFilters(urlFilters, connector);
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
},
|
},
|
||||||
"author": "Formbricks <hola@formbricks.com>",
|
"author": "Formbricks <hola@formbricks.com>",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "3.25.76",
|
"zod": "3.24.4",
|
||||||
"pino": "10.0.0",
|
"pino": "10.0.0",
|
||||||
"pino-opentelemetry-transport": "2.0.0",
|
"pino-opentelemetry-transport": "2.0.0",
|
||||||
"pino-pretty": "13.1.1"
|
"pino-pretty": "13.1.1"
|
||||||
|
|||||||
@@ -141,18 +141,18 @@ function DropdownVariant({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<ElementError errorMessage={errorMessage} dir={dir} />
|
<ElementError errorMessage={errorMessage} dir={dir} />
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="rounded-input min-h-input bg-input-bg border-input-border text-input-text py-input-y px-input-x w-full justify-between"
|
className="rounded-input bg-option-bg rounded-option border-option-border h-input my-0 w-full justify-between border"
|
||||||
aria-invalid={Boolean(errorMessage)}
|
aria-invalid={Boolean(errorMessage)}
|
||||||
aria-label={headline}>
|
aria-label={headline}>
|
||||||
<span className="font-input font-input-weight text-input-text truncate">{displayText}</span>
|
<span className="font-input font-input-weight text-input-text truncate">{displayText}</span>
|
||||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronDown className="label-headline ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
@@ -235,7 +235,7 @@ function DropdownVariant({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
aria-required={required}
|
aria-required={required}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
className="mt-2 w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ function SingleSelect({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Options */}
|
{/* Options */}
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
{variant === "dropdown" ? (
|
{variant === "dropdown" ? (
|
||||||
<>
|
<>
|
||||||
<ElementError errorMessage={errorMessage} dir={dir} />
|
<ElementError errorMessage={errorMessage} dir={dir} />
|
||||||
@@ -160,11 +160,11 @@ function SingleSelect({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="rounded-input min-h-input bg-input-bg border-input-border text-input-text py-input-y px-input-x w-full justify-between"
|
className="rounded-input bg-option-bg rounded-option border-option-border h-input my-0 w-full justify-between border"
|
||||||
aria-invalid={Boolean(errorMessage)}
|
aria-invalid={Boolean(errorMessage)}
|
||||||
aria-label={headline}>
|
aria-label={headline}>
|
||||||
<span className="font-input font-input-weight text-input-text truncate">{displayText}</span>
|
<span className="font-input font-input-weight text-input-text truncate">{displayText}</span>
|
||||||
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronDown className="label-headline ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
@@ -226,7 +226,7 @@ function SingleSelect({
|
|||||||
placeholder={otherOptionPlaceholder}
|
placeholder={otherOptionPlaceholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
dir={dir}
|
dir={dir}
|
||||||
className="mt-2 w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -30,17 +30,3 @@ export const ZDisplayFilters = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type TDisplayFilters = z.infer<typeof ZDisplayFilters>;
|
export type TDisplayFilters = z.infer<typeof ZDisplayFilters>;
|
||||||
|
|
||||||
export const ZDisplayWithContact = z.object({
|
|
||||||
id: z.string().cuid2(),
|
|
||||||
createdAt: z.date(),
|
|
||||||
surveyId: z.string(),
|
|
||||||
contact: z
|
|
||||||
.object({
|
|
||||||
id: z.string(),
|
|
||||||
attributes: z.record(z.string()),
|
|
||||||
})
|
|
||||||
.nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TDisplayWithContact = z.infer<typeof ZDisplayWithContact>;
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "6.14.0",
|
"@prisma/client": "6.14.0",
|
||||||
"zod": "3.25.76",
|
"zod": "3.24.4",
|
||||||
"zod-openapi": "4.2.4",
|
"zod-openapi": "4.2.4",
|
||||||
"node-html-parser": "7.0.1"
|
"node-html-parser": "7.0.1"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+42
-40
@@ -28,7 +28,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
next:
|
next:
|
||||||
specifier: 16.1.6
|
specifier: 16.1.6
|
||||||
version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
version: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
react:
|
react:
|
||||||
specifier: 19.2.3
|
specifier: 19.2.3
|
||||||
version: 19.2.3
|
version: 19.2.3
|
||||||
@@ -294,10 +294,10 @@ importers:
|
|||||||
version: 1.2.6(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
version: 1.2.6(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
'@sentry/nextjs':
|
'@sentry/nextjs':
|
||||||
specifier: 10.5.0
|
specifier: 10.5.0
|
||||||
version: 10.5.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.12))
|
version: 10.5.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.12))
|
||||||
'@t3-oss/env-nextjs':
|
'@t3-oss/env-nextjs':
|
||||||
specifier: 0.13.4
|
specifier: 0.13.4
|
||||||
version: 0.13.4(arktype@2.1.29)(typescript@5.8.3)(zod@3.25.76)
|
version: 0.13.4(arktype@2.1.29)(typescript@5.8.3)(zod@3.24.4)
|
||||||
'@tailwindcss/forms':
|
'@tailwindcss/forms':
|
||||||
specifier: 0.5.10
|
specifier: 0.5.10
|
||||||
version: 0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.8.3)))
|
version: 0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.15.18)(typescript@5.8.3)))
|
||||||
@@ -384,13 +384,13 @@ importers:
|
|||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
next:
|
next:
|
||||||
specifier: 16.1.6
|
specifier: 16.1.6
|
||||||
version: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
version: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
next-auth:
|
next-auth:
|
||||||
specifier: 4.24.12
|
specifier: 4.24.12
|
||||||
version: 4.24.12(patch_hash=7ac5717a8d7d2049442182b5d83ab492d33fe774ff51ff5ea3884628b77df87b)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
version: 4.24.12(patch_hash=7ac5717a8d7d2049442182b5d83ab492d33fe774ff51ff5ea3884628b77df87b)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
next-safe-action:
|
next-safe-action:
|
||||||
specifier: 7.10.8
|
specifier: 7.10.8
|
||||||
version: 7.10.8(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)
|
version: 7.10.8(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4)
|
||||||
node-fetch:
|
node-fetch:
|
||||||
specifier: 3.3.2
|
specifier: 3.3.2
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
@@ -482,11 +482,11 @@ importers:
|
|||||||
specifier: file:vendor/xlsx-0.20.3.tgz
|
specifier: file:vendor/xlsx-0.20.3.tgz
|
||||||
version: file:apps/web/vendor/xlsx-0.20.3.tgz
|
version: file:apps/web/vendor/xlsx-0.20.3.tgz
|
||||||
zod:
|
zod:
|
||||||
specifier: 3.25.76
|
specifier: 3.24.4
|
||||||
version: 3.25.76
|
version: 3.24.4
|
||||||
zod-openapi:
|
zod-openapi:
|
||||||
specifier: 4.2.4
|
specifier: 4.2.4
|
||||||
version: 4.2.4(zod@3.25.76)
|
version: 4.2.4(zod@3.24.4)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@formbricks/config-typescript':
|
'@formbricks/config-typescript':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@@ -582,8 +582,8 @@ importers:
|
|||||||
specifier: 5.8.1
|
specifier: 5.8.1
|
||||||
version: 5.8.1
|
version: 5.8.1
|
||||||
zod:
|
zod:
|
||||||
specifier: 3.25.76
|
specifier: 3.24.4
|
||||||
version: 3.25.76
|
version: 3.24.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@formbricks/config-typescript':
|
'@formbricks/config-typescript':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@@ -685,11 +685,11 @@ importers:
|
|||||||
specifier: 11.1.0
|
specifier: 11.1.0
|
||||||
version: 11.1.0
|
version: 11.1.0
|
||||||
zod:
|
zod:
|
||||||
specifier: 3.25.76
|
specifier: 3.24.4
|
||||||
version: 3.25.76
|
version: 3.24.4
|
||||||
zod-openapi:
|
zod-openapi:
|
||||||
specifier: 4.2.4
|
specifier: 4.2.4
|
||||||
version: 4.2.4(zod@3.25.76)
|
version: 4.2.4(zod@3.24.4)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@formbricks/config-typescript':
|
'@formbricks/config-typescript':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@@ -829,8 +829,8 @@ importers:
|
|||||||
specifier: 13.1.1
|
specifier: 13.1.1
|
||||||
version: 13.1.1
|
version: 13.1.1
|
||||||
zod:
|
zod:
|
||||||
specifier: 3.25.76
|
specifier: 3.24.4
|
||||||
version: 3.25.76
|
version: 3.24.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@formbricks/config-typescript':
|
'@formbricks/config-typescript':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@@ -1067,11 +1067,11 @@ importers:
|
|||||||
specifier: 7.0.1
|
specifier: 7.0.1
|
||||||
version: 7.0.1
|
version: 7.0.1
|
||||||
zod:
|
zod:
|
||||||
specifier: 3.25.76
|
specifier: 3.24.4
|
||||||
version: 3.25.76
|
version: 3.24.4
|
||||||
zod-openapi:
|
zod-openapi:
|
||||||
specifier: 4.2.4
|
specifier: 4.2.4
|
||||||
version: 4.2.4(zod@3.25.76)
|
version: 4.2.4(zod@3.24.4)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@formbricks/config-typescript':
|
'@formbricks/config-typescript':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@@ -11758,8 +11758,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.21.4
|
zod: ^3.21.4
|
||||||
|
|
||||||
zod@3.25.76:
|
zod@3.24.4:
|
||||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
@@ -17049,7 +17049,7 @@ snapshots:
|
|||||||
|
|
||||||
'@sentry/core@10.5.0': {}
|
'@sentry/core@10.5.0': {}
|
||||||
|
|
||||||
'@sentry/nextjs@10.5.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.12))':
|
'@sentry/nextjs@10.5.0(@opentelemetry/context-async-hooks@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.5.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.5.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.99.8(esbuild@0.25.12))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
'@opentelemetry/semantic-conventions': 1.38.0
|
'@opentelemetry/semantic-conventions': 1.38.0
|
||||||
@@ -17062,7 +17062,7 @@ snapshots:
|
|||||||
'@sentry/vercel-edge': 10.5.0
|
'@sentry/vercel-edge': 10.5.0
|
||||||
'@sentry/webpack-plugin': 4.6.1(encoding@0.1.13)(webpack@5.99.8(esbuild@0.25.12))
|
'@sentry/webpack-plugin': 4.6.1(encoding@0.1.13)(webpack@5.99.8(esbuild@0.25.12))
|
||||||
chalk: 3.0.0
|
chalk: 3.0.0
|
||||||
next: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
next: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
resolve: 1.22.8
|
resolve: 1.22.8
|
||||||
rollup: 4.54.0
|
rollup: 4.54.0
|
||||||
stacktrace-parser: 0.1.11
|
stacktrace-parser: 0.1.11
|
||||||
@@ -17928,19 +17928,19 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@t3-oss/env-core@0.13.4(arktype@2.1.29)(typescript@5.8.3)(zod@3.25.76)':
|
'@t3-oss/env-core@0.13.4(arktype@2.1.29)(typescript@5.8.3)(zod@3.24.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
arktype: 2.1.29
|
arktype: 2.1.29
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
zod: 3.25.76
|
zod: 3.24.4
|
||||||
|
|
||||||
'@t3-oss/env-nextjs@0.13.4(arktype@2.1.29)(typescript@5.8.3)(zod@3.25.76)':
|
'@t3-oss/env-nextjs@0.13.4(arktype@2.1.29)(typescript@5.8.3)(zod@3.24.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@t3-oss/env-core': 0.13.4(arktype@2.1.29)(typescript@5.8.3)(zod@3.25.76)
|
'@t3-oss/env-core': 0.13.4(arktype@2.1.29)(typescript@5.8.3)(zod@3.24.4)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.8.3
|
typescript: 5.8.3
|
||||||
zod: 3.25.76
|
zod: 3.24.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- arktype
|
- arktype
|
||||||
|
|
||||||
@@ -22186,13 +22186,13 @@ snapshots:
|
|||||||
|
|
||||||
neo-async@2.6.2: {}
|
neo-async@2.6.2: {}
|
||||||
|
|
||||||
next-auth@4.24.12(patch_hash=7ac5717a8d7d2049442182b5d83ab492d33fe774ff51ff5ea3884628b77df87b)(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
next-auth@4.24.12(patch_hash=7ac5717a8d7d2049442182b5d83ab492d33fe774ff51ff5ea3884628b77df87b)(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(nodemailer@7.0.11)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.4
|
'@babel/runtime': 7.28.4
|
||||||
'@panva/hkdf': 1.2.1
|
'@panva/hkdf': 1.2.1
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
jose: 4.15.9
|
jose: 4.15.9
|
||||||
next: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
next: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
oauth: 0.9.15
|
oauth: 0.9.15
|
||||||
openid-client: 5.7.1
|
openid-client: 5.7.1
|
||||||
preact: 10.28.2
|
preact: 10.28.2
|
||||||
@@ -22203,13 +22203,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
nodemailer: 7.0.11
|
nodemailer: 7.0.11
|
||||||
|
|
||||||
next-safe-action@7.10.8(next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76):
|
next-safe-action@7.10.8(next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.24.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
next: 16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
next: 16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
react-dom: 19.2.3(react@19.2.3)
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
zod: 3.25.76
|
zod: 3.24.4
|
||||||
|
|
||||||
next@16.0.10(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
next@16.0.10(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -22219,7 +22219,7 @@ snapshots:
|
|||||||
postcss: 8.4.31
|
postcss: 8.4.31
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
react-dom: 19.2.3(react@19.2.3)
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
styled-jsx: 5.1.6(react@19.2.3)
|
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@next/swc-darwin-arm64': 16.0.10
|
'@next/swc-darwin-arm64': 16.0.10
|
||||||
'@next/swc-darwin-x64': 16.0.10
|
'@next/swc-darwin-x64': 16.0.10
|
||||||
@@ -22236,7 +22236,7 @@ snapshots:
|
|||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
|
|
||||||
next@16.1.6(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
next@16.1.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.1.6
|
'@next/env': 16.1.6
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
@@ -22245,7 +22245,7 @@ snapshots:
|
|||||||
postcss: 8.4.31
|
postcss: 8.4.31
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
react-dom: 19.2.3(react@19.2.3)
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
styled-jsx: 5.1.6(react@19.2.3)
|
styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@next/swc-darwin-arm64': 16.1.6
|
'@next/swc-darwin-arm64': 16.1.6
|
||||||
'@next/swc-darwin-x64': 16.1.6
|
'@next/swc-darwin-x64': 16.1.6
|
||||||
@@ -23991,10 +23991,12 @@ snapshots:
|
|||||||
|
|
||||||
stubborn-utils@1.0.2: {}
|
stubborn-utils@1.0.2: {}
|
||||||
|
|
||||||
styled-jsx@5.1.6(react@19.2.3):
|
styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
client-only: 0.0.1
|
client-only: 0.0.1
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
|
optionalDependencies:
|
||||||
|
'@babel/core': 7.28.5
|
||||||
|
|
||||||
stylis@4.3.6: {}
|
stylis@4.3.6: {}
|
||||||
|
|
||||||
@@ -25087,8 +25089,8 @@ snapshots:
|
|||||||
|
|
||||||
yoga-layout@3.2.1: {}
|
yoga-layout@3.2.1: {}
|
||||||
|
|
||||||
zod-openapi@4.2.4(zod@3.25.76):
|
zod-openapi@4.2.4(zod@3.24.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.76
|
zod: 3.24.4
|
||||||
|
|
||||||
zod@3.25.76: {}
|
zod@3.24.4: {}
|
||||||
|
|||||||
@@ -20,9 +20,6 @@ sonar.scm.exclusions.disabled=false
|
|||||||
# Encoding of the source code
|
# Encoding of the source code
|
||||||
sonar.sourceEncoding=UTF-8
|
sonar.sourceEncoding=UTF-8
|
||||||
|
|
||||||
# Node.js memory limit for JS/TS analysis (in MB)
|
|
||||||
sonar.javascript.node.maxspace=8192
|
|
||||||
|
|
||||||
# Coverage
|
# Coverage
|
||||||
sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.tsx,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/route.tsx,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/openapi/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,packages/js-core/src/index.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,**/*.svg,apps/web/modules/ui/components/icons/**,apps/web/modules/ui/components/table/**,packages/survey-ui/**/*.stories.*
|
sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.tsx,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/route.tsx,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/openapi/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,packages/js-core/src/index.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,**/*.svg,apps/web/modules/ui/components/icons/**,apps/web/modules/ui/components/table/**,packages/survey-ui/**/*.stories.*
|
||||||
sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.tsx,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/route.tsx,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/openapi/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,packages/js-core/src/index.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,**/*.svg,apps/web/modules/ui/components/icons/**,apps/web/modules/ui/components/table/**,packages/survey-ui/**/*.stories.*
|
sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.tsx,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/route.tsx,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/openapi/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,packages/js-core/src/index.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,**/*.svg,apps/web/modules/ui/components/icons/**,apps/web/modules/ui/components/table/**,packages/survey-ui/**/*.stories.*
|
||||||
|
|||||||
Reference in New Issue
Block a user