mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-18 23:28:32 -05:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf2ef36ceb | |||
| aa040595c3 | |||
| 81c2bd365a | |||
| b26945698d | |||
| 208d83eb08 | |||
| 0a7482da0f |
+39
-8
@@ -5,29 +5,35 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TSurvey, TSurveyElementSummaryOpenText } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { useWorkspace } from "@/app/(app)/workspaces/[workspaceId]/context/workspace-context";
|
||||
import { timeSince } from "@/lib/time";
|
||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
||||
import { renderHyperlinkedContent } from "@/modules/analysis/utils";
|
||||
import { PersonAvatar } from "@/modules/ui/components/avatars";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { EmptyState } from "@/modules/ui/components/empty-state";
|
||||
import { IdBadge } from "@/modules/ui/components/id-badge";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/modules/ui/components/table";
|
||||
import { ElementSummaryHeader } from "./ElementSummaryHeader";
|
||||
import { ResponseSampleModal } from "./ResponseSampleModal";
|
||||
|
||||
interface OpenTextSummaryProps {
|
||||
elementSummary: TSurveyElementSummaryOpenText;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const OpenTextSummary = ({ elementSummary, survey, locale }: OpenTextSummaryProps) => {
|
||||
export const OpenTextSummary = ({
|
||||
elementSummary,
|
||||
survey,
|
||||
locale,
|
||||
isReadOnly,
|
||||
}: Readonly<OpenTextSummaryProps>) => {
|
||||
const { t } = useTranslation();
|
||||
const { workspace } = useWorkspace();
|
||||
const [visibleResponses, setVisibleResponses] = useState(10);
|
||||
const [selectedResponseId, setSelectedResponseId] = useState<string | null>(null);
|
||||
|
||||
const handleLoadMore = () => {
|
||||
// Increase the number of visible responses by 10, not exceeding the total number of responses
|
||||
setVisibleResponses((prevVisibleResponses) =>
|
||||
Math.min(prevVisibleResponses + 10, elementSummary.samples.length)
|
||||
);
|
||||
@@ -48,17 +54,31 @@ export const OpenTextSummary = ({ elementSummary, survey, locale }: OpenTextSumm
|
||||
<TableRow>
|
||||
<TableHead className="w-1/4">{t("common.user")}</TableHead>
|
||||
<TableHead className="w-2/4">{t("common.response")}</TableHead>
|
||||
<TableHead className="w-1/4">{t("common.time")}</TableHead>
|
||||
<TableHead className="w-1/6">{t("common.time")}</TableHead>
|
||||
<TableHead className="w-1/6">{t("common.response_id")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{elementSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
<TableRow key={response.id}>
|
||||
<TableRow
|
||||
key={response.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={t("workspace.surveys.summary.open_response_details")}
|
||||
className="cursor-pointer hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-400"
|
||||
onClick={() => setSelectedResponseId(response.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
setSelectedResponseId(response.id);
|
||||
}
|
||||
}}>
|
||||
<TableCell className="w-1/4">
|
||||
{response.contact ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/workspaces/${workspace?.id}/contacts/${response.contact.id}`}>
|
||||
href={`/workspaces/${survey.workspaceId}/contacts/${response.contact.id}`}
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.contact.id} />
|
||||
</div>
|
||||
@@ -80,9 +100,12 @@ export const OpenTextSummary = ({ elementSummary, survey, locale }: OpenTextSumm
|
||||
? renderHyperlinkedContent(response.value)
|
||||
: response.value}
|
||||
</TableCell>
|
||||
<TableCell className="w-1/4">
|
||||
<TableCell className="w-1/6">
|
||||
{timeSince(new Date(response.updatedAt).toISOString(), locale)}
|
||||
</TableCell>
|
||||
<TableCell className="w-1/6" onClick={(e) => e.stopPropagation()}>
|
||||
<IdBadge id={response.id} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -96,6 +119,14 @@ export const OpenTextSummary = ({ elementSummary, survey, locale }: OpenTextSumm
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ResponseSampleModal
|
||||
responseId={selectedResponseId}
|
||||
onClose={() => setSelectedResponseId(null)}
|
||||
survey={survey}
|
||||
isReadOnly={isReadOnly}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
"use client";
|
||||
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TResponseWithQuotas } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { SingleResponseCard } from "@/modules/analysis/components/SingleResponseCard";
|
||||
import {
|
||||
getResponseAction,
|
||||
getTagsByWorkspaceIdAction,
|
||||
} from "@/modules/analysis/components/SingleResponseCard/actions";
|
||||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from "@/modules/ui/components/dialog";
|
||||
import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
|
||||
|
||||
interface ResponseSampleModalProps {
|
||||
responseId: string | null;
|
||||
onClose: () => void;
|
||||
survey: TSurvey;
|
||||
isReadOnly: boolean;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const ResponseSampleModal = ({
|
||||
responseId,
|
||||
onClose,
|
||||
survey,
|
||||
isReadOnly,
|
||||
locale,
|
||||
}: Readonly<ResponseSampleModalProps>) => {
|
||||
const { t } = useTranslation();
|
||||
const [response, setResponse] = useState<TResponseWithQuotas | null>(null);
|
||||
const [tags, setTags] = useState<TTag[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
// Cache fetched data per response ID to avoid re-fetching on re-open
|
||||
const cache = useRef<Map<string, { response: TResponseWithQuotas; tags: TTag[] }>>(new Map());
|
||||
// Track the in-flight request so stale resolutions can be ignored when the user
|
||||
// switches rows quickly.
|
||||
const latestRequestId = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!responseId) return;
|
||||
|
||||
const cached = cache.current.get(responseId);
|
||||
if (cached) {
|
||||
setResponse(cached.response);
|
||||
setTags(cached.tags);
|
||||
setErrorMessage(null);
|
||||
return;
|
||||
}
|
||||
|
||||
latestRequestId.current = responseId;
|
||||
setIsLoading(true);
|
||||
setResponse(null);
|
||||
setErrorMessage(null);
|
||||
|
||||
Promise.all([
|
||||
getResponseAction({ responseId }),
|
||||
getTagsByWorkspaceIdAction({ workspaceId: survey.workspaceId }),
|
||||
])
|
||||
.then(([responseResult, tagsResult]) => {
|
||||
// Discard if a newer request has started or the modal has been closed.
|
||||
if (latestRequestId.current !== responseId) return;
|
||||
|
||||
const responseError = getFormattedErrorMessage(responseResult);
|
||||
const tagsError = getFormattedErrorMessage(tagsResult);
|
||||
const fetchedResponse = responseResult?.data ?? null;
|
||||
const fetchedTags = tagsResult?.data ?? [];
|
||||
|
||||
if (responseError || tagsError || !fetchedResponse) {
|
||||
const message = responseError || tagsError || t("common.something_went_wrong");
|
||||
toast.error(message);
|
||||
setErrorMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = { response: fetchedResponse, tags: fetchedTags };
|
||||
cache.current.set(responseId, entry);
|
||||
setResponse(entry.response);
|
||||
setTags(entry.tags);
|
||||
})
|
||||
.catch(() => {
|
||||
if (latestRequestId.current !== responseId) return;
|
||||
const message = t("common.something_went_wrong");
|
||||
toast.error(message);
|
||||
setErrorMessage(message);
|
||||
})
|
||||
.finally(() => {
|
||||
if (latestRequestId.current !== responseId) return;
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [responseId, survey.workspaceId, t]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!open) {
|
||||
// Drop any in-flight request so it can't commit after close.
|
||||
latestRequestId.current = null;
|
||||
setErrorMessage(null);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={!!responseId} onOpenChange={handleOpenChange}>
|
||||
<DialogContent width="wide">
|
||||
<VisuallyHidden asChild>
|
||||
<DialogTitle>{t("common.response")}</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
<VisuallyHidden asChild>
|
||||
<DialogDescription>{t("common.response")}</DialogDescription>
|
||||
</VisuallyHidden>
|
||||
<DialogBody>
|
||||
{isLoading ? (
|
||||
<div className="py-12">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
) : errorMessage ? (
|
||||
<div className="py-12 text-center text-sm text-slate-600">{errorMessage}</div>
|
||||
) : response ? (
|
||||
<SingleResponseCard
|
||||
survey={survey}
|
||||
response={response}
|
||||
environmentTags={tags}
|
||||
isReadOnly={isReadOnly}
|
||||
locale={locale}
|
||||
/>
|
||||
) : null}
|
||||
</DialogBody>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
+9
-1
@@ -41,9 +41,16 @@ interface SummaryListProps {
|
||||
responseCount: number | null;
|
||||
survey: TSurvey;
|
||||
locale: TUserLocale;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryListProps) => {
|
||||
export const SummaryList = ({
|
||||
summary,
|
||||
responseCount,
|
||||
survey,
|
||||
locale,
|
||||
isReadOnly,
|
||||
}: Readonly<SummaryListProps>) => {
|
||||
const { workspace } = useWorkspaceContext();
|
||||
const { setSelectedFilter, selectedFilter } = useResponseFilter();
|
||||
const { t } = useTranslation();
|
||||
@@ -116,6 +123,7 @@ export const SummaryList = ({ summary, responseCount, survey, locale }: SummaryL
|
||||
elementSummary={elementSummary}
|
||||
survey={survey}
|
||||
locale={locale}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-1
@@ -49,6 +49,7 @@ interface SummaryPageProps {
|
||||
locale: TUserLocale;
|
||||
initialSurveySummary?: TSurveySummary;
|
||||
isQuotasAllowed: boolean;
|
||||
isReadOnly: boolean;
|
||||
}
|
||||
|
||||
export const SummaryPage = ({
|
||||
@@ -57,7 +58,8 @@ export const SummaryPage = ({
|
||||
locale,
|
||||
initialSurveySummary,
|
||||
isQuotasAllowed,
|
||||
}: SummaryPageProps) => {
|
||||
isReadOnly,
|
||||
}: Readonly<SummaryPageProps>) => {
|
||||
const { t } = useTranslation();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -225,6 +227,7 @@ export const SummaryPage = ({
|
||||
responseCount={surveySummary.meta.totalResponses}
|
||||
survey={surveyMemoized}
|
||||
locale={locale}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+2
-1
@@ -22,7 +22,7 @@ import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getWorkspaceAuth } from "@/modules/workspaces/lib/utils";
|
||||
|
||||
const SurveyPage = async (props: { params: Promise<{ workspaceId: string; surveyId: string }> }) => {
|
||||
const SurveyPage = async (props: Readonly<{ params: Promise<{ workspaceId: string; surveyId: string }> }>) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
|
||||
@@ -88,6 +88,7 @@ const SurveyPage = async (props: { params: Promise<{ workspaceId: string; survey
|
||||
locale={user.locale ?? DEFAULT_LOCALE}
|
||||
initialSurveySummary={initialSurveySummary}
|
||||
isQuotasAllowed={isQuotasAllowed}
|
||||
isReadOnly={isReadOnly}
|
||||
/>
|
||||
|
||||
<IdBadge id={surveyId} label={t("common.survey_id")} variant="column" />
|
||||
|
||||
@@ -3446,6 +3446,7 @@ checksums:
|
||||
workspace/surveys/summary/no_identified_impressions: c3bc42e6feb9010ced905ded51c5afc4
|
||||
workspace/surveys/summary/no_responses_found: f10190cffdda4ca1bed479acbb89b13f
|
||||
workspace/surveys/summary/nps_promoters_tooltip: dea6a683c0c36189e325656d5a7596b8
|
||||
workspace/surveys/summary/open_response_details: 0e5de115b5e605f68ea857cf8ef5533a
|
||||
workspace/surveys/summary/other_values_found: 48a74ee68c05f7fb162072b50c683b6a
|
||||
workspace/surveys/summary/overall: 6c6d6533013d4739766af84b2871bca6
|
||||
workspace/surveys/summary/promoters: 41fbb8d0439227661253a82fda39f521
|
||||
|
||||
@@ -216,6 +216,43 @@ export const getResponse = reactCache(async (responseId: string): Promise<TRespo
|
||||
}
|
||||
});
|
||||
|
||||
export const getResponseWithQuotas = reactCache(
|
||||
async (responseId: string): Promise<TResponseWithQuotas | null> => {
|
||||
validateInputs([responseId, ZId]);
|
||||
|
||||
try {
|
||||
const responsePrisma = await prisma.response.findUnique({
|
||||
where: {
|
||||
id: responseId,
|
||||
},
|
||||
select: {
|
||||
...responseSelection,
|
||||
quotaLinks: {
|
||||
where: { status: "screenedIn" },
|
||||
include: { quota: { select: { id: true, name: true } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!responsePrisma) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { quotaLinks, ...rest } = responsePrisma;
|
||||
return {
|
||||
...mapResponsePrismaToResponse(rest),
|
||||
quotas: quotaLinks.map((ql) => ql.quota),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getResponseSnapshotForPipeline = async (responseId: string): Promise<TResponse | null> => {
|
||||
validateInputs([responseId, ZId]);
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
getResponseBySingleUseId,
|
||||
getResponseCountBySurveyId,
|
||||
getResponseDownloadFile,
|
||||
getResponseWithQuotas,
|
||||
getResponsesByWorkspaceId,
|
||||
responseSelection,
|
||||
updateResponse,
|
||||
@@ -170,6 +171,70 @@ describe("Tests for getResponse service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for getResponseWithQuotas service", () => {
|
||||
describe("Happy Path", () => {
|
||||
test("Returns the response with screened-in quotas", async () => {
|
||||
prisma.response.findUnique.mockResolvedValue(mockResponseWithQuotas);
|
||||
|
||||
const result = await getResponseWithQuotas(mockResponseWithQuotas.id);
|
||||
|
||||
expect(result).toEqual({
|
||||
...expectedResponseWithoutPerson,
|
||||
quotas: mockResponseWithQuotas.quotaLinks.map(
|
||||
(ql: { quota: { id: string; name: string } }) => ql.quota
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
test("Returns an empty quotas array when no quotaLinks are screened in", async () => {
|
||||
prisma.response.findUnique.mockResolvedValue({ ...mockResponse, quotaLinks: [] } as any);
|
||||
|
||||
const result = await getResponseWithQuotas(mockResponse.id);
|
||||
|
||||
expect(result).toEqual({ ...expectedResponseWithoutPerson, quotas: [] });
|
||||
});
|
||||
|
||||
test("Selects only screened-in quotaLinks", async () => {
|
||||
prisma.response.findUnique.mockResolvedValue({ ...mockResponse, quotaLinks: [] } as any);
|
||||
|
||||
await getResponseWithQuotas(mockResponse.id);
|
||||
|
||||
const findUniqueCall = prisma.response.findUnique.mock.calls.at(-1)?.[0];
|
||||
expect(findUniqueCall?.select?.quotaLinks).toEqual({
|
||||
where: { status: "screenedIn" },
|
||||
include: { quota: { select: { id: true, name: true } } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sad Path", () => {
|
||||
testInputValidation(getResponseWithQuotas, "123#");
|
||||
|
||||
test("Returns null when no response is found", async () => {
|
||||
prisma.response.findUnique.mockResolvedValue(null);
|
||||
|
||||
const result = await getResponseWithQuotas(mockResponse.id);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("Throws DatabaseError on PrismaClientKnownRequestError", async () => {
|
||||
const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error", {
|
||||
code: PrismaErrorType.UniqueConstraintViolation,
|
||||
clientVersion: "0.0.1",
|
||||
});
|
||||
prisma.response.findUnique.mockRejectedValue(errToThrow);
|
||||
|
||||
await expect(getResponseWithQuotas(mockResponse.id)).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
test("Rethrows generic errors", async () => {
|
||||
prisma.response.findUnique.mockRejectedValue(new Error("boom"));
|
||||
|
||||
await expect(getResponseWithQuotas(mockResponse.id)).rejects.toThrow("boom");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tests for getSurveySummary service", () => {
|
||||
describe("Happy Path", () => {
|
||||
test("Returns a summary of the survey responses", async () => {
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "Keine Impressionen von identifizierten Kontakten",
|
||||
"no_responses_found": "Keine Antworten gefunden",
|
||||
"nps_promoters_tooltip": "{percentage}% der Befragten haben eine Bewertung von 9 oder 10 gegeben (NPS-Promotoren).",
|
||||
"open_response_details": "Details zu offenen Antworten",
|
||||
"other_values_found": "Andere Werte gefunden",
|
||||
"overall": "Gesamt",
|
||||
"promoters": "Promotoren",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "No impressions from identified contacts",
|
||||
"no_responses_found": "No responses found",
|
||||
"nps_promoters_tooltip": "{percentage}% of respondents gave a rating of 9 or 10 (NPS promoters).",
|
||||
"open_response_details": "Open response details",
|
||||
"other_values_found": "Other values found",
|
||||
"overall": "Overall",
|
||||
"promoters": "Promoters",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "No hay impresiones de contactos identificados",
|
||||
"no_responses_found": "No se han encontrado respuestas",
|
||||
"nps_promoters_tooltip": "El {percentage}% de los encuestados dieron una puntuación de 9 o 10 (promotores NPS).",
|
||||
"open_response_details": "Detalles de respuesta abierta",
|
||||
"other_values_found": "Otros valores encontrados",
|
||||
"overall": "General",
|
||||
"promoters": "Promotores",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "Aucune impression des contacts identifiés",
|
||||
"no_responses_found": "Aucune réponse trouvée",
|
||||
"nps_promoters_tooltip": "{percentage} % des répondants ont donné une note de 9 ou 10 (promoteurs NPS).",
|
||||
"open_response_details": "Détails des réponses ouvertes",
|
||||
"other_values_found": "D'autres valeurs trouvées",
|
||||
"overall": "Globalement",
|
||||
"promoters": "Promoteurs",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "Nincsenek azonosított partnerektől származó megtekintések",
|
||||
"no_responses_found": "Nem találhatók válaszok",
|
||||
"nps_promoters_tooltip": "A válaszadók {percentage}%-a 9-es vagy 10-es értékelést adott (NPS promoters).",
|
||||
"open_response_details": "Nyitott válasz részletei",
|
||||
"other_values_found": "Más értékek találhatók",
|
||||
"overall": "Összesen",
|
||||
"promoters": "Népszerűsítők",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "識別済みコンタクトからのインプレッションはありません",
|
||||
"no_responses_found": "回答が見つかりません",
|
||||
"nps_promoters_tooltip": "回答者の{percentage}%が9または10の評価をしました(NPSプロモーター)。",
|
||||
"open_response_details": "自由回答の詳細",
|
||||
"other_values_found": "他の値が見つかりました",
|
||||
"overall": "全体",
|
||||
"promoters": "推奨者",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "Geen weergaven van geïdentificeerde contacten",
|
||||
"no_responses_found": "Geen reacties gevonden",
|
||||
"nps_promoters_tooltip": "{percentage}% van de respondenten gaf een beoordeling van 9 of 10 (NPS promoters).",
|
||||
"open_response_details": "Details open antwoorden",
|
||||
"other_values_found": "Andere waarden gevonden",
|
||||
"overall": "Algemeen",
|
||||
"promoters": "Promoters",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "Nenhuma impressão de contatos identificados",
|
||||
"no_responses_found": "Nenhuma resposta encontrada",
|
||||
"nps_promoters_tooltip": "{percentage}% dos entrevistados deram uma nota de 9 ou 10 (promotores NPS).",
|
||||
"open_response_details": "Detalhes das respostas abertas",
|
||||
"other_values_found": "Outros valores encontrados",
|
||||
"overall": "No geral",
|
||||
"promoters": "Promotores",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "Sem impressões de contactos identificados",
|
||||
"no_responses_found": "Nenhuma resposta encontrada",
|
||||
"nps_promoters_tooltip": "{percentage}% dos inquiridos deram uma classificação de 9 ou 10 (promotores NPS).",
|
||||
"open_response_details": "Detalhes de respostas abertas",
|
||||
"other_values_found": "Outros valores encontrados",
|
||||
"overall": "Geral",
|
||||
"promoters": "Promotores",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "Nicio impresie de la contactele identificate",
|
||||
"no_responses_found": "Nu s-au găsit răspunsuri",
|
||||
"nps_promoters_tooltip": "{percentage}% dintre respondenți au acordat o evaluare de 9 sau 10 (promotori NPS).",
|
||||
"open_response_details": "Detalii răspunsuri deschise",
|
||||
"other_values_found": "Alte valori găsite",
|
||||
"overall": "General",
|
||||
"promoters": "Promotori",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "Нет показов от идентифицированных контактов",
|
||||
"no_responses_found": "Ответы не найдены",
|
||||
"nps_promoters_tooltip": "{percentage}% респондентов дали оценку 9 или 10 (промоутеры NPS).",
|
||||
"open_response_details": "Детали открытых ответов",
|
||||
"other_values_found": "Найдены другие значения",
|
||||
"overall": "В целом",
|
||||
"promoters": "Сторонники",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "Inga visningar från identifierade kontakter",
|
||||
"no_responses_found": "Inga svar hittades",
|
||||
"nps_promoters_tooltip": "{percentage}% av respondenterna gav ett betyg på 9 eller 10 (NPS-ambassadörer).",
|
||||
"open_response_details": "Detaljer för öppna svar",
|
||||
"other_values_found": "Andra värden hittades",
|
||||
"overall": "Övergripande",
|
||||
"promoters": "Ambassadörer",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "Tanımlanmış kişilerden gösterim yok",
|
||||
"no_responses_found": "Yanıt bulunamadı",
|
||||
"nps_promoters_tooltip": "Yanıt verenlerin %{percentage}'si 9 veya 10 puan verdi (NPS tavsiye edenler).",
|
||||
"open_response_details": "Açık yanıt detayları",
|
||||
"other_values_found": "Diğer değerler bulundu",
|
||||
"overall": "Genel",
|
||||
"promoters": "Tavsiye edenler",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "没有已识别联系人的展示次数",
|
||||
"no_responses_found": "未找到响应",
|
||||
"nps_promoters_tooltip": "{percentage}% 的受访者给出了 9 或 10 分的评价(NPS 推荐者)。",
|
||||
"open_response_details": "开放式回答详情",
|
||||
"other_values_found": "找到其他值",
|
||||
"overall": "整体",
|
||||
"promoters": "推荐者",
|
||||
|
||||
@@ -3597,6 +3597,7 @@
|
||||
"no_identified_impressions": "沒有來自已識別聯絡人的曝光次數",
|
||||
"no_responses_found": "找不到回應",
|
||||
"nps_promoters_tooltip": "{percentage}% 的受訪者給予 9 或 10 分評價(NPS 推薦者)。",
|
||||
"open_response_details": "開放式回覆詳情",
|
||||
"other_values_found": "找到其他值",
|
||||
"overall": "整體",
|
||||
"promoters": "推廣者",
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { deleteResponse, getResponse } from "@/lib/response/service";
|
||||
import { createTag } from "@/lib/tag/service";
|
||||
import { deleteResponse, getResponseWithQuotas } from "@/lib/response/service";
|
||||
import { createTag, getTagsByWorkspaceId } from "@/lib/tag/service";
|
||||
import { addTagToRespone, deleteTagOnResponse } from "@/lib/tagOnResponse/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client/action-client-middleware";
|
||||
@@ -173,6 +173,32 @@ export const deleteResponseAction = authenticatedActionClient.inputSchema(ZDelet
|
||||
})
|
||||
);
|
||||
|
||||
const ZGetTagsByWorkspaceIdAction = z.object({
|
||||
workspaceId: ZId,
|
||||
});
|
||||
|
||||
export const getTagsByWorkspaceIdAction = authenticatedActionClient
|
||||
.inputSchema(ZGetTagsByWorkspaceIdAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromWorkspaceId(parsedInput.workspaceId),
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "workspaceTeam",
|
||||
minPermission: "read",
|
||||
workspaceId: parsedInput.workspaceId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return await getTagsByWorkspaceId(parsedInput.workspaceId);
|
||||
});
|
||||
|
||||
const ZGetResponseAction = z.object({
|
||||
responseId: ZId,
|
||||
});
|
||||
@@ -196,5 +222,5 @@ export const getResponseAction = authenticatedActionClient
|
||||
],
|
||||
});
|
||||
|
||||
return await getResponse(parsedInput.responseId);
|
||||
return await getResponseWithQuotas(parsedInput.responseId);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user