feat: adds response count indicator (#2213)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Piyush Gupta
2024-03-13 15:48:37 +05:30
committed by GitHub
parent 09cb61ae1e
commit 2f11aa6c14
13 changed files with 189 additions and 107 deletions

View File

@@ -4,7 +4,7 @@ import { getServerSession } from "next-auth";
import { revalidatePath } from "next/cache";
import { authOptions } from "@formbricks/lib/authOptions";
import { getResponses, getSurveySummary } from "@formbricks/lib/response/service";
import { getResponseCountBySurveyId, getResponses, getSurveySummary } from "@formbricks/lib/response/service";
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
import { AuthorizationError } from "@formbricks/types/errors";
import { TResponse, TResponseFilterCriteria, TSurveySummary } from "@formbricks/types/responses";
@@ -58,3 +58,16 @@ export const getSurveySummaryAction = async (
return await getSurveySummary(surveyId, filterCriteria);
};
export const getResponseCountAction = async (
surveyId: string,
filters?: TResponseFilterCriteria
): Promise<number> => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
return await getResponseCountBySurveyId(surveyId, filters);
};

View File

@@ -8,9 +8,15 @@ interface SurveyResultsTabProps {
activeId: string;
environmentId: string;
surveyId: string;
responseCount: number | null;
}
export default function SurveyResultsTab({ activeId, environmentId, surveyId }: SurveyResultsTabProps) {
export default function SurveyResultsTab({
activeId,
environmentId,
surveyId,
responseCount,
}: SurveyResultsTabProps) {
const tabs = [
{
id: "summary",
@@ -20,7 +26,7 @@ export default function SurveyResultsTab({ activeId, environmentId, surveyId }:
},
{
id: "responses",
label: "Responses",
label: `Responses ${responseCount !== null ? `(${responseCount})` : ""}`,
icon: <InboxIcon className="h-5 w-5" />,
href: `/environments/${environmentId}/surveys/${surveyId}/responses?referer=true`,
},

View File

@@ -1,13 +0,0 @@
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
export const getAnalysisData = async (surveyId: string, environmentId: string) => {
const [survey, responseCount] = await Promise.all([
getSurvey(surveyId),
getResponseCountBySurveyId(surveyId),
]);
if (!survey) throw new Error(`Survey not found: ${surveyId}`);
if (survey.environmentId !== environmentId) throw new Error(`Survey not found: ${surveyId}`);
return { responseCount, survey };
};

View File

@@ -1,7 +1,10 @@
"use client";
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { getResponsesAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
import {
getResponseCountAction,
getResponsesAction,
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
import SurveyResultsTabs from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyResultsTabs";
import ResponseTimeline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline";
import CustomFilter from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter";
@@ -47,6 +50,7 @@ const ResponsePage = ({
responsesPerPage,
membershipRole,
}: ResponsePageProps) => {
const [responseCount, setResponseCount] = useState<number | null>(null);
const [responses, setResponses] = useState<TResponse[]>([]);
const [page, setPage] = useState<number>(1);
const [hasMore, setHasMore] = useState<boolean>(true);
@@ -64,23 +68,6 @@ const ResponsePage = ({
return checkForRecallInHeadline(survey);
}, [survey]);
useEffect(() => {
if (!searchParams?.get("referer")) {
resetState();
}
}, [searchParams, resetState]);
useEffect(() => {
const fetchInitialResponses = async () => {
const responses = await getResponsesAction(surveyId, 1, responsesPerPage, filters);
if (responses.length < responsesPerPage) {
setHasMore(false);
}
setResponses(responses);
};
fetchInitialResponses();
}, [surveyId, filters, responsesPerPage]);
const fetchNextPage = useCallback(async () => {
const newPage = page + 1;
const newResponses = await getResponsesAction(surveyId, newPage, responsesPerPage, filters);
@@ -99,9 +86,35 @@ const ResponsePage = ({
setResponses(responses.map((response) => (response.id === responseId ? updatedResponse : response)));
};
useEffect(() => {
if (!searchParams?.get("referer")) {
resetState();
}
}, [searchParams, resetState]);
useEffect(() => {
const fetchInitialResponses = async () => {
const responses = await getResponsesAction(surveyId, 1, responsesPerPage, filters);
if (responses.length < responsesPerPage) {
setHasMore(false);
}
setResponses(responses);
};
fetchInitialResponses();
}, [surveyId, filters, responsesPerPage]);
useEffect(() => {
const handleResponsesCount = async () => {
const responseCount = await getResponseCountAction(surveyId, filters);
setResponseCount(responseCount);
};
handleResponsesCount();
}, [filters, surveyId]);
useEffect(() => {
setPage(1);
setHasMore(true);
setResponses([]);
}, [filters]);
return (
@@ -119,7 +132,12 @@ const ResponsePage = ({
<CustomFilter environmentTags={environmentTags} attributes={attributes} survey={survey} />
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} product={product} user={user} />
</div>
<SurveyResultsTabs activeId="responses" environmentId={environment.id} surveyId={surveyId} />
<SurveyResultsTabs
activeId="responses"
environmentId={environment.id}
surveyId={surveyId}
responseCount={responseCount}
/>
<ResponseTimeline
environment={environment}
surveyId={surveyId}

View File

@@ -20,7 +20,7 @@ import RatingSummary from "./RatingSummary";
interface SummaryListProps {
summary: TSurveySummary["summary"];
responseCount: number;
responseCount: number | null;
environment: TEnvironment;
survey: TSurvey;
}
@@ -30,7 +30,7 @@ export default function SummaryList({ summary, environment, responseCount, surve
<div className="mt-10 space-y-8">
{survey.type === "web" && responseCount === 0 && !environment.widgetSetupCompleted ? (
<EmptyInAppSurveys environment={environment} />
) : responseCount === 0 ? (
) : !responseCount ? (
<EmptySpaceFiller
type="response"
environment={environment}

View File

@@ -1,7 +1,10 @@
"use client";
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { getSurveySummaryAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
import {
getResponseCountAction,
getSurveySummaryAction,
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
import SurveyResultsTabs from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyResultsTabs";
import SummaryDropOffs from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryDropOffs";
import SummaryList from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList";
@@ -24,6 +27,21 @@ import ContentWrapper from "@formbricks/ui/ContentWrapper";
import ResultsShareButton from "../../../components/ResultsShareButton";
const initialSurveySummary: TSurveySummary = {
meta: {
completedPercentage: 0,
completedResponses: 0,
displayCount: 0,
dropOffPercentage: 0,
dropOffCount: 0,
startsPercentage: 0,
totalResponses: 0,
ttcAverage: 0,
},
dropOff: [],
summary: [],
};
interface SummaryPageProps {
environment: TEnvironment;
survey: TSurvey;
@@ -34,7 +52,6 @@ interface SummaryPageProps {
environmentTags: TTag[];
attributes: TSurveyPersonAttributes;
membershipRole?: TMembershipRole;
responseCount: number;
}
const SummaryPage = ({
@@ -47,23 +64,10 @@ const SummaryPage = ({
environmentTags,
attributes,
membershipRole,
responseCount,
}: SummaryPageProps) => {
const [responseCount, setResponseCount] = useState<number | null>(null);
const { selectedFilter, dateRange, resetState } = useResponseFilter();
const [surveySummary, setSurveySummary] = useState<TSurveySummary>({
meta: {
completedPercentage: 0,
completedResponses: 0,
displayCount: 0,
dropOffPercentage: 0,
dropOffCount: 0,
startsPercentage: 0,
totalResponses: 0,
ttcAverage: 0,
},
dropOff: [],
summary: [],
});
const [surveySummary, setSurveySummary] = useState<TSurveySummary>(initialSurveySummary);
const [showDropOffs, setShowDropOffs] = useState<boolean>(false);
const filters = useMemo(
@@ -72,11 +76,18 @@ const SummaryPage = ({
);
useEffect(() => {
const fetchSurveySummary = async () => {
const handleInitialData = async () => {
const responseCount = await getResponseCountAction(surveyId, filters);
setResponseCount(responseCount);
if (responseCount === 0) {
setSurveySummary(initialSurveySummary);
return;
}
const response = await getSurveySummaryAction(surveyId, filters);
setSurveySummary(response);
};
fetchSurveySummary();
handleInitialData();
}, [filters, surveyId]);
const searchParams = useSearchParams();
@@ -105,7 +116,12 @@ const SummaryPage = ({
<CustomFilter environmentTags={environmentTags} attributes={attributes} survey={survey} />
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} product={product} user={user} />
</div>
<SurveyResultsTabs activeId="summary" environmentId={environment.id} surveyId={surveyId} />
<SurveyResultsTabs
activeId="summary"
environmentId={environment.id}
surveyId={surveyId}
responseCount={responseCount}
/>
<SummaryMetadata
survey={survey}
surveySummary={surveySummary.meta}

View File

@@ -1,6 +1,6 @@
import { getAnalysisData } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data";
import SummaryPage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage";
import { getServerSession } from "next-auth";
import { notFound } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { WEBAPP_URL } from "@formbricks/lib/constants";
@@ -8,6 +8,7 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getResponsePersonAttributes } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { getUser } from "@formbricks/lib/user/service";
@@ -18,10 +19,19 @@ export default async function Page({ params }) {
throw new Error("Unauthorized");
}
const [{ survey, responseCount }, environment] = await Promise.all([
getAnalysisData(params.surveyId, params.environmentId),
getEnvironment(params.environmentId),
]);
const surveyId = params.surveyId;
if (!surveyId) {
return notFound();
}
const survey = await getSurvey(surveyId);
if (!survey) {
throw new Error("Survey not found");
}
const environment = await getEnvironment(survey.environmentId);
if (!environment) {
throw new Error("Environment not found");
}
@@ -59,7 +69,6 @@ export default async function Page({ params }) {
environmentTags={tags}
attributes={attributes}
membershipRole={currentUserMembership?.role}
responseCount={responseCount}
/>
</>
);

View File

@@ -8,6 +8,7 @@ interface SurveyResultsTabProps {
activeId: string;
environmentId: string;
surveyId: string;
responseCount: number | null;
sharingKey: string;
}
@@ -15,6 +16,7 @@ export default function SurveyResultsTab({
activeId,
environmentId,
surveyId,
responseCount,
sharingKey,
}: SurveyResultsTabProps) {
const tabs = [
@@ -26,7 +28,7 @@ export default function SurveyResultsTab({
},
{
id: "responses",
label: "Responses",
label: `Responses ${responseCount !== null ? `(${responseCount})` : ""}`,
icon: <InboxIcon className="h-5 w-5" />,
href: `/share/${sharingKey}/responses?referer=true`,
},

View File

@@ -4,7 +4,10 @@ import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/comp
import { getFormattedFilters } from "@/app/lib/surveys/surveys";
import SurveyResultsTabs from "@/app/share/[sharingKey]/(analysis)/components/SurveyResultsTabs";
import ResponseTimeline from "@/app/share/[sharingKey]/(analysis)/responses/components/ResponseTimeline";
import { getResponsesBySurveySharingKeyAction } from "@/app/share/[sharingKey]/action";
import {
getResponseCountBySurveySharingKeyAction,
getResponsesBySurveySharingKeyAction,
} from "@/app/share/[sharingKey]/action";
import CustomFilter from "@/app/share/[sharingKey]/components/CustomFilter";
import SummaryHeader from "@/app/share/[sharingKey]/components/SummaryHeader";
import { useSearchParams } from "next/navigation";
@@ -43,6 +46,7 @@ const ResponsePage = ({
const [responses, setResponses] = useState<TResponse[]>([]);
const [page, setPage] = useState<number>(1);
const [hasMore, setHasMore] = useState<boolean>(true);
const [responseCount, setResponseCount] = useState<number | null>(null);
const { selectedFilter, dateRange, resetState } = useResponseFilter();
@@ -57,6 +61,21 @@ const ResponsePage = ({
return checkForRecallInHeadline(survey);
}, [survey]);
const fetchNextPage = useCallback(async () => {
const newPage = page + 1;
const newResponses = await getResponsesBySurveySharingKeyAction(
sharingKey,
newPage,
responsesPerPage,
filters
);
if (newResponses.length === 0 || newResponses.length < responsesPerPage) {
setHasMore(false);
}
setResponses([...responses, ...newResponses]);
setPage(newPage);
}, [filters, page, responses, responsesPerPage, sharingKey]);
useEffect(() => {
if (!searchParams?.get("referer")) {
resetState();
@@ -74,20 +93,13 @@ const ResponsePage = ({
fetchInitialResponses();
}, [filters, responsesPerPage, sharingKey]);
const fetchNextPage = useCallback(async () => {
const newPage = page + 1;
const newResponses = await getResponsesBySurveySharingKeyAction(
sharingKey,
newPage,
responsesPerPage,
filters
);
if (newResponses.length === 0 || newResponses.length < responsesPerPage) {
setHasMore(false);
}
setResponses([...responses, ...newResponses]);
setPage(newPage);
}, [filters, page, responses, responsesPerPage, sharingKey]);
useEffect(() => {
const handleResponsesCount = async () => {
const responseCount = await getResponseCountBySurveySharingKeyAction(sharingKey, filters);
setResponseCount(responseCount);
};
handleResponsesCount();
}, [filters, sharingKey]);
return (
<ContentWrapper>
@@ -97,6 +109,7 @@ const ResponsePage = ({
activeId="responses"
environmentId={environment.id}
surveyId={surveyId}
responseCount={responseCount}
sharingKey={sharingKey}
/>
<ResponseTimeline

View File

@@ -6,7 +6,10 @@ import SummaryList from "@/app/(app)/environments/[environmentId]/surveys/[surve
import SummaryMetadata from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata";
import { getFormattedFilters } from "@/app/lib/surveys/surveys";
import SurveyResultsTabs from "@/app/share/[sharingKey]/(analysis)/components/SurveyResultsTabs";
import { getSummaryBySurveySharingKeyAction } from "@/app/share/[sharingKey]/action";
import {
getResponseCountBySurveySharingKeyAction,
getSummaryBySurveySharingKeyAction,
} from "@/app/share/[sharingKey]/action";
import CustomFilter from "@/app/share/[sharingKey]/components/CustomFilter";
import SummaryHeader from "@/app/share/[sharingKey]/components/SummaryHeader";
import { useSearchParams } from "next/navigation";
@@ -20,6 +23,21 @@ import { TSurvey } from "@formbricks/types/surveys";
import { TTag } from "@formbricks/types/tags";
import ContentWrapper from "@formbricks/ui/ContentWrapper";
const initialSurveySummary: TSurveySummary = {
meta: {
completedPercentage: 0,
completedResponses: 0,
displayCount: 0,
dropOffPercentage: 0,
dropOffCount: 0,
startsPercentage: 0,
totalResponses: 0,
ttcAverage: 0,
},
dropOff: [],
summary: [],
};
interface SummaryPageProps {
environment: TEnvironment;
survey: TSurvey;
@@ -28,7 +46,6 @@ interface SummaryPageProps {
sharingKey: string;
environmentTags: TTag[];
attributes: TSurveyPersonAttributes;
responseCount: number;
}
const SummaryPage = ({
@@ -39,23 +56,11 @@ const SummaryPage = ({
sharingKey,
environmentTags,
attributes,
responseCount,
}: SummaryPageProps) => {
const [responseCount, setResponseCount] = useState<number | null>(null);
const { selectedFilter, dateRange, resetState } = useResponseFilter();
const [surveySummary, setSurveySummary] = useState<TSurveySummary>({
meta: {
completedPercentage: 0,
completedResponses: 0,
displayCount: 0,
dropOffPercentage: 0,
dropOffCount: 0,
startsPercentage: 0,
totalResponses: 0,
ttcAverage: 0,
},
dropOff: [],
summary: [],
});
const [surveySummary, setSurveySummary] = useState<TSurveySummary>(initialSurveySummary);
const [showDropOffs, setShowDropOffs] = useState<boolean>(false);
const filters = useMemo(
@@ -64,11 +69,18 @@ const SummaryPage = ({
);
useEffect(() => {
const fetchSurveySummary = async () => {
const handleInitialData = async () => {
const responseCount = await getResponseCountBySurveySharingKeyAction(sharingKey, filters);
setResponseCount(responseCount);
if (responseCount === 0) {
setSurveySummary(initialSurveySummary);
return;
}
const response = await getSummaryBySurveySharingKeyAction(sharingKey, filters);
setSurveySummary(response);
};
fetchSurveySummary();
handleInitialData();
}, [filters, sharingKey]);
survey = useMemo(() => {
@@ -91,6 +103,7 @@ const SummaryPage = ({
activeId="summary"
environmentId={environment.id}
surveyId={surveyId}
responseCount={responseCount}
sharingKey={sharingKey}
/>
<SummaryMetadata

View File

@@ -1,4 +1,3 @@
import { getAnalysisData } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data";
import SummaryPage from "@/app/share/[sharingKey]/(analysis)/summary/components/SummaryPage";
import { notFound } from "next/navigation";
@@ -23,10 +22,7 @@ export default async function Page({ params }) {
if (!survey) {
throw new Error("Survey not found");
}
const [{ responseCount }, environment] = await Promise.all([
getAnalysisData(survey.id, survey.environmentId),
getEnvironment(survey.environmentId),
]);
const environment = await getEnvironment(survey.environmentId);
if (!environment) {
throw new Error("Environment not found");
@@ -50,7 +46,6 @@ export default async function Page({ params }) {
product={product}
environmentTags={tags}
attributes={attributes}
responseCount={responseCount}
/>
</>
);

View File

@@ -1,6 +1,6 @@
"use server";
import { getResponses, getSurveySummary } from "@formbricks/lib/response/service";
import { getResponseCountBySurveyId, getResponses, getSurveySummary } from "@formbricks/lib/response/service";
import { getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { TResponse, TResponseFilterCriteria, TSurveySummary } from "@formbricks/types/responses";
@@ -28,3 +28,13 @@ export const getSummaryBySurveySharingKeyAction = async (
return await getSurveySummary(surveyId, filterCriteria);
};
export const getResponseCountBySurveySharingKeyAction = async (
sharingKey: string,
filterCriteria?: TResponseFilterCriteria
): Promise<number> => {
const surveyId = await getSurveyIdByResultShareKey(sharingKey);
if (!surveyId) throw new AuthorizationError("Not authorized");
return await getResponseCountBySurveyId(surveyId, filterCriteria);
};

View File

@@ -308,10 +308,10 @@ export const buildWhereClause = (filterCriteria?: TResponseFilterCriteria) => {
};
export const getResponsesFileName = (surveyName: string, extension: string) => {
surveyName = sanitizeString(surveyName);
const sanitizedSurveyName = sanitizeString(surveyName);
const formattedDateString = getTodaysDateTimeFormatted("-");
return `export-${surveyName.split(" ").join("-")}-${formattedDateString}.${extension}`.toLocaleLowerCase();
return `export-${sanitizedSurveyName.split(" ").join("-")}-${formattedDateString}.${extension}`.toLocaleLowerCase();
};
export const extracMetadataKeys = (obj: TResponse["meta"]) => {